mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
`GET /api/sessions/:id/last-response` branches to a Codex-specific reader, then falls through to scanning `~/.claude/projects` for a transcript. OpenCode, Gemini and Antigravity render their own TUIs and never write one, so that scan finds nothing and the response viewer is permanently empty for all three modes. For these CLIs the pane IS the transcript, so segment it. `response-viewer-transcript.ts` is a pure, dependency-free parser that splits a terminal buffer into prompt / response / status / tool blocks, keying off the `›` prompt marker, status dividers and `• Calling|Called` tool-activity lines. The route uses it to answer with the LAST response, and to carry the parsed blocks under `?context=full`. Codex keeps its existing branch: it has real rollout files, which are a better source than scraped pane text. The response shape is unchanged for every other mode, and Claude panes are explicitly pinned to the Claude transcript path so a real transcript can never be shadowed by scraped text. Tests: 14 parser cases plus a route suite covering all three modes, the `?context=full` payload, an empty pane, and the Claude regression guard.
324 lines
13 KiB
TypeScript
324 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { getLastTranscriptResponse, parseExternalCliTranscript } from '../src/web/response-viewer-transcript.js';
|
||
|
||
describe('response viewer transcript parser', () => {
|
||
it('extracts structured COD transcript blocks and keeps labeled status/tool entries', () => {
|
||
const transcript = `
|
||
╭──────────────────────────────────────────────────────╮
|
||
│ >_ OpenAI Codex (v0.143.0) │
|
||
│ model: gpt-5.4 medium /model to change │
|
||
│ directory: /mnt/c/Users/aakhter/.../kb │
|
||
╰──────────────────────────────────────────────────────╯
|
||
|
||
Tip: Use /side to start a side conversation in a temporary fork without polluting the main thread.
|
||
|
||
› i need to create a naming recommendation for project helix
|
||
|
||
gpt-5.4 medium · kb · main · Ready · Context 100% left
|
||
|
||
• Explored
|
||
└ Read SKILL.md
|
||
|
||
Subject: Naming Recommendation for Project Helix
|
||
|
||
The product helps customers move virtual machine workloads between hypervisors while keeping operations
|
||
stable. It improves visibility into application dependencies, supports pre-migration validation, and reduces
|
||
the risk
|
||
and effort involved in moving workloads.
|
||
|
||
› Improve documentation in @filename
|
||
|
||
gpt-5.4 medium · kb · main · Ready · Context 92% left
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
|
||
expect(blocks.map((block) => block.kind)).toEqual([
|
||
'status',
|
||
'prompt',
|
||
'status',
|
||
'tool',
|
||
'response',
|
||
'prompt',
|
||
'status',
|
||
]);
|
||
expect(blocks[0]?.label).toBe('Status');
|
||
expect(blocks[3]?.label).toBe('Tool');
|
||
expect(blocks[4]?.text).toContain('reduces the risk and effort involved');
|
||
expect(blocks[4]?.text).not.toContain('reduces\nthe risk');
|
||
});
|
||
|
||
it('returns the most recent completed response when the last block is a new prompt', () => {
|
||
const transcript = `
|
||
› say again
|
||
|
||
Final polished answer
|
||
With two lines
|
||
|
||
› Improve documentation in @filename
|
||
|
||
gpt-5.4 medium · kb · main · Ready · Context 92% left
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
|
||
expect(getLastTranscriptResponse(blocks)).toBe('Final polished answer\nWith two lines');
|
||
});
|
||
|
||
it('drops box-drawing dividers from the last response and treats worked-for lines as status', () => {
|
||
const transcript = `
|
||
────────────────────────────────────────────────────────────────────────
|
||
|
||
• The launcher supports CODEMAN_APP_DIR, so I can deploy this exact worktree without merging it back first.
|
||
|
||
────────────────────────────────────────────────────────────────────────
|
||
|
||
• Deployed.
|
||
|
||
The local Codeman service is now running from the worktree at app/.worktrees/cod-215-response-viewer on
|
||
commit 8bbcf77bafbdbf01a34653386c256b685898ad06.
|
||
|
||
Verified:
|
||
|
||
- process pid: 2708947
|
||
- HTTPS health: https://127.0.0.1:3000/ returned 401 as expected
|
||
|
||
One detail: I had to restart it outside the sandbox because the sandboxed launch path could not see tmux.
|
||
|
||
─ Worked for 1m 51s ───────────────────────────────────────────────────
|
||
|
||
› what are these lines in the middle column?
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
|
||
expect(getLastTranscriptResponse(blocks)).toContain('• Deployed.');
|
||
expect(getLastTranscriptResponse(blocks)).not.toContain('────────────────');
|
||
expect(getLastTranscriptResponse(blocks)).not.toContain('Worked for 1m 51s');
|
||
expect(blocks.some((block) => block.kind === 'status' && block.text === 'Worked for 1m 51s')).toBe(true);
|
||
});
|
||
|
||
// COD-226: multiline prompt continuations (2-space Codex gutter) must stay in the
|
||
// Prompt block, not be misclassified as Response. The gutter is authoritative ahead
|
||
// of every structural detector (divider, prompt-marker, tool, status).
|
||
describe('COD-226 multiline prompt gutter is authoritative', () => {
|
||
it('keeps bullet/prose continuations and internal blank lines in the Prompt block', () => {
|
||
const transcript = `
|
||
› i think we can improve this slide. or maybe a follow on slide. here is what i'm thinking:
|
||
* left hand side: current AI stack (frontier model, cloud hosted, sovereign concerns)
|
||
right hand -> future enterprise stack: frontier model (optional, cloud), on-prem model router, OSS models
|
||
|
||
make the point that the right hand side addresses the concerns.
|
||
|
||
gpt-5.4 medium · kb · main · Ready · Context 100% left
|
||
|
||
• Explored
|
||
└ Read SKILL.md
|
||
|
||
Here is the actual assistant answer that starts at column zero and is a real response.
|
||
|
||
› next prompt
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
const promptBlocks = blocks.filter((b) => b.kind === 'prompt');
|
||
|
||
// Exactly two prompts, and the first holds the whole multiline prompt.
|
||
expect(promptBlocks).toHaveLength(2);
|
||
expect(promptBlocks[0]?.text).toContain('left hand side');
|
||
expect(promptBlocks[0]?.text).toContain('right hand');
|
||
expect(promptBlocks[0]?.text).toContain('make the point that the right hand side');
|
||
|
||
// The continuation must NOT have leaked into a Response block.
|
||
const responseBlocks = blocks.filter((b) => b.kind === 'response');
|
||
expect(responseBlocks.some((b) => b.text.includes('make the point'))).toBe(false);
|
||
expect(getLastTranscriptResponse(blocks)).toContain('actual assistant answer');
|
||
expect(getLastTranscriptResponse(blocks)).not.toContain('make the point');
|
||
});
|
||
|
||
it('does not let an indented divider inside a prompt flush the Prompt block', () => {
|
||
const transcript = `
|
||
› compare these two layouts
|
||
first layout uses a single column
|
||
────────────────────────────
|
||
second layout uses two columns
|
||
|
||
The response begins here at column zero.
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
const promptBlocks = blocks.filter((b) => b.kind === 'prompt');
|
||
|
||
expect(promptBlocks).toHaveLength(1);
|
||
expect(promptBlocks[0]?.text).toContain('first layout');
|
||
expect(promptBlocks[0]?.text).toContain('second layout uses two columns');
|
||
expect(getLastTranscriptResponse(blocks)).toBe('The response begins here at column zero.');
|
||
});
|
||
|
||
it('treats a gutter-indented literal › as prompt content, not a new prompt', () => {
|
||
const transcript = `
|
||
› here is my question about the ui
|
||
› should this arrow start a new prompt?
|
||
no it should not — it is part of my question
|
||
|
||
Answer at column zero.
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
const promptBlocks = blocks.filter((b) => b.kind === 'prompt');
|
||
|
||
// The gutter-indented › must NOT open a second prompt.
|
||
expect(promptBlocks).toHaveLength(1);
|
||
expect(promptBlocks[0]?.text).toContain('here is my question');
|
||
expect(promptBlocks[0]?.text).toContain('no it should not');
|
||
expect(getLastTranscriptResponse(blocks)).toBe('Answer at column zero.');
|
||
});
|
||
|
||
// The two exact live roadmap-tab examples from the ticket (AC: use both verbatim).
|
||
it('live example 1: two-line prompt keeps the gutter continuation in the Prompt block', () => {
|
||
const transcript = `
|
||
› create uid for each feature so it's easy to ref.
|
||
for the p200 - why is diffentiation only 2/5?
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
const promptBlocks = blocks.filter((b) => b.kind === 'prompt');
|
||
|
||
expect(promptBlocks).toHaveLength(1);
|
||
expect(promptBlocks[0]?.text).toContain('create uid for each feature');
|
||
expect(promptBlocks[0]?.text).toContain('for the p200 - why is diffentiation only 2/5?');
|
||
// The continuation must not have leaked into a Response block.
|
||
expect(blocks.some((b) => b.kind === 'response')).toBe(false);
|
||
});
|
||
|
||
it('live example 2: five-line prompt (bullet + prose + blank + prose) stays one Prompt block', () => {
|
||
const transcript = `
|
||
› i think we can improve this slide. or maybe a follow on slide. here is what i'm thinking:
|
||
* left hand side... current AI stack: (frontier model), large component cloud hosted, soverign concerns etc.
|
||
right hand -> future-enterprise-stack: frontier-model (optional, in cloud), on-prem: model router, OSS models, rest of stack (gpus, data, etc.)
|
||
|
||
make the point that the right hand side addresses the concerns.
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
const promptBlocks = blocks.filter((b) => b.kind === 'prompt');
|
||
|
||
expect(promptBlocks).toHaveLength(1);
|
||
expect(promptBlocks[0]?.text).toContain('left hand side');
|
||
expect(promptBlocks[0]?.text).toContain('right hand -> future-enterprise-stack');
|
||
expect(promptBlocks[0]?.text).toContain('make the point that the right hand side addresses the concerns');
|
||
expect(blocks.some((b) => b.kind === 'response')).toBe(false);
|
||
});
|
||
|
||
it('still separates a single-line prompt from a column-zero response (no regression)', () => {
|
||
const transcript = `
|
||
› say again
|
||
|
||
Final polished answer at column zero.
|
||
|
||
› next
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
const promptBlocks = blocks.filter((b) => b.kind === 'prompt');
|
||
|
||
expect(promptBlocks).toHaveLength(2);
|
||
expect(promptBlocks[0]?.text).toBe('say again');
|
||
expect(getLastTranscriptResponse(blocks)).toBe('Final polished answer at column zero.');
|
||
});
|
||
});
|
||
|
||
// COD-227: Last Response must return the final assistant answer, not tool logs.
|
||
// A response bullet beginning with a tool-like verb (• Created …) must not be
|
||
// classified as Tool, and genuine • Calling / • Called blocks must be classified
|
||
// as Tool. Disambiguator: a verb-bullet is a tool header only when followed by a
|
||
// box-drawing result tree (└│├); Calling/Called are always tool markers.
|
||
describe('COD-227 tool-header vs response disambiguation', () => {
|
||
const MINIMAL_REPRO = `
|
||
› new jira issue
|
||
|
||
• The fresh read shows a formatting problem.
|
||
|
||
• Calling
|
||
└ atlassian.jira_update_issue({})
|
||
|
||
• Called atlassian.jira_get_issue({})
|
||
└ { result: true }
|
||
|
||
• Created COD-226: View Response → More misclassifies multiline prompt continuations as responses.
|
||
|
||
It includes:
|
||
|
||
- Two concrete failures
|
||
- Regression-test criteria
|
||
`.trim();
|
||
|
||
it('returns the final • Created … answer, not the Jira Calling/Called tool log', () => {
|
||
const blocks = parseExternalCliTranscript(MINIMAL_REPRO, 'codex');
|
||
const last = getLastTranscriptResponse(blocks);
|
||
|
||
expect(last).toContain('Created COD-226');
|
||
expect(last).toContain('Two concrete failures');
|
||
// Must exclude tool invocations, raw results, and earlier commentary.
|
||
expect(last).not.toContain('atlassian.jira');
|
||
expect(last).not.toContain('Calling');
|
||
expect(last).not.toContain('Called');
|
||
expect(last).not.toContain('fresh read');
|
||
});
|
||
|
||
it('classifies genuine • Calling / • Called (with box-drawing results) as Tool', () => {
|
||
const blocks = parseExternalCliTranscript(MINIMAL_REPRO, 'codex');
|
||
const toolText = blocks
|
||
.filter((b) => b.kind === 'tool')
|
||
.map((b) => b.text)
|
||
.join('\n');
|
||
|
||
expect(toolText).toContain('Calling');
|
||
expect(toolText).toContain('Called');
|
||
expect(toolText).toContain('atlassian.jira_update_issue');
|
||
// The final answer must not have been swallowed into the tool block.
|
||
expect(toolText).not.toContain('Created COD-226');
|
||
});
|
||
|
||
it('labels the repro chronologically: Prompt, Response (commentary), Tool, Response (final)', () => {
|
||
const blocks = parseExternalCliTranscript(MINIMAL_REPRO, 'codex');
|
||
|
||
expect(blocks.map((b) => b.kind)).toEqual(['prompt', 'response', 'tool', 'response']);
|
||
expect(blocks[1]?.text).toContain('fresh read');
|
||
expect(blocks[3]?.text).toContain('Created COD-226');
|
||
});
|
||
|
||
it('does not classify a verb-prefixed prose bullet as Tool when no result tree follows', () => {
|
||
const transcript = `
|
||
› do it
|
||
|
||
• Created COD-999: a brand new issue with a descriptive title.
|
||
|
||
Follow-up prose that belongs to the same answer.
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
|
||
expect(blocks.some((b) => b.kind === 'tool')).toBe(false);
|
||
expect(getLastTranscriptResponse(blocks)).toContain('Created COD-999');
|
||
expect(getLastTranscriptResponse(blocks)).toContain('Follow-up prose');
|
||
});
|
||
|
||
it('does not regress a genuine verb tool block that has a box-drawing continuation', () => {
|
||
const transcript = `
|
||
› look around
|
||
|
||
• Explored
|
||
└ Read SKILL.md
|
||
|
||
Here is the assistant answer at column zero.
|
||
`.trim();
|
||
|
||
const blocks = parseExternalCliTranscript(transcript, 'codex');
|
||
|
||
expect(blocks.some((b) => b.kind === 'tool' && b.text.includes('Explored'))).toBe(true);
|
||
expect(getLastTranscriptResponse(blocks)).toBe('Here is the assistant answer at column zero.');
|
||
});
|
||
});
|
||
});
|