UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

228 lines (207 loc) • 7.72 kB
import { formatTokens } from './cost'; import './instruments'; import { judgedHeadline } from './headline'; import { CHANGE_CLOSED, DELIVERY_MISSING, NEW_COMMITS_AFTER_CLOSE, NO_VERDICT_WORDS, } from './desktop-view'; import { buildBandView, type BandView } from './model'; import { BAND_BODIES, layoutShell, type BandLayout, type BandRow, type BandSegment, type BodyContext, type ShellHeader, } from './shell'; import { shipRail } from './ship'; import { FEATURE_WORDS, NO_WORKFLOW, readyFeatureRoute, routeFromEarlierTurn, statusEntryRoute, statusEntryRow, } from './status-entry'; import { IDLE_WORDS, closedLine, minimisedLine, needsYouLine, visibilityMode, type BandVisibility, } from './visibility'; import { waitingOnHuman } from './attention'; import type { PaqadHudSnapshot, PaqadTone } from './types'; export type { BandLayout, BandRow, BandSegment }; const ELLIPSIS = '…'; export const FEATURE_DEVELOPMENT = 'feature-development'; export const IDLE_HEADLINE = IDLE_WORDS; export const READY_FEATURE_LINE = `paqad ${FEATURE_WORDS} · ${IDLE_WORDS}`; export function rowText(row: BandRow): string { return row.map((segment) => segment.text).join(''); } export function truncateRow(row: BandRow, columns: number): BandRow { const width = Math.max(1, Math.floor(columns)); if ([...rowText(row)].length <= width) return row; const out: BandRow = []; let room = width - 1; for (const segment of row) { if (room <= 0) break; const chars = [...segment.text]; const text = chars.slice(0, room).join(''); out.push({ ...segment, text }); room -= chars.length; } out.push({ text: ELLIPSIS }); return out.filter((segment) => segment.text.length > 0); } export function changeChip(snapshot: PaqadHudSnapshot | null): string | null { const change = snapshot?.active ? snapshot.change : null; if (!change) return null; if (change.issue) return /^\d+$/.test(change.issue) ? `#${change.issue}` : change.issue; return change.id ? change.id.slice(-6) : null; } export function tokensChip(snapshot: PaqadHudSnapshot | null): string | null { const cost = snapshot?.active ? snapshot.cost : null; if (!cost) return null; const tokens = `${formatTokens(cost.tokens.total)} tokens`; return cost.scope === 'whole-session' ? `${tokens} · whole session` : tokens; } const VERDICT_TONE: Record<'pass' | 'fail' | 'inconclusive', PaqadTone> = { pass: 'good', fail: 'bad', inconclusive: 'warn', }; const VERDICT_MARK: Record<'pass' | 'fail' | 'inconclusive', string> = { pass: '●', fail: '✗', inconclusive: '◐', }; const CAUTION_MARK = '▲'; export function closedChangeRow(snapshot: PaqadHudSnapshot, columns: number): BandRow | null { const closed = snapshot.closed_change; if (!closed) return null; const kind = snapshot.verdict?.kind ?? null; const word = closed.word ?? NO_VERDICT_WORDS; const verdict: BandSegment = closed.word && kind ? { text: `${VERDICT_MARK[kind]} ${word}`, tone: VERDICT_TONE[kind] } : closed.word ? { text: word } : { text: `${CAUTION_MARK} ${word}`, tone: 'warn' }; const number = snapshot.ship?.pr_number ?? null; const pr: BandSegment = number !== null && snapshot.ship?.pr_url ? { text: ` PR #${number}` } : { text: ` ${CAUTION_MARK} ${DELIVERY_MISSING}`, tone: 'warn' }; const row: BandRow = [ { text: 'paqad' }, { text: ` ${CHANGE_CLOSED} · `, dim: true }, verdict, pr, ]; if (closed.new_commits > 0) { row.push({ text: ` ${CAUTION_MARK} ${NEW_COMMITS_AFTER_CLOSE}`, tone: 'warn' }); } return truncateRow(row, columns); } export function allGreenNow(snapshot: PaqadHudSnapshot | null): boolean { return ( snapshot?.active === true && snapshot.decisions.pending.length === 0 && shipRail(snapshot).allGreen ); } export function allGreenKey(snapshot: PaqadHudSnapshot | null): string | null { if (!allGreenNow(snapshot)) return null; const change = snapshot?.change?.dir ?? snapshot?.change?.id ?? 'change'; return `${change}:${snapshot?.ship?.pr_head ?? 'head'}`; } export function bandHeadline(_snapshot: PaqadHudSnapshot, view: BandView): string { return view.headline; } export function shellHeader(snapshot: PaqadHudSnapshot | null, view: BandView): ShellHeader { const active = snapshot?.active === true; const verdict = active && !judgedHeadline(snapshot) ? (snapshot?.verdict ?? null) : null; return { tone: active ? view.tone : 'neutral', chip: changeChip(snapshot), headline: snapshot && active ? view.headline : IDLE_HEADLINE, note: active ? view.note : null, gap: active ? view.gap : null, activity: active ? view.activity : [], verdict: verdict ? { word: verdict.word, tone: VERDICT_TONE[verdict.kind] } : null, flags: active ? view.flags : 0, lane: active ? (snapshot?.lane ?? null) : null, tokens: tokensChip(snapshot), }; } const SHOWN: BandVisibility = { minimised: false, closed: false, hidden: false }; export function layoutBand( snapshot: PaqadHudSnapshot | null, size: { columns: number; maxRows: number }, context: BodyContext, visibility: BandVisibility = SHOWN, ): BandLayout { if (snapshot && (snapshot.route === null || snapshot.route === NO_WORKFLOW)) { return { kind: 'none' }; } const entryRoute = statusEntryRoute(snapshot); if (snapshot && entryRoute) { return { kind: 'line', row: statusEntryRow(snapshot, entryRoute, context.now, context.turnStartedAt ?? null), }; } const view = buildBandView(snapshot, context.now, context.live); const mode = visibilityMode(visibility); if (mode !== 'band' && waitingOnHuman(snapshot, context.live)) { return { kind: 'line', row: needsYouLine() }; } if (snapshot && !snapshot.active) { const shown = mode !== 'closed' && mode !== 'hidden'; const closedRow = shown ? closedChangeRow(snapshot, size.columns) : null; if (closedRow) return { kind: 'line', row: closedRow }; const drawsReady = mode !== 'closed' && mode !== 'hidden' && readyFeatureRoute(snapshot) && !routeFromEarlierTurn(snapshot, context.turnStartedAt ?? null); return drawsReady ? { kind: 'line', row: truncateRow([{ text: READY_FEATURE_LINE }], size.columns) } : { kind: 'none' }; } if (mode === 'closed' || mode === 'hidden') { return { kind: 'line', row: closedLine(mode, view) }; } const minimised = minimisedLine(snapshot, view, size.columns, context.now); if (mode === 'minimised') return { kind: 'line', row: minimised }; const active = snapshot?.active === true; const route = snapshot?.route ?? FEATURE_DEVELOPMENT; const renderer = active ? BAND_BODIES[route] : undefined; const body = snapshot && renderer ? renderer(snapshot, context) : null; const header = { ...shellHeader(snapshot, view), unavailable: context.unavailable === true }; return layoutShell(header, body, size, minimised); } const SPINNER_WORDS: Record<string, string> = { ticket_intake: 'Reading the ticket', planning: 'Planning', specification: 'Writing the spec', development: 'Building', review: 'Reviewing', checks: 'Running checks', documentation_sync: 'Syncing the docs', delivery: 'Delivering', }; export function spinnerWord(snapshot: PaqadHudSnapshot | null): string | null { if (!snapshot?.active) return null; const running = snapshot.stages.find((stage) => stage.state === 'running'); return running ? (SPINNER_WORDS[running.stage] ?? null) : null; } export function laneMode(snapshot: PaqadHudSnapshot | null): string | null { return snapshot?.active && snapshot.lane ? `paqad ${snapshot.lane} lane` : null; }