From 1cccef8a89dc1d79a235033378187b21509a36d4 Mon Sep 17 00:00:00 2001 From: TheCheetah Date: Thu, 7 May 2026 16:52:26 +0100 Subject: [PATCH 1/3] feat(start): show resume context preview --- src/commands/start.ts | 49 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/commands/start.ts b/src/commands/start.ts index bc4f41d..cb15df3 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -372,10 +372,13 @@ export async function startCommand(options: StartOptions) { if (resumeSessionId) { const meta = loadSessionMeta(resumeSessionId); - const msgs = loadSessionHistory(resumeSessionId).length; + const history = loadSessionHistory(resumeSessionId); const when = meta ? new Date(meta.updatedAt).toLocaleString() : 'unknown'; console.log(chalk.green(` Resuming session ${resumeSessionId.slice(0, 24)}…`)); - console.log(chalk.dim(` ${msgs} messages · last active ${when}\n`)); + console.log(chalk.dim(` ${history.length} messages · last active ${when}`)); + const preview = formatResumePreview(history); + if (preview) console.log(preview); + console.log(''); } } @@ -446,6 +449,48 @@ async function runOneShot(agentConfig: AgentConfig, prompt: string): Promise { + const text = extractVisibleText(msg).replace(/\s+/g, ' ').trim(); + if (!text) return null; + return { role: msg.role, text: text.length > 180 ? `${text.slice(0, 177)}...` : text }; + }) + .filter((entry): entry is { role: 'user' | 'assistant'; text: string } => entry !== null); + + if (entries.length === 0) return ''; + + const render = (entry: { role: 'user' | 'assistant'; text: string }) => { + const label = entry.role === 'user' ? 'you' : 'assistant'; + return chalk.dim(` ${label}: ${entry.text}`); + }; + + const started = entries.slice(0, 4).map(render); + const recentStart = entries.length > 10 ? -6 : 4; + const recent = entries.slice(recentStart).map(render); + + const lines = [chalk.dim(' Context preview:'), ...started]; + if (recent.length > 0) { + if (entries.length > 10) lines.push(chalk.dim(' ...')); + lines.push(...recent); + } + + return lines.join('\n'); +} + +function extractVisibleText(msg: Dialogue): string { + if (typeof msg.content === 'string') return msg.content; + if (!Array.isArray(msg.content)) return ''; + + return msg.content + .map((part) => { + if ('type' in part && part.type === 'text') return part.text; + return ''; + }) + .filter(Boolean) + .join('\n'); +} + // ─── Ink UI (interactive terminal) ───────────────────────────────────────── async function runWithInkUI( From f9ea308c62e74c67a7277cf8432f7410ec5be220 Mon Sep 17 00:00:00 2001 From: TheCheetah Date: Thu, 7 May 2026 16:57:11 +0100 Subject: [PATCH 2/3] feat(ui): render resumed context in scrollback --- src/commands/start.ts | 34 +++++++++++++--------------------- src/ui/app.tsx | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/commands/start.ts b/src/commands/start.ts index cb15df3..8b686b9 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -347,6 +347,7 @@ export async function startCommand(options: StartOptions) { // Resolve resume target, if requested. let resumeSessionId: string | undefined; + let resumeTranscript: Array<{ role: 'user' | 'assistant'; text: string }> | undefined; if (options.resume || options.continue) { const { pickSession } = await import('../ui/session-picker.js'); const { loadSessionMeta, loadSessionHistory } = await import('../session/storage.js'); @@ -375,10 +376,8 @@ export async function startCommand(options: StartOptions) { const history = loadSessionHistory(resumeSessionId); const when = meta ? new Date(meta.updatedAt).toLocaleString() : 'unknown'; console.log(chalk.green(` Resuming session ${resumeSessionId.slice(0, 24)}…`)); - console.log(chalk.dim(` ${history.length} messages · last active ${when}`)); - const preview = formatResumePreview(history); - if (preview) console.log(preview); - console.log(''); + console.log(chalk.dim(` ${history.length} messages · last active ${when}\n`)); + resumeTranscript = buildResumeTranscript(history); } } @@ -416,7 +415,7 @@ export async function startCommand(options: StartOptions) { if (process.stdin.isTTY) { await runWithInkUI(agentConfig, model, workDir, version, walletInfo, (cb) => { onBalanceFetched = cb; - }, fetchBalance, importedKickoffPrompt); + }, fetchBalance, importedKickoffPrompt, resumeTranscript); } else { await runWithBasicUI(agentConfig, model, workDir, importedKickoffPrompt); } @@ -449,7 +448,7 @@ async function runOneShot(agentConfig: AgentConfig, prompt: string): Promise { const entries = history .map((msg) => { const text = extractVisibleText(msg).replace(/\s+/g, ' ').trim(); @@ -458,24 +457,15 @@ function formatResumePreview(history: Dialogue[]): string { }) .filter((entry): entry is { role: 'user' | 'assistant'; text: string } => entry !== null); - if (entries.length === 0) return ''; - - const render = (entry: { role: 'user' | 'assistant'; text: string }) => { - const label = entry.role === 'user' ? 'you' : 'assistant'; - return chalk.dim(` ${label}: ${entry.text}`); - }; + if (entries.length === 0) return []; - const started = entries.slice(0, 4).map(render); + const started = entries.slice(0, 4); const recentStart = entries.length > 10 ? -6 : 4; - const recent = entries.slice(recentStart).map(render); - - const lines = [chalk.dim(' Context preview:'), ...started]; - if (recent.length > 0) { - if (entries.length > 10) lines.push(chalk.dim(' ...')); - lines.push(...recent); - } + const recent = entries.slice(recentStart); - return lines.join('\n'); + return entries.length > 10 + ? [...started, { role: 'assistant', text: '...' }, ...recent] + : [...started, ...recent]; } function extractVisibleText(msg: Dialogue): string { @@ -502,6 +492,7 @@ async function runWithInkUI( onBalanceReady?: (cb: (bal: string) => void) => void, fetchBalance?: () => Promise, initialInput?: string, + initialTranscript?: Array<{ role: 'user' | 'assistant'; text: string }>, ) { const startSnapshot = snapshotStats(); const ui = launchInkUI({ @@ -510,6 +501,7 @@ async function runWithInkUI( version, walletAddress: walletInfo?.address, walletBalance: walletInfo?.balance, + initialTranscript, chain: walletInfo?.chain, onModelChange: (newModel: string, reason?: 'user' | 'system') => { agentConfig.model = newModel; diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 5923626..8cdabf2 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -255,6 +255,7 @@ interface AppProps { workDir: string; walletAddress: string; walletBalance: string; + initialTranscript?: Array<{ role: 'user' | 'assistant'; text: string }>; startWithPicker?: boolean; chain: string; onSubmit: (input: string) => void; @@ -265,7 +266,7 @@ interface AppProps { function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, - startWithPicker, onSubmit, onModelChange, onAbort, onExit, + initialTranscript, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }: AppProps) { const { exit } = useApp(); // Track terminal rows so we can cap the dynamic-region height. Ink wipes the @@ -283,7 +284,14 @@ function RunCodeApp({ // Last completed tool — shown in dynamic area so it can be expanded/collapsed with Tab const [expandableTool, setExpandableTool] = useState<(ToolStatus & { key: string }) | null>(null); // Full responses committed to Static immediately — goes into terminal scrollback - const [committedResponses, setCommittedResponses] = useState>([]); + const [committedResponses, setCommittedResponses] = useState>(() => + (initialTranscript ?? []).map((entry, idx) => ({ + key: `${entry.role === 'user' ? 'user' : 'resume'}-${idx}`, + text: entry.role === 'user' ? formatUserPromptForDisplay(entry.text) : entry.text, + tokens: { input: 0, output: 0, calls: 0 }, + cost: 0, + })) + ); // Short preview of latest response shown in dynamic area (last ~5 lines, cleared on next turn) const [responsePreview, setResponsePreview] = useState(''); const [currentModel, setCurrentModel] = useState(initialModel || PICKER_MODELS_FLAT[0].id); @@ -1494,6 +1502,7 @@ export function launchInkUI(opts: { version: string; walletAddress?: string; walletBalance?: string; + initialTranscript?: Array<{ role: 'user' | 'assistant'; text: string }>; chain?: string; showPicker?: boolean; onModelChange?: (model: string, reason?: 'user' | 'system') => void; @@ -1510,6 +1519,7 @@ export function launchInkUI(opts: { workDir={opts.workDir} walletAddress={opts.walletAddress || 'not set — run: franklin setup'} walletBalance={opts.walletBalance || 'unknown'} + initialTranscript={opts.initialTranscript} chain={opts.chain || 'base'} startWithPicker={opts.showPicker} onSubmit={(value) => { From 3784729e33f8026ed04e76110c25eb8ca973fef9 Mon Sep 17 00:00:00 2001 From: TheCheetah Date: Thu, 7 May 2026 17:06:37 +0100 Subject: [PATCH 3/3] fix(ui): make resume context preview standalone --- src/ui/app.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 8cdabf2..18c7c7d 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -287,7 +287,9 @@ function RunCodeApp({ const [committedResponses, setCommittedResponses] = useState>(() => (initialTranscript ?? []).map((entry, idx) => ({ key: `${entry.role === 'user' ? 'user' : 'resume'}-${idx}`, - text: entry.role === 'user' ? formatUserPromptForDisplay(entry.text) : entry.text, + text: entry.role === 'user' + ? chalk.hex('#FFD700').bold('❯ ') + chalk.hex('#FFD700').bold(entry.text) + : entry.text, tokens: { input: 0, output: 0, calls: 0 }, cost: 0, }))