Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Fixed

- **PR TBD** by @franksong2702 — Thinking cards now suppress exact snippets that are already shown as user-visible interim assistant text, avoiding duplicated progress lines when an agent emits the same sentence through both reasoning and interim-assistant callbacks.

- **PR #2322** by @Michaelyklam (refs #2271) — LAN Ollama models selected from endpoint-discovered `custom:<host>-<port>` / `custom:<host>:<port>` picker entries now route through the configured `ollama` provider and base URL instead of surfacing a missing `CUSTOM_*_API_KEY` error. The picker still surfaces endpoint-discovered entries; the fix is to recognize them as UI routing hints matching the configured local-server base URL and resolve them via the actual `ollama` provider.


Expand Down
23 changes: 21 additions & 2 deletions static/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
let assistantText='';
let reasoningText='';
let liveReasoningText='';
let visibleInterimSnippets=[];
let _latestGoalStatus=null;
let _pendingGoalContinuation=null;
let assistantRow=null;
Expand Down Expand Up @@ -527,6 +528,19 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
function _closeSource(){
closeLiveStream(activeSid, streamId);
}
function _stripLiveVisibleAssistantEchoFromThinking(text, snippets){
let out=String(text||'');
(Array.isArray(snippets)?snippets:[]).forEach(snippet=>{
const visible=String(snippet||'').trim();
if(visible.length<20) return;
out=out.split(visible).join('');
});
return out.trim();
}
function _liveThinkingText(){
const clean=_stripLiveVisibleAssistantEchoFromThinking(liveReasoningText, visibleInterimSnippets);
return clean || 'Thinking…';
}
function syncInflightAssistantMessage(){
const inflight=INFLIGHT[activeSid];
if(!inflight) return;
Expand Down Expand Up @@ -1190,9 +1204,14 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
return;
}
assistantText+=visible;
visibleInterimSnippets.push(visible);
syncInflightAssistantMessage();
if(!S.session||S.session.session_id!==activeSid) return;
const parsed=_parseStreamState();
if(window._showThinking!==false){
if(typeof updateThinking==='function') updateThinking(_liveThinkingText());
else appendThinking(_liveThinkingText());
}
if(String((parsed&&parsed.displayText)||'').trim()||assistantRow) ensureAssistantRow();
_scheduleRender();
});
Expand All @@ -1209,8 +1228,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
// finalizeThinkingCard(). The old rAF-only path caused a race where
// the thinking row was still a spinner when finalized.
if(window._showThinking!==false){
if(typeof updateThinking==='function') updateThinking(liveReasoningText||'Thinking…');
else appendThinking(liveReasoningText);
if(typeof updateThinking==='function') updateThinking(_liveThinkingText());
else appendThinking(_liveThinkingText());
}
_scheduleRender();
});
Expand Down
13 changes: 13 additions & 0 deletions static/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -2308,6 +2308,16 @@ function _sanitizeThinkingDisplayText(text){
return stripped.trim();
}

function _stripVisibleAssistantEchoFromThinking(thinkingText, visibleText){
let out=String(thinkingText||'');
const visible=String(visibleText||'');
if(!out||!visible) return out.trim();
visible.split(/\n{2,}/).map(s=>s.trim()).filter(s=>s.length>=20).forEach(snippet=>{
out=out.split(snippet).join('');
});
return out.trim();
}

function renderMd(raw){
let s=(raw||'').replace(/\r\n/g,'\n').replace(/\r/g,'\n');
// ── Entity decode: must run FIRST so &gt; lines become > for the blockquote
Expand Down Expand Up @@ -5401,6 +5411,9 @@ function renderMessages(options){
content='**Error:** No response received after context compression. Please retry.';
}
const displayContent=isUser?_stripWorkspaceDisplayPrefix(content):content;
if(thinkingText&&!isUser){
thinkingText=_stripVisibleAssistantEchoFromThinking(thinkingText, displayContent);
}
const isLastAssistant=!isUser&&vi===renderVisWithIdx.length-1;
const nextRendered=renderVisWithIdx[vi+1];
const isTurnFinalAssistant=!isUser&&(!nextRendered||!nextRendered.m||nextRendered.m.role!=='assistant');
Expand Down
27 changes: 27 additions & 0 deletions tests/test_ui_tool_call_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")


def _function_body(src: str, name: str) -> str:
Expand Down Expand Up @@ -233,6 +234,32 @@ def test_live_activity_summary_shows_readable_progress_without_persisted_content
"Readable progress must not reintroduce the noisy secondary tool-name list."
)

def test_live_thinking_suppresses_visible_interim_echoes(self):
interim_match = re.search(r"source\.addEventListener\('interim_assistant',e=>\{(.*?)\n\s*\}\);", MESSAGES_JS, re.S)
assert interim_match, "interim_assistant listener not found"
interim_fn = interim_match.group(1)
live_thinking_fn = _function_body(MESSAGES_JS, "_liveThinkingText")

assert "visibleInterimSnippets.push(visible)" in interim_fn, (
"Visible interim commentary should be remembered so the live Thinking card does not echo it."
)
assert "_stripLiveVisibleAssistantEchoFromThinking" in live_thinking_fn, (
"Live Thinking text should suppress exact visible interim commentary echoes."
)

def test_settled_thinking_suppresses_visible_assistant_echoes(self):
render_fn = _function_body(UI_JS, "renderMessages")
helper = _function_body(UI_JS, "_stripVisibleAssistantEchoFromThinking")
assert "_stripVisibleAssistantEchoFromThinking(thinkingText, displayContent)" in render_fn, (
"Settled Thinking cards should not repeat text already rendered as visible assistant content."
)
assert "s.length>=20" in helper, (
"Thinking echo suppression should ignore tiny snippets to avoid over-stripping reasoning."
)
assert "out.split(snippet).join('')" in helper, (
"Thinking echo suppression should remove exact visible assistant snippets from reasoning display."
)

def test_tools_and_thinking_share_one_collapsed_activity_dropdown(self):
ui_min = re.sub(r"\s+", "", UI_JS)
assert "functionensureActivityGroup(" in ui_min, (
Expand Down
Loading