/** * Activity-anchored todo window for the pi widget and the slim overlay: the widget passes a budget of * unwrapped render lines, the overlay supplies its own `fits`; the in-progress row stays visible. */ import { countTodos, isTodoStatus, type TodoItem, type TodoStatus } from './vocab.ts'; export const TODO_MARKS: Record = { pending: '○', in_progress: '▶', completed: '✓', blocked: '■', abandoned: '✗', }; /** Mark for a snapshot's status, tolerating corrupt data (`?` instead of a prototype member). */ export const todoMark = (status: unknown): string => (isTodoStatus(status) ? TODO_MARKS[status] : '?'); export function formatTodoSummary(items: readonly TodoItem[]): string { const c = countTodos(items); return `todo: ${c.inProgress}▶ ${c.pending}○ ${c.blocked}■ ${c.completed}✓`; } /** Shared phase rendering so widget and overlay order entries identically. */ export function renderTodoGroups(items: readonly TodoItem[]): string[] { const groups = new Map(); for (const it of items) { const key = it.phase ?? ''; const arr = groups.get(key) ?? []; arr.push(it); groups.set(key, arr); } const lines: string[] = []; for (const [phase, list] of groups) { if (phase) lines.push(` [${phase}]`); for (const it of list) { const suffix = it.status === 'blocked' && it.blocker ? ` — ${it.blocker}` : ''; lines.push(` ${todoMark(it.status)} ${it.content}${suffix}`); } } return lines; } /** * Expand around the active anchor while `fits(start, end)` holds: the first in_progress item, else the * last open one, growing toward the tail first. All-completed lists shrink from the head instead. */ export function anchorTodoRange( items: readonly TodoItem[], fits: (start: number, end: number) => boolean, ): [number, number] { if (items.length === 0) return [0, 0]; if (fits(0, items.length)) return [0, items.length]; const anchor = items.findIndex((it) => it.status === 'in_progress'); const anchorIdx = anchor >= 0 ? anchor : items.reduce((acc, it, i) => (it.status !== 'completed' ? i : acc), -1); if (anchorIdx < 0) { let end = items.length; while (end > 1 && !fits(0, end)) end -= 1; return [0, end]; } let start = anchorIdx; let end = anchorIdx + 1; let growTail = true; while ((end < items.length || start > 0) && (end - start) < items.length) { if (growTail && end < items.length) { end += 1; if (!fits(start, end)) { end -= 1; break; } } else if (start > 0) { start -= 1; if (!fits(start, end)) { start += 1; break; } } else if (end < items.length) { end += 1; if (!fits(start, end)) { end -= 1; break; } } else { break; } growTail = !growTail; } return [start, end]; }