| import assert from 'node:assert/strict'; |
| import { before, describe, test } from 'node:test'; |
| import { visibleWidth } from '@earendil-works/pi-tui'; |
| import { _setColorLevelForTesting } from '../tui-ansi.js'; |
| import type { PipeShellOutput, PtyShellOutput, ShellRunToolResult } from '@maka/core'; |
| import type { SessionEvent, ToolResultContent } from '@maka/core'; |
| import type { StoredMessage } from '@maka/core'; |
| import { |
| appendUserPrompt, |
| applyShellRunViewUpdateToTranscript, |
| applyMakaSessionEventToTranscript, |
| applyShellRunUpdateToTranscript, |
| createMakaPiTranscriptState, |
| renderMakaPiActivityStrip, |
| renderMakaPiTranscript, |
| refreshRunningShellRunElapsed, |
| reconcileToolsWithStoredMessages, |
| replaceTranscriptWithStoredMessages, |
| submitCompactToTranscript, |
| toggleAllThinkingExpansion, |
| toggleAllToolExpansion, |
| } from '../pi-transcript.js'; |
| |
| // Pin the color level so ANSI-escape assertions are hermetic. Detection reads |
| // process.env.TERM/COLORTERM at module load, so ambient terminal capability |
| // (truecolor locally, unset/dumb on CI runners) would otherwise decide whether |
| // color escapes appear. Level 3 (truecolor) is the development default these |
| // tests lock (#1064/#1066); matches tui-ansi.test.ts's reset convention. |
| before(() => _setColorLevelForTesting(3)); |
| |
| describe('Maka Pi TUI transcript', () => { |
| test('formats long-running turn durations with readable units', () => { |
| const metadata = (turnElapsedMs: number) => ({ |
| title: 'Test session', |
| connectionSlug: 'test', |
| cwd: '/repo', |
| model: 'test-model', |
| permissionMode: 'ask', |
| turnElapsedMs, |
| }); |
| |
| assert.equal(stripAnsi(renderMakaPiActivityStrip(metadata(0), 80)), 'Working… 0s'); |
| assert.equal(stripAnsi(renderMakaPiActivityStrip(metadata(59_999), 80)), 'Working… 59s'); |
| assert.equal(stripAnsi(renderMakaPiActivityStrip(metadata(60_000), 80)), 'Working… 1m'); |
| assert.equal(stripAnsi(renderMakaPiActivityStrip(metadata(83_000), 80)), 'Working… 1m 23s'); |
| assert.equal( |
| stripAnsi(renderMakaPiActivityStrip(metadata(3_661_000), 80)), |
| 'Working… 1h 1m 1s', |
| ); |
| assert.equal( |
| stripAnsi(renderMakaPiActivityStrip(metadata(90_061_000), 80)), |
| 'Working… 1d 1h 1m 1s', |
| ); |
| }); |
| |
| test('keeps assistant text after a tool call visible after the tool block', () => { |
| const state = createMakaPiTranscriptState(); |
| appendUserPrompt(state, 'inspect the package'); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'text_delta', |
| messageId: 'message-1', |
| text: 'I will inspect it.', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Read', |
| args: { path: 'package.json' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: false, |
| content: { kind: 'text', text: '{ "name": "maka-agent" }' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'text_delta', |
| messageId: 'message-1', |
| text: 'The package is named maka-agent.', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'complete', |
| stopReason: 'end_turn', |
| }), |
| ); |
| |
| assert.deepEqual( |
| state.entries.map((entry) => entry.kind), |
| ['user', 'assistant', 'tool', 'assistant'], |
| ); |
| assert.equal( |
| state.entries[1]?.kind === 'assistant' ? state.entries[1].text : '', |
| 'I will inspect it.', |
| ); |
| assert.equal( |
| state.entries[3]?.kind === 'assistant' ? state.entries[3].text : '', |
| 'The package is named maka-agent.', |
| ); |
| }); |
| |
| test('reconciles durable tool details without resetting live turn state', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ type: 'tool_start', toolUseId: 'tool-1', toolName: 'Read', args: {} }), |
| ); |
| state.entries.push({ kind: 'notice', level: 'error', text: 'Turn failed: provider_error' }); |
| state.steering = ['Keep going']; |
| state.pendingFallback = [{ text: 'Try again', enqueue: 'steer' }]; |
| |
| assert.equal( |
| reconcileToolsWithStoredMessages(state, 'turn-1', [ |
| { |
| type: 'tool_call', |
| id: 'tool-1', |
| turnId: 'turn-1', |
| ts: 3, |
| toolName: 'Read', |
| args: { path: 'README.md' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'tool-result-1', |
| turnId: 'turn-1', |
| ts: 4, |
| toolUseId: 'tool-1', |
| isError: false, |
| content: { kind: 'text', text: 'README contents' }, |
| }, |
| ]), |
| true, |
| ); |
| |
| const tool = state.entries.find( |
| (entry): entry is Extract<(typeof state.entries)[number], { kind: 'tool' }> => |
| entry.kind === 'tool', |
| ); |
| assert.deepEqual(tool?.input, { path: 'README.md' }); |
| assert.deepEqual(tool?.result, { kind: 'text', text: 'README contents' }); |
| assert.deepEqual(state.steering, ['Keep going']); |
| assert.deepEqual(state.pendingFallback, [{ text: 'Try again', enqueue: 'steer' }]); |
| assert.equal(state.entries.at(-1)?.kind, 'notice'); |
| }); |
| |
| test('removes a live poll card that the durable transcript folds into its Bash parent', () => { |
| const state = createMakaPiTranscriptState(); |
| for (const tool of [ |
| { toolUseId: 'bash-1', toolName: 'Bash' }, |
| { toolUseId: 'poll-1', toolName: 'Read' }, |
| ]) { |
| applyMakaSessionEventToTranscript(state, event({ type: 'tool_start', ...tool, args: {} })); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: tool.toolUseId, |
| isError: false, |
| content: { kind: 'text', text: '' }, |
| }), |
| ); |
| } |
| const initialRun = shellRun({ ref: 'maka://runtime/session-1/run-1', revision: 1 }); |
| const polledRun = shellRun({ ref: 'maka://runtime/session-1/run-1', revision: 2 }); |
| |
| reconcileToolsWithStoredMessages(state, 'turn-1', [ |
| { |
| type: 'tool_call', |
| id: 'bash-1', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-1', |
| isError: false, |
| content: initialRun, |
| }, |
| { |
| type: 'tool_call', |
| id: 'poll-1', |
| turnId: 'turn-1', |
| ts: 3, |
| toolName: 'Read', |
| args: { ref: initialRun.ref }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'poll-result', |
| turnId: 'turn-1', |
| ts: 4, |
| toolUseId: 'poll-1', |
| isError: false, |
| content: polledRun, |
| }, |
| ]); |
| |
| const tools = state.entries.filter( |
| (entry): entry is Extract<(typeof state.entries)[number], { kind: 'tool' }> => |
| entry.kind === 'tool', |
| ); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-1'], |
| ); |
| assert.equal(tools[0]?.result?.kind === 'shell_run' ? tools[0].result.revision : undefined, 2); |
| }); |
| |
| test('renders steering messages with human-facing text and falls back to model-facing text', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'steering_message', |
| messageId: 'steering-display', |
| content: { |
| text: '<system-reminder>internal context</system-reminder>\nShow the result', |
| displayText: 'Show the result', |
| }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'steering_message', |
| messageId: 'steering-plain', |
| content: { text: 'Also include the tests' }, |
| }), |
| ); |
| |
| assert.deepEqual(state.entries, [ |
| { kind: 'user', text: 'Show the result' }, |
| { kind: 'user', text: 'Also include the tests' }, |
| ]); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /Show the result/); |
| assert.match(rendered, /Also include the tests/); |
| assert.doesNotMatch(rendered, /internal context/); |
| }); |
| |
| test('shows a fixed system notice when the configured step limit is reached', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript(state, event({ type: 'complete', stopReason: 'step_limit' })); |
| |
| assert.deepEqual(state.entries, [ |
| { |
| kind: 'notice', |
| level: 'info', |
| text: 'Reached the configured step limit. The task may be incomplete. Send “continue” to resume.', |
| }, |
| ]); |
| }); |
| |
| test('restores the step-limit system notice from stored history', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { type: 'system_note', id: 'notice-1', turnId: 'turn-1', ts: 1, kind: 'step_limit' }, |
| ]); |
| |
| assert.deepEqual(state.entries, [ |
| { |
| kind: 'notice', |
| level: 'info', |
| text: 'Reached the configured step limit. The task may be incomplete. Send “continue” to resume.', |
| }, |
| ]); |
| }); |
| |
| test('reports completed manual compact runs when there was nothing to compact', async () => { |
| const state = createMakaPiTranscriptState(); |
| const driver = new RecordingDriver([ |
| event({ type: 'token_usage', input: 0, output: 0 }), |
| event({ type: 'complete', stopReason: 'end_turn' }), |
| ]); |
| |
| await submitCompactToTranscript({ state, driver }); |
| |
| assert.equal(driver.compactCalls, 1); |
| assert.ok( |
| state.entries.some( |
| (entry) => entry.kind === 'notice' && entry.text === 'Nothing to compact.', |
| ), |
| ); |
| }); |
| |
| test('reports manual compact failed-open diagnostics instead of no-op success', async () => { |
| const state = createMakaPiTranscriptState(); |
| const driver = new RecordingDriver([ |
| event({ |
| type: 'token_usage', |
| input: 0, |
| output: 0, |
| contextBudget: { |
| enabled: true, |
| estimatedTokensBefore: 100, |
| estimatedTokensAfter: 100, |
| keptTurns: 2, |
| droppedTurns: 0, |
| keptEvents: 4, |
| droppedEvents: 0, |
| compactionDecisions: [ |
| { |
| stage: 'priorReplay', |
| sourceKind: 'runtimeEvents', |
| decision: 'failedOpen', |
| boundaryKind: 'historyCompact', |
| failOpenReason: 'output_length', |
| }, |
| ], |
| }, |
| }), |
| event({ type: 'complete', stopReason: 'end_turn' }), |
| ]); |
| |
| await submitCompactToTranscript({ state, driver }); |
| |
| assert.ok( |
| state.entries.some( |
| (entry) => |
| entry.kind === 'notice' && |
| entry.level === 'error' && |
| entry.text === 'Context compaction skipped: output_length.', |
| ), |
| ); |
| assert.equal( |
| state.entries.some( |
| (entry) => entry.kind === 'notice' && entry.text === 'Nothing to compact.', |
| ), |
| false, |
| ); |
| }); |
| |
| test('shows failed-open compact diagnostics before success diagnostics', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'token_usage', |
| input: 0, |
| output: 0, |
| contextBudget: { |
| enabled: true, |
| estimatedTokensBefore: 100, |
| estimatedTokensAfter: 40, |
| keptTurns: 1, |
| droppedTurns: 2, |
| keptEvents: 2, |
| droppedEvents: 4, |
| compactionDecisions: [ |
| { |
| stage: 'priorReplay', |
| sourceKind: 'runtimeEvents', |
| decision: 'replaced', |
| boundaryKind: 'historyCompact', |
| }, |
| { |
| stage: 'priorReplay', |
| sourceKind: 'runtimeEvents', |
| decision: 'failedOpen', |
| boundaryKind: 'historyCompact', |
| failOpenReason: 'write_failed', |
| }, |
| ], |
| }, |
| }), |
| ); |
| |
| assert.deepEqual( |
| state.entries |
| .filter((entry) => entry.kind === 'notice') |
| .map((entry) => ({ level: entry.level, text: entry.text })), |
| [{ level: 'error', text: 'Context compaction skipped: write_failed.' }], |
| ); |
| }); |
| |
| test('rebuilds transcript from stored session messages', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'user', |
| id: 'user-1', |
| turnId: 'turn-1', |
| ts: 1, |
| text: 'What did we decide?', |
| }, |
| { |
| type: 'assistant', |
| id: 'assistant-1', |
| turnId: 'turn-1', |
| ts: 2, |
| text: 'We decided to keep the TUI small.', |
| thinking: { text: 'recall the decision' }, |
| modelId: 'deepseek-v4-flash', |
| }, |
| { |
| type: 'tool_call', |
| id: 'tool-1', |
| turnId: 'turn-1', |
| ts: 3, |
| toolName: 'Read', |
| args: { path: 'README.md' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'tool-result-1', |
| turnId: 'turn-1', |
| ts: 4, |
| toolUseId: 'tool-1', |
| isError: false, |
| content: { kind: 'text', text: 'README contents' }, |
| }, |
| ] satisfies StoredMessage[]); |
| |
| // Stored thinking happened before the reply text, so it resumes above it. |
| assert.deepEqual( |
| state.entries.map((entry) => entry.kind), |
| ['user', 'thinking', 'assistant', 'tool'], |
| ); |
| assert.equal( |
| state.entries[0]?.kind === 'user' ? state.entries[0].text : '', |
| 'What did we decide?', |
| ); |
| assert.equal( |
| state.entries[1]?.kind === 'thinking' ? state.entries[1].text : '', |
| 'recall the decision', |
| ); |
| assert.equal( |
| state.entries[2]?.kind === 'assistant' ? state.entries[2].text : '', |
| 'We decided to keep the TUI small.', |
| ); |
| assert.equal( |
| state.entries[3]?.kind === 'tool' ? state.entries[3].output : '', |
| 'README contents', |
| ); |
| }); |
| |
| test('folds stored background-task polling into its parent Bash card on resume', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'bash-bg', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }, |
| { |
| type: 'tool_call', |
| id: 'read-bg', |
| turnId: 'turn-1', |
| ts: 3, |
| toolName: 'Read', |
| args: { ref }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'read-result', |
| turnId: 'turn-1', |
| ts: 4, |
| toolUseId: 'read-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'starting\ndone\n', |
| completedAt: 5_000, |
| updatedAt: 5_000, |
| exitCode: 0, |
| }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools.length, 1); |
| assert.equal(tools[0]?.toolUseId, 'bash-bg'); |
| assert.equal(tools[0]?.status, 'done'); |
| assert.equal( |
| tools[0]?.result?.kind === 'shell_run' && tools[0].result.output?.mode === 'pipes' |
| ? tools[0].result.output.stdout |
| : '', |
| 'starting\ndone\n', |
| ); |
| }); |
| |
| test('keeps a stored errored Read poll as a card without folding it into the parent Bash card', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'bash-bg', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| stdout: 'starting\n', |
| revision: 1, |
| updatedAt: 2_000, |
| }), |
| }, |
| { |
| type: 'tool_call', |
| id: 'read-bg', |
| turnId: 'turn-1', |
| ts: 3, |
| toolName: 'Read', |
| args: { ref }, |
| }, |
| // isError is the call-level authoritative status: even with a well-formed |
| // shell_run payload, a failed poll must survive replay as its own error |
| // card and must not mutate the parent. |
| { |
| type: 'tool_result', |
| id: 'read-result', |
| turnId: 'turn-1', |
| ts: 4, |
| toolUseId: 'read-bg', |
| isError: true, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| stdout: 'starting\nnewer\n', |
| revision: 2, |
| updatedAt: 5_000, |
| }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-bg', 'read-bg'], |
| ); |
| assert.equal(tools[1]?.status, 'error'); |
| // The parent keeps its own revision, output, and status — the failed poll |
| // changes nothing. |
| assert.equal(tools[0]?.status, 'running'); |
| assert.equal(tools[0]?.result?.kind === 'shell_run' ? tools[0].result.revision : undefined, 1); |
| assert.equal( |
| tools[0]?.result?.kind === 'shell_run' && tools[0].result.output?.mode === 'pipes' |
| ? tools[0].result.output.stdout |
| : '', |
| 'starting\n', |
| ); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /● Read/); |
| }); |
| |
| test('keeps a stored errored StopBackgroundTask poll as a card without folding it into the parent', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'bash-bg', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'sleep 30' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| stdout: 'starting\n', |
| revision: 1, |
| updatedAt: 2_000, |
| }), |
| }, |
| { |
| type: 'tool_call', |
| id: 'stop-bg', |
| turnId: 'turn-1', |
| ts: 3, |
| toolName: 'StopBackgroundTask', |
| args: { ref }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'stop-result', |
| turnId: 'turn-1', |
| ts: 4, |
| toolUseId: 'stop-bg', |
| isError: true, |
| content: shellRun({ |
| ref, |
| status: 'cancelled', |
| stdout: 'starting\n', |
| revision: 2, |
| completedAt: 5_000, |
| exitCode: 130, |
| }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-bg', 'stop-bg'], |
| ); |
| assert.equal(tools[1]?.status, 'error'); |
| assert.equal(tools[0]?.status, 'running'); |
| assert.equal(tools[0]?.result?.kind === 'shell_run' ? tools[0].result.revision : undefined, 1); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /● StopBackgroundTask/); |
| }); |
| |
| test('renders a PTY around the cursor when compact and as three head plus three tail rows when expanded', () => { |
| const state = createMakaPiTranscriptState(); |
| const screen = Array.from({ length: 8 }, (_, index) => `pty-row-${index + 1}`).join('\n'); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-pty', |
| toolName: 'Bash', |
| args: { command: 'interactive', pty: true }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-pty', |
| isError: false, |
| content: shellRun({ |
| mode: 'pty', |
| output: ptyOutput({ |
| screen, |
| cursor: { x: 0, y: 6, visible: true }, |
| }), |
| }), |
| }), |
| ); |
| |
| // Compact: a running PTY Bash shows only the disc row; the PTY screen |
| // lives in the expanded card. |
| const compact = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(compact, /● Bash \$ interactive \(running\)/); |
| assert.doesNotMatch(compact, /pty-row-1/); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| for (const row of [1, 2, 3, 6, 7, 8]) assert.match(expanded, new RegExp(`pty-row-${row}`)); |
| assert.doesNotMatch(expanded, /pty-row-[45]/); |
| }); |
| |
| test('Ctrl+O leaves tool cards above the live viewport untouched (#1097)', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-early', |
| toolName: 'Bash', |
| args: { command: 'early-build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-early', |
| isError: false, |
| content: terminalResult( |
| `early-head\n${Array.from({ length: 30 }, (_, i) => `early-row-${i}`).join('\n')}`, |
| ), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'text_delta', |
| messageId: 'message-1', |
| text: Array.from({ length: 20 }, (_, i) => `filler-${i}`).join('\n\n'), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-late', |
| toolName: 'Bash', |
| args: { command: 'late-build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-late', |
| isError: false, |
| content: terminalResult( |
| `late-head\n${Array.from({ length: 30 }, (_, i) => `late-row-${i}`).join('\n')}`, |
| ), |
| }), |
| ); |
| |
| const before = renderMakaPiTranscript(state, meta(), 100); |
| const early = state.entries.find( |
| (entry): entry is Extract<typeof entry, { kind: 'tool' }> => |
| entry.kind === 'tool' && entry.toolUseId === 'tool-early', |
| ); |
| const late = state.entries.find( |
| (entry): entry is Extract<typeof entry, { kind: 'tool' }> => |
| entry.kind === 'tool' && entry.toolUseId === 'tool-late', |
| ); |
| assert.ok(early && late); |
| // Scroll state as MakaPiLayoutComponent records it: the live viewport |
| // starts exactly where the late card begins, leaving the early card in |
| // scrollback above it. |
| const viewportTop = state.renderGeometry.entryFirstLine?.get(late); |
| assert.ok(viewportTop !== undefined && viewportTop > 0); |
| state.renderGeometry.viewportTop = viewportTop; |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| assert.equal(state.expandAllTools, true); |
| assert.equal(early.expanded, false); |
| assert.equal(late.expanded, true); |
| |
| const after = renderMakaPiTranscript(state, meta(), 100); |
| // Everything above the viewport is terminal scrollback pi-tui cannot |
| // rewrite without a scrollback-clearing full redraw; those lines must |
| // stay byte-identical. |
| assert.deepEqual(after.slice(0, viewportTop), before.slice(0, viewportTop)); |
| const afterText = after.map(stripAnsi).join('\n'); |
| assert.match(afterText, /late-head/); |
| assert.doesNotMatch(afterText, /early-head/); |
| }); |
| |
| test('Ctrl+O with a head-scrolled expanded card flips the default back and leaves a notice (#1134)', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-big', |
| toolName: 'Bash', |
| args: { command: 'big-diff' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-big', |
| isError: false, |
| content: terminalResult( |
| `big-head\n${Array.from({ length: 80 }, (_, i) => `big-row-${i}`).join('\n')}`, |
| ), |
| }), |
| ); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| assert.equal(state.expandAllTools, true); |
| const entry = state.entries.find( |
| (candidate): candidate is Extract<typeof candidate, { kind: 'tool' }> => |
| candidate.kind === 'tool', |
| ); |
| assert.ok(entry); |
| assert.equal(entry.expanded, true); |
| |
| // Expanding grew the document past the terminal: the card's head is now |
| // terminal scrollback and only its tail is inside the live viewport. |
| const before = renderMakaPiTranscript(state, meta(), 100); |
| const firstLine = state.renderGeometry.entryFirstLine?.get(entry); |
| assert.ok(firstLine !== undefined); |
| const viewportTop = firstLine + 5; |
| assert.ok(viewportTop < before.length); |
| state.renderGeometry.viewportTop = viewportTop; |
| |
| // The second Ctrl+O cannot collapse the card (its head is in scrollback), |
| // but it must still flip the default back and say why nothing moved. |
| assert.equal(toggleAllToolExpansion(state), true); |
| assert.equal(state.expandAllTools, false); |
| assert.equal(entry.expanded, true); |
| |
| const notice = state.entries[state.entries.length - 1]; |
| assert.equal(notice.kind, 'notice'); |
| assert.equal(notice.kind === 'notice' && notice.level, 'info'); |
| assert.match(notice.kind === 'notice' ? notice.text : '', /starts collapsed/); |
| |
| const after = renderMakaPiTranscript(state, meta(), 100); |
| assert.deepEqual(after.slice(0, viewportTop), before.slice(0, viewportTop)); |
| assert.match(after.map(stripAnsi).join('\n'), /Note: /); |
| |
| // A third Ctrl+O keeps flipping the default and keeps saying so. |
| assert.equal(toggleAllToolExpansion(state), true); |
| assert.equal(state.expandAllTools, true); |
| const third = state.entries[state.entries.length - 1]; |
| assert.match(third.kind === 'notice' ? third.text : '', /starts expanded/); |
| }); |
| |
| test('Ctrl+O reports nothing to toggle when the session has no tool card at all', () => { |
| const state = createMakaPiTranscriptState(); |
| assert.equal(toggleAllToolExpansion(state), false); |
| assert.equal(state.expandAllTools, false); |
| assert.equal( |
| state.entries.some((entry) => entry.kind === 'notice'), |
| false, |
| ); |
| }); |
| |
| test('tool cards born after an expand-all start expanded', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: 'first' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: false, |
| content: terminalResult('ok\n'), |
| }), |
| ); |
| assert.equal(toggleAllToolExpansion(state), true); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-2', |
| toolName: 'Bash', |
| args: { command: 'second' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-2', |
| isError: false, |
| content: terminalResult( |
| `second-head\n${Array.from({ length: 10 }, (_, i) => `second-row-${i}`).join('\n')}`, |
| ), |
| }), |
| ); |
| |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /second-head/); |
| }); |
| |
| test('Ctrl+T leaves thinking entries above the live viewport untouched (#1097)', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-1', |
| text: 'early-secret-reasoning', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'text_delta', |
| messageId: 'message-1', |
| text: Array.from({ length: 20 }, (_, i) => `filler-${i}`).join('\n\n'), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-2', |
| text: 'late-visible-reasoning', |
| }), |
| ); |
| |
| const before = renderMakaPiTranscript(state, meta(), 100); |
| const late = state.entries.find( |
| (entry): entry is Extract<typeof entry, { kind: 'thinking' }> => |
| entry.kind === 'thinking' && entry.messageId === 'message-2', |
| ); |
| assert.ok(late); |
| const viewportTop = state.renderGeometry.entryFirstLine?.get(late); |
| assert.ok(viewportTop !== undefined && viewportTop > 0); |
| state.renderGeometry.viewportTop = viewportTop; |
| |
| assert.equal(toggleAllThinkingExpansion(state), true); |
| |
| const after = renderMakaPiTranscript(state, meta(), 100); |
| assert.deepEqual(after.slice(0, viewportTop), before.slice(0, viewportTop)); |
| const afterText = after.map(stripAnsi).join('\n'); |
| assert.match(afterText, /late-visible-reasoning/); |
| assert.doesNotMatch(afterText, /early-secret-reasoning/); |
| }); |
| |
| test('Ctrl+T with only head-scrolled thinking flips the default back and leaves a notice (#1134)', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-1', |
| text: Array.from({ length: 80 }, (_, i) => `reasoning-row-${i}`).join('\n'), |
| }), |
| ); |
| |
| assert.equal(toggleAllThinkingExpansion(state), true); |
| assert.equal(state.expandAllThinking, true); |
| |
| const before = renderMakaPiTranscript(state, meta(), 100); |
| const entry = state.entries.find( |
| (candidate): candidate is Extract<typeof candidate, { kind: 'thinking' }> => |
| candidate.kind === 'thinking', |
| ); |
| assert.ok(entry); |
| const firstLine = state.renderGeometry.entryFirstLine?.get(entry); |
| assert.ok(firstLine !== undefined); |
| const viewportTop = firstLine + 10; |
| state.renderGeometry.viewportTop = viewportTop; |
| |
| assert.equal(toggleAllThinkingExpansion(state), true); |
| assert.equal(state.expandAllThinking, false); |
| assert.equal(entry.expanded, true); |
| |
| const notice = state.entries[state.entries.length - 1]; |
| assert.equal(notice.kind, 'notice'); |
| assert.match(notice.kind === 'notice' ? notice.text : '', /starts collapsed/); |
| |
| const after = renderMakaPiTranscript(state, meta(), 100); |
| assert.deepEqual(after.slice(0, viewportTop), before.slice(0, viewportTop)); |
| }); |
| |
| test('replays WriteStdin as a human-readable operation row while merging its PTY revision into Bash', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/pty-1'; |
| const rawInput = 'echo hello\r'; |
| const updatedOutput = ptyOutput({ screen: 'READY\nUNIQUE-PTY-FRAME' }); |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'bash-pty', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'interactive', pty: true }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-pty', |
| isError: false, |
| content: shellRun({ |
| ref, |
| mode: 'pty', |
| revision: 1, |
| output: ptyOutput({ screen: 'READY' }), |
| }), |
| }, |
| { |
| type: 'tool_call', |
| id: 'write-pty', |
| turnId: 'turn-2', |
| ts: 3, |
| toolName: 'WriteStdin', |
| args: { ref, input: rawInput, size: { cols: 100, rows: 30 } }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'write-result', |
| turnId: 'turn-2', |
| ts: 4, |
| toolUseId: 'write-pty', |
| isError: false, |
| content: shellRun({ |
| ref, |
| mode: 'pty', |
| revision: 2, |
| updatedAt: 2_000, |
| output: updatedOutput, |
| operation: { |
| kind: 'pty_control', |
| failed: false, |
| input: { bytes: Buffer.byteLength(rawInput, 'utf8'), queued: true }, |
| resize: { cols: 100, rows: 30, applied: true, changed: true }, |
| }, |
| }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools.length, 2); |
| assert.equal(tools[0]?.result?.kind === 'shell_run' ? tools[0].result.revision : undefined, 2); |
| assert.equal( |
| tools[0]?.result?.kind === 'shell_run' ? tools[0].result.operation : undefined, |
| undefined, |
| ); |
| assert.deepEqual(tools[1]?.kind === 'tool' ? tools[1].input : undefined, { |
| ref, |
| inputPreview: { |
| text: 'echo hello\\r', |
| bytes: Buffer.byteLength(rawInput, 'utf8'), |
| truncated: false, |
| }, |
| size: { cols: 100, rows: 30 }, |
| }); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /Entered: echo hello\\r/); |
| assert.match(rendered, /Resized to 100x30/); |
| assert.equal(rendered.split('UNIQUE-PTY-FRAME').length - 1, 1); |
| }); |
| |
| test('projects raw live WriteStdin args at the TUI transcript boundary', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/pty-live'; |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'write-live', |
| toolName: 'WriteStdin', |
| args: { ref, input: 'echo live\r' }, |
| }), |
| ); |
| |
| const entry = state.entries.find( |
| (candidate): candidate is Extract<typeof candidate, { kind: 'tool' }> => |
| candidate.kind === 'tool', |
| ); |
| assert.deepEqual(entry?.input, { |
| ref, |
| inputPreview: { text: 'echo live\\r', bytes: 10, truncated: false }, |
| }); |
| }); |
| |
| test('restores the total elapsed time of a settled background Bash card', () => { |
| const state = createMakaPiTranscriptState(); |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'bash-bg', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| status: 'completed', |
| startedAt: 1_000, |
| updatedAt: 6_000, |
| completedAt: 6_000, |
| exitCode: 0, |
| }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /● Bash \$ npm test \(5s\)/); |
| }); |
| |
| test('rebuilds automatic context compaction notes from stored session messages', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'system_note', |
| id: 'note-1', |
| turnId: 'turn-1', |
| ts: 1, |
| kind: 'context_compacted', |
| }, |
| ] satisfies StoredMessage[]); |
| |
| assert.deepEqual( |
| state.entries.filter((entry) => entry.kind === 'notice'), |
| [ |
| { |
| kind: 'notice', |
| level: 'info', |
| text: 'Context compacted to keep this session within the model window.', |
| }, |
| ], |
| ); |
| }); |
| |
| test('rebuilds fail-open notes without claiming history was trimmed', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'system_note', |
| id: 'note-failed-open', |
| turnId: 'turn-1', |
| ts: 1, |
| kind: 'context_compaction_failed_open', |
| }, |
| ] satisfies StoredMessage[]); |
| |
| assert.deepEqual( |
| state.entries.filter((entry) => entry.kind === 'notice'), |
| [ |
| { |
| kind: 'notice', |
| level: 'info', |
| text: 'Context summary failed; the session continued without a new summary.', |
| }, |
| ], |
| ); |
| }); |
| |
| test('renders every transcript line within the terminal width', () => { |
| const state = createMakaPiTranscriptState(); |
| appendUserPrompt(state, 'please inspect a very long path under packages/runtime/src'); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'text_delta', |
| messageId: 'message-1', |
| text: 'I will inspect `packages/runtime/src/very-long-file-name.ts` now.', |
| }), |
| ); |
| |
| const lines = renderMakaPiTranscript( |
| state, |
| { |
| title: 'Maka', |
| cwd: '/Users/yuhan/workspace/oss/maka-agent/.worktree/maka-cli-tui', |
| model: 'deepseek-v4-flash', |
| connectionSlug: 'deepseek', |
| permissionMode: 'bypass', |
| busy: true, |
| }, |
| 12, |
| ); |
| |
| assert.ok(lines.every((line) => visibleWidth(line) <= 12)); |
| }); |
| |
| test('surfaces context compaction diagnostics as transcript notes', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'token_usage', |
| input: 1200, |
| output: 100, |
| contextBudget: { |
| enabled: true, |
| policyName: 'cli-default-history-budget', |
| maxHistoryEstimatedTokens: 32000, |
| estimatedTokensBefore: 42000, |
| estimatedTokensAfter: 18000, |
| keptTurns: 3, |
| droppedTurns: 5, |
| keptEvents: 7, |
| droppedEvents: 20, |
| highWaterReason: 'history_compact', |
| compactionDecisions: [ |
| { |
| stage: 'priorReplay', |
| sourceKind: 'runtimeEvents', |
| decision: 'replaced', |
| boundaryKind: 'historyCompact', |
| coveredTurns: 5, |
| coveredRuntimeEvents: 20, |
| estimatedTokensSaved: 24000, |
| }, |
| ], |
| }, |
| }), |
| ); |
| |
| const visibleLines = renderMakaPiTranscript( |
| state, |
| { |
| title: 'Maka', |
| cwd: '/tmp/project', |
| model: 'deepseek-v4-flash', |
| connectionSlug: 'deepseek', |
| permissionMode: 'bypass', |
| }, |
| 120, |
| ).map(stripAnsi); |
| |
| assert.ok(visibleLines.some((line) => line.includes('Context compacted'))); |
| assert.ok(visibleLines.some((line) => line.includes('historyCompact'))); |
| assert.ok(visibleLines.some((line) => line.includes('saved ~24000 tokens'))); |
| }); |
| |
| test('ignores legacy generic permission requests', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'permission_request', |
| requestId: 'permission-1', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| category: 'shell_unsafe', |
| reason: 'shell_dangerous', |
| args: { command: 'npm test' }, |
| hint: 'Run tests before editing.', |
| }), |
| ); |
| |
| assert.equal(state.pendingInteraction, undefined); |
| assert.deepEqual(state.queuedInteractions, []); |
| }); |
| |
| test('renders an unboxed session sandbox boundary request with exact scopes', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'sandbox_boundary_request', |
| requestId: 'boundary-1', |
| toolUseId: 'tool-boundary', |
| justification: 'Read the user-selected file.', |
| expansion: { |
| filesystem: { |
| entries: [{ path: '/outside/file.txt', access: 'read', scope: 'exact' }], |
| }, |
| network: { enabled: true }, |
| }, |
| }), |
| ); |
| |
| const visibleLines = renderMakaPiTranscript( |
| state, |
| { |
| title: 'Maka', |
| cwd: '/tmp/project', |
| model: 'test', |
| connectionSlug: 'test', |
| permissionMode: 'auto', |
| }, |
| 100, |
| ).map(stripAnsi); |
| |
| assert.equal(state.pendingInteraction?.requestId, 'boundary-1'); |
| assert.ok(visibleLines.some((line) => line.includes('Allow access outside the workspace?'))); |
| assert.ok(visibleLines.some((line) => line.includes('Read the user-selected file.'))); |
| assert.ok(visibleLines.some((line) => line.includes('read exact /outside/file.txt'))); |
| assert.ok(visibleLines.some((line) => line.includes('network enabled'))); |
| assert.ok(visibleLines.some((line) => line.includes('y/Enter allow for session'))); |
| assert.ok(visibleLines.some((line) => line.includes('n/Esc deny'))); |
| assert.ok(visibleLines.every((line) => !line.includes(' a '))); |
| }); |
| |
| test('queues sandbox boundary and user-question requests in arrival order', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'sandbox_boundary_request', |
| requestId: 'boundary-1', |
| toolUseId: 'tool-1', |
| justification: 'Read a selected file.', |
| expansion: { |
| filesystem: { |
| entries: [{ path: '/outside/file.txt', access: 'read', scope: 'exact' }], |
| }, |
| }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'user_question_request', |
| requestId: 'question-1', |
| toolUseId: 'tool-2', |
| questions: [{ question: 'Choose', options: [{ label: 'A' }, { label: 'B' }] }], |
| }), |
| ); |
| |
| assert.equal(state.pendingInteraction?.requestId, 'boundary-1'); |
| assert.deepEqual( |
| state.queuedInteractions.map((item) => item.requestId), |
| ['question-1'], |
| ); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'sandbox_boundary_decision_ack', |
| requestId: 'boundary-1', |
| toolUseId: 'tool-1', |
| decision: 'allow', |
| status: 'applied', |
| revision: 1, |
| }), |
| ); |
| assert.equal(state.pendingInteraction?.requestId, 'question-1'); |
| assert.deepEqual(state.queuedInteractions, []); |
| }); |
| |
| test('deduplicates sandbox boundary interactions by request id', () => { |
| const state = createMakaPiTranscriptState(); |
| const first = event({ |
| type: 'sandbox_boundary_request', |
| requestId: 'boundary-1', |
| toolUseId: 'tool-1', |
| justification: 'Read first.', |
| expansion: { |
| filesystem: { entries: [{ path: '/first', access: 'read', scope: 'exact' }] }, |
| }, |
| }); |
| const question = event({ |
| type: 'user_question_request', |
| requestId: 'question-1', |
| toolUseId: 'question-tool', |
| questions: [{ question: 'Choose', options: [{ label: 'A' }, { label: 'B' }] }], |
| }); |
| const second = event({ |
| type: 'sandbox_boundary_request', |
| requestId: 'boundary-2', |
| toolUseId: 'tool-2', |
| justification: 'Read second.', |
| expansion: { |
| filesystem: { entries: [{ path: '/second', access: 'read', scope: 'exact' }] }, |
| }, |
| }); |
| const third = event({ |
| type: 'sandbox_boundary_request', |
| requestId: 'boundary-3', |
| toolUseId: 'tool-3', |
| justification: 'Read third.', |
| expansion: { |
| filesystem: { entries: [{ path: '/third', access: 'read', scope: 'exact' }] }, |
| }, |
| }); |
| |
| applyMakaSessionEventToTranscript(state, first); |
| applyMakaSessionEventToTranscript(state, question); |
| applyMakaSessionEventToTranscript(state, second); |
| applyMakaSessionEventToTranscript(state, third); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| ...first, |
| id: 'boundary-request-replay', |
| justification: 'Replayed first.', |
| }), |
| ); |
| |
| assert.equal(state.pendingInteraction?.requestId, 'boundary-1'); |
| assert.equal( |
| state.pendingInteraction?.type === 'sandbox_boundary_request' |
| ? state.pendingInteraction.justification |
| : undefined, |
| 'Read first.', |
| ); |
| assert.deepEqual( |
| state.queuedInteractions.map((item) => item.requestId), |
| ['question-1', 'boundary-2', 'boundary-3'], |
| ); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'sandbox_boundary_decision_ack', |
| requestId: 'boundary-3', |
| toolUseId: 'tool-3', |
| decision: 'deny', |
| status: 'denied', |
| revision: 0, |
| }), |
| ); |
| assert.deepEqual( |
| state.queuedInteractions.map((item) => item.requestId), |
| ['question-1', 'boundary-2'], |
| ); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'sandbox_boundary_decision_ack', |
| requestId: 'boundary-2', |
| toolUseId: 'tool-2', |
| decision: 'deny', |
| status: 'denied', |
| revision: 0, |
| }), |
| ); |
| assert.deepEqual( |
| state.queuedInteractions.map((item) => item.requestId), |
| ['question-1'], |
| ); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'sandbox_boundary_decision_ack', |
| requestId: 'boundary-1', |
| toolUseId: 'tool-1', |
| decision: 'deny', |
| status: 'denied', |
| revision: 0, |
| }), |
| ); |
| assert.equal(state.pendingInteraction?.requestId, 'question-1'); |
| assert.deepEqual(state.queuedInteractions, []); |
| }); |
| |
| test('orders thinking entries by arrival, before text and around tools', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-1', |
| text: 'plan ', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-1', |
| text: 'first', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Read', |
| args: { path: 'a.ts' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: false, |
| content: { kind: 'text', text: 'ok' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'text_delta', |
| messageId: 'message-1', |
| text: 'the answer', |
| }), |
| ); |
| |
| // Entries mirror event order: thinking, then the tool, then the reply. |
| assert.deepEqual( |
| state.entries.map((entry) => entry.kind), |
| ['thinking', 'tool', 'assistant'], |
| ); |
| assert.equal(state.entries[0]?.kind === 'thinking' ? state.entries[0].text : '', 'plan first'); |
| |
| const collapsed = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi); |
| const markerIndex = collapsed.findIndex((line) => line.includes('Thinking…')); |
| const toolIndex = collapsed.findIndex((line) => line.includes('● Read')); |
| const answerIndex = collapsed.findIndex((line) => line.includes('the answer')); |
| assert.ok(markerIndex >= 0); |
| assert.ok(markerIndex < toolIndex); |
| assert.ok(toolIndex < answerIndex); |
| assert.equal( |
| collapsed.some((line) => line.includes('plan first')), |
| false, |
| ); |
| |
| assert.equal(toggleAllThinkingExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi); |
| const bodyIndex = expanded.findIndex((line) => line.includes('plan first')); |
| assert.ok(bodyIndex >= 0); |
| assert.ok(bodyIndex < expanded.findIndex((line) => line.includes('the answer'))); |
| }); |
| |
| test('separates the thinking marker from tool rows with blank lines', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: 'ls' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: false, |
| content: { kind: 'text', text: 'ok' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-1', |
| text: 'plan the next step', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-2', |
| toolName: 'Bash', |
| args: { command: 'pwd' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-2', |
| isError: false, |
| content: { kind: 'text', text: 'ok' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-3', |
| toolName: 'Bash', |
| args: { command: 'whoami' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-3', |
| isError: false, |
| content: { kind: 'text', text: 'ok' }, |
| }), |
| ); |
| |
| const lines = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi); |
| const markerIndex = lines.findIndex((line) => line.includes('Thinking…')); |
| assert.ok(markerIndex > 0); |
| |
| // Thinking reads as model output: a blank line sets it apart from the |
| // tool rows on both sides. |
| assert.equal(lines[markerIndex - 1], ''); |
| assert.equal(lines[markerIndex + 1], ''); |
| |
| // Consecutive tool rows stay compact — no blank line between them. |
| const toolIndices = lines |
| .map((line, index) => (line.includes('● Bash') ? index : -1)) |
| .filter((index) => index >= 0); |
| assert.deepEqual(toolIndices, [markerIndex - 2, markerIndex + 2, markerIndex + 3]); |
| }); |
| |
| test('replaces the streamed thinking entry when thinking_complete arrives after the reply', () => { |
| const state = createMakaPiTranscriptState(); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-1', |
| text: 'partial thought', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'text_delta', |
| messageId: 'message-1', |
| text: 'the reply', |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_complete', |
| messageId: 'message-1', |
| text: 'the complete thought', |
| }), |
| ); |
| |
| // No duplicate thinking entry; the streamed one is replaced in place. |
| assert.deepEqual( |
| state.entries.map((entry) => entry.kind), |
| ['thinking', 'assistant'], |
| ); |
| assert.equal( |
| state.entries[0]?.kind === 'thinking' ? state.entries[0].text : '', |
| 'the complete thought', |
| ); |
| }); |
| |
| test('keeps tool cards compact until the latest tool is expanded', () => { |
| const state = createMakaPiTranscriptState(); |
| // `head-line` is first; the compact one-line summary shows only the output |
| // size, never output content, and expanding reveals the full stdout. |
| const stdout = `head-line\n${Array.from({ length: 30 }, (_, i) => `row-${i}`).join('\n')}`; |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: false, |
| content: terminalResult(stdout), |
| }), |
| ); |
| |
| const compactLines = renderMakaPiTranscript(state, meta(), 120).map(stripAnsi); |
| const compact = compactLines.join('\n'); |
| |
| // Compact cards are a single line (plus the leading blank separator). |
| assert.equal(compactLines.length, 2); |
| assert.match(compact, /● Bash \$ npm test/); |
| assert.match(compact, /\(31 lines\)/); |
| assert.doesNotMatch(compact, /head-line/); |
| assert.doesNotMatch(compact, /row-29/); |
| // The annotation's shapes carry the affordance; no expand marker remains. |
| assert.doesNotMatch(compact, /›/); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 120).map(stripAnsi).join('\n'); |
| |
| assert.match(expanded, /head-line/); |
| assert.match(expanded, /row-29/); |
| }); |
| |
| test('summarizes a failing Bash tool with a red exit code, not the error text', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: true, |
| content: terminalResult('some earlier output', 'first error\nfinal error line\n', { |
| status: 'failed', |
| exitCode: 1, |
| }), |
| }), |
| ); |
| |
| const lines = renderMakaPiTranscript(state, meta(), 120); |
| assert.equal(lines.length, 2); |
| const compact = lines.map(stripAnsi).join('\n'); |
| assert.match(compact, /\(exit 1\)/); |
| // The error text itself lives only in the expanded card. |
| assert.doesNotMatch(compact, /final error line/); |
| // The exit code is red. |
| assert.match(lines.join('\n'), /\x1b\[31mexit 1\x1b\[39m/); |
| }); |
| |
| test('shows the first real command line when a Bash command leads with comments', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: '# look for the setting\ngrep -n "setting" src/config.ts' }, |
| }), |
| ); |
| |
| const compact = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(compact, /\$ grep -n "setting" src\/config\.ts/); |
| assert.doesNotMatch(compact, /look for the setting/); |
| }); |
| |
| test('summarizes a silent successful command as (no output)', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: 'true' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: false, |
| content: terminalResult('', '', { cmd: 'true' }), |
| }), |
| ); |
| |
| const lines = renderMakaPiTranscript(state, meta(), 120).map(stripAnsi); |
| assert.equal(lines.length, 2); |
| assert.match(lines.join('\n'), /\(no output\)/); |
| assert.doesNotMatch(lines.join('\n'), /\(Ctrl\+O\)/); |
| }); |
| |
| test('shows the latest live output line while a tool is running', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: 'npm run build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'tool-1', |
| seq: 1, |
| stream: 'stdout', |
| chunk: 'step one\n', |
| redacted: false, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'tool-1', |
| seq: 2, |
| stream: 'stdout', |
| chunk: 'step two\n', |
| redacted: false, |
| }), |
| ); |
| |
| const lines = renderMakaPiTranscript(state, meta(), 120).map(stripAnsi); |
| assert.equal(lines.length, 2); |
| const compact = lines.join('\n'); |
| assert.match(compact, /● Bash \$ npm run build \(running\)/); |
| assert.doesNotMatch(compact, /step one/); |
| assert.doesNotMatch(compact, /step two/); |
| |
| // Live output lives in the expanded card for a running tool. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 120).map(stripAnsi).join('\n'); |
| assert.match(expanded, /step two/); |
| }); |
| |
| test('replaces live Bash output with the authoritative terminal snapshot', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'tool-1', |
| toolName: 'Bash', |
| args: { command: 'printf "step one\\nstep two\\n"' }, |
| }), |
| ); |
| for (const [seq, chunk] of [ |
| [1, 'step one\n'], |
| [2, 'step two\n'], |
| ] as const) { |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'tool-1', |
| seq, |
| stream: 'stdout', |
| chunk, |
| redacted: false, |
| }), |
| ); |
| } |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'tool-1', |
| isError: false, |
| content: shellRun({ |
| status: 'completed', |
| stdout: 'step one\nstep two\n', |
| completedAt: 2_000, |
| exitCode: 0, |
| }), |
| }), |
| ); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 120).map(stripAnsi).join('\n'); |
| const outputLines = expanded.split('\n').map((line) => line.trim()); |
| assert.equal(outputLines.filter((line) => line === 'step one').length, 1); |
| assert.equal(outputLines.filter((line) => line === 'step two').length, 1); |
| }); |
| |
| test('folds concurrent child lifecycles into their parent agent cards', () => { |
| const state = createMakaPiTranscriptState(); |
| for (const [toolUseId, profile] of [ |
| ['agent-a', 'local_read'], |
| ['agent-b', 'web_research'], |
| ['agent-c', 'local_read'], |
| ] as const) { |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId, |
| toolName: 'agent_spawn', |
| args: { profile, task: `Run ${profile}` }, |
| }), |
| ); |
| } |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'agent-a', |
| seq: 1, |
| stream: 'stdout', |
| chunk: 'Child tool started: Read\n', |
| redacted: false, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'agent-b', |
| seq: 1, |
| stream: 'stdout', |
| chunk: 'Child tool started: WebSearch\n', |
| redacted: false, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'agent-a', |
| isError: false, |
| content: subagentResult({ |
| agentName: 'Local Read', |
| turnId: 'child-a', |
| summary: 'local result', |
| }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'agent-b', |
| isError: true, |
| content: subagentResult({ |
| agentName: 'Web Research', |
| turnId: 'child-b', |
| status: 'failed', |
| summary: 'network failed', |
| }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'agent-c', |
| isError: true, |
| content: subagentResult({ |
| agentName: 'Local Read', |
| turnId: 'child-c', |
| status: 'cancelled', |
| summary: 'stopped', |
| }), |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => [tool.toolUseId, tool.status]), |
| [ |
| ['agent-a', 'done'], |
| ['agent-b', 'failed'], |
| ['agent-c', 'aborted'], |
| ], |
| ); |
| assert.equal(toggleAllToolExpansion(state), true); |
| const rendered = renderMakaPiTranscript(state, meta(), 120).map(stripAnsi).join('\n'); |
| assert.match(rendered, /Child tool started: Read/); |
| assert.match(rendered, /Child tool started: WebSearch/); |
| assert.match(rendered, /local result/); |
| assert.match(rendered, /network failed/); |
| assert.match(rendered, /stopped/); |
| }); |
| |
| test('restores one parent card with its child terminal state', () => { |
| const state = createMakaPiTranscriptState(); |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'agent-a', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'agent_spawn', |
| args: { profile: 'local_read', task: 'Inspect.' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'agent-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'agent-a', |
| isError: true, |
| content: subagentResult({ |
| agentName: 'Local Read', |
| turnId: 'child-a', |
| status: 'cancelled', |
| summary: 'stopped', |
| }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools.length, 1); |
| assert.equal(tools[0]?.toolUseId, 'agent-a'); |
| assert.equal(tools[0]?.status, 'aborted'); |
| }); |
| |
| test('keeps a background Bash card running until the process settles', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'sleep 30' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| ref: 'maka://runtime/background-tasks/bg-1', |
| status: 'running', |
| cwd: '/repo', |
| cmd: 'sleep 30', |
| startedAt: 1_000, |
| updatedAt: 11_000, |
| }), |
| durationMs: 10_000, |
| }), |
| ); |
| |
| const tool = state.entries.find((entry) => entry.kind === 'tool'); |
| assert.equal(tool?.kind === 'tool' ? tool.status : undefined, 'running'); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /● Bash \$ sleep 30 \(running 10s\)/); |
| assert.doesNotMatch(rendered, /done/); |
| assert.equal(rendered.split('$ sleep 30').length - 1, 1); |
| }); |
| |
| test('shows live elapsed time and stop guidance on a running background Bash card', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'sleep 30' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ startedAt: 1_000, updatedAt: 2_000 }), |
| }), |
| ); |
| |
| assert.equal(refreshRunningShellRunElapsed(state, 13_500), true); |
| const compact = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(compact, /running 13s/); |
| assert.doesNotMatch(compact, /Ask Maka to stop this task/); |
| |
| // Stop guidance is expanded-only for a running background Bash shell_run. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(expanded, /Ask Maka to stop this task/); |
| }); |
| |
| test('describes a detached background Bash card by ownership, not lifecycle', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ stdout: '', stderr: '' }), |
| }), |
| ); |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'branch', |
| ownership: { kind: 'source_owned', sourceSessionId: 'source', ownerSessionId: 'source' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ stdout: '', stderr: '' }), |
| }); |
| |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /\(detached\)/); |
| assert.doesNotMatch(rendered, /continues in source session/); |
| |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'branch', |
| ownership: { kind: 'source_unavailable', sourceSessionId: 'source' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ stdout: '', stderr: '' }), |
| }); |
| const unavailable = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(unavailable, /\(source unavailable\)/); |
| assert.doesNotMatch(unavailable, /Ask Maka to stop this task/); |
| }); |
| |
| test('never renders a background-task Read card while a poll is in flight', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| |
| // The poll is in flight, but no Read row ever appears — the parent card is |
| // the only tool entry throughout. |
| assert.deepEqual( |
| state.entries.filter((entry) => entry.kind === 'tool').map((tool) => tool.toolUseId), |
| ['bash-bg'], |
| ); |
| const inFlight = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(inFlight, /● Read/); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| stdout: 'starting\nstill running\n', |
| updatedAt: 5_000, |
| }), |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-bg'], |
| ); |
| assert.equal( |
| tools[0]?.result?.kind === 'shell_run' && tools[0].result.output?.mode === 'pipes' |
| ? tools[0].result.output.stdout |
| : '', |
| 'starting\nstill running\n', |
| ); |
| const settled = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(settled, /● Read/); |
| }); |
| |
| test('surfaces an errored poll carrying shell_run content instead of folding it', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| // isError is the call-level authoritative status: even with a well-formed |
| // shell_run payload, the failed call must not fold into the parent. |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: true, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| stdout: 'starting\nnewer\n', |
| updatedAt: 5_000, |
| }), |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-bg', 'read-bg'], |
| ); |
| assert.equal(tools[1]?.status, 'error'); |
| // The parent keeps its pre-error revision — the failed call changes nothing. |
| assert.equal( |
| tools[0]?.result?.kind === 'shell_run' && tools[0].result.output?.mode === 'pipes' |
| ? tools[0].result.output.stdout |
| : '', |
| 'starting\n', |
| ); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /● Read/); |
| }); |
| |
| test('keeps an errored non-folded poll card instead of splicing it into the parent', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| // The Read starts before the parent carries its shell_run result, so it is |
| // not folded at tool_start. |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: true, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| stdout: 'starting\nnewer\n', |
| updatedAt: 5_000, |
| }), |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-bg', 'read-bg'], |
| ); |
| assert.equal(tools[1]?.status, 'error'); |
| assert.equal( |
| tools[0]?.result?.kind === 'shell_run' && tools[0].result.output?.mode === 'pipes' |
| ? tools[0].result.output.stdout |
| : '', |
| 'starting\n', |
| ); |
| }); |
| |
| test('surfaces an errored background-task poll as a card instead of swallowing it', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: true, |
| content: { kind: 'text', text: 'background task no longer exists' }, |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools.length, 2); |
| const poll = tools[1]; |
| assert.equal(poll?.toolUseId, 'read-bg'); |
| assert.equal(poll?.toolName, 'Read'); |
| assert.equal(poll?.status, 'error'); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /● Read/); |
| // The error disc carries the failure state; free-text error content stays |
| // out of the compact row under #1086. |
| assert.match(rendered, /\(1 line · 32 bytes\)/); |
| assert.doesNotMatch(rendered, /background task no longer exists/); |
| }); |
| |
| test('never renders a StopBackgroundTask card while the stop is in flight', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'sleep 30' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running' }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'stop-bg', |
| toolName: 'StopBackgroundTask', |
| args: { ref }, |
| }), |
| ); |
| |
| // No transient stop row while the stop call is in flight. |
| assert.deepEqual( |
| state.entries.filter((entry) => entry.kind === 'tool').map((tool) => tool.toolUseId), |
| ['bash-bg'], |
| ); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'stop-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'cancelled', completedAt: 8_000, exitCode: 130 }), |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-bg'], |
| ); |
| assert.equal(tools[0]?.status, 'aborted'); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(rendered, /● StopBackgroundTask/); |
| }); |
| |
| test('never folds a WriteStdin aimed at a background-task ref', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'top' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running' }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'stdin-bg', |
| toolName: 'WriteStdin', |
| args: { ref, input: 'q' }, |
| }), |
| ); |
| |
| // WriteStdin is a real interaction with the process, not polling: its card |
| // renders from tool_start on. |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['bash-bg', 'stdin-bg'], |
| ); |
| assert.equal(tools[1]?.status, 'running'); |
| }); |
| |
| test('stays silent for a hydration catch-up update that settles a resumed card', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| |
| // A resumed session: stored history still records the run as running. |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'bash-bg', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| // Durable state says the run settled while away: the card flips, but |
| // catch-up replay is not a live event, so no notice fires. |
| const applied = applyShellRunViewUpdateToTranscript( |
| state, |
| { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'starting\ndone\n', |
| completedAt: 48_000, |
| exitCode: 0, |
| }), |
| }, |
| { announceSettle: false }, |
| ); |
| |
| assert.equal(applied, true); |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools[0]?.status, 'done'); |
| assert.equal( |
| state.entries.some((entry) => entry.kind === 'notice'), |
| false, |
| ); |
| }); |
| |
| test('announces at the transcript tail when a running background task completes', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'starting\ndone\n', |
| completedAt: 48_000, |
| exitCode: 0, |
| }), |
| }); |
| |
| const notice = state.entries[state.entries.length - 1]; |
| assert.equal(notice?.kind, 'notice'); |
| assert.equal(notice?.kind === 'notice' ? notice.level : '', 'info'); |
| const text = notice?.kind === 'notice' ? notice.text : ''; |
| assert.match(text, /Background task completed: npm test/); |
| assert.match(text, /exit 0/); |
| assert.match(text, /47s/); |
| }); |
| |
| test('notifies a settle exactly once across a folded poll and the live update', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| |
| // The model's poll observes the settle first: exactly one notice. |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'starting\ndone\n', |
| completedAt: 48_000, |
| exitCode: 0, |
| }), |
| }), |
| ); |
| let notices = state.entries.filter((entry) => entry.kind === 'notice'); |
| assert.equal(notices.length, 1); |
| assert.equal(notices[0]?.kind === 'notice' ? notices[0].level : '', 'info'); |
| assert.match( |
| notices[0]?.kind === 'notice' ? notices[0].text : '', |
| /Background task completed: npm test/, |
| ); |
| |
| // The event-driven update reporting the same settle must not re-notify. |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'starting\ndone\n', |
| completedAt: 48_000, |
| exitCode: 0, |
| }), |
| }); |
| notices = state.entries.filter((entry) => entry.kind === 'notice'); |
| assert.equal(notices.length, 1); |
| }); |
| |
| test('announces a detached background task settle exactly once when its owner completes it', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| cmd: 'build', |
| stdout: 'starting\n', |
| updatedAt: 2_000, |
| }), |
| }), |
| ); |
| |
| // An inherited run is presented as `detached` while its resource keeps |
| // running; this must not silence its later settle. |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-branch', |
| ownership: { |
| kind: 'source_owned', |
| sourceSessionId: 'session-1', |
| ownerSessionId: 'session-1', |
| }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'running', |
| cmd: 'build', |
| stdout: 'starting\n', |
| updatedAt: 3_000, |
| revision: 3_000, |
| }), |
| }); |
| const detached = state.entries.find((entry) => entry.kind === 'tool'); |
| assert.equal(detached?.kind === 'tool' ? detached.status : '', 'detached'); |
| assert.equal( |
| state.entries.some((entry) => entry.kind === 'notice'), |
| false, |
| ); |
| |
| // The owner's live subscription settles the run: the detached card still |
| // announces exactly once. |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'completed', |
| cmd: 'build', |
| stdout: 'starting\ndone\n', |
| completedAt: 48_000, |
| exitCode: 0, |
| revision: 48_000, |
| }), |
| }); |
| const notices = state.entries.filter((entry) => entry.kind === 'notice'); |
| assert.equal(notices.length, 1); |
| assert.equal(notices[0]?.kind === 'notice' ? notices[0].level : '', 'info'); |
| assert.match( |
| notices[0]?.kind === 'notice' ? notices[0].text : '', |
| /Background task completed: build/, |
| ); |
| }); |
| |
| test('announces a detached background task orphaned settle as an error exactly once', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| cmd: 'build', |
| stdout: 'starting\n', |
| updatedAt: 2_000, |
| }), |
| }), |
| ); |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-branch', |
| ownership: { |
| kind: 'source_owned', |
| sourceSessionId: 'session-1', |
| ownerSessionId: 'session-1', |
| }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'running', |
| cmd: 'build', |
| stdout: 'starting\n', |
| updatedAt: 3_000, |
| revision: 3_000, |
| }), |
| }); |
| assert.equal( |
| state.entries.some((entry) => entry.kind === 'notice'), |
| false, |
| ); |
| |
| // The owner reports the run orphaned: an error-level notice with the |
| // `orphaned` verb, fired exactly once from the detached card. |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'orphaned', |
| cmd: 'build', |
| completedAt: 20_000, |
| exitCode: 1, |
| revision: 20_000, |
| }), |
| }); |
| const notices = state.entries.filter((entry) => entry.kind === 'notice'); |
| assert.equal(notices.length, 1); |
| assert.equal(notices[0]?.kind === 'notice' ? notices[0].level : '', 'error'); |
| assert.match( |
| notices[0]?.kind === 'notice' ? notices[0].text : '', |
| /Background task orphaned: build/, |
| ); |
| }); |
| |
| test('notifies a settle exactly once when the live update precedes the folded poll', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| |
| // The event-driven update reports the settle first: exactly one notice. |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'starting\ndone\n', |
| completedAt: 48_000, |
| exitCode: 0, |
| }), |
| }); |
| let notices = state.entries.filter((entry) => entry.kind === 'notice'); |
| assert.equal(notices.length, 1); |
| assert.match( |
| notices[0]?.kind === 'notice' ? notices[0].text : '', |
| /Background task completed: npm test/, |
| ); |
| |
| // A folded poll observing the same settle afterward must not re-notify. |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'starting\ndone\n', |
| completedAt: 48_000, |
| exitCode: 0, |
| }), |
| }), |
| ); |
| notices = state.entries.filter((entry) => entry.kind === 'notice'); |
| assert.equal(notices.length, 1); |
| }); |
| |
| test('announces a failed background task as an error with its exit and message', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm run build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', cmd: 'npm run build' }), |
| }), |
| ); |
| |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'failed', |
| cmd: 'npm run build', |
| completedAt: 13_000, |
| exitCode: 1, |
| failureMessage: 'compiler exited\nwith diagnostics', |
| }), |
| }); |
| |
| const notice = state.entries[state.entries.length - 1]; |
| assert.equal(notice?.kind, 'notice'); |
| assert.equal(notice?.kind === 'notice' ? notice.level : '', 'error'); |
| const text = notice?.kind === 'notice' ? notice.text : ''; |
| assert.match(text, /Background task failed: npm run build/); |
| assert.match(text, /exit 1/); |
| assert.match(text, /12s/); |
| // Only the first line of a multi-line failure message joins the notice. |
| assert.match(text, /compiler exited/); |
| assert.doesNotMatch(text, /with diagnostics/); |
| }); |
| |
| test('announces a stopped background task as a plain note', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'sleep 30' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', cmd: 'sleep 30' }), |
| }), |
| ); |
| |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ |
| ref, |
| status: 'cancelled', |
| cmd: 'sleep 30', |
| completedAt: 8_000, |
| exitCode: 130, |
| }), |
| }); |
| |
| const notice = state.entries[state.entries.length - 1]; |
| assert.equal(notice?.kind, 'notice'); |
| assert.equal(notice?.kind === 'notice' ? notice.level : '', 'info'); |
| assert.match(notice?.kind === 'notice' ? notice.text : '', /Background task stopped: sleep 30/); |
| }); |
| |
| test('stays silent for a background task already settled in stored history', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| |
| replaceTranscriptWithStoredMessages(state, [ |
| { |
| type: 'tool_call', |
| id: 'bash-bg', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'bash-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'completed', |
| stdout: 'done\n', |
| completedAt: 5_000, |
| updatedAt: 5_000, |
| exitCode: 0, |
| }), |
| }, |
| ] satisfies StoredMessage[]); |
| |
| assert.equal( |
| state.entries.some((entry) => entry.kind === 'notice'), |
| false, |
| ); |
| }); |
| |
| test('announces a timed-out background task with human-readable text', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running' }), |
| }), |
| ); |
| |
| applyShellRunViewUpdateToTranscript(state, { |
| sessionId: 'session-1', |
| ownership: { kind: 'local' }, |
| sourceTurnId: 'turn-1', |
| sourceToolCallId: 'bash-bg', |
| result: shellRun({ ref, status: 'timed_out', completedAt: 60_000 }), |
| }); |
| |
| const notice = state.entries[state.entries.length - 1]; |
| assert.equal(notice?.kind, 'notice'); |
| assert.equal(notice?.kind === 'notice' ? notice.level : '', 'error'); |
| const text = notice?.kind === 'notice' ? notice.text : ''; |
| assert.match(text, /Background task timed out: npm test/); |
| assert.doesNotMatch(text, /timed_out/); |
| }); |
| |
| test('drops a folded poll when the turn aborts mid-flight', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running' }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| assert.equal(state.pendingShellRunPolls.size, 1); |
| |
| applyMakaSessionEventToTranscript(state, event({ type: 'abort', reason: 'user_stop' })); |
| |
| assert.equal(state.pendingShellRunPolls.size, 0); |
| }); |
| |
| test('folds a background-task Read result into its parent Bash card', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'npm test' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout: 'starting\n', updatedAt: 2_000 }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: false, |
| content: shellRun({ |
| ref, |
| status: 'running', |
| stdout: 'starting\nstill running\n', |
| updatedAt: 5_000, |
| }), |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools.length, 1); |
| assert.equal(tools[0]?.toolUseId, 'bash-bg'); |
| assert.equal( |
| tools[0]?.result?.kind === 'shell_run' && tools[0].result.output?.mode === 'pipes' |
| ? tools[0].result.output.stdout |
| : '', |
| 'starting\nstill running\n', |
| ); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(rendered, /● Read/); |
| // Running card keeps the live tail in the expanded card. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(expanded, /still running/); |
| }); |
| |
| test('shows polled background output instead of a stale live delta', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'bash-bg', |
| seq: 1, |
| stream: 'stdout', |
| chunk: 'starting\n', |
| redacted: false, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, stdout: '', updatedAt: 2_000 }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: false, |
| content: shellRun({ ref, stdout: 'starting\n50%\n', updatedAt: 3_000 }), |
| }), |
| ); |
| |
| // Live output lives in the expanded card for a running tool. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /50%/); |
| }); |
| |
| test('shows stdout as latest when it arrives after stderr', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| const result = shellRun({ |
| stdout: '99%\n', |
| stderr: 'warning\n', |
| latestStream: 'stdout', |
| updatedAt: 3_000, |
| }); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: result, |
| }), |
| ); |
| |
| // Live output lives in the expanded card for a running tool. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /99%/); |
| }); |
| |
| test('re-renders a background Bash card when polling replaces output with the same length', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'watch' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, stdout: 'aaaa\n', updatedAt: 2_000 }), |
| }), |
| ); |
| // Live output lives in the expanded card for a running tool. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const before = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(before, /aaaa/); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-bg', |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-bg', |
| isError: false, |
| content: shellRun({ ref, stdout: 'bbbb\n', updatedAt: 3_000 }), |
| }), |
| ); |
| const after = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(after, /bbbb/); |
| assert.doesNotMatch(after, /aaaa/); |
| }); |
| |
| test('keeps background-task Read cards when their parent Bash card is missing', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| for (const [toolUseId, stdout] of [ |
| ['read-1', 'first\n'], |
| ['read-2', 'second\n'], |
| ] as const) { |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId, |
| toolName: 'Read', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId, |
| isError: false, |
| content: shellRun({ ref, status: 'running', stdout }), |
| }), |
| ); |
| } |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools.length, 2); |
| assert.deepEqual( |
| tools.map((tool) => tool.toolUseId), |
| ['read-1', 'read-2'], |
| ); |
| }); |
| |
| test('folds StopBackgroundTask into its parent Bash card as aborted', () => { |
| const state = createMakaPiTranscriptState(); |
| const ref = 'maka://runtime/background-tasks/bg-1'; |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'sleep 30' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'running' }), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'stop-bg', |
| toolName: 'StopBackgroundTask', |
| args: { ref }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'stop-bg', |
| isError: false, |
| content: shellRun({ ref, status: 'cancelled', completedAt: 8_000, exitCode: 130 }), |
| }), |
| ); |
| |
| const tools = state.entries.filter((entry) => entry.kind === 'tool'); |
| assert.equal(tools.length, 1); |
| assert.equal(tools[0]?.status, 'aborted'); |
| const lines = renderMakaPiTranscript(state, meta(), 100); |
| const rendered = lines.map(stripAnsi).join('\n'); |
| assert.match(rendered, /● Bash \$ sleep 30 \(7s · cancelled · exit 130\)/); |
| assert.doesNotMatch(rendered, /● StopBackgroundTask/); |
| // An aborted background task uses the danger disc (red). |
| assert.match(lines.join('\n'), /\x1b\[31m●\x1b\[39m/); |
| }); |
| |
| test('reserves a fixed-shape annotation when it fits alongside the row head', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-cap', |
| toolName: 'Bash', |
| args: { command: 'npm run build ' + 'x'.repeat(60) }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-cap', |
| isError: false, |
| content: shellRun({ |
| status: 'timed_out', |
| startedAt: 0, |
| updatedAt: 1_234_000, |
| completedAt: 1_234_000, |
| exitCode: 124, |
| }), |
| }), |
| ); |
| |
| const lines = renderMakaPiTranscript(state, meta(), 60).map(stripAnsi); |
| const row = lines[1]!; |
| // The complete fixed-shape annotation is reserved before the long command |
| // target, so the timeout and exit code remain readable. |
| assert.match(row, /\(1234s · timed_out · exit 124\)$/); |
| assert.ok(visibleWidth(row) <= 60, `row width ${visibleWidth(row)} exceeds 60`); |
| }); |
| |
| test('keeps a duration-bearing generic outcome readable when it exceeds the old cap', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'generic-duration', |
| toolName: 'McpTool', |
| args: { target: 'x'.repeat(80) }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'generic-duration', |
| isError: false, |
| content: { kind: 'text', text: 'line\n'.repeat(100) }, |
| durationMs: 12_000, |
| }), |
| ); |
| |
| const row = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi)[1]!; |
| assert.match(row, /\(12s · 100 lines · 500 bytes\)$/); |
| assert.doesNotMatch(row, /\(1…\)$/); |
| assert.ok(visibleWidth(row) <= 80, `row width ${visibleWidth(row)} exceeds 80`); |
| }); |
| |
| test('counts stdout and stderr lines per stream, not at the join boundary', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-fg', |
| toolName: 'Bash', |
| args: { command: 'fg' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-fg', |
| isError: false, |
| content: terminalResult('out\n', 'err\n'), |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'bg' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| status: 'completed', |
| startedAt: 0, |
| updatedAt: 2_000, |
| completedAt: 2_000, |
| exitCode: 0, |
| stdout: 'out\n', |
| stderr: 'err\n', |
| }), |
| }), |
| ); |
| |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| // `out\n` + `err\n` is two lines — joining before counting would invent a |
| // third line at the stream boundary. |
| assert.match(rendered, /● Bash \$ fg \(2 lines\)/); |
| assert.match(rendered, /● Bash \$ bg \(2s · 2 lines\)/); |
| }); |
| |
| test('applies a runtime-published terminal update directly to its parent Bash card', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ status: 'running', updatedAt: 2_000 }), |
| }), |
| ); |
| |
| const applied = applyShellRunUpdateToTranscript( |
| state, |
| 'bash-bg', |
| shellRun({ |
| status: 'completed', |
| stdout: 'done\n', |
| updatedAt: 5_000, |
| completedAt: 5_000, |
| exitCode: 0, |
| }), |
| ); |
| |
| assert.equal(applied, true); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /● Bash \$ build \(4s · 1 line\)/); |
| // Compact shows the output size, not the output content. |
| assert.doesNotMatch(rendered, /done/); |
| }); |
| |
| test('does not erase a runtime-published output update with an equal-time handoff result', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyShellRunUpdateToTranscript( |
| state, |
| 'bash-bg', |
| shellRun({ stdout: 'starting\n', updatedAt: 2_000 }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ updatedAt: 2_000, revision: 2_000, omitOutput: true }), |
| }), |
| ); |
| |
| const tool = state.entries.find((entry) => entry.kind === 'tool'); |
| assert.equal( |
| tool?.kind === 'tool' && |
| tool.result?.kind === 'shell_run' && |
| tool.result.output?.mode === 'pipes' |
| ? tool.result.output.stdout |
| : '', |
| 'starting\n', |
| ); |
| }); |
| |
| test('summarizes Read results as a line/byte count and never replays file content', () => { |
| const state = createMakaPiTranscriptState(); |
| const fileText = Array.from({ length: 4 }, (_, i) => `content-line-${i}`).join('\n'); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-1', |
| toolName: 'Read', |
| args: { path: 'src/app.ts', offset: 10, limit: 20 }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-1', |
| isError: false, |
| content: { kind: 'json', value: { content: fileText } }, |
| }), |
| ); |
| |
| const compactLines = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi); |
| assert.equal(compactLines.length, 2); |
| const compact = compactLines.join('\n'); |
| assert.match(compact, /src\/app\.ts offset 10 limit 20/); |
| assert.match(compact, /\(4 lines\)/); |
| assert.doesNotMatch(compact, /content-line-0/); |
| |
| // A successful Read pulled the file into the model's context; expanding the |
| // card confirms the read but must not dump the file into the transcript. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(expanded, /content-line-0/); |
| assert.match(expanded, /Read 4 lines, 59 bytes/); |
| }); |
| |
| test('shows maka://runtime resource Read output in full, never summarized or capped', () => { |
| const state = createMakaPiTranscriptState(); |
| // A runtime resource read returns live state (background-task metadata + |
| // output) that only lives in the transcript. Its body opens with several |
| // metadata lines, so it must be neither summarized nor head/tail-capped. |
| const body = [ |
| 'ref: maka://runtime/background-tasks/abc', |
| 'status: running', |
| 'cwd: /repo', |
| 'command: npm test', |
| 'started: 1', |
| 'updated: 2', |
| '', |
| 'stdout:', |
| 'first-output-line', |
| 'middle-output-line', |
| 'last-output-line', |
| ].join('\n'); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'read-rt', |
| toolName: 'Read', |
| args: { ref: 'maka://runtime/background-tasks/abc' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'read-rt', |
| isError: false, |
| content: { kind: 'text', text: body }, |
| }), |
| ); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(expanded, /command: npm test/); // a mid-body line a cap would hide |
| assert.match(expanded, /stdout:/); |
| assert.match(expanded, /middle-output-line/); |
| assert.doesNotMatch(expanded, /lines hidden/); |
| assert.doesNotMatch(expanded, /Read \d+ lines,/); |
| }); |
| |
| test('keeps shell_run status and exit visible while capping its stream body', () => { |
| const state = createMakaPiTranscriptState(); |
| // A background command's status/exit is the whole point of expanding the |
| // card; a bare head/tail cap would keep only `$ cmd` + the last stdout lines |
| // and hide whether the process failed or timed out. |
| const stdout = Array.from({ length: 10 }, (_, i) => `out-line-${i}`).join('\n'); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'shell-1', |
| toolName: 'StopBackgroundTask', |
| args: { ref: 'bg-42' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'shell-1', |
| isError: false, |
| content: shellRun({ |
| ref: 'bg-42', |
| status: 'failed', |
| cwd: '/repo', |
| cmd: 'npm run watch', |
| startedAt: 1, |
| updatedAt: 2, |
| completedAt: 2, |
| exitCode: 137, |
| failureMessage: 'killed by signal', |
| stdout, |
| stderr: 'boom-stderr', |
| }), |
| }), |
| ); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| // Failure metadata a bare head/tail cap would bury stays visible. |
| assert.match(expanded, /failed/); |
| assert.match(expanded, /exit 137/); |
| assert.match(expanded, /killed by signal/); |
| assert.match(expanded, /bg-42/); |
| // The command/cwd live on the result, not the ref-only input, so the |
| // expanded card must repeat them to say which process this was. |
| assert.match(expanded, /npm run watch/); |
| // The stream body is still capped, and stderr keeps its label. |
| assert.match(expanded, /lines hidden/); |
| assert.match(expanded, /\[stderr\]/); |
| assert.match(expanded, /boom-stderr/); |
| }); |
| |
| test('renders a report-style result in full instead of head/tail capping it', () => { |
| const state = createMakaPiTranscriptState(); |
| // Report-style kinds (agent reports, summaries) are content the user expands |
| // to read in full; unlike a raw command dump they must not be capped. |
| const report = Array.from({ length: 12 }, (_, i) => `report-line-${i}`).join('\n'); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'sum-1', |
| toolName: 'Task', |
| args: {}, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'sum-1', |
| isError: false, |
| content: { kind: 'summary', original: 'x', summarized: report, reason: 'too_large' }, |
| }), |
| ); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(expanded, /report-line-0/); |
| assert.match(expanded, /report-line-6/); // a mid-body line a head/tail cap would hide |
| assert.match(expanded, /report-line-11/); |
| assert.doesNotMatch(expanded, /lines hidden/); |
| }); |
| |
| test('keeps generic compact summaries bounded for malformed and oversized results', () => { |
| const cases = [ |
| { |
| toolUseId: 'malformed', |
| content: { kind: 'text', text: undefined } as unknown as ToolResultContent, |
| }, |
| { |
| toolUseId: 'malformed-truthy', |
| content: { kind: 'text', text: 42 } as unknown as ToolResultContent, |
| }, |
| { |
| toolUseId: 'oversized', |
| content: { kind: 'text', text: 'x'.repeat(20_000) } satisfies ToolResultContent, |
| }, |
| ]; |
| |
| for (const testCase of cases) { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: testCase.toolUseId, |
| toolName: 'mcp__local__result', |
| args: {}, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: testCase.toolUseId, |
| isError: false, |
| content: testCase.content, |
| }), |
| ); |
| |
| const row = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi)[1] ?? ''; |
| assert.ok(visibleWidth(row) <= 80, `row width ${visibleWidth(row)} exceeds 80`); |
| if (testCase.toolUseId.startsWith('malformed')) { |
| assert.match(row, /\(no output\)/); |
| if (testCase.toolUseId === 'malformed-truthy') { |
| assert.equal(toggleAllToolExpansion(state), true); |
| assert.doesNotMatch( |
| renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n'), |
| /42/, |
| ); |
| } |
| } else { |
| assert.match(row, /\(1 line · 20000 bytes\)/); |
| assert.doesNotMatch(row, /x{20}/); |
| } |
| } |
| }); |
| |
| test('renders the same generic result summary for live and stored transcript paths', () => { |
| const live = createMakaPiTranscriptState(); |
| const stored = createMakaPiTranscriptState(); |
| const text = 'live and replay\nuse the same shape\n'; |
| |
| applyMakaSessionEventToTranscript( |
| live, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'mcp-replay', |
| toolName: 'mcp__github__search', |
| args: { query: 'replay' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| live, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'mcp-replay', |
| isError: false, |
| content: { kind: 'text', text }, |
| }), |
| ); |
| replaceTranscriptWithStoredMessages(stored, [ |
| { |
| type: 'tool_call', |
| id: 'mcp-replay', |
| turnId: 'turn-1', |
| ts: 1, |
| toolName: 'mcp__github__search', |
| args: { query: 'replay' }, |
| }, |
| { |
| type: 'tool_result', |
| id: 'mcp-result', |
| turnId: 'turn-1', |
| ts: 2, |
| toolUseId: 'mcp-replay', |
| isError: false, |
| content: { kind: 'text', text }, |
| }, |
| ] satisfies StoredMessage[]); |
| |
| assert.deepEqual( |
| renderMakaPiTranscript(live, meta(), 120).map(stripAnsi), |
| renderMakaPiTranscript(stored, meta(), 120).map(stripAnsi), |
| ); |
| }); |
| |
| test('caps long terminal output to head and tail lines when expanded', () => { |
| const state = createMakaPiTranscriptState(); |
| const stdout = Array.from({ length: 20 }, (_, i) => `out-${i}`).join('\n'); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-1', |
| toolName: 'Bash', |
| args: { command: 'seq 20' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-1', |
| isError: false, |
| content: terminalResult(stdout), |
| }), |
| ); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| // First three and last three lines survive; the middle collapses to a marker. |
| assert.match(expanded, /out-0\n/); |
| assert.match(expanded, /out-2\n/); |
| assert.match(expanded, /out-17\n/); |
| assert.match(expanded, /out-19/); |
| assert.doesNotMatch(expanded, /out-10\b/); |
| assert.match(expanded, /⋯ 14 lines hidden ⋯/); |
| }); |
| |
| test('orders and de-dupes tool_output_delta by seq and marks redacted chunks', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-1', |
| toolName: 'Bash', |
| args: { command: 'run' }, |
| }), |
| ); |
| // Out-of-order + duplicate seq + a redacted chunk. |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'bash-1', |
| seq: 2, |
| stream: 'stdout', |
| chunk: 'SECOND', |
| redacted: false, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'bash-1', |
| seq: 1, |
| stream: 'stdout', |
| chunk: 'FIRST', |
| redacted: false, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'bash-1', |
| seq: 1, |
| stream: 'stdout', |
| chunk: 'DUPLICATE', |
| redacted: false, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'bash-1', |
| seq: 3, |
| stream: 'stderr', |
| chunk: 'secret', |
| redacted: true, |
| }), |
| ); |
| |
| // Compact: a running tool shows only the disc row; live output (including |
| // the redaction marker) lives in the expanded card. |
| const compact = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(compact, /secret/); |
| assert.doesNotMatch(compact, /\[redacted\]/); |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.ok(rendered.indexOf('FIRST') < rendered.indexOf('SECOND')); |
| assert.doesNotMatch(rendered, /DUPLICATE/); |
| assert.doesNotMatch(rendered, /secret/); |
| assert.match(rendered, /\[redacted\]/); |
| assert.match(rendered, /\[stderr\]/); |
| }); |
| |
| test('renders the redaction marker for an empty redacted output delta', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'redacted-empty', |
| toolName: 'Bash', |
| args: { command: 'secret' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'redacted-empty', |
| seq: 1, |
| stream: 'stdout', |
| chunk: '', |
| redacted: true, |
| }), |
| ); |
| |
| // Live output lives in the expanded card for a running tool. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /\[redacted\]/); |
| }); |
| |
| test('caps a long live stream group in the expanded card', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-stream', |
| toolName: 'Bash', |
| args: { command: 'seq 20' }, |
| }), |
| ); |
| // Ten single-line stdout chunks form one stream group; the expanded card |
| // head/tail caps the group body just like a finished command dump. |
| for (let i = 0; i < 10; i += 1) { |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'bash-stream', |
| seq: i, |
| stream: 'stdout', |
| chunk: `${i === 0 ? '' : '\n'}stream-line-${i}`, |
| redacted: false, |
| }), |
| ); |
| } |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(expanded, /stream-line-0/); |
| assert.match(expanded, /stream-line-9/); |
| assert.match(expanded, /lines hidden/); |
| assert.doesNotMatch(expanded, /stream-line-5/); // a middle line the cap hides |
| }); |
| |
| test('retains the newest live output when a stream exceeds its buffer limit', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bounded', |
| toolName: 'Bash', |
| args: { command: 'verbose' }, |
| }), |
| ); |
| const chunks = Array.from( |
| { length: 9 }, |
| (_, i) => `chunk-${i}-start\n${'x\n'.repeat(4_090)}chunk-${i}-end\n`, |
| ); |
| for (const [i, chunk] of chunks.entries()) { |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_output_delta', |
| toolUseId: 'bash-bounded', |
| seq: i, |
| stream: 'stdout', |
| chunk, |
| redacted: false, |
| }), |
| ); |
| } |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(expanded, /chunk-0-start\b/); |
| assert.match(expanded, /chunk-8-end\b/); |
| const droppedChars = chunks.reduce((total, chunk) => total + chunk.length, 0) - 64 * 1024; |
| assert.match(expanded, new RegExp(`${droppedChars} earlier live-output chars truncated`)); |
| }); |
| |
| test('drops the oldest progress when the chunk count reaches its limit', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'progress-many-chunks', |
| toolName: 'Workflow', |
| args: {}, |
| }), |
| ); |
| for (let i = 0; i < 513; i += 1) { |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_progress', |
| toolUseId: 'progress-many-chunks', |
| chunk: `progress-${i}\n`, |
| }), |
| ); |
| } |
| |
| assert.equal(toggleAllToolExpansion(state), true); |
| const expanded = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.doesNotMatch(expanded, /progress-0\b/); |
| assert.match(expanded, /progress-512\b/); |
| }); |
| }); |
| |
| describe('transcript entry render memoization', () => { |
| test('re-renders live progress after the bounded buffer reaches a stable length', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'progress-cache', |
| toolName: 'Workflow', |
| args: {}, |
| }), |
| ); |
| for (let i = 0; i < 512; i += 1) { |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_progress', |
| toolUseId: 'progress-cache', |
| chunk: `progress-${i}\n`, |
| }), |
| ); |
| } |
| assert.equal(toggleAllToolExpansion(state), true); |
| const before = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(before, /progress-511\b/); |
| |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_progress', |
| toolUseId: 'progress-cache', |
| chunk: 'progress-512\n', |
| }), |
| ); |
| const after = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(after, /progress-512\b/); |
| assert.doesNotMatch(after, /progress-0\b/); |
| }); |
| |
| test('re-renders thinking when a same-length final replaces the streamed text', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_delta', |
| messageId: 'message-1', |
| text: 'AAAA', |
| }), |
| ); |
| assert.equal(toggleAllThinkingExpansion(state), true); |
| const streamed = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n'); |
| assert.match(streamed, /AAAA/); |
| |
| // thinking_complete replaces the text in place; same length must still bust |
| // the render cache so the final reasoning is shown, not the streamed draft. |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'thinking_complete', |
| messageId: 'message-1', |
| text: 'BBBB', |
| }), |
| ); |
| const finalized = renderMakaPiTranscript(state, meta(), 80).map(stripAnsi).join('\n'); |
| assert.match(finalized, /BBBB/); |
| assert.doesNotMatch(finalized, /AAAA/); |
| }); |
| |
| test('merges a latestStream-only ShellRun update into the card result', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ |
| stdout: 'AAAA', |
| stderr: 'BBBB', |
| updatedAt: 3_000, |
| latestStream: 'stderr', |
| status: 'completed', |
| completedAt: 3_000, |
| exitCode: 0, |
| }), |
| }), |
| ); |
| // The compact row carries only the line count and the expanded card shows |
| // both streams, so a latestStream flip is observable only on the result |
| // itself — the render must still re-run from a fresh memo entry. |
| const latestStream = () => { |
| const tool = state.entries.find((entry) => entry.kind === 'tool'); |
| return tool?.kind === 'tool' && |
| tool.result?.kind === 'shell_run' && |
| tool.result.output?.mode === 'pipes' |
| ? tool.result.output.latestStream |
| : undefined; |
| }; |
| assert.equal(latestStream(), 'stderr'); |
| |
| applyShellRunUpdateToTranscript( |
| state, |
| 'bash-bg', |
| shellRun({ |
| stdout: 'AAAA', |
| stderr: 'BBBB', |
| updatedAt: 3_000, |
| revision: 3_001, |
| latestStream: 'stdout', |
| status: 'completed', |
| completedAt: 3_000, |
| exitCode: 0, |
| }), |
| ); |
| assert.equal(latestStream(), 'stdout'); |
| const rendered = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(rendered, /\(2s · 2 lines\)/); |
| }); |
| |
| test('re-renders equal-length ShellRun output only when revision advances', () => { |
| const state = createMakaPiTranscriptState(); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_start', |
| toolUseId: 'bash-bg', |
| toolName: 'Bash', |
| args: { command: 'build' }, |
| }), |
| ); |
| applyMakaSessionEventToTranscript( |
| state, |
| event({ |
| type: 'tool_result', |
| toolUseId: 'bash-bg', |
| isError: false, |
| content: shellRun({ stdout: 'AAAA', updatedAt: 3_000, latestStream: 'stdout' }), |
| }), |
| ); |
| // Live output lives in the expanded card for a running tool. |
| assert.equal(toggleAllToolExpansion(state), true); |
| const before = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(before, /AAAA/); |
| |
| applyShellRunUpdateToTranscript( |
| state, |
| 'bash-bg', |
| shellRun({ |
| stdout: 'BBBB', |
| updatedAt: 3_000, |
| revision: 3_001, |
| latestStream: 'stdout', |
| }), |
| ); |
| const after = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); |
| assert.match(after, /BBBB/); |
| assert.doesNotMatch(after, /AAAA/); |
| }); |
| }); |
| |
| function meta() { |
| return { |
| title: 'Maka', |
| cwd: '/tmp/project', |
| model: 'deepseek-v4-flash', |
| connectionSlug: 'deepseek', |
| permissionMode: 'ask', |
| } as const; |
| } |
| |
| function terminalResult( |
| stdout: string, |
| stderr = '', |
| overrides: Partial< |
| Omit<Extract<ToolResultContent, { kind: 'terminal' }>, 'kind' | 'output'> |
| > = {}, |
| ): Extract<ToolResultContent, { kind: 'terminal' }> { |
| return { |
| kind: 'terminal', |
| cwd: '/repo', |
| cmd: 'echo', |
| status: 'completed', |
| exitCode: 0, |
| ...overrides, |
| output: { |
| mode: 'pipes', |
| stdout, |
| stderr, |
| stdoutTruncated: false, |
| stderrTruncated: false, |
| redacted: false, |
| }, |
| } as const; |
| } |
| |
| type ShellRunCommonOverrides = Partial< |
| Pick< |
| ShellRunToolResult, |
| | 'ref' |
| | 'status' |
| | 'cwd' |
| | 'cmd' |
| | 'startedAt' |
| | 'updatedAt' |
| | 'completedAt' |
| | 'exitCode' |
| | 'failureMessage' |
| | 'revision' |
| | 'timeoutMs' |
| | 'operation' |
| > |
| >; |
| |
| type PipeShellRunFixtureOverrides = ShellRunCommonOverrides & { |
| mode?: 'pipes'; |
| output?: PipeShellOutput; |
| stdout?: string; |
| stderr?: string; |
| stdoutTruncated?: boolean; |
| stderrTruncated?: boolean; |
| latestStream?: 'stdout' | 'stderr'; |
| omitOutput?: boolean; |
| }; |
| |
| type PtyShellRunFixtureOverrides = ShellRunCommonOverrides & { |
| mode: 'pty'; |
| output?: PtyShellOutput; |
| omitOutput?: boolean; |
| }; |
| |
| function shellRun( |
| overrides: PtyShellRunFixtureOverrides, |
| ): Extract<ShellRunToolResult, { mode: 'pty' }>; |
| function shellRun( |
| overrides?: PipeShellRunFixtureOverrides, |
| ): Extract<ShellRunToolResult, { mode: 'pipes' }>; |
| function shellRun( |
| overrides: PipeShellRunFixtureOverrides | PtyShellRunFixtureOverrides = {}, |
| ): ShellRunToolResult { |
| if (overrides.mode === 'pty') { |
| const { mode: _mode, output, omitOutput, operation, ...state } = overrides; |
| const compact = { |
| kind: 'shell_run', |
| ref: 'maka://runtime/background-tasks/bg-1', |
| mode: 'pty', |
| status: 'running', |
| cwd: '/repo', |
| cmd: 'npm test', |
| revision: state.revision ?? state.updatedAt ?? state.completedAt ?? 1, |
| startedAt: 1_000, |
| updatedAt: 1_000, |
| ...state, |
| } as const; |
| if (omitOutput) { |
| if (operation) throw new Error('Compact ShellRun fixtures cannot carry an operation'); |
| return compact; |
| } |
| const snapshot = { ...compact, output: output ?? ptyOutput() }; |
| return operation ? { ...snapshot, operation } : snapshot; |
| } |
| const { |
| mode: _mode, |
| output: explicitOutput, |
| stdout = '', |
| stderr = '', |
| stdoutTruncated = false, |
| stderrTruncated = false, |
| latestStream, |
| omitOutput, |
| operation, |
| ...state |
| } = overrides; |
| const output = explicitOutput ?? { |
| mode: 'pipes' as const, |
| stdout, |
| stderr, |
| ...(latestStream ? { latestStream } : {}), |
| stdoutTruncated, |
| stderrTruncated, |
| redacted: false, |
| }; |
| const compact = { |
| kind: 'shell_run', |
| ref: 'maka://runtime/background-tasks/bg-1', |
| mode: 'pipes', |
| status: 'running', |
| cwd: '/repo', |
| cmd: 'npm test', |
| revision: state.revision ?? state.updatedAt ?? state.completedAt ?? 1, |
| startedAt: 1_000, |
| updatedAt: 1_000, |
| ...state, |
| } as const; |
| if (omitOutput) { |
| if (operation) throw new Error('Compact ShellRun fixtures cannot carry an operation'); |
| return compact; |
| } |
| const snapshot = { ...compact, output }; |
| if (!operation) return snapshot; |
| if (operation.kind !== 'stop') { |
| throw new Error('Pipe ShellRun fixtures cannot carry a PTY control operation'); |
| } |
| return { ...snapshot, operation }; |
| } |
| |
| function ptyOutput(overrides: Partial<PtyShellOutput> = {}): PtyShellOutput { |
| return { |
| mode: 'pty', |
| screen: '', |
| scrollback: '', |
| cols: 80, |
| rows: 24, |
| cursor: { x: 0, y: 0, visible: true }, |
| alternateScreen: false, |
| truncated: false, |
| redacted: false, |
| ...overrides, |
| }; |
| } |
| |
| class RecordingDriver { |
| compactCalls = 0; |
| |
| constructor(private readonly events: SessionEvent[]) {} |
| |
| async *compactSession(): AsyncIterable<SessionEvent> { |
| this.compactCalls += 1; |
| for (const event of this.events) yield event; |
| } |
| } |
| |
| function event(input: { type: SessionEvent['type'] } & Record<string, unknown>): SessionEvent { |
| return { |
| id: `${input.type}-id`, |
| turnId: 'turn-1', |
| ts: 1, |
| ...input, |
| } as SessionEvent; |
| } |
| |
| function subagentResult( |
| overrides: Partial<Extract<ToolResultContent, { kind: 'subagent' }>> = {}, |
| ): Extract<ToolResultContent, { kind: 'subagent' }> { |
| return { |
| kind: 'subagent', |
| agentName: 'Local Read', |
| turnId: 'child-turn', |
| status: 'completed', |
| permissionMode: 'explore', |
| summary: 'done', |
| artifactIds: [], |
| ...overrides, |
| }; |
| } |
| |
| function stripAnsi(text: string): string { |
| return text.replace(/\x1b\[[0-9;]*m/g, ''); |
| } |