export interface DelegateRun { delegateRunId: string; parentRunId: string | null; description: string; depth: number; status: 'success' | 'aborted' | 'needs_user_input' | 'running'; startTs: string; endTs: string | null; eventCount: number; toolCalls: number; } export interface DelegateRunNode extends DelegateRun { children: DelegateRunNode[]; } /** サブタスクジョブに紐づく delegate run のグループ。Task 7 でレンダリングされる。 */ export interface SubtaskDelegateGroup { jobId: string; issueNumber: number; depth: number; status: string; runs: DelegateRun[]; } /** GET /:id/delegate-runs のレスポンス型。 */ export interface DelegateRunsResult { runs: DelegateRun[]; subtasks: SubtaskDelegateGroup[]; } /** delegate run の status に対応する i18n ラベルキーと Tailwind 色クラス。 */ export function delegateStatusBadge( status: DelegateRun['status'], ): { labelKey: string; cls: string } { switch (status) { case 'success': return { labelKey: 'subtasks.delegateStatus.success', cls: 'bg-emerald-100 text-emerald-800' }; case 'aborted': return { labelKey: 'subtasks.delegateStatus.aborted', cls: 'bg-red-100 text-red-800' }; case 'needs_user_input': return { labelKey: 'subtasks.delegateStatus.needs_user_input', cls: 'bg-amber-100 text-amber-800' }; default: return { labelKey: 'subtasks.delegateStatus.running', cls: 'bg-blue-100 text-blue-800' }; } } /** endTs があればそこまで、無ければ now までの経過。clock skew で負にならないようガード。 */ export function formatElapsed(startTs: string, endTs: string | null, now: number): string { const end = endTs ? new Date(endTs).getTime() : now; const ms = Math.max(0, end - new Date(startTs).getTime()); if (ms < 1000) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.floor(ms / 60_000)}m${Math.floor((ms % 60_000) / 1000)}s`; } /** * Build a tree from a flat list of DelegateRun records. * Runs whose parentRunId is null, or whose parent is not in the set, * are treated as roots (orphan fallback — no data loss). */ export function buildDelegateRunTree(runs: DelegateRun[]): DelegateRunNode[] { const nodes = new Map(); for (const r of runs) { nodes.set(r.delegateRunId, { ...r, children: [] }); } const roots: DelegateRunNode[] = []; for (const node of nodes.values()) { const parent = node.parentRunId ? nodes.get(node.parentRunId) : undefined; if (parent) { parent.children.push(node); } else { roots.push(node); } } return roots; }