spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/tui/demo.ts4 * Description: Runnable mocked-session demo (`npx tsx src/tui/demo.ts`) — the Phase 2 acceptance artifact; every number5 * on screen comes from the scripted mock events, never fabricated by the UI.6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 */1011import { SessionEventBus } from "../session/index.js";12import { ulid } from "../shared/index.js";13import { TuiApp } from "./app.js";14import type { PermissionDecision } from "./app.js";1516const AUTO = !process.stdin.isTTY || process.env["KHAELOR_DEMO_AUTO"] === "1";17const SPEED = process.env["KHAELOR_DEMO_FAST"] === "1" ? 0.2 : 1;1819const sessionId = ulid();20const bus = new SessionEventBus({ sessionId });2122// ───────────────────────── script machinery ─────────────────────────2324class ScriptCancelled extends Error {}2526let cancelled = false;27const timers = new Set<ReturnType<typeof setTimeout>>();28const sleepers = new Set<() => void>();2930function sleep(ms: number): Promise<void> {31 return new Promise((resolve) => {32 const done = (): void => {33 timers.delete(t);34 sleepers.delete(done);35 resolve();36 };37 const t = setTimeout(done, ms * SPEED);38 timers.add(t);39 sleepers.add(done);40 });41}4243function checkpoint(): void {44 if (cancelled) throw new ScriptCancelled();45}4647function cancelScript(): void {48 cancelled = true;49 for (const t of timers) clearTimeout(t);50 timers.clear();51 // Wake pending sleeps so awaiting script steps reach their checkpoint and stop.52 for (const wake of [...sleepers]) wake();53}5455// ───────────────────────── mock session driver ─────────────────────────5657let requestSeq = 0;58let toolSeq = 0;59let busyTurn = false;60const openToolUseIds = new Set<string>();61let pendingPermission: { id: string; toolUseId: string } | null = null;6263function decidePermission(id: string, decision: PermissionDecision): void {64 if (pendingPermission === null || pendingPermission.id !== id) return;65 pendingPermission = null;66 if (decision === "deny") {67 bus.publishDurable({68 type: "permission.denied",69 payload: { permissionRequestId: id, source: "user", feedback: "User denied the command." },70 });71 } else {72 bus.publishDurable({73 type: "permission.granted",74 payload: {75 permissionRequestId: id,76 scope: decision === "allow-always" ? "always-project" : "once",77 },78 });79 }80}8182const app = new TuiApp({83 bus,84 model: "claude-sonnet-4-5",85 thinking: "adaptive",86 cwdLabel: "~/dev/khaelor",87 gitBranch: "main",88 contextWindow: 200_000,89 // Demo pricing for the mocked model — inputs to the mock, clearly not real billing.90 pricing: { inputPerMTok: 3, outputPerMTok: 15, cacheReadPerMTok: 0.3, cacheWritePerMTok: 3.75 },91 mentionProvider: (query) =>92 [93 { id: "src/session/store.ts", label: "src/session/store.ts" },94 { id: "src/session/bus.ts", label: "src/session/bus.ts" },95 { id: "src/tui/app.ts", label: "src/tui/app.ts" },96 { id: "tests/session/store.test.ts", label: "tests/session/store.test.ts" },97 ].filter((i) => query === "" || i.label.includes(query)),98 actions: {99 submit(text, opts) {100 if (opts.shell) {101 // Shell execution arrives with the tool runtime (Phase 5); the mock102 // records the intent honestly instead of faking output.103 bus.publishDurable({104 type: "user.steering-queued",105 payload: { text: `(shell) ${text}` },106 });107 return;108 }109 if (busyTurn) {110 bus.publishDurable({ type: "user.steering-queued", payload: { text } });111 return;112 }113 bus.publishDurable({ type: "user.message-created", payload: { text, mentions: [] } });114 void scriptedReply();115 },116 interrupt() {117 if (!busyTurn) return;118 cancelScript();119 bus.publishDurable({120 type: "user.interrupted",121 payload: { scope: "turn", pendingToolUseIds: [...openToolUseIds] },122 });123 for (const toolUseId of openToolUseIds) {124 bus.publishDurable({125 type: "tool.cancelled",126 payload: {127 toolUseId,128 reason: "interrupted",129 modelText: "[Tool execution cancelled by user]",130 },131 });132 }133 openToolUseIds.clear();134 busyTurn = false;135 },136 permission: decidePermission,137 quit() {138 shutdown(0);139 },140 },141});142143function startRequest(purpose: "main" = "main"): string {144 requestSeq += 1;145 const requestId = `req-${requestSeq}`;146 bus.publishDurable({147 type: "model.request-started",148 payload: {149 requestId,150 model: "claude-sonnet-4-5",151 purpose,152 contextStats: { estimatedInputTokens: 4200 + requestSeq * 900, sections: [] },153 },154 });155 return requestId;156}157158async function streamText(159 requestId: string,160 blockIndex: number,161 text: string,162 onProgress?: (fraction: number) => void,163): Promise<void> {164 // ~30 deltas/s: one delta every 33 ms.165 const chunk = 7;166 for (let i = 0; i < text.length; i += chunk) {167 checkpoint();168 bus.publishEphemeral({169 type: "model.text-delta",170 payload: { requestId, blockIndex, text: text.slice(i, i + chunk) },171 });172 onProgress?.(i / text.length);173 await sleep(33);174 }175 checkpoint();176 bus.publishDurable({177 type: "model.text-block-completed",178 payload: { requestId, blockIndex, text },179 });180}181182interface ToolRun {183 requestId: string;184 name: "read" | "grep" | "glob" | "edit" | "write" | "bash" | "process";185 input: Record<string, unknown>;186 durationMs: number;187 summary: string;188 kind: "read" | "search" | "edit" | "exec" | "process";189 outputChunks?: string[];190 extraUi?: { diffStats?: { added: number; removed: number }; exitCode?: number; matchCount?: number };191}192193async function runTool(run: ToolRun): Promise<string> {194 toolSeq += 1;195 const toolUseId = `tool-${toolSeq}`;196 openToolUseIds.add(toolUseId);197 bus.publishEphemeral({198 type: "model.tool-call-started",199 payload: { requestId: run.requestId, blockIndex: toolSeq + 10, toolUseId, toolName: run.name },200 });201 bus.publishDurable({202 type: "tool.requested",203 payload: {204 requestId: run.requestId,205 blockIndex: toolSeq + 10,206 toolUseId,207 toolName: run.name,208 input: run.input,209 },210 });211 bus.publishDurable({ type: "tool.started", payload: { toolUseId, toolName: run.name } });212213 const chunks = run.outputChunks ?? [];214 const perChunk = chunks.length > 0 ? run.durationMs / chunks.length : run.durationMs;215 if (chunks.length > 0) {216 for (const chunk of chunks) {217 checkpoint();218 await sleep(perChunk);219 bus.publishEphemeral({ type: "tool.output", payload: { toolUseId, chunk } });220 }221 } else {222 await sleep(run.durationMs);223 }224 checkpoint();225226 openToolUseIds.delete(toolUseId);227 bus.publishDurable({228 type: "tool.completed",229 payload: {230 toolUseId,231 modelText: `(mock result of ${run.name})`,232 durationMs: run.durationMs,233 ui: { kind: run.kind, summary: run.summary, ...(run.extraUi ?? {}) },234 },235 });236 return toolUseId;237}238239// ───────────────────────── the scripted session ─────────────────────────240241const RESPONSE_MARKDOWN = `I found the failure point in \`SessionStore.append\` — writes are not retried on transient \`EAGAIN\`. Here is the plan:242243## Plan2442451. Wrap the journal write in a bounded retry helper2462. Keep the **fsync** on the final attempt only2473. Add a regression test for the *torn-write* recovery path248249\`\`\`ts250async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {251 let lastError: unknown;252 for (let i = 0; i < attempts; i++) {253 try {254 return await fn();255 } catch (error) {256 lastError = error;257 await delay(2 ** i * 10); // 10ms, 20ms, 40ms258 }259 }260 throw lastError;261}262\`\`\`263264| step | file | risk |265| --- | --- | --- |266| retry helper | src/shared/retry.ts | low |267| journal wiring | src/session/store.ts | medium |268| recovery test | tests/session/store.test.ts | low |269270Starting with the store change now.`;271272const FINAL_MARKDOWN = `# Done273274All checks passed. The store now retries transient write failures with exponential backoff, and the recovery path is covered by a regression test.275276\`\`\`bash277# verify locally278npm test -- tests/session/store.test.ts # 18 tests, 0.4s279\`\`\`280281Next 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.`;282283const EDIT_DIFF = `@@ -84,7 +84,9 @@ export class SessionStore {284- private append(line: string): void {285- this.fd.writeSync(line);286+ private async append(line: string): Promise<void> {287+ await withRetry(() => this.fd.write(line));288+ this.journalLength += 1;289 }`;290291async function mainScript(): Promise<void> {292 await sleep(700);293 checkpoint();294 busyTurn = true;295 bus.publishDurable({296 type: "user.message-created",297 payload: { text: "add retry logic to the session store, then run the tests", mentions: [] },298 });299 const req1 = startRequest();300301 // Thinking (status line reads `● Thinking · Xs` from real deltas).302 for (let i = 0; i < 8; i++) {303 checkpoint();304 bus.publishEphemeral({305 type: "model.thinking-delta",306 payload: { requestId: req1, blockIndex: 0, text: "…" },307 });308 await sleep(150);309 }310311 await runTool({312 requestId: req1,313 name: "read",314 input: { file_path: "src/session/store.ts" },315 durationMs: 420,316 summary: "Read src/session/store.ts · 212 lines",317 kind: "read",318 });319 await runTool({320 requestId: req1,321 name: "grep",322 input: { pattern: "retry" },323 durationMs: 300,324 summary: 'Search "retry" · 6 matches',325 kind: "search",326 extraUi: { matchCount: 6 },327 });328329 await streamText(req1, 1, RESPONSE_MARKDOWN);330 await sleep(300);331332 // Permission round: panel appears; Enter/A/Esc decide; auto-allow in AUTO mode.333 const permissionId = `perm-${requestSeq}`;334 pendingPermission = { id: permissionId, toolUseId: `tool-${toolSeq + 1}` };335 bus.publishDurable({336 type: "permission.requested",337 payload: {338 permissionRequestId: permissionId,339 toolUseId: `tool-${toolSeq + 1}`,340 capability: "process.execute",341 descriptor: "npm install p-retry",342 suggestion: { capability: "process.execute", pattern: "npm install *" },343 },344 });345 const waitStart = Date.now();346 while (pendingPermission !== null) {347 checkpoint();348 if (AUTO && Date.now() - waitStart > 2500 * SPEED) {349 decidePermission(permissionId, "allow-once");350 break;351 }352 await sleep(50);353 }354 await sleep(150);355 checkpoint();356357 await runTool({358 requestId: req1,359 name: "bash",360 input: { command: "npm install p-retry" },361 durationMs: 1100,362 summary: "Run npm install p-retry · exit 0 · 1.1s",363 kind: "exec",364 extraUi: { exitCode: 0 },365 outputChunks: ["added 1 package in 0.9s\n"],366 });367368 // The edit: one-liner + ✓ summary + expandable diff (`d`).369 const editToolId = await runTool({370 requestId: req1,371 name: "edit",372 input: { file_path: "src/session/store.ts" },373 durationMs: 380,374 summary: "Edit src/session/store.ts · +14 −3",375 kind: "edit",376 extraUi: { diffStats: { added: 14, removed: 3 } },377 });378 bus.publishDurable({379 type: "file.modified",380 payload: {381 path: "src/session/store.ts",382 operation: "edit",383 diffStats: { added: 14, removed: 3 },384 diff: EDIT_DIFF,385 toolUseId: editToolId,386 },387 });388 await sleep(300);389390 // npm test with streaming output; Ctrl+T expansion is exercised in AUTO mode.391 const testRun = runTool({392 requestId: req1,393 name: "bash",394 input: { command: "npm test" },395 durationMs: 2200,396 summary: "Run npm test · passed · 2.2s",397 kind: "exec",398 extraUi: { exitCode: 0 },399 outputChunks: [400 "> vitest run\n",401 " ✓ tests/session/store.test.ts (18 tests)\n",402 " ✓ tests/session/bus.test.ts (12 tests)\n",403 " ✓ tests/session/projections.test.ts (21 tests)\n",404 "Test Files 3 passed (3)\n",405 " Tests 51 passed (51)\n",406 ],407 });408 if (AUTO) {409 await sleep(700);410 app.pressKey({ type: "ctrl", ch: "t" }); // expand the live tool tail411 }412 await testRun;413414 bus.publishDurable({415 type: "model.response-completed",416 payload: {417 requestId: req1,418 stopReason: "tool_use",419 usage: { inputTokens: 18_450, outputTokens: 1_240, cacheReadTokens: 41_020, cacheWriteTokens: 3_800 },420 durationMs: 9_400,421 },422 });423 await sleep(400);424 checkpoint();425426 // Final response — interrupted mid-stream by Esc (scripted in AUTO mode,427 // fired at a stream-progress point so it never races stream completion).428 const req2 = startRequest();429 let escSent = false;430 await streamText(req2, 0, FINAL_MARKDOWN, (fraction) => {431 if (AUTO && !escSent && fraction >= 0.45) {432 escSent = true;433 app.pressKey({ type: "esc" });434 }435 });436 bus.publishDurable({437 type: "model.response-completed",438 payload: {439 requestId: req2,440 stopReason: "end_turn",441 usage: { inputTokens: 21_300, outputTokens: 460, cacheReadTokens: 44_800, cacheWriteTokens: 0 },442 durationMs: 3_100,443 },444 });445 busyTurn = false;446}447448/** Post-script interactivity: a submitted message gets a small honest scripted reply. */449async function scriptedReply(): Promise<void> {450 busyTurn = true;451 cancelled = false;452 const requestId = startRequest();453 try {454 await sleep(400);455 await streamText(456 requestId,457 0,458 "This is a **mocked** session — the reply is scripted. The real Anthropic integration arrives in Phase 3.",459 );460 bus.publishDurable({461 type: "model.response-completed",462 payload: {463 requestId,464 stopReason: "end_turn",465 usage: { inputTokens: 900, outputTokens: 40, cacheReadTokens: 0, cacheWriteTokens: 0 },466 durationMs: 800,467 },468 });469 } catch (error) {470 if (!(error instanceof ScriptCancelled)) throw error;471 }472 busyTurn = false;473}474475// ───────────────────────── lifecycle ─────────────────────────476477let exiting = false;478function shutdown(code: number): void {479 if (exiting) return;480 exiting = true;481 cancelScript();482 app.stop();483 process.stdout.write("khaelor demo complete\n");484 process.exit(code);485}486487async function main(): Promise<void> {488 await app.start();489 try {490 await mainScript();491 } catch (error) {492 if (!(error instanceof ScriptCancelled)) {493 app.stop();494 throw error;495 }496 }497 if (AUTO) {498 // Headless / CI: linger briefly so the final frame settles, then exit.499 cancelled = false;500 await sleep(1200);501 shutdown(0);502 }503 // Interactive: stay alive — type, mention (@), open palettes (/, Ctrl+K),504 // expand the diff (d), inspect cost (/cost), quit with Ctrl+D or /quit.505}506507void main();508