Stage 313: PR #1813 — hide workspace metadata in user bubbles by @ai-ag2026
This commit is contained in:
commit
49501959b8
4 changed files with 47 additions and 49 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
const displayContent=isUser?_stripWorkspaceDisplayPrefix(content):content;
|
||||
const isLastAssistant=!isUser&&vi===renderVisWithIdx.length-1;
|
||||
let filesHtml='';
|
||||
if(m.attachments&&m.attachments.length){
|
||||
|
|
@ -4586,7 +4590,7 @@ function renderMessages(options){
|
|||
return _renderAttachmentHtml(fname,fileUrl);
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
let bodyHtml = isUser ? _renderUserFencedBlocks(content) : renderMd(_stripXmlToolCallsDisplay(String(content)));
|
||||
let bodyHtml = isUser ? _renderUserFencedBlocks(displayContent) : renderMd(_stripXmlToolCallsDisplay(String(displayContent)));
|
||||
if(!isUser&&m.provider_details){
|
||||
bodyHtml += `<details class="provider-error-details"><summary>Provider details</summary><pre><code>${esc(String(m.provider_details))}</code></pre></details>`;
|
||||
}
|
||||
|
|
@ -4627,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}<div class="msg-body">${bodyHtml}</div>${footHtml}`;
|
||||
inner.appendChild(row);
|
||||
userRows.set(rawIdx, row);
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
39
tests/test_workspace_display_prefix.py
Normal file
39
tests/test_workspace_display_prefix.py
Normal file
|
|
@ -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<visWithIdx.length;vi++)")
|
||||
assert loop_start != -1, "message render loop not found"
|
||||
loop_end = src.find("if(!currentAssistantTurn)", loop_start)
|
||||
assert loop_end != -1, "assistant render branch not found after user branch"
|
||||
render_prefix = src[loop_start:loop_end]
|
||||
|
||||
display_idx = render_prefix.find("const displayContent=isUser?_stripWorkspaceDisplayPrefix(content):content;")
|
||||
context_idx = render_prefix.find("if(_isContextCompactionMessage(m))")
|
||||
user_idx = render_prefix.find("if(isUser)")
|
||||
assert display_idx != -1, "display content stripper not used in render loop"
|
||||
assert context_idx != -1, "context compaction branch not found"
|
||||
assert user_idx != -1, "user render branch not found"
|
||||
assert display_idx < context_idx < user_idx
|
||||
assert "_renderUserFencedBlocks(displayContent)" in render_prefix
|
||||
assert "row.dataset.rawText=String(displayContent).trim();" in render_prefix
|
||||
Loading…
Reference in a new issue