diff --git a/src/commands/start.ts b/src/commands/start.ts index bc4f41d..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'); @@ -372,10 +373,11 @@ 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}\n`)); + resumeTranscript = buildResumeTranscript(history); } } @@ -413,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); } @@ -446,6 +448,39 @@ async function runOneShot(agentConfig: AgentConfig, prompt: string): Promise { + const entries = history + .map((msg) => { + 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 started = entries.slice(0, 4); + const recentStart = entries.length > 10 ? -6 : 4; + const recent = entries.slice(recentStart); + + return entries.length > 10 + ? [...started, { role: 'assistant', text: '...' }, ...recent] + : [...started, ...recent]; +} + +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( @@ -457,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({ @@ -465,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..18c7c7d 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,16 @@ 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' + ? chalk.hex('#FFD700').bold('❯ ') + chalk.hex('#FFD700').bold(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 +1504,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 +1521,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) => {