/** * KHAELOR * File: src/tui/demo.ts * Description: Runnable mocked-session demo (`npx tsx src/tui/demo.ts`) — the Phase 2 acceptance artifact; every number * on screen comes from the scripted mock events, never fabricated by the UI. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { SessionEventBus } from "../session/index.js"; import { ulid } from "../shared/index.js"; import { TuiApp } from "./app.js"; import type { PermissionDecision } from "./app.js"; const AUTO = !process.stdin.isTTY || process.env["KHAELOR_DEMO_AUTO"] === "1"; const SPEED = process.env["KHAELOR_DEMO_FAST"] === "1" ? 0.2 : 1; const sessionId = ulid(); const bus = new SessionEventBus({ sessionId }); // ───────────────────────── script machinery ───────────────────────── class ScriptCancelled extends Error {} let cancelled = false; const timers = new Set>(); const sleepers = new Set<() => void>(); function sleep(ms: number): Promise { return new Promise((resolve) => { const done = (): void => { timers.delete(t); sleepers.delete(done); resolve(); }; const t = setTimeout(done, ms * SPEED); timers.add(t); sleepers.add(done); }); } function checkpoint(): void { if (cancelled) throw new ScriptCancelled(); } function cancelScript(): void { cancelled = true; for (const t of timers) clearTimeout(t); timers.clear(); // Wake pending sleeps so awaiting script steps reach their checkpoint and stop. for (const wake of [...sleepers]) wake(); } // ───────────────────────── mock session driver ───────────────────────── let requestSeq = 0; let toolSeq = 0; let busyTurn = false; const openToolUseIds = new Set(); let pendingPermission: { id: string; toolUseId: string } | null = null; function decidePermission(id: string, decision: PermissionDecision): void { if (pendingPermission === null || pendingPermission.id !== id) return; pendingPermission = null; if (decision === "deny") { bus.publishDurable({ type: "permission.denied", payload: { permissionRequestId: id, source: "user", feedback: "User denied the command." }, }); } else { bus.publishDurable({ type: "permission.granted", payload: { permissionRequestId: id, scope: decision === "allow-always" ? "always-project" : "once", }, }); } } const app = new TuiApp({ bus, model: "claude-sonnet-4-5", thinking: "adaptive", cwdLabel: "~/dev/khaelor", gitBranch: "main", contextWindow: 200_000, // Demo pricing for the mocked model — inputs to the mock, clearly not real billing. pricing: { inputPerMTok: 3, outputPerMTok: 15, cacheReadPerMTok: 0.3, cacheWritePerMTok: 3.75 }, mentionProvider: (query) => [ { id: "src/session/store.ts", label: "src/session/store.ts" }, { id: "src/session/bus.ts", label: "src/session/bus.ts" }, { id: "src/tui/app.ts", label: "src/tui/app.ts" }, { id: "tests/session/store.test.ts", label: "tests/session/store.test.ts" }, ].filter((i) => query === "" || i.label.includes(query)), actions: { submit(text, opts) { if (opts.shell) { // Shell execution arrives with the tool runtime (Phase 5); the mock // records the intent honestly instead of faking output. bus.publishDurable({ type: "user.steering-queued", payload: { text: `(shell) ${text}` }, }); return; } if (busyTurn) { bus.publishDurable({ type: "user.steering-queued", payload: { text } }); return; } bus.publishDurable({ type: "user.message-created", payload: { text, mentions: [] } }); void scriptedReply(); }, interrupt() { if (!busyTurn) return; cancelScript(); bus.publishDurable({ type: "user.interrupted", payload: { scope: "turn", pendingToolUseIds: [...openToolUseIds] }, }); for (const toolUseId of openToolUseIds) { bus.publishDurable({ type: "tool.cancelled", payload: { toolUseId, reason: "interrupted", modelText: "[Tool execution cancelled by user]", }, }); } openToolUseIds.clear(); busyTurn = false; }, permission: decidePermission, quit() { shutdown(0); }, }, }); function startRequest(purpose: "main" = "main"): string { requestSeq += 1; const requestId = `req-${requestSeq}`; bus.publishDurable({ type: "model.request-started", payload: { requestId, model: "claude-sonnet-4-5", purpose, contextStats: { estimatedInputTokens: 4200 + requestSeq * 900, sections: [] }, }, }); return requestId; } async function streamText( requestId: string, blockIndex: number, text: string, onProgress?: (fraction: number) => void, ): Promise { // ~30 deltas/s: one delta every 33 ms. const chunk = 7; for (let i = 0; i < text.length; i += chunk) { checkpoint(); bus.publishEphemeral({ type: "model.text-delta", payload: { requestId, blockIndex, text: text.slice(i, i + chunk) }, }); onProgress?.(i / text.length); await sleep(33); } checkpoint(); bus.publishDurable({ type: "model.text-block-completed", payload: { requestId, blockIndex, text }, }); } interface ToolRun { requestId: string; name: "read" | "grep" | "glob" | "edit" | "write" | "bash" | "process"; input: Record; durationMs: number; summary: string; kind: "read" | "search" | "edit" | "exec" | "process"; outputChunks?: string[]; extraUi?: { diffStats?: { added: number; removed: number }; exitCode?: number; matchCount?: number }; } async function runTool(run: ToolRun): Promise { toolSeq += 1; const toolUseId = `tool-${toolSeq}`; openToolUseIds.add(toolUseId); bus.publishEphemeral({ type: "model.tool-call-started", payload: { requestId: run.requestId, blockIndex: toolSeq + 10, toolUseId, toolName: run.name }, }); bus.publishDurable({ type: "tool.requested", payload: { requestId: run.requestId, blockIndex: toolSeq + 10, toolUseId, toolName: run.name, input: run.input, }, }); bus.publishDurable({ type: "tool.started", payload: { toolUseId, toolName: run.name } }); const chunks = run.outputChunks ?? []; const perChunk = chunks.length > 0 ? run.durationMs / chunks.length : run.durationMs; if (chunks.length > 0) { for (const chunk of chunks) { checkpoint(); await sleep(perChunk); bus.publishEphemeral({ type: "tool.output", payload: { toolUseId, chunk } }); } } else { await sleep(run.durationMs); } checkpoint(); openToolUseIds.delete(toolUseId); bus.publishDurable({ type: "tool.completed", payload: { toolUseId, modelText: `(mock result of ${run.name})`, durationMs: run.durationMs, ui: { kind: run.kind, summary: run.summary, ...(run.extraUi ?? {}) }, }, }); return toolUseId; } // ───────────────────────── the scripted session ───────────────────────── const RESPONSE_MARKDOWN = `I found the failure point in \`SessionStore.append\` — writes are not retried on transient \`EAGAIN\`. Here is the plan: ## Plan 1. Wrap the journal write in a bounded retry helper 2. Keep the **fsync** on the final attempt only 3. Add a regression test for the *torn-write* recovery path \`\`\`ts async function withRetry(fn: () => Promise, attempts = 3): Promise { let lastError: unknown; for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (error) { lastError = error; await delay(2 ** i * 10); // 10ms, 20ms, 40ms } } throw lastError; } \`\`\` | step | file | risk | | --- | --- | --- | | retry helper | src/shared/retry.ts | low | | journal wiring | src/session/store.ts | medium | | recovery test | tests/session/store.test.ts | low | Starting with the store change now.`; const FINAL_MARKDOWN = `# Done All checks passed. The store now retries transient write failures with exponential backoff, and the recovery path is covered by a regression test. \`\`\`bash # verify locally npm test -- tests/session/store.test.ts # 18 tests, 0.4s \`\`\` Next I would wire the same helper into the checkpoint writer — it shares the failure mode and the fix is mechanical, but it touches the compaction path so it deserves its own review.`; const EDIT_DIFF = `@@ -84,7 +84,9 @@ export class SessionStore { - private append(line: string): void { - this.fd.writeSync(line); + private async append(line: string): Promise { + await withRetry(() => this.fd.write(line)); + this.journalLength += 1; }`; async function mainScript(): Promise { await sleep(700); checkpoint(); busyTurn = true; bus.publishDurable({ type: "user.message-created", payload: { text: "add retry logic to the session store, then run the tests", mentions: [] }, }); const req1 = startRequest(); // Thinking (status line reads `● Thinking · Xs` from real deltas). for (let i = 0; i < 8; i++) { checkpoint(); bus.publishEphemeral({ type: "model.thinking-delta", payload: { requestId: req1, blockIndex: 0, text: "…" }, }); await sleep(150); } await runTool({ requestId: req1, name: "read", input: { file_path: "src/session/store.ts" }, durationMs: 420, summary: "Read src/session/store.ts · 212 lines", kind: "read", }); await runTool({ requestId: req1, name: "grep", input: { pattern: "retry" }, durationMs: 300, summary: 'Search "retry" · 6 matches', kind: "search", extraUi: { matchCount: 6 }, }); await streamText(req1, 1, RESPONSE_MARKDOWN); await sleep(300); // Permission round: panel appears; Enter/A/Esc decide; auto-allow in AUTO mode. const permissionId = `perm-${requestSeq}`; pendingPermission = { id: permissionId, toolUseId: `tool-${toolSeq + 1}` }; bus.publishDurable({ type: "permission.requested", payload: { permissionRequestId: permissionId, toolUseId: `tool-${toolSeq + 1}`, capability: "process.execute", descriptor: "npm install p-retry", suggestion: { capability: "process.execute", pattern: "npm install *" }, }, }); const waitStart = Date.now(); while (pendingPermission !== null) { checkpoint(); if (AUTO && Date.now() - waitStart > 2500 * SPEED) { decidePermission(permissionId, "allow-once"); break; } await sleep(50); } await sleep(150); checkpoint(); await runTool({ requestId: req1, name: "bash", input: { command: "npm install p-retry" }, durationMs: 1100, summary: "Run npm install p-retry · exit 0 · 1.1s", kind: "exec", extraUi: { exitCode: 0 }, outputChunks: ["added 1 package in 0.9s\n"], }); // The edit: one-liner + ✓ summary + expandable diff (`d`). const editToolId = await runTool({ requestId: req1, name: "edit", input: { file_path: "src/session/store.ts" }, durationMs: 380, summary: "Edit src/session/store.ts · +14 −3", kind: "edit", extraUi: { diffStats: { added: 14, removed: 3 } }, }); bus.publishDurable({ type: "file.modified", payload: { path: "src/session/store.ts", operation: "edit", diffStats: { added: 14, removed: 3 }, diff: EDIT_DIFF, toolUseId: editToolId, }, }); await sleep(300); // npm test with streaming output; Ctrl+T expansion is exercised in AUTO mode. const testRun = runTool({ requestId: req1, name: "bash", input: { command: "npm test" }, durationMs: 2200, summary: "Run npm test · passed · 2.2s", kind: "exec", extraUi: { exitCode: 0 }, outputChunks: [ "> vitest run\n", " ✓ tests/session/store.test.ts (18 tests)\n", " ✓ tests/session/bus.test.ts (12 tests)\n", " ✓ tests/session/projections.test.ts (21 tests)\n", "Test Files 3 passed (3)\n", " Tests 51 passed (51)\n", ], }); if (AUTO) { await sleep(700); app.pressKey({ type: "ctrl", ch: "t" }); // expand the live tool tail } await testRun; bus.publishDurable({ type: "model.response-completed", payload: { requestId: req1, stopReason: "tool_use", usage: { inputTokens: 18_450, outputTokens: 1_240, cacheReadTokens: 41_020, cacheWriteTokens: 3_800 }, durationMs: 9_400, }, }); await sleep(400); checkpoint(); // Final response — interrupted mid-stream by Esc (scripted in AUTO mode, // fired at a stream-progress point so it never races stream completion). const req2 = startRequest(); let escSent = false; await streamText(req2, 0, FINAL_MARKDOWN, (fraction) => { if (AUTO && !escSent && fraction >= 0.45) { escSent = true; app.pressKey({ type: "esc" }); } }); bus.publishDurable({ type: "model.response-completed", payload: { requestId: req2, stopReason: "end_turn", usage: { inputTokens: 21_300, outputTokens: 460, cacheReadTokens: 44_800, cacheWriteTokens: 0 }, durationMs: 3_100, }, }); busyTurn = false; } /** Post-script interactivity: a submitted message gets a small honest scripted reply. */ async function scriptedReply(): Promise { busyTurn = true; cancelled = false; const requestId = startRequest(); try { await sleep(400); await streamText( requestId, 0, "This is a **mocked** session — the reply is scripted. The real Anthropic integration arrives in Phase 3.", ); bus.publishDurable({ type: "model.response-completed", payload: { requestId, stopReason: "end_turn", usage: { inputTokens: 900, outputTokens: 40, cacheReadTokens: 0, cacheWriteTokens: 0 }, durationMs: 800, }, }); } catch (error) { if (!(error instanceof ScriptCancelled)) throw error; } busyTurn = false; } // ───────────────────────── lifecycle ───────────────────────── let exiting = false; function shutdown(code: number): void { if (exiting) return; exiting = true; cancelScript(); app.stop(); process.stdout.write("khaelor demo complete\n"); process.exit(code); } async function main(): Promise { await app.start(); try { await mainScript(); } catch (error) { if (!(error instanceof ScriptCancelled)) { app.stop(); throw error; } } if (AUTO) { // Headless / CI: linger briefly so the final frame settles, then exit. cancelled = false; await sleep(1200); shutdown(0); } // Interactive: stay alive — type, mention (@), open palettes (/, Ctrl+K), // expand the diff (d), inspect cost (/cost), quit with Ctrl+D or /quit. } void main();