mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(story-mode): no celebration when the turn is not a clean finish
From review round 8: "Mission complete!" with confetti and cheering helpers sat over balloons saying the page will not work as opened, a helper's tests have not run, or a problem is left. The final panel is now calm (no confetti, a neutral robot) whenever work was still out at the turn's end, nothing worked, or Claude's last words name an open problem (`page.openNote`: a key point about something wrong, not done, not checked or not fixed); the last of these reads "Done, with a catch". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
faca7571e2
commit
88040c997d
17 files changed
+80
-7
No files matched your search
@@ -1074,7 +1074,7 @@ A turn is one prompt and everything Claude did for it. `origin` says who sent th
|
||||
|
||||
### Background work
|
||||
|
||||
A helper started in the background (`run_in_background`, or as a teammate: `status: teammate_spawned`) has `helper.background` and `helper.running: true` until it reports back. A `<task-notification>` (matched by tool call) or a `<teammate-message>` (matched by name) joins the answer, tool count and duration back onto the helper in the turn that started it. A report that arrives after that turn ended starts a turn of its own (origin `task`) whose prompt event carries the report as `output` and the helper as `helper`; one that lands mid-turn is an event with intent `notice` instead of a new mission. `page.helpersRunning` counts the helpers still out, and `page.finishedLater` the helpers and background commands that finished only after their turn ended (each such event carries `finishedLater: true`); the final panel then says so instead of a clean finish.
|
||||
A helper started in the background (`run_in_background`, or as a teammate: `status: teammate_spawned`) has `helper.background` and `helper.running: true` until it reports back. A `<task-notification>` (matched by tool call) or a `<teammate-message>` (matched by name) joins the answer, tool count and duration back onto the helper in the turn that started it. A report that arrives after that turn ended starts a turn of its own (origin `task`) whose prompt event carries the report as `output` and the helper as `helper`; one that lands mid-turn is an event with intent `notice` instead of a new mission. `page.helpersRunning` counts the helpers still out, and `page.finishedLater` the helpers and background commands that finished only after their turn ended (each such event carries `finishedLater: true`); the final panel then says so instead of a clean finish. `page.openNote` says Claude's last words name an open problem or something not done or checked (one of its `keyPoints`), so that panel is not a celebration either.
|
||||
|
||||
A Bash call sent to the background (its result is only `Command running in background with ID: …`) has `running: true` and no `ok`: it started, nothing more is known, and its `output` says so instead of carrying the launch note meant for Claude. Its `<task-notification>` settles `ok` from the exit code in the summary (the `output` then names the exit code, and `background: true` stays, so the page says the run happened in the background), and reading the job's output file later is a test run when that output reports test counts.
|
||||
|
||||
|
||||
+15
-4
@@ -275,9 +275,14 @@ export function balloonText(text: string): string {
|
||||
* expected, a correction of an earlier warning. WEAK: a bare negation ("I didn't push"), kept only when
|
||||
* nothing strong is there.
|
||||
*/
|
||||
/** The open-problem part of a key point: something wrong, not done, not checked, or still out. */
|
||||
const OPEN_PROBLEM = new RegExp(
|
||||
String.raw`\b(problem|known (issue|quirk)s?|quirks?|caveat|limitation|heads up|warning|not yet|haven't|have not|never|unfixed|left (open|unfixed|out)|still (fails?|failing|broken|missing|open|running|working)|must be (served|run)|(won't|doesn't|does not|will not) (work|fix)|(isn't|is not|not) fixed|can't tell|isn't (confirmed|committed|pushed|tested|done|checked)|not (checked|tested|verified|confirmed|committed|pushed|opened))\b`,
|
||||
'i'
|
||||
);
|
||||
const KEY_POINT_STRONG = new RegExp(
|
||||
[
|
||||
String.raw`\b(problem|known (issue|quirk)s?|quirks?|caveat|limitation|heads up|warning|not yet|haven't|have not|never|unfixed|left (open|unfixed|out)|still (fails?|failing|broken|missing|open|running|working)|must be (served|run)|(won't|doesn't|does not|will not) work|can't tell|isn't (confirmed|committed|pushed|tested|done|checked)|not (checked|tested|verified|confirmed|committed|pushed|opened))\b`,
|
||||
OPEN_PROBLEM.source,
|
||||
String.raw`\b(as you (asked|expected|said|wanted|suggested)|you asked|your (--?[\w-]+|request|option|flag|change))\b`,
|
||||
String.raw`\b(out of date|looked too early|turned out|was wrong)\b`,
|
||||
].join('|'),
|
||||
@@ -1991,7 +1996,7 @@ const REPORT_TALK: Partial<Record<StoryPanelKind, RegExp>> = {
|
||||
};
|
||||
const ANNOUNCES = /\b(I'll|I will|let me|let's|I'm going to|next,? I|now I)\b/i;
|
||||
|
||||
function groupPanels(events: StoryEvent[]): { panels: DraftPanel[]; trailingSay?: string } {
|
||||
function groupPanels(events: StoryEvent[]): { panels: DraftPanel[]; trailingSay?: string; openNote?: boolean } {
|
||||
const panels: DraftPanel[] = [];
|
||||
let pendingSay: string | undefined;
|
||||
let lastSaid: StoryEvent | undefined;
|
||||
@@ -2037,7 +2042,12 @@ function groupPanels(events: StoryEvent[]): { panels: DraftPanel[]; trailingSay?
|
||||
pendingSay = undefined;
|
||||
});
|
||||
// The closing words keep a caveat that follows them ("Nothing is committed: this is no git repo.").
|
||||
return { panels, trailingSay: lastSaid !== undefined ? closingText(lastSaid.label, lastSaid.keyPoints) : pendingSay };
|
||||
return {
|
||||
panels,
|
||||
trailingSay: lastSaid !== undefined ? closingText(lastSaid.label, lastSaid.keyPoints) : pendingSay,
|
||||
// Claude's last words name an open problem: the final panel is not a celebration.
|
||||
openNote: !!lastSaid?.keyPoints?.some((k) => OPEN_PROBLEM.test(k)),
|
||||
};
|
||||
}
|
||||
|
||||
/** Fix → check → fix → check (4 panels or more) is one "loop" panel: try, test, repeat. */
|
||||
@@ -2611,7 +2621,7 @@ export function buildStoryPage(turn: StoryTurn): StoryPage {
|
||||
if (todos) e.todos = todos;
|
||||
}
|
||||
|
||||
const { panels: grouped, trailingSay } = groupPanels(events);
|
||||
const { panels: grouped, trailingSay, openNote } = groupPanels(events);
|
||||
const room = STORY_LIMITS.panelsPerPage - (turn.ended ? 1 : 0);
|
||||
const drafts = capPanels(collapseLoops(grouped), room);
|
||||
const panels = drafts.map((d, i) => decorate(d, events, turn, i));
|
||||
@@ -2628,6 +2638,7 @@ export function buildStoryPage(turn: StoryTurn): StoryPage {
|
||||
// The advisor is a second opinion, not a helper that does work.
|
||||
helpers: events.filter((e) => e.helper && e.tool !== 'Advisor' && e.intent !== 'prompt').length,
|
||||
helpersRunning: events.filter((e) => e.helper?.running).length,
|
||||
...(openNote && turn.ended && !turn.interrupted ? { openNote: true } : {}),
|
||||
...(events.some((e) => e.finishedLater && e.intent !== 'prompt')
|
||||
? { finishedLater: events.filter((e) => e.finishedLater && e.intent !== 'prompt').length }
|
||||
: {}),
|
||||
|
||||
@@ -210,6 +210,8 @@ export interface StoryPage extends StoryTurnSummary {
|
||||
helpersRunning: number;
|
||||
/** Helpers and background commands of this turn that finished only after it ended. */
|
||||
finishedLater?: number;
|
||||
/** Claude's last words name an open problem or something not done or checked: no clean finish. */
|
||||
openNote?: boolean;
|
||||
/** Every tool call of the turn failed (a blocked command, nothing that worked): no celebration. */
|
||||
noneWorked?: boolean;
|
||||
}
|
||||
|
||||
@@ -1436,7 +1436,14 @@ const StoryArt = (() => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (p.page.noneWorked) {
|
||||
// Not a clean finish (nothing worked, work still out, or Claude named an open problem): no party.
|
||||
const calm =
|
||||
p.page.noneWorked ||
|
||||
p.page.helpersRunning ||
|
||||
p.page.finishedLater ||
|
||||
p.page.openNote ||
|
||||
p.page.events.some((e) => e.running && !e.helper);
|
||||
if (calm) {
|
||||
agent(ctx, ax, 250, { frame: f, scale: sc, mood: 'neutral', l: [-34, -48], r: [34, -48] });
|
||||
return;
|
||||
}
|
||||
@@ -2484,7 +2491,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
: page.helpersRunning || page.finishedLater || page.events.some((e) => e.running && !e.helper)
|
||||
? // Work was still out when the turn ended (and maybe still is): not a clean finish.
|
||||
'Done for now'
|
||||
: 'Mission complete!';
|
||||
: page.openNote
|
||||
? 'Done, with a catch'
|
||||
: 'Mission complete!';
|
||||
const chips = rt.splash.querySelector('.story-chips');
|
||||
const items = [];
|
||||
if (page.durationMs !== undefined) items.push({ text: this._storyDuration(page.durationMs) });
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 2,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 2,
|
||||
"openNote": true,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -81,6 +82,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -127,6 +129,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -176,6 +179,7 @@
|
||||
"helpers": 1,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 2,
|
||||
"openNote": true,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -242,6 +246,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -291,6 +296,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -337,6 +343,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -396,6 +403,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -432,6 +440,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 1,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 1,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -108,6 +109,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -164,6 +166,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -101,6 +102,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -147,6 +149,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -155,6 +156,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -227,6 +229,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -306,6 +309,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -104,6 +105,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -160,6 +161,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": true,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 2,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 2,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -111,6 +112,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": true,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -167,6 +169,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -226,6 +229,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -318,6 +322,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": false,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 1,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 1,
|
||||
"openNote": true,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -121,6 +122,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": true,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"helpers": 0,
|
||||
"helpersRunning": 0,
|
||||
"finishedLater": 0,
|
||||
"openNote": true,
|
||||
"panels": [
|
||||
{
|
||||
"kind": "mission",
|
||||
|
||||
@@ -40,6 +40,7 @@ function pagesOf(name: string) {
|
||||
helpers: p.helpers,
|
||||
helpersRunning: p.helpersRunning,
|
||||
finishedLater: p.finishedLater ?? 0,
|
||||
openNote: p.openNote ?? false,
|
||||
panels: p.panels.map((x) => ({
|
||||
kind: x.kind,
|
||||
ok: x.ok ?? null,
|
||||
@@ -159,6 +160,8 @@ describe('Story Mode on real sessions', () => {
|
||||
const last = (name: string, turn: number) => pagesOf(name)[turn].panels.at(-1)!;
|
||||
// An open problem, said further into the final message than its first sentence.
|
||||
expect(last('web-page-node-test', 0).balloon).toMatch(/won't work if you double-click index\.html/);
|
||||
// ...and the turn is not drawn as a clean finish.
|
||||
expect(pagesOf('web-page-node-test')[0].openNote).toBe(true);
|
||||
expect(last('text-plan-dry-run-helper', 1).balloon).toMatch(/doesn't fix it/);
|
||||
// What you expected, and what you asked for mid-turn.
|
||||
expect(last('errors-push-append', 2).balloon).toMatch(/As you expected, git describe --tags then failed/);
|
||||
|
||||
@@ -851,6 +851,21 @@ describe('story-mode.js overlay', () => {
|
||||
expect($$('.story-ln--add').map((l) => l.textContent)).toEqual(['+more']);
|
||||
});
|
||||
|
||||
it('a turn whose last words name an open problem ends "Done, with a catch", not "Mission complete!"', async () => {
|
||||
const words = [
|
||||
'I built the pomodoro timer and its tests, and all of them pass.',
|
||||
'It keeps the time in memory and redraws the clock every second while it runs. '.repeat(3),
|
||||
"Opening the page: it won't work if you double-click index.html.",
|
||||
].join(' ');
|
||||
boot(storyOf([prompt('build it'), ...edit('e1', '/work/demo-shop/t.js', ['+x']), said(words), end(1000)]));
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
expect($('.story-panel[data-kind="complete"] .story-splash-title')!.textContent).toBe('Done, with a catch');
|
||||
expect($('.story-panel[data-kind="complete"] .story-balloon-text')!.textContent).toMatch(
|
||||
/won't work if you double-click/
|
||||
);
|
||||
});
|
||||
|
||||
it('the final panel counts the commits of the whole turn, and says when the work was uploaded', async () => {
|
||||
boot(
|
||||
storyOf([
|
||||
|
||||
Reference in new issue
Block a user