paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
108 lines (97 loc) • 3.76 kB
text/typescript
import { formatStopwatch, formatTokens, formatUsd } from './cost';
import type { BandRow } from './shell';
import type { PaqadHudSnapshot, PaqadWorkflow } from './types';
export const WORKFLOW_WORDS: Readonly<Record<string, string>> = {
'project-question': 'question',
discovery: 'discovery',
'codebase-health': 'codebase health',
pentest: 'pentest',
'design-test': 'design test',
'site-map': 'site map',
'documentation-update': 'docs',
'module-documentation': 'module docs',
'root-cause-analysis': 'root-cause analysis',
'rules-analyze': 'rules analysis',
};
export const NO_WORKFLOW = 'no-workflow';
export const FEATURE_ROUTE = 'feature-development';
export const FEATURE_WORDS = 'Feature';
const NO_CHANGE_YET = 'no-change-yet';
export function workflowWords(name: string): string {
return WORKFLOW_WORDS[name] ?? name.replace(/-/g, ' ');
}
export function statusEntryRoute(snapshot: PaqadHudSnapshot | null): string | null {
const route = snapshot?.route ?? null;
if (route === null || route === NO_WORKFLOW || route === FEATURE_ROUTE) return null;
return route;
}
export function readyFeatureRoute(snapshot: PaqadHudSnapshot | null): boolean {
return (
snapshot?.route === FEATURE_ROUTE &&
!snapshot.active &&
snapshot.scope?.reason === NO_CHANGE_YET
);
}
export function routeFromEarlierTurn(
snapshot: PaqadHudSnapshot,
turnStartedAt: number | null,
): boolean {
const generatedAt = Date.parse(snapshot.generated_at);
return turnStartedAt !== null && Number.isFinite(generatedAt) && generatedAt < turnStartedAt;
}
export function workflowCost(workflow: PaqadWorkflow | null): string | null {
if (!workflow || workflow.tokens === null) return null;
const tokens = `${formatTokens(workflow.tokens)} tokens`;
return workflow.usd === null ? tokens : `${tokens} · ~${formatUsd(workflow.usd)}`;
}
export function statusEntryLine(
route: string,
workflow: PaqadWorkflow | null,
runningMs: number | null,
): string {
const parts = ['paqad', workflowWords(route)];
if (runningMs !== null) parts.push(`running ${formatStopwatch(runningMs)}`);
parts.push(workflowCost(workflow) ?? 'counting');
return parts.join(' · ');
}
export function statusEntryRow(
snapshot: PaqadHudSnapshot,
route: string,
now: number,
turnStartedAt: number | null,
): BandRow {
const workflow = snapshot.workflow?.name === route ? snapshot.workflow : null;
const runningMs = turnStartedAt === null ? null : Math.max(0, now - turnStartedAt);
return [
{
text: statusEntryLine(route, workflow, runningMs),
opens: 'cost',
key: 'paqad-status-entry',
},
];
}
export function workflowCostLines(snapshot: PaqadHudSnapshot, route: string): string[] {
const workflow = snapshot.workflow?.name === route ? snapshot.workflow : null;
const words = workflowWords(route);
const heading = `${words[0]!.toUpperCase()}${words.slice(1)}`;
if (!workflow || workflow.tokens === null) {
return [heading, 'Cost: counting. paqad reads it from the transcript at the end of each turn.'];
}
const money =
workflow.usd === null
? 'no money figure, since no model it used is in the price table'
: `~${formatUsd(workflow.usd)} estimate at API prices`;
const lines = [
heading,
`Cost: ${formatTokens(workflow.tokens)} tokens, ${money}.`,
workflow.started_at
? `Counted from when this workflow started in this session, ${workflow.started_at}, this workflow only.`
: 'Counted from when this workflow started in this session, this workflow only.',
];
if (workflow.unpriced.length > 0) {
lines.push(
`No price for ${workflow.unpriced.join(', ')}: its tokens are in the total, its money is not.`,
);
}
return lines;
}