release 1.0.1: fix resume showing a blank transcript
A resumed session loaded its wire messages but never rebuilt the on-screen history, so -r/-c and /resume rendered a blank transcript. Convert the stored messages back into transcript lines and seed the panel with them at mount and after /resume.
This commit is contained in:
+13
-3
@@ -36,7 +36,7 @@ import { CommandMenu, InstallConfirm, Picker } from './Pickers';
|
||||
import { contextPanel, costPanel, todosPanel, toolsPanel } from './panel-bodies';
|
||||
import { PromptInput } from './PromptInput';
|
||||
import { accent, glyph } from './theme';
|
||||
import { nextKey, resultSummary, toolDetail, withResult, type Line, type NewLine } from './transcript';
|
||||
import { historyFromMessages, nextKey, resultSummary, toolDetail, withResult, type Line, type NewLine } from './transcript';
|
||||
|
||||
export { createApprovalBridge, createNoticeBus, createSubagentBus, applySubagentEvent };
|
||||
export type { ApprovalBridge, NoticeBus, SubagentBus };
|
||||
@@ -128,7 +128,11 @@ export function App({
|
||||
stdout.off('resize', onResize);
|
||||
};
|
||||
}, [stdout]);
|
||||
const [history, setHistory] = useState<Line[]>([]);
|
||||
// Seeded from the resumed history: a session loaded with -r/-c should show its
|
||||
// saved conversation rather than a blank transcript. Only read once, at mount.
|
||||
const [history, setHistory] = useState<Line[]>(() =>
|
||||
session.messages.length === 0 ? [] : historyFromMessages(session.messages as { role?: string; content?: unknown }[]),
|
||||
);
|
||||
const [draft, setDraft] = useState('');
|
||||
const [live, setLive] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -684,7 +688,13 @@ export function App({
|
||||
push({ kind: 'user', text: chosen.trim() });
|
||||
try {
|
||||
const msg = await hooks.resumeSession(action.id);
|
||||
setHistory([]);
|
||||
// Reflect the freshly loaded history: session.messages now holds the
|
||||
// restored wire messages, and the transcript must show them again.
|
||||
setHistory(
|
||||
session.messages.length === 0
|
||||
? []
|
||||
: historyFromMessages(session.messages as { role?: string; content?: unknown }[]),
|
||||
);
|
||||
push({ kind: 'info', text: msg });
|
||||
} catch (e) {
|
||||
push({ kind: 'error', text: e instanceof Error ? e.message : String(e) });
|
||||
|
||||
@@ -197,6 +197,102 @@ export function withResult(lines: Line[], name: string, result: string, ok: bool
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tool output that was stored on a `tool-result`. The SDK json-wraps a
|
||||
* string return as `{ type: 'text', value }`, so a restored message needs the
|
||||
* same unwrap the live stream already produced at save time.
|
||||
*/
|
||||
function toolResultText(output: unknown): string {
|
||||
if (typeof output === 'string') return output;
|
||||
if (
|
||||
output !== null &&
|
||||
typeof output === 'object' &&
|
||||
'value' in output &&
|
||||
typeof (output as { value: unknown }).value === 'string'
|
||||
) {
|
||||
return (output as { value: string }).value;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(output);
|
||||
} catch {
|
||||
return String(output);
|
||||
}
|
||||
}
|
||||
|
||||
type StoredPart = {
|
||||
type?: string;
|
||||
toolName?: string;
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
text?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* The transcript lines a saved `ModelMessage[]` becomes, so a resumed session
|
||||
* renders its history instead of starting blank.
|
||||
*
|
||||
* Mirrors how the live loop paints: user strings as user lines, assistant text
|
||||
* as an assistant line, assistant `tool-call` parts as tool lines, and each
|
||||
* `role: 'tool'` result attached to the newest unanswered call of that name just
|
||||
* like `withResult` does. A result with no matching call (a pruned lead-in) is
|
||||
* dropped rather than left floating.
|
||||
*/
|
||||
export function historyFromMessages(messages: readonly { role?: string; content?: unknown }[]): Line[] {
|
||||
const lines: Line[] = [];
|
||||
|
||||
const textOf = (content: unknown): string =>
|
||||
typeof content === 'string'
|
||||
? content
|
||||
: Array.isArray(content)
|
||||
? (content as StoredPart[]).filter((p) => p.type === 'text' && typeof p.text === 'string').map((p) => p.text as string).join('')
|
||||
: '';
|
||||
|
||||
const toolPartsOf = (content: unknown): StoredPart[] =>
|
||||
Array.isArray(content) ? (content as StoredPart[]).filter((p) => p.type === 'tool-call') : [];
|
||||
|
||||
for (const m of messages) {
|
||||
switch (m.role) {
|
||||
case 'user': {
|
||||
const text = textOf(m.content).trim();
|
||||
if (text) lines.push({ key: nextKey(), kind: 'user', text });
|
||||
break;
|
||||
}
|
||||
case 'assistant': {
|
||||
const text = textOf(m.content).trim();
|
||||
if (text) lines.push({ key: nextKey(), kind: 'assistant', text });
|
||||
for (const p of toolPartsOf(m.content)) {
|
||||
const name = p.toolName ?? '';
|
||||
if (!name) continue;
|
||||
lines.push({ key: nextKey(), kind: 'tool', name, detail: toolDetail(name, p.input), ok: true });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool': {
|
||||
const parts = Array.isArray(m.content) ? (m.content as StoredPart[]) : [];
|
||||
for (const p of parts) {
|
||||
if (p.type !== 'tool-result' && p.type !== 'tool-error') continue;
|
||||
const name = p.toolName ?? '';
|
||||
const result =
|
||||
p.type === 'tool-error'
|
||||
? toolResultText(p.output) || 'tool failed'
|
||||
: resultSummary(name, toolResultText(p.output));
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i]!;
|
||||
if (line.kind !== 'tool' || line.name !== name || line.result !== undefined) continue;
|
||||
lines[i] = { ...line, result, ok: p.type !== 'tool-error' };
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** A task list as markdown, for the `/todos` panel. */
|
||||
export const todoLines = (todos: readonly { status: keyof typeof TODO_MARK; content: string; note?: string }[]) =>
|
||||
todos.length > 0
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
* fails inside the shipped binary. A constant is compiled in and always correct.
|
||||
* `scripts/release.ts` checks it against the release tag so the two cannot drift.
|
||||
*/
|
||||
export const VERSION = '1.0.0';
|
||||
export const VERSION = '1.0.1';
|
||||
|
||||
/** What `--version` prints: enough to identify a build from a bug report. */
|
||||
export function versionLine(): string {
|
||||
|
||||
Reference in New Issue
Block a user