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:
Codeman maintainer committed 2026-10-18 15:23:19 +02:00
1 parent faca7571e2
commit 88040c997d
17 files changed
+80 -7

No files matched your search

+15 -4
View File
@@ -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 }
: {}),
+2
View File
@@ -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;
}
+11 -2
View File
@@ -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) });