diff --git a/src/story-mode.ts b/src/story-mode.ts index 4666cece..acd6daa0 100644 --- a/src/story-mode.ts +++ b/src/story-mode.ts @@ -2434,6 +2434,10 @@ function touchedBefore(file: string, before: StoryEvent[]): boolean { return before.some((e) => editsOf(e).some((d) => d.file === file)); } +/** A command made only of git lookups ("git log --oneline; git status -sb", "git -C origin.git log"). */ +const GIT_LOOK = + /^(?:(?:git (?:-C \S+ |--git-dir=\S+ )?(?:log|status|show|ls-remote|branch|remote|describe|rev-parse|diff)\b[^;&|\n]*|(?:echo|head|tail|wc|sort)\b[^;&|\n]*)\s*(?:&&|\|\||;|\||\n|$)\s*)+$/; + /** One or two files by name ("calc.py and test_calc.py"), more by count ("3 files"). */ const fileWords = (files: string[], noun = 'file'): string => files.length && files.length <= 2 ? files.map((f) => basename(f)).join(' and ') : plural(files.length, noun); @@ -2543,6 +2547,8 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde const shown = files.filter((f) => !/(^|\/)\.codeman[-.]/.test(f)); // A file or two by name; in a turn a report started, reading is checking what came back. const checking = turn.origin === 'task' || turn.origin === 'peer'; + // Only git lookups (log, status, ls-remote) after a save in this turn: Claude checks its save. + const savedBefore = events.slice(0, Math.min(...draft.events)).some((e) => (e.commits ?? 0) > 0 || !!e.ships); const named = shown.map((f) => basename(f)); story = evs.every((e) => e.tool === 'ToolSearch') ? evs.some((e) => /PlanMode/.test(e.label)) @@ -2554,7 +2560,9 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde ? `Claude reads ${plural(shown.length, 'file')} to find its way around.` : evs.every((e) => e.intent === 'search') ? 'Claude searches the code.' - : 'Claude looks around the project.'; + : savedBefore && evs.every((e) => e.tool === 'Bash' && GIT_LOOK.test(e.label.trim())) + ? 'Claude checks what it saved.' + : 'Claude looks around the project.'; // A tool lookup's query (`select:EnterPlanMode,ExitPlanMode`) means nothing to a reader. if (evs.every((e) => e.tool === 'ToolSearch')) panel.notes = []; break; diff --git a/test/fixtures/story-corpus/errors-push-append.expected.json b/test/fixtures/story-corpus/errors-push-append.expected.json index cba63484..eb7a42b7 100644 --- a/test/fixtures/story-corpus/errors-push-append.expected.json +++ b/test/fixtures/story-corpus/errors-push-append.expected.json @@ -205,7 +205,7 @@ { "kind": "investigate", "ok": null, - "story": "Claude looks around the project.", + "story": "Claude checks what it saved.", "unaskedSave": false, "director": "Bash · git --git-dir=origin.git log --oneline master; git status -…", "balloon": null, @@ -298,7 +298,7 @@ { "kind": "investigate", "ok": null, - "story": "Claude looks around the project.", + "story": "Claude checks what it saved.", "unaskedSave": false, "director": "Bash · git --git-dir=origin.git log --oneline -1 master; git statu…", "balloon": null, diff --git a/test/fixtures/story-corpus/long-turn-prompt-added.expected.json b/test/fixtures/story-corpus/long-turn-prompt-added.expected.json index d9787ed0..678a212f 100644 --- a/test/fixtures/story-corpus/long-turn-prompt-added.expected.json +++ b/test/fixtures/story-corpus/long-turn-prompt-added.expected.json @@ -313,7 +313,7 @@ { "kind": "investigate", "ok": null, - "story": "Claude looks around the project.", + "story": "Claude checks what it saved.", "unaskedSave": false, "director": "Bash · git log --oneline", "balloon": "Step 16:", diff --git a/test/story-mode.test.ts b/test/story-mode.test.ts index 051e3c7f..e99b1841 100644 --- a/test/story-mode.test.ts +++ b/test/story-mode.test.ts @@ -1084,6 +1084,25 @@ describe('buildStoryPage', () => { expect(page.panels.map((p) => p.caption.story)).toContain('Claude changes run.sh; the tests pass.'); }); + it('git lookups after a save are Claude checking its save, and stay fast on a crafted command', () => { + const look = (saveFirst: boolean, command: string) => + pageOf([ + prompt('go'), + ...(saveFirst ? bash('b0', 'git commit -qm "add a"', 'Commit', '') : []), + ...bash('b1', command, 'Look', 'ok'), + end(1000), + ]).panels.find((p) => p.kind === 'investigate')?.caption.story; + expect(look(true, 'git --git-dir=origin.git log --oneline -1 master; git status -sb | head -1')).toBe( + 'Claude checks what it saved.' + ); + expect(look(false, 'git log --oneline; git status -sb | head -1')).toBe('Claude looks around the project.'); + // The wording reads the event's display label (clipped), so a long command costs nothing; the + // panel's kind still comes from the whole command. + const start = performance.now(); + expect(look(true, `git log ${'a '.repeat(100_000)}&& cat x`)).toBeTypeOf('string'); + expect(performance.now() - start).toBeLessThan(1500); + }); + it('a short ending keeps the sentence that leaves something open', () => { const page = pageOf([ prompt('run them'),