From ae22a802380a2b044bc922edd9a63d6ce05cf4ca Mon Sep 17 00:00:00 2001 From: ai-ag2026 Date: Thu, 7 May 2026 14:45:11 +0200 Subject: [PATCH 1/3] fix: hide workspace metadata in user bubbles --- api/streaming.py | 19 ++---------- static/ui.js | 4 +++ tests/test_issue1217_transcript_compaction.py | 30 ------------------- 3 files changed, 6 insertions(+), 47 deletions(-) diff --git a/api/streaming.py b/api/streaming.py index 0d91a05a..90c2a68c 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -586,11 +586,6 @@ def _message_text(value) -> str: return _strip_thinking_markup(str(value or '').strip()) -def _strip_workspace_prefix(text: str) -> str: - """Remove WebUI's model-facing workspace tag from display identity text.""" - return re.sub(r'^\s*\[Workspace:[^\]]+\]\s*', '', str(text or '')).strip() - - def _first_exchange_snippets(messages): """Return (first_user_text, first_assistant_text) snippets for title generation. @@ -1438,12 +1433,6 @@ def _message_identity(msg): role = str(msg.get('role') or '') content = msg.get('content', '') text = _message_text(content) - if role == 'user': - # WebUI sends the model a workspace-prefixed user_message while the - # visible optimistic bubble contains only the human text. Treat them as - # the same turn for merge/dedup purposes; otherwise compaction results - # render two adjacent user bubbles ("Ok" and "[Workspace...]\nOk"). - text = _strip_workspace_prefix(text) if not text and not msg.get('tool_call_id') and not msg.get('tool_calls'): return None return ( @@ -1482,7 +1471,7 @@ def _find_current_user_turn(messages, msg_text): if not isinstance(msg, dict) or msg.get('role') != 'user': continue fallback = idx - text = " ".join(_strip_workspace_prefix(_message_text(msg.get('content', ''))).split()) + text = " ".join(_message_text(msg.get('content', '')).split()) if needle and (needle in text or text in needle): return idx return fallback @@ -1569,11 +1558,7 @@ def _merge_display_messages_after_agent_result(previous_display, previous_contex continue if _is_context_compression_marker(msg) and key is not None and key in seen: continue - display_msg = msg - if key is not None and key == current_user_key and isinstance(msg, dict) and msg.get('role') == 'user': - display_msg = copy.deepcopy(msg) - display_msg['content'] = msg_text - merged.append(copy.deepcopy(display_msg)) + merged.append(copy.deepcopy(msg)) if key is not None: seen.add(key) return merged diff --git a/static/ui.js b/static/ui.js index ff666883..597aeb12 100644 --- a/static/ui.js +++ b/static/ui.js @@ -67,6 +67,9 @@ function _isBacktickFenceClose(line,minLen){ * All non-fenced text stays escaped (no bold/italic/link interpretation). */ +function _stripWorkspaceDisplayPrefix(text){ + return String(text||'').replace(/^\s*\[Workspace:[^\]]+\]\s*/,'').trim(); +} function _renderUserFencedBlocks(text){ const stash=[]; let s=String(text||''); @@ -4573,6 +4576,7 @@ function renderMessages(options){ } } const isUser=m.role==='user'; + if(isUser) content=_stripWorkspaceDisplayPrefix(content); const isLastAssistant=!isUser&&vi===renderVisWithIdx.length-1; let filesHtml=''; if(m.attachments&&m.attachments.length){ diff --git a/tests/test_issue1217_transcript_compaction.py b/tests/test_issue1217_transcript_compaction.py index 7d14f124..72c6f08a 100644 --- a/tests/test_issue1217_transcript_compaction.py +++ b/tests/test_issue1217_transcript_compaction.py @@ -47,36 +47,6 @@ def test_session_persists_model_context_separately_from_display_transcript(tmp_p assert _sanitize_messages_for_api(_session_context_messages(reloaded)) == compacted_context -def test_workspace_prefixed_current_user_after_compaction_is_not_duplicated(): - previous_display = [ - {"role": "user", "content": "older prompt"}, - {"role": "assistant", "content": "older answer"}, - ] - previous_context = list(previous_display) - compacted_result = [ - { - "role": "assistant", - "content": "[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted.", - }, - {"role": "user", "content": "[Workspace: /home/manfred/.hermes/workspace]\nOk, mache weiter"}, - {"role": "assistant", "content": "continuing"}, - ] - - merged = _merge_display_messages_after_agent_result( - previous_display, - previous_context, - compacted_result, - "Ok, mache weiter", - ) - - assert [m["role"] for m in merged] == ["user", "assistant", "assistant", "user", "assistant"] - assert [m["content"] for m in merged[-2:]] == [ - "Ok, mache weiter", - "continuing", - ] - assert sum(1 for m in merged if m.get("role") == "user" and "Ok, mache weiter" in m.get("content", "")) == 1 - - def test_compacted_agent_result_keeps_old_prompts_and_appends_current_turn(): previous_display = [ {"role": "user", "content": "first prompt that must remain visible"}, From 9633ed345ba610843f1c2680a36ed36d8beffbb0 Mon Sep 17 00:00:00 2001 From: ai-ag2026 Date: Thu, 7 May 2026 17:16:15 +0200 Subject: [PATCH 2/3] fix: preserve context card render ordering --- static/ui.js | 6 ++-- tests/test_workspace_display_prefix.py | 39 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/test_workspace_display_prefix.py diff --git a/static/ui.js b/static/ui.js index 597aeb12..bd47dda2 100644 --- a/static/ui.js +++ b/static/ui.js @@ -4576,7 +4576,7 @@ function renderMessages(options){ } } const isUser=m.role==='user'; - if(isUser) content=_stripWorkspaceDisplayPrefix(content); + const displayContent=isUser?_stripWorkspaceDisplayPrefix(content):content; const isLastAssistant=!isUser&&vi===renderVisWithIdx.length-1; let filesHtml=''; if(m.attachments&&m.attachments.length){ @@ -4590,7 +4590,7 @@ function renderMessages(options){ return _renderAttachmentHtml(fname,fileUrl); }).join('')}`; } - let bodyHtml = isUser ? _renderUserFencedBlocks(content) : renderMd(_stripXmlToolCallsDisplay(String(content))); + let bodyHtml = isUser ? _renderUserFencedBlocks(displayContent) : renderMd(_stripXmlToolCallsDisplay(String(displayContent))); if(!isUser&&m.provider_details){ bodyHtml += `
Provider details
${esc(String(m.provider_details))}
`; } @@ -4631,7 +4631,7 @@ function renderMessages(options){ row.className='msg-row'; row.dataset.msgIdx=rawIdx; row.dataset.role='user'; - row.dataset.rawText=String(content).trim(); + row.dataset.rawText=String(displayContent).trim(); row.innerHTML=`${filesHtml}
${bodyHtml}
${footHtml}`; inner.appendChild(row); userRows.set(rawIdx, row); diff --git a/tests/test_workspace_display_prefix.py b/tests/test_workspace_display_prefix.py new file mode 100644 index 00000000..98528011 --- /dev/null +++ b/tests/test_workspace_display_prefix.py @@ -0,0 +1,39 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def _read(relpath: str) -> str: + return (ROOT / relpath).read_text(encoding="utf-8") + + +def test_workspace_display_prefix_helper_strips_leading_metadata_only(): + src = _read("static/ui.js") + start = src.find("function _stripWorkspaceDisplayPrefix") + assert start != -1, "workspace display prefix stripper not found" + end = src.find("function _renderUserFencedBlocks", start) + assert end != -1, "user fenced block renderer not found after prefix stripper" + helper = src[start:end] + + assert r"^\s*\[Workspace:[^\]]+\]\s*" in helper + assert ".trim()" in helper + + +def test_user_render_uses_stripped_display_content_without_preempting_context_cards(): + src = _read("static/ui.js") + loop_start = src.find("for(let vi=0;vi Date: Thu, 7 May 2026 17:20:38 +0200 Subject: [PATCH 3/3] chore: rerun ci for workspace prefix fix