paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
228 lines (207 loc) • 7.72 kB
text/typescript
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;
}