paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
97 lines (89 loc) • 2.81 kB
text/typescript
import { subagentTitle } from './agents';
import { formatStopwatch, shortAge } from './cost';
import { IDLE_LIVE, type LiveState } from './live';
import type { Signal } from './palette';
import type { PaqadHudSnapshot, PaqadTone } from './types';
export const WAITING_FOR_ANSWER = 'Waiting for your answer';
export const SUBAGENT_ROW = 7;
export const NO_RECORD = 'Not yet. paqad has no record for this session.';
export const SIGNOFF_ROW = 21;
function checkedNote(snapshot: PaqadHudSnapshot, row: number, now: number): string | null {
if (row !== SIGNOFF_ROW || snapshot.signoff?.state !== 'awaiting_sign_off') return null;
const checked = Date.parse(snapshot.signoff.checked_at ?? '');
return Number.isFinite(checked) ? `checked ${shortAge(now - checked)} ago` : null;
}
export interface DesktopHeadline {
row: number;
signal: Signal;
tone: PaqadTone;
live: boolean;
text: string;
word: string | null;
note: string | null;
gap: string | null;
clock: string | null;
activity: string[];
}
const TONE_SIGNAL: Readonly<Record<PaqadTone, Signal>> = {
neutral: 'idle',
working: 'accent',
'needs-you': 'attention',
good: 'pass',
warn: 'caution',
bad: 'fail',
};
function waitingRow(row: number, text: string): DesktopHeadline {
return {
row,
signal: 'attention',
tone: 'needs-you',
live: false,
text,
word: WAITING_FOR_ANSWER,
note: null,
gap: null,
clock: null,
activity: [],
};
}
export function snapshotHeadline(
snapshot: PaqadHudSnapshot,
now: number,
live: LiveState = IDLE_LIVE,
): DesktopHeadline {
if (live.waitingOn === 'question') return waitingRow(3, WAITING_FOR_ANSWER);
const head = snapshot.headline;
const title = subagentTitle(live.agents?.running ?? []);
if (title !== null && live.agents && head.tone !== 'needs-you') {
const finished = live.agents.finished;
return {
row: SUBAGENT_ROW,
signal: 'accent',
tone: 'working',
live: true,
text: title,
word: 'Working',
note: finished > 0 ? `${finished} finished` : null,
gap: head.gap ?? null,
clock: null,
activity: [...live.agents.lines],
};
}
const running = head.tone === 'working';
const from = head.clock_from ? Date.parse(head.clock_from) : Number.NaN;
return {
row: head.row ?? 0,
signal: TONE_SIGNAL[head.tone],
tone: head.tone,
live: running,
text: head.text || NO_RECORD,
word: head.word ?? null,
note: head.note ?? checkedNote(snapshot, head.row ?? 0, now),
gap: head.gap ?? null,
clock: running && Number.isFinite(from) ? formatStopwatch(now - from) : null,
activity: [],
};
}
export function judgedHeadline(snapshot: PaqadHudSnapshot | null): boolean {
return typeof snapshot?.headline.row === 'number';
}