feat(desktop): reconcile live tool events, polish thread chrome, harden boot

- chat-messages: match tool rows by overlapping query/context/preview values
  so preview-first `tool.progress` rows reliably adopt later stable-id
  `tool.start` payloads instead of spawning ghost rows or mis-merging
  parallel same-name calls; preserve prior args/result across phases.
- tui_gateway: emit full args + parsed result on `tool.start` / `tool.complete`,
  drop redundant `tool.started` re-emit from `tool.progress`.
- electron/main: prefer SOURCE_REPO_ROOT before PATH `hermes` in dev so
  local backend edits actually run; split hardening helpers into
  `electron/hardening.cjs` with tests.
- thread/tool UI: one-shot enter animation keyed by stable ids, braille
  spinner for running rows, Cursor-like disclosure rows, drill-down +
  duration/count formatting via new tool-fallback-model.
- composer: extract `text-utils`, drop liquid-glass overrides.
- right-rail: split preview-pane into preview-console / preview-file.
- runtime: incremental external-store runtime + runtime-readiness gate;
  onboarding store + tests; route-resume hook test.
- regression tests for live tool reconciliation (parallel tools, id-less
  progress, preview-first rows, structured args/results).
This commit is contained in:
Brooklyn Nicholson
2026-05-11 21:38:47 -04:00
parent fdf73f0adf
commit d208f2c2c0
64 changed files with 5614 additions and 2703 deletions
+9 -3
View File
@@ -1591,12 +1591,12 @@ def _on_tool_start(sid: str, tool_call_id: str, name: str, args: dict):
_emit(
"tool.start",
sid,
{"tool_id": tool_call_id, "name": name, "context": _tool_ctx(name, args)},
{"tool_id": tool_call_id, "name": name, "args": args, "context": _tool_ctx(name, args)},
)
def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result: str):
payload = {"tool_id": tool_call_id, "name": name}
payload = {"tool_id": tool_call_id, "name": name, "args": args}
session = _sessions.get(sid)
snapshot = None
started_at = None
@@ -1606,6 +1606,10 @@ def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result
duration_s = time.time() - started_at if started_at else None
if duration_s is not None:
payload["duration_s"] = duration_s
try:
payload["result"] = json.loads(result)
except Exception:
payload["result"] = result
summary = _tool_summary(name, result, duration_s)
if summary:
payload["summary"] = summary
@@ -1645,7 +1649,9 @@ def _on_tool_progress(
if not _tool_progress_enabled(sid):
return
if event_type == "tool.started" and name:
_emit("tool.progress", sid, {"name": name, "preview": preview or ""})
# `_on_tool_start` already emits the authoritative `tool.start` with
# the stable tool id and args. Emitting another id-less progress row
# here makes the desktop live view diverge from hydrated history.
return
if event_type == "reasoning.available" and preview:
_emit("reasoning.available", sid, {"text": str(preview)})