mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(story-mode): the rest of the PR review: lookups, polling, frame budget, linear links
From review round 7 (PR review): - With no hook path (normal after a server restart), the transcript was looked up by conversation id only; it now also tries the Codeman id and a recovered session's `restored-<fragment>` transcript, as last-response does, and keeps what it found briefly (a lookup reads every project folder; a miss is kept only 4 s). - A session Story Mode cannot draw is not polled, and a page with nothing drawn yet is polled like a finished one (every 15 s), not every 4 s. - Helper steps: one request per helper is drawn, the newest; a failed refetch never writes over steps already shown. - The Director's cut toggle, a skin change and the fonts arriving redraw within the frame budget, not every panel in one task. - A bare name under an `ls` header finds its folder in one pass over the lines; a search per link over the text before it was quadratic (about 1.5 s for 700 links on agent output). - A page of many questions still keeps to 12 panels: questions fold last. - Opening Story Mode while it is open never starts a second runtime; the status announcer carries data-i18n-skip; the turn key in the ETag is clipped. - The crafted-input timing test runs at sizes where the old code took 6 to 29 s and the new one under 10 ms, with a 1.5 s bound. - Docs: `older`, `origin` (`scheduled`), which route answers a 500; mobile.css's stale comment; an orphaned doc comment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e7ce3b8e56
commit
42d1c75a19
10 files changed
+191
-41
No files matched your search
@@ -8,7 +8,7 @@ import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createRouteTestHarness } from './_route-test-utils.js';
|
||||
import { registerStoryRoutes } from '../../src/web/routes/story-routes.js';
|
||||
import { clearStoryLookupCache, registerStoryRoutes } from '../../src/web/routes/story-routes.js';
|
||||
import { clearStoryReaderCache, readStoryEntries } from '../../src/story-reader.js';
|
||||
import { claudeProjectsDir } from '../../src/utils/claude-transcript.js';
|
||||
import { bash, demoTurn, edit, end, prompt, read, resetClock, result, said, tool } from '../story-fixtures.js';
|
||||
@@ -32,6 +32,7 @@ async function setup(opts: { hookPath?: string | null; mode?: string; remote?: u
|
||||
beforeEach(() => {
|
||||
resetClock();
|
||||
clearStoryReaderCache();
|
||||
clearStoryLookupCache();
|
||||
// Where Claude Code writes transcripts (under the test's temp HOME): the only place a hook path is read.
|
||||
mkdirSync(claudeProjectsDir(), { recursive: true });
|
||||
dir = mkdtempSync(join(claudeProjectsDir(), 'story-route-'));
|
||||
@@ -202,6 +203,14 @@ describe('GET /api/sessions/:id/story', () => {
|
||||
expect(res.json().data).toMatchObject({ supported: true, turns: [], page: null });
|
||||
});
|
||||
|
||||
it('with no hook path, finds the transcript by the Codeman id too (as last-response does)', async () => {
|
||||
const byId = join(dir, 'test-session-1.jsonl');
|
||||
writeFileSync(byId, jsonl(demoTurn()));
|
||||
const { app } = await setup({ hookPath: null });
|
||||
const body = (await app.inject({ method: 'GET', url: URL })).json();
|
||||
expect(body.data.page?.panels.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('says when the list leaves turns out past the 60 it shows, even in a small file', async () => {
|
||||
const lines: unknown[] = [];
|
||||
for (let i = 0; i < 80; i++) lines.push(prompt(`job ${i}`), said('ok'), end(1000));
|
||||
|
||||
@@ -666,6 +666,22 @@ describe('story-mode.js overlay', () => {
|
||||
expect($('.story-sr')!.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('a session Story Mode cannot draw is not polled', async () => {
|
||||
let poll: (() => void) | undefined;
|
||||
boot({ supported: false, reason: 'A shell session.' });
|
||||
win.setInterval = ((fn: () => void) => {
|
||||
poll = fn;
|
||||
return 1;
|
||||
}) as never;
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
app._story.lastIdlePoll = 0;
|
||||
calls.length = 0;
|
||||
poll!();
|
||||
await settle();
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('Tab stays inside the overlay', async () => {
|
||||
boot(storyOf(demoTurn()));
|
||||
app.openStoryMode(SID);
|
||||
|
||||
+24
-6
@@ -984,17 +984,35 @@ describe('buildStoryPage', () => {
|
||||
const prompt1 = (content: string) => () => buildStoryTurns(entriesOf([prompt(content), said('ok'), end(1000)]));
|
||||
const bashOut = (command: string, output: string) => () =>
|
||||
pageOf([prompt('go'), ...bash('b1', command, 'x', output), end(1000)]);
|
||||
// At these sizes the old code took 6 to 29 s per shape and the new one takes under 10 ms: the
|
||||
// bound sits two orders of magnitude from both, so a loaded runner cannot flake it.
|
||||
const shapes: Array<[string, () => unknown]> = [
|
||||
['teammate openers', prompt1(rep('<teammate-message teammate_id="a">x ', 400_000))],
|
||||
['system-reminder openers', prompt1('hi ' + rep('<system-reminder>x ', 400_000))],
|
||||
['task-notification field openers', prompt1('<task-notification>\n' + rep('<summary>x ', 400_000))],
|
||||
['git commit tokens', bashOut(rep('git commit ', 8000), 'ok')],
|
||||
['teammate openers', prompt1(rep('<teammate-message teammate_id="a">x ', 1_000_000))],
|
||||
['system-reminder openers', prompt1('hi ' + rep('<system-reminder>x ', 1_000_000))],
|
||||
['task-notification field openers', prompt1('<task-notification>\n' + rep('<summary>x ', 1_000_000))],
|
||||
['git commit tokens', bashOut(rep('git commit ', 16_000), 'ok')],
|
||||
[
|
||||
'modified-note phrases',
|
||||
bashOut('python3 x.py', rep("This command modified 1 file you've previously read: a", 400_000)),
|
||||
bashOut('python3 x.py', rep("This command modified 1 file you've previously read: a", 1_000_000)),
|
||||
],
|
||||
];
|
||||
for (const [name, fn] of shapes) expect([name, ms(fn) < 250]).toEqual([name, true]);
|
||||
for (const [name, fn] of shapes) expect([name, ms(fn) < 1500]).toEqual([name, true]);
|
||||
});
|
||||
|
||||
it('a turn of many questions still keeps to 12 panels: the oldest questions fold last', () => {
|
||||
const lines: unknown[] = [prompt('ask me a lot')];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
lines.push(
|
||||
tool(`q${i}`, 'AskUserQuestion', { questions: [{ question: `Question ${i}?`, options: [{ label: 'Yes' }] }] }),
|
||||
result(`q${i}`, 'ok', { toolUseResult: { answers: { [`Question ${i}?`]: 'Yes' } } }),
|
||||
...read(`r${i}`, `/w/f${i}.ts`)
|
||||
);
|
||||
}
|
||||
lines.push(end(1000));
|
||||
const page = pageOf(lines);
|
||||
expect(page.panels.length).toBeLessThanOrEqual(12);
|
||||
expect(page.panels[0].kind).toBe('mission');
|
||||
expect(page.panels.at(-1)!.kind).toBe('complete');
|
||||
});
|
||||
|
||||
it('a helper messaged twice reads as one finished run, not as superseded', () => {
|
||||
|
||||
Reference in new issue
Block a user