diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js index 2e3cfa77..b244d521 100644 --- a/interfaces/admin/static/chat.js +++ b/interfaces/admin/static/chat.js @@ -1730,6 +1730,21 @@ function formatToolPanelText(name, payload, options = {}) { }; const n = String(name || "").trim() || "tool"; const p = payload && typeof payload === "object" ? payload : {}; + if (String(p.phase || "") === "call" && (p.tool_name != null || p.arguments !== undefined)) { + const tn = String(p.tool_name || "").trim() || "tool"; + let argsStr = ""; + try { + argsStr = + p.arguments && typeof p.arguments === "object" + ? JSON.stringify(p.arguments, null, 2) + : String(p.arguments ?? ""); + } catch (_) { + argsStr = String(p.arguments ?? ""); + } + const tid = String(p.tool_call_id || "").trim(); + const head = currentLang === "zh" ? `[调用] ${tn}` : `[call] ${tn}`; + return normalizeStreamText([head, tid ? `tool_call_id: ${tid}` : "", "", argsStr].filter(Boolean).join("\n")); + } const r = p.result && typeof p.result === "object" ? p.result : p; // SQL audit payload: keep line breaks and key fields visible. if (r && typeof r === "object" && (r.input_sql || r.executed_sql || r.sql_guard)) { @@ -4370,16 +4385,15 @@ async function renderChatUi() { const title = String(seg.title || "tool"); const body = String(seg.body || ""); const images = Array.isArray(seg.images) ? seg.images : []; - if (!showToolOutput && !images.length) continue; + // Live stream: always render tool cards from session.tool so过程态完整;showToolOutput 仍用于历史聚合气泡。 flushTextBuf(); const audit = seg.sqlAudit && typeof seg.sqlAudit === "object" ? seg.sqlAudit : null; - if (showToolOutput) { - if (audit) { - const guard = audit.guard && typeof audit.guard === "object" ? audit.guard : {}; - const autoLimit = _sqlLimitSuffix(audit.inputSql, audit.executedSql); - const executedDiffHtml = _renderExecutedSqlWithAddedHighlight(audit.inputSql, audit.executedSql); - blocks.push( - `
${escapeHtml(title)} · SQL audit + if (audit) { + const guard = audit.guard && typeof audit.guard === "object" ? audit.guard : {}; + const autoLimit = _sqlLimitSuffix(audit.inputSql, audit.executedSql); + const executedDiffHtml = _renderExecutedSqlWithAddedHighlight(audit.inputSql, audit.executedSql); + blocks.push( + `
${escapeHtml(title)} · SQL audit
input SQL
${escapeHtml(String(audit.inputSql || ""))}
executed SQL (added highlighted)
${executedDiffHtml}
@@ -4394,12 +4408,11 @@ ${autoLimit ? `
auto-added claus rows_returned=${escapeHtml(String(audit.rowsReturned != null ? audit.rowsReturned : ""))}
`, - ); - } else { - blocks.push( - `
${escapeHtml(title)}
${escapeHtml(body)}
`, - ); - } + ); + } else { + blocks.push( + `
${escapeHtml(title)}
${escapeHtml(body)}
`, + ); } for (const im of images) { const src = String((im && im.src) || "").trim(); @@ -4508,6 +4521,14 @@ ${autoLimit ? `
auto-added claus const liveTag = currentLang === "zh" ? "[实时全量]" : "[LIVE FULL]"; const key = `${name}:${++toolSeq}`; const rawPayload = p.payload != null ? p.payload : p; + const titleTool = + rawPayload && typeof rawPayload === "object" && String(rawPayload.tool_name || "").trim() + ? String(rawPayload.tool_name || "").trim() + : ""; + const displayName = + name === "tool_use_call" && titleTool + ? `${currentLang === "zh" ? "调用" : "call"} · ${titleTool}` + : name; const body = formatToolPanelText(name, rawPayload, { streamMode: true }); const sqlAudit = extractSqlAuditPayload(rawPayload); const images = extractToolImageItems(rawPayload); @@ -4536,7 +4557,14 @@ ${autoLimit ? `
auto-added claus return false; }; if (_containsRefAttachments(rawPayload)) sawStreamToolRefAttachments = true; - chatStreamSegments.push({ type: "tool", key, title: `${name} ${liveTag}`, body, sqlAudit, images }); + chatStreamSegments.push({ + type: "tool", + key, + title: `${displayName} ${liveTag}`.trim(), + body, + sqlAudit, + images, + }); }; const appendFinalAssistant = async (message, fallbackText) => { const wsAttachments = diff --git a/platform/llm/transports/google_gemini_sse.py b/platform/llm/transports/google_gemini_sse.py index 20585aca..507e729e 100644 --- a/platform/llm/transports/google_gemini_sse.py +++ b/platform/llm/transports/google_gemini_sse.py @@ -224,6 +224,8 @@ class GoogleGeminiChatModel(ChatModel): txt = str(p.get("text") or "") if txt: thinking_parts.append(txt) + if on_token: + on_token(txt) sig = p.get("thoughtSignature") if isinstance(sig, str) and sig: thinking_signature = sig diff --git a/platform/llm/transports/openai_chat_completions.py b/platform/llm/transports/openai_chat_completions.py index 30286fcf..260f3db2 100644 --- a/platform/llm/transports/openai_chat_completions.py +++ b/platform/llm/transports/openai_chat_completions.py @@ -551,8 +551,11 @@ class OpenAIChatModel(ChatModel): reasoning_parts = getattr(msg, "reasoning_content", None) or "" reasoning_text = str(reasoning_parts).strip() if reasoning_parts else "" content = msg.content or "" - if on_token and content: - on_token(content) + if on_token: + if reasoning_text: + on_token(reasoning_text) + if content: + on_token(content) tool_calls: list[LLMToolCall] = [] if msg.tool_calls: @@ -620,6 +623,8 @@ class OpenAIChatModel(ChatModel): rc = getattr(delta, "reasoning_content", None) or "" if rc: reasoning_parts.append(rc) + if on_token: + on_token(rc) if delta.tool_calls: for tc in delta.tool_calls: idx = int(tc.index) diff --git a/runtime/direct_loop.py b/runtime/direct_loop.py index 46b3fb22..20f56f62 100644 --- a/runtime/direct_loop.py +++ b/runtime/direct_loop.py @@ -1354,6 +1354,22 @@ def run_oclaw_direct_loop( # force one no-tool synthesis pass to guarantee a visible assistant body. hit_tool_round_limit = True + # Stream UI: emit one session.tool per pending call before execution (tool_use_result fires after). + if callable(on_tool_ui): + for tc in step.llm_tool_calls: + try: + on_tool_ui( + "tool_use_call", + { + "phase": "call", + "tool_name": str(getattr(tc, "name", "") or ""), + "tool_call_id": str(getattr(tc, "id", "") or ""), + "arguments": dict(getattr(tc, "arguments", {}) or {}), + }, + ) + except Exception: + pass + elapsed_ms, results_by_id = _execute_tool_step( skill_exec=skill_exec, store=store,