From ae22a802380a2b044bc922edd9a63d6ce05cf4ca Mon Sep 17 00:00:00 2001 From: ai-ag2026 Date: Thu, 7 May 2026 14:45:11 +0200 Subject: [PATCH] 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"},