UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

205 lines 9.89 kB
import { pickedIds } from './events.js'; // The terminal surface for the agent's event stream: render one {@link FrameworkEvent} as one // human-readable line. This is the CLI's counterpart to the dashboard's read-model // projections (run-view.ts) — a pure formatter over the same union, kept out of events.ts so // the event contract stays a plain data module (and browser-safe for the client bundle). /** Render a {@link FrameworkEvent} as one human-readable line (terminal surface). */ export function formatFrameworkEvent(event) { switch (event.kind) { case 'session': return `◆ ${event.fake ? 'fake' : event.driver}${event.model ? ` (${event.model})` : ''} in ${event.workspace}${event.sessionLink ? ` — ${event.sessionLink}` : ''}`; case 'session-update': return ` session ${event.sessionId}${event.sessionLink ? ` — ${event.sessionLink}` : ''}`; case 'system-prompt': return ` system prompt sent (${event.text.length} chars)`; case 'preview': return `▶ your app is running at ${event.url}`; case 'browser-stream': return `◆ browser preview: http://127.0.0.1:${event.port}/stream`; case 'browser': return `◆ browser: ${event.url}`; case 'log': return ` ${event.message}`; case 'error': return `✗ ${event.headline}${event.detail ? `\n ${event.detail.replace(/\n/g, '\n ')}` : ''}`; case 'view': return `▶ view: ${event.title}`; case 'session-name': return ` session: ${event.name}`; case 'ready-for-merge': return `✓ ready for merge`; case 'open-pr': return ` pull request written${event.title ? `: ${event.title}` : ''}`; case 'settled': return `◆ done for now — waiting for your next message`; case 'ticket': return ` implementing ${event.path}`; case 'branch': return ` branch: ${event.branch}`; case 'cloud-anchor': return ` hand-off anchor: ${event.sha.slice(0, 7)}`; case 'pull-request': return ` pull request: #${event.number}`; case 'on-before-mergeable': switch (event.outcome) { case 'queued': return `✓ post-merge cleanup: quality follow-ups queued`; case 'incomplete': return ` ! post-merge cleanup: queueing did not complete cleanly`; case 'skipped': return ` ~ post-merge cleanup skipped: ${skipReason(event.reason)}`; } case 'handoff-armed': { // Said as what will happen, not as two flags: the line is read once, at a glance. // A merge-armed agent's PR is opened ready, not draft, and lands by itself — the line must say // so (#1382): merging unattended is the one consequence a reader cannot be left to infer. if (event.pr && event.merge) return ` when this ends: push the branch, open a PR, and merge it`; if (event.pr) return ` when this ends: push the branch and open a draft PR`; if (event.push) return ` when this ends: push the branch`; return ` when this ends: nothing — push and PR are both off`; } case 'handoff': { // The merge half rides the same event (#1216) and gets its own line: after "auto-merge // was on", silence about the merge reads as "it merged" (#1363), so every outcome — // armed, merged, withheld, failed — is said. const merge = event.outcome !== 'failed' && event.merge ? `\n${mergeLine(event.merge)}` : ''; switch (event.outcome) { case 'done': return (event.url ? `✓ opened ${event.url}` : `✓ branch pushed`) + merge; case 'skipped': return ` ~ handoff skipped: ${handoffSkipReason(event.reason)}` + merge; case 'failed': return ` ! could not ${event.step === 'pr' ? 'open the PR' : 'push the branch'}: ${event.error}`; } } case 'usage': { const turns = `over ${event.turns} turn${event.turns === 1 ? '' : 's'}`; // No price to show: report the tokens the agent *did* report, rather than a // `$0.0000` that would read as free (#540). if (event.costUsd === undefined) { const tokens = event.inputTokens + event.cacheReadTokens + event.outputTokens; return ` tokens: ${tokens.toLocaleString('en-US')} (${event.outputTokens.toLocaleString('en-US')} out) ${turns} — no price reported`; } return ` spend: $${event.costUsd.toFixed(4)} ${turns}`; } case 'choice': { const mark = (o) => event.multi ? (o.default ? '[x]' : '[ ]') : o.id === event.recommended ? '●' : '○'; const opts = event.options.map(o => ` ${mark(o)} ${o.label}`).join('\n'); return `? ${event.title}\n${opts}`; } case 'choice-resolved': return ` ✓ chose ${pickedIds(event.picked).join(', ') || '(none)'} (${event.by})`; case 'driver': return formatDriverEvent(event.event); case 'intent': return `▶ "${truncate(event.text)}"`; case 'end': return event.ok ? '✓ finished' : event.stopped ? '■ stopped' : `✗ failed: ${event.detail ?? 'unknown error'}`; } } /** One line for the merge half of a handoff (#1216/#1363), matching the CLI's own wording. */ function mergeLine(merge) { switch (merge.outcome) { case 'auto-armed': return '✓ auto-merge armed: the PR lands when its checks pass'; case 'watched': return '✓ merge on green: the daemon merges the PR when its checks pass'; case 'merged': return '✓ merged the PR'; case 'withheld': return ` ~ merge withheld: ${mergeWithheldWhy(merge.reason)}`; case 'failed': return ` ! could not merge the PR: ${merge.error}`; } } /** * Say why an armed merge was withheld (#1363) in the reader's terms. Exported for the CLI's * own stdout line, so the two surfaces cannot drift. */ export function mergeWithheldWhy(reason) { switch (reason) { case 'not-ready-for-merge': return 'the session never signalled ready-for-merge'; case 'session-todo-open': return "the session's own TODO file still has open entries"; } } /** Say why the post-merge cleanup declined in the reader's terms, not the guard's (#835). */ function skipReason(reason) { switch (reason) { case 'not-ready-for-merge': return 'the session never signalled ready-for-merge'; case 'run-stopped': return 'the run was stopped'; case 'fake-run': return 'this was a fake run'; case 'no-session-name': return 'the session never called setSessionName()'; case 'no-bin-path': return 'the framework binary path is unknown'; } } /** Why the end-of-session handoff did nothing (#1102), as a reason rather than a code. */ function handoffSkipReason(reason) { switch (reason) { case 'not-armed': return 'push and PR are both off for this session'; case 'branch-gone': return 'the branch no longer exists'; case 'no-commits': return 'the session committed nothing'; case 'commit-failed': return "the session's pending work could not be committed, so nothing was published (the work stays on its branch)"; case 'no-remote': return 'this repo has no remote to push to'; case 'already-open': return 'the branch already has a pull request'; case 'already-landed': return "the branch's pull request already landed everything the session did"; case 'already-pushed': return 'the branch is already on the remote'; case 'run-stopped': return 'the run was stopped'; case 'fake-run': return 'this was a fake run'; } } function formatDriverEvent(event) { switch (event.type) { case 'start': return ` › prompt: ${truncate(event.prompt, 140)}`; // Normally consumed by telemetry before it reaches a stream (#1322); rendered anyway so a // stray one reads as what it is rather than crashing the formatter. case 'session': return ` session ${event.sessionId}`; case 'text': return ` ${truncate(event.text)}`; case 'action': return ` · ${event.label}`; case 'result': return ` ‹ turn complete`; case 'rate-limit': return ` ${formatRateLimit(event.limit)}`; case 'error': return ` ! agent error: ${event.message}`; case 'notice': return ` ~ ${event.message}`; } } /** Quiet on the happy path: only worth a line when the quota is actually tight. */ function formatRateLimit(limit) { const resets = new Date(limit.resetsAt).toISOString(); if (limit.status === 'rejected') return `✗ quota exhausted (${limit.window}), resets ${resets}`; if (limit.status === 'allowed_warning') return `! quota running low (${limit.window}), resets ${resets}`; return `· quota ${limit.status} (${limit.window}), resets ${resets}`; } function truncate(text, max = 100) { const flat = text.replace(/\s+/g, ' ').trim(); return flat.length > max ? flat.slice(0, max - 1) + '…' : flat; } //# sourceMappingURL=terminal.js.map