feat(chat): terminal chat parity for every tool; Desktop-like artifacts

- The chat over a terminal pane now covers Claude, Codex, omp, Cursor and
  Antigravity: Cursor's agent transcript and agy's step log
  (brain/<id>/.system_generated/logs/transcript_full.jsonl) are read as
  conversations; Cursor sessions are claimed by their workspace
- Replies appear as soon as they are written: the projects-tree watcher
  broadcasts `transcript_changed` (at most every 250 ms per session) and the
  chat re-reads the transcript while the terminal underneath draws
- Stop shows from the pane's own busy line (Claude/Codex `esc to interrupt`,
  agy `… esc to cancel`), or fresh output for omp/Cursor — no hooks needed
- Permission prompts read off the terminal become a card with the tool, its
  command and "Deny with a note"; Codex gets Permission mode / Model buttons
  that open its own numbered pickers, answered from the menu card
- Artifacts: cards for pages/documents/diagrams/tables/React components, a
  Preview | Code panel with versions, copy, download, save to the Artifacts
  Hub, full screen; sandboxed iframes without allow-same-origin
- Claude panes get CLAUDE_CODE_ENABLE_TODO_TOOLS=1 so the Progress panel has a
  plan; multi-question pane menus read their Next row and question tabs
- The realtime chat bridge (structured stdio, /api/chat-bridge, /ws-chat)
  from the unreleased ad9d475 is not kept
This commit is contained in:
2026-10-21 15:44:17 +07:00
parent aee4fc190e
commit 383b9ae231
41 changed files with 2582 additions and 426 deletions
+33 -5
View File
@@ -1032,7 +1032,9 @@ function startWorkflowPoll(broadcast) {
* sessions show up immediately rather than on the next poll. Events for
* files already in `mtimeCache` (active transcripts being appended to) are
* ignored, so a busy session never thrashes the importer — the poll picks
* up its growth. Recursive watching is used only on macOS/Windows (native,
* up its growth; such an append only broadcasts `transcript_changed`
* `{session_id}` (at most every 250 ms per session), so an open chat reads
* the new lines at once. Recursive watching is used only on macOS/Windows (native,
* stable); on Linux, where Node's userland recursive watcher trips on the
* high-churn projects tree (see lib/cc-watcher.js), we watch the root plus
* each immediate child folder non-recursively instead.
@@ -1148,11 +1150,37 @@ function startSessionSync(broadcast) {
}, DEBOUNCE_MS);
if (debounce.unref) debounce.unref();
}
// Only a path we don't already track is interesting (a new session file or a
// new project folder). Appends to a known active transcript are left to the
// poll, so the watcher never re-parses a busy session every write.
// An append to a session's own transcript: a chat showing that session reads
// it now instead of at its next poll. One `transcript_changed` per session
// per PUSH_MS — a notice only, nothing is parsed here.
const PUSH_MS = 250;
const pushing = new Map(); // session id → pending timer
function pushTranscript(fullPath) {
const m =
/(?:^|[\\/])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(
fullPath
);
if (!m || pushing.has(m[1])) return;
const sessionId = m[1];
const timer = setTimeout(() => {
pushing.delete(sessionId);
try {
broadcast("transcript_changed", { session_id: sessionId });
} catch {
/* best-effort — the chat's poll still catches up */
}
}, PUSH_MS);
if (timer.unref) timer.unref();
pushing.set(sessionId, timer);
}
// Only a path we don't already track is interesting to the importer (a new
// session file or a new project folder). Appends to a known active
// transcript are left to the poll, so the watcher never re-parses a busy
// session every write — it only tells the chats (`pushTranscript`).
function onFsEvent(fullPath) {
if (fullPath && mtimeCache.has(fullPath)) return;
if (fullPath && mtimeCache.has(fullPath)) return void pushTranscript(fullPath);
if (fullPath) pushTranscript(fullPath);
scheduleSweep();
}