mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(story-mode): appends without invented line numbers, printed files you can open, whose words start a turn
The rest of review round 5 (a phone with touch): - Text appended with `cat >> f <<EOF` or `echo x >> f` was drawn as lines 1 to 8 of the file; where an append lands is not in the command, so the diff now says "Added at the end of the file (8 lines)" with no numbers. - A command that prints a file and does something else (`cat textstats.py && pytest`) kept no record of the file; it is now on the event, resolved against the command's own folder, and the output drawer offers "Open textstats.py". (A file rewritten by a python here-document still cannot be diffed from the command alone; Claude Code's own "modified" note, when it comes, is used.) - A background job's report that starts a turn was a balloon credited to "you" in the API (the page already drew it as a note); its speaker is now "note". - The output drawer tagged a command still running in the background "ok"; it says "in the background". - The Header buttons hint says that on a phone, Story Mode opens from Session Options. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
76d5c61eda
commit
6a1bac6ebe
7 files changed
+119
-15
No files matched your search
+17
-8
@@ -446,11 +446,12 @@ function echoWrites(command: string, cwd?: string): StoryEdit[] {
|
||||
created: false,
|
||||
hunks: [
|
||||
{
|
||||
oldStart: m[5] === '>>' ? 1 : 0,
|
||||
oldStart: 0,
|
||||
oldLines: 0,
|
||||
newStart: 1,
|
||||
newStart: m[5] === '>>' ? 0 : 1,
|
||||
newLines: lines.length,
|
||||
lines: lines.slice(0, 40).map((l) => `+${clip(l, STORY_LIMITS.lineChars)}`),
|
||||
...(m[5] === '>>' ? { atEnd: true } : {}),
|
||||
},
|
||||
],
|
||||
truncated: lines.length > 40,
|
||||
@@ -610,7 +611,11 @@ export function heredocWrites(command: string, cwd?: string): StoryEdit[] {
|
||||
added: body.length,
|
||||
removed: 0,
|
||||
created: false,
|
||||
hunks: [{ oldStart: append ? 1 : 0, oldLines: 0, newStart: 1, newLines: body.length, lines: kept }],
|
||||
hunks: [
|
||||
append
|
||||
? { oldStart: 0, oldLines: 0, newStart: 0, newLines: body.length, lines: kept, atEnd: true }
|
||||
: { oldStart: 0, oldLines: 0, newStart: 1, newLines: body.length, lines: kept },
|
||||
],
|
||||
truncated: kept.length < body.length,
|
||||
});
|
||||
}
|
||||
@@ -814,10 +819,12 @@ function reduceInput(name: string, input: unknown, cwd?: string): ReducedInput {
|
||||
input.headline = clip(segment, 200);
|
||||
const writes = heredocWrites(full, cwd);
|
||||
if (writes.length) input.writes = writes;
|
||||
if (intent === 'look') {
|
||||
const reads = printedFiles(full);
|
||||
if (reads.length) input.reads = reads;
|
||||
}
|
||||
// Files it prints, whatever else it does (`cat a.py && pytest`): the drawer offers them. A
|
||||
// relative name resolves against the line's cwd (printedFiles gives none after a `cd`).
|
||||
const reads = printedFiles(full).map((f) =>
|
||||
f.startsWith('/') || f.startsWith('~') || !cwd ? f : `${cwd.replace(/\/+$/, '')}/${f.replace(/^\.\//, '')}`
|
||||
);
|
||||
if (reads.length) input.reads = reads;
|
||||
{
|
||||
// Counted whatever wins: a test run that ends in a commit still saves the work.
|
||||
const heads = commandSegments(withoutHeredocBodies(full.slice(0, 4000))).map(segmentHead);
|
||||
@@ -1705,7 +1712,7 @@ const MINOR = new Set<StoryPanelKind>(['investigate', 'research', 'plan', 'run',
|
||||
interface DraftPanel {
|
||||
kind: StoryPanelKind;
|
||||
events: number[];
|
||||
balloon?: { who: 'you' | 'claude'; text: string };
|
||||
balloon?: { who: 'you' | 'claude' | 'note'; text: string };
|
||||
rounds?: number;
|
||||
}
|
||||
|
||||
@@ -1959,6 +1966,8 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
: origin === 'peer'
|
||||
? 'Another session chimes in while Claude works.'
|
||||
: lines[origin];
|
||||
// Words that started the turn but are not yours are a note, not your balloon.
|
||||
if (panel.balloon && !command && origin !== 'human') panel.balloon = { ...panel.balloon, who: 'note' };
|
||||
director = command
|
||||
? `Command · ${clip(evs[0].label, 60)}`
|
||||
: `Prompt · ${plural(words, 'word')}${origin === 'human' ? '' : ` · ${origin}`}`;
|
||||
|
||||
+4
-1
@@ -61,6 +61,8 @@ export interface StoryHunk {
|
||||
newStart: number;
|
||||
newLines: number;
|
||||
lines: string[];
|
||||
/** Lines appended at the end of the file (`>>`): where they start is not known, so no line numbers. */
|
||||
atEnd?: boolean;
|
||||
}
|
||||
|
||||
/** What one Edit/Write call changed. Counts are exact even when `hunks` was cut to size. */
|
||||
@@ -152,7 +154,8 @@ export interface StoryPanel {
|
||||
/** Indices into the page's `events`. */
|
||||
events: number[];
|
||||
caption: { story: string; director: string };
|
||||
balloon?: { who: 'you' | 'claude'; text: string };
|
||||
/** `note`: words that start a turn but are not yours (a background job's report, another session). */
|
||||
balloon?: { who: 'you' | 'claude' | 'note'; text: string };
|
||||
files?: string[];
|
||||
notes?: string[];
|
||||
helpers?: Array<{ name: string; type: string; task?: string; event: number }>;
|
||||
|
||||
@@ -2068,7 +2068,7 @@
|
||||
|
||||
<div class="set-group">
|
||||
<div class="set-group-head"><h4>Header buttons</h4><span class="set-scope">device</span></div>
|
||||
<p class="set-group-hint">Tap to show a control in the header. Multi-monitor is the one entry here that syncs across devices.</p>
|
||||
<p class="set-group-hint">Tap to show a control in the header. Multi-monitor is the one entry here that syncs across devices. On a phone, Story Mode opens from Session Options instead.</p>
|
||||
<div class="set-group-body">
|
||||
<div class="set-chips" data-search="header buttons plan usage font stats lifecycle response story file viewer attachments monitor session away cron redraw split tiles">
|
||||
<label class="set-chip" data-preview="header" data-preview-order="1" data-preview-text="A+"><input type="checkbox" id="appSettingsShowFontControls"><svg class="set-chip-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20 10 5l6 15"/><path d="M6.5 15h7"/><path d="M18 12h4M20 10v4"/></svg><span>Font Size</span></label>
|
||||
|
||||
@@ -820,6 +820,11 @@ body.story-open .toast-container {
|
||||
background: var(--story-panel);
|
||||
border: 2px solid var(--story-ink);
|
||||
}
|
||||
.story-out-files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.story-df-head,
|
||||
.story-out-head {
|
||||
position: sticky;
|
||||
|
||||
@@ -3243,7 +3243,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
const code = el('div', 'story-code');
|
||||
for (const h of edit.hunks || []) {
|
||||
code.append(
|
||||
el('div', 'story-ln story-ln--hunk', `@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@`)
|
||||
el(
|
||||
'div',
|
||||
'story-ln story-ln--hunk',
|
||||
h.atEnd
|
||||
? `Added at the end of the file (${h.newLines} ${h.newLines === 1 ? 'line' : 'lines'})`
|
||||
: `@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@`
|
||||
)
|
||||
);
|
||||
let o = h.oldStart;
|
||||
let n = h.newStart;
|
||||
@@ -3251,8 +3257,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
const mark = line[0];
|
||||
const row = el('div', `story-ln${mark === '+' ? ' story-ln--add' : mark === '-' ? ' story-ln--del' : ''}`);
|
||||
row.append(
|
||||
el('span', 'story-ln-no', mark === '+' ? '' : String(o)),
|
||||
el('span', 'story-ln-no', mark === '-' ? '' : String(n)),
|
||||
el('span', 'story-ln-no', mark === '+' || h.atEnd ? '' : String(o)),
|
||||
el('span', 'story-ln-no', mark === '-' || h.atEnd ? '' : String(n)),
|
||||
el('span', 'story-ln-mk', mark === '+' ? '+' : mark === '-' ? '−' : ''),
|
||||
el('span', 'story-ln-tx', line.slice(1) || ' ')
|
||||
);
|
||||
@@ -3273,10 +3279,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
const shown = this._storyShortPath(ev.label, { dot: ev.tool === 'Bash' });
|
||||
const cmd = el('span', 'story-out-cmd', `${ev.tool === 'Bash' ? '$ ' : `${ev.tool} `}${shown}`);
|
||||
if (ev.label !== shown) cmd.title = ev.label;
|
||||
const waiting = ev.pending || ev.running;
|
||||
const status = el(
|
||||
'span',
|
||||
`story-tag story-tag--${ev.ok === false ? 'bad' : ev.pending ? 'pending' : 'ok'}`,
|
||||
ev.ok === false ? 'error' : ev.pending ? 'running' : 'ok'
|
||||
`story-tag story-tag--${ev.ok === false ? 'bad' : waiting ? 'pending' : 'ok'}`,
|
||||
ev.ok === false ? 'error' : ev.running ? 'in the background' : waiting ? 'running' : 'ok'
|
||||
);
|
||||
head.append(cmd, status);
|
||||
section.append(head);
|
||||
@@ -3296,6 +3303,18 @@ Object.assign(CodemanApp.prototype, {
|
||||
b.dataset.storyOpen = `diff:event:${index}`;
|
||||
section.append(b);
|
||||
}
|
||||
// Files it printed (`cat a.py`): open them as they are now.
|
||||
if (ev.files?.length && ev.intent !== 'look') {
|
||||
const row = el('div', 'story-out-files');
|
||||
for (const f of ev.files.slice(0, 4)) {
|
||||
const b = el('button', 'story-btn-s', `Open ${f.split('/').pop()}`);
|
||||
b.type = 'button';
|
||||
b.title = f;
|
||||
b.addEventListener('click', (e) => this._storyOpenFile(this._storyResolvePath(f), e.currentTarget));
|
||||
row.append(b);
|
||||
}
|
||||
section.append(row);
|
||||
}
|
||||
} else if (ev.outputTruncated) {
|
||||
section.append(el('p', 'story-empty', 'This output was left out to keep a long turn fast.'));
|
||||
} else if (target && !target.startsWith('output:')) {
|
||||
|
||||
Reference in new issue
Block a user