fix(story-mode): read how current Claude Code builds deliver prompts and end turns

Found by replaying round-2 review transcripts while the reviews ran, then
surveyed across 150 local transcripts:

- A prompt typed while Claude works writes no user line in current builds,
  only a queued_command attachment where Claude took it in (602 of them
  locally, 485 of those background-task reports). It now joins the running
  turn; a report that lands mid-turn is a notice on the wire, not a new
  mission panel, and a background helper's answer joins back from it.
- Some sessions never write turn_duration (13 of 135 locally): the Stop
  hook's stop_hook_summary (never preventedContinuation in 815 seen) now
  ends the turn, timed by its own span. Before, such a session's turns
  never ended.
- A helper started as a teammate (teammate_spawned) is a background helper;
  its <teammate-message> reports (text or a JSON idle notification) join it
  by name, and the turn they start reads "readme-writer: README done".
- A <cross-session-message> names its sender; pasted text loses Claude
  Code's <pasted_content> wrapper (it showed in balloons and the picker).
- The mission panel, its balloon and its drawer follow who sent each
  prompt (you, a background job, another session), not who started the turn.
- A caption names a script by its first line that says something, not
  `set -e`, a bare `cd` or `S=/tmp/...`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 10:41:14 +02:00
1 parent 03cb3a5961
commit 848566828f
5 files changed
+343 -26

No files matched your search

+22 -5
View File
@@ -735,7 +735,15 @@ const StoryArt = (() => {
},
draw(ctx, W, H, f, p, hit) {
const ax = Math.max(W * 0.78, W - 104);
hit('prompt', ax - 56, 110, 112, 140, p.page?.origin === 'task' ? 'Read the report' : 'Read the whole prompt');
const from = p.page?.events[p.panel.events[0]]?.origin;
hit(
'prompt',
ax - 56,
110,
112,
140,
from === 'task' ? 'Read the report' : from === 'peer' ? 'Read the message' : 'Read the whole prompt'
);
const a = agent(ctx, ax, 244, {
frame: f,
mood: 'listen',
@@ -2069,7 +2077,9 @@ Object.assign(CodemanApp.prototype, {
rt.balloon.append(balloonSpan);
}
balloonSpan.textContent = balloonText || '';
const notice = p.kind === 'mission' && index === 0 && page.origin && page.origin !== 'human';
// A prompt somebody else sent (a background job, another session) is a notice, not your speech.
const origin = page.events[p.events[0]]?.origin ?? (index === 0 ? page.origin : 'human');
const notice = p.kind === 'mission' && origin && origin !== 'human';
rt.balloon.className = `story-balloon ${
notice
? 'story-balloon--note'
@@ -2646,9 +2656,16 @@ Object.assign(CodemanApp.prototype, {
source.view === 'diff' && items.length && !items.some((x) => x.ev.edit || x.ev.pending) ? 'output' : source.view;
if (view === 'prompt') {
const prompts = items.filter((x) => x.ev.intent === 'prompt');
const task = page.origin === 'task' && prompts[0]?.ev.id === page.events[0]?.id;
kind = task ? 'Report' : 'Mission';
title = task ? 'A background job reports back' : prompts.length > 1 ? 'What you asked' : 'Your prompt';
const from = prompts[0]?.ev.origin ?? 'human';
kind = from === 'task' ? 'Report' : from === 'peer' ? 'Message' : 'Mission';
title =
from === 'task'
? 'A background job reports back'
: from === 'peer'
? 'A message from another session'
: prompts.length > 1
? 'What you asked'
: 'Your prompt';
for (const { ev } of prompts) {
out.push(el('pre', 'story-pre story-prompt', ev.label));
if (ev.output !== undefined) {