UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

187 lines 9.34 kB
import { readAllAgents, readLiveMetas } from '../store/index.js'; import { collectQueue } from './queue.js'; import { readTickets } from './tickets.js'; import { TICKETS_DIR } from '../tickets.js'; /** How many recent projects the Overview surfaces. */ const RECENT_LIMIT = 5; /** How many recent sessions the home rail pools across every project. */ const RECENT_RUNS_LIMIT = 30; /** * Every project's sessions pooled and sorted newest-first (capped), so the shared sidebar (#shared- * shell) can show recents on the home/Overview where no single project is selected. Each row carries * the project it belongs to, so selecting it jumps into that project's session. Forgiving — a project * whose agents cannot be read simply contributes nothing. */ export async function buildRecentAgents(projects, deps = {}) { const readAgents = deps.agents ?? readAllAgents; const all = []; for (const project of projects) { for (const agent of await readAgents(project.path).catch(() => [])) { all.push({ projectId: project.id, projectName: project.name, agent }); } } all.sort((a, b) => (b.agent.startedAt ?? '').localeCompare(a.agent.startedAt ?? '')); return all.slice(0, RECENT_RUNS_LIMIT); } /** * Every registered project's tickets, one list per project (#1144) — the cross-project Tickets * page. Unlike {@link buildHotTickets} this does not pool or bucket: a ticket belongs to one * project, and the page's whole point is reading each project's backlog (and reaching its own * import/update) rather than one merged feed. Kept in registry order, project included even when * its list comes back empty, so import stays reachable there — the same read failing simply * leaves that project's list empty rather than dropping the section. */ export async function collectAllTickets(projects, deps = {}) { const readT = deps.tickets ?? readTickets; return Promise.all(projects.map(async (project) => ({ projectId: project.id, projectName: project.name, tickets: await readT(project.path).catch(() => []), }))); } /** Where the ticket format's 10-0 scale starts reading as high. */ const HIGH_PRIORITY_FLOOR = 7; /** * Whether a `priority:` value reads as "do this soon". The ticket format's own scale * (`ticketing_format.md`: `10-0 … 10: critical — act immediately, 0: only if capacity`), so 7 and * up qualify — NOT the P0/P1 convention, whose low-numbers-first reading is not this format. * Getting that backwards is what kept a backlog full of `Priority: 8` tickets off the card * entirely. Word spellings (`high`/`urgent`/…) are no longer read: the format says 0-10. */ function isHighPriority(priority) { const n = Number.parseInt(priority, 10); return !Number.isNaN(n) && n >= HIGH_PRIORITY_FLOOR; } /** * A ticket's lane (#1139), or null when it is in none of the three the card shows: * - in-progress: an agent is implementing it right now (#1117), or failing that the agent has * planned it, so work is under way in the older, inferred sense. * - ai-queue: it sits in the AI Queue — an open `TODO_AGENTS.md` entry links to it — so the * framework will pick it up on its own. * - high-priority: none of the above, but flagged high priority; what a human would likely queue next. * * Precedence follows that order: work already under way outranks a queued ticket, which outranks a * bare priority flag. Everything else is dropped — the card is a shortlist, not the whole backlog. * * `implementing` is the only hard evidence and exists for a drain agent only, so the plan proxy * still carries every ticket someone is working by hand. */ export function ticketBucket(ticket, opts = {}) { if (opts.implementing || ticket.planned) return 'in-progress'; if (opts.queued) return 'ai-queue'; if (ticket.priority && isHighPriority(ticket.priority)) return 'high-priority'; return null; } /** `[title](target)` at the very start of a queue entry — where a queued ticket's link is written. */ const QUEUE_LEADING_LINK = /^\s*\[[^\]]+\]\(([^)\s]+)\)/; /** * The ticket file an open queue entry points at, or undefined when it is not a ticket link. Mirrors * the dashboard's `queueEntryLabel`: only a link at the START of the entry names the work, and only * one under `tickets/` is a ticket. Returned as the bare filename, the key {@link WorkspaceTicket.file} * uses. */ function queuedTicketFile(entry) { const link = QUEUE_LEADING_LINK.exec(entry); if (!link) return undefined; const prefix = `${TICKETS_DIR}/`; return link[1].startsWith(prefix) ? link[1].slice(prefix.length) : undefined; } /** How many hot tickets the Overview pools before the card trims per lane. */ const HOT_TICKETS_LIMIT = 60; /** * Every project's tickets pooled and bucketed for the Overview's "hot tickets" card (#1139): what is * being worked on (implementing/planned), what sits in the AI Queue (an open `TODO_AGENTS.md` * entry links to it), and what is merely flagged high priority. Ordered lane-first (in-progress, * ai-queue, high-priority), file order within a lane; a ticket in none of the three is dropped. * Forgiving — a project whose tickets cannot be read simply contributes nothing. */ export async function buildHotTickets(projects, deps = {}) { const readT = deps.tickets ?? readTickets; const readAgents = deps.liveAgents ?? readLiveMetas; // The AI Queue: which tickets an open TODO_AGENTS.md entry links to, per project (#1139). const queues = await (deps.queue ?? (p => collectQueue(p)))(projects); const queuedByProject = new Map(); for (const q of queues) { const files = new Set(); for (const item of q.items) { if (item.done) continue; const file = queuedTicketFile(item.text); if (file) files.add(file); } queuedByProject.set(q.projectId, files); } const all = []; for (const project of projects) { // Which of this project's tickets are being implemented right now, by agent id (#1117). Built // per project because a ticket path is only unique within its own repo. const implementing = new Map(); for (const meta of await readAgents(project.path).catch(() => [])) { if (meta.status !== 'running' || !meta.ticket) continue; implementing.set(meta.ticket, meta.id); } const queued = queuedByProject.get(project.id) ?? new Set(); for (const ticket of await readT(project.path).catch(() => [])) { const agentId = implementing.get(`${TICKETS_DIR}/${ticket.file}`); const bucket = ticketBucket(ticket, { implementing: agentId !== undefined, queued: queued.has(ticket.file) }); // A ticket in none of the three shown lanes is left off the card entirely. if (!bucket) continue; all.push({ projectId: project.id, projectName: project.name, bucket, ticket, ...(agentId ? { agentId } : {}), }); } } const lane = { 'in-progress': 0, 'ai-queue': 1, 'high-priority': 2 }; all.sort((a, b) => lane[a.bucket] - lane[b.bucket]); return all.slice(0, HOT_TICKETS_LIMIT); } /** * Build the cross-project Overview: the running agents (every live agent of each project, one per * worktree since #736), the total open TODO count (from {@link collectQueue}), and the most * recently active projects (by {@link ProjectSummary.lastActivityAt}). Forgiving — a project * with no live run, or none running, simply contributes nothing to `active`. */ export async function buildOverview(projects, deps = {}) { const liveAgents = deps.liveAgents ?? readLiveMetas; const queue = deps.queue ?? (p => collectQueue(p)); const active = []; for (const project of projects) { // Every live agent of the project (#738), not just the one that used to sit at its path. for (const meta of await liveAgents(project.path).catch(() => [])) { if (meta.status !== 'running') continue; active.push({ projectId: project.id, projectName: project.name, agentId: meta.id, cwd: meta.cwd, status: meta.status, ...(meta.intent ? { intent: meta.intent } : {}), ...(meta.updatedAt ? { updatedAt: meta.updatedAt } : {}), ...(meta.sessionName ? { sessionName: meta.sessionName } : {}), ...(meta.readyForMerge ? { readyForMerge: true } : {}), }); } } active.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '')); const queues = await queue(projects); const queueOpen = queues.reduce((sum, q) => sum + q.open, 0); const recent = projects .filter(p => p.lastActivityAt) .sort((a, b) => (b.lastActivityAt ?? '').localeCompare(a.lastActivityAt ?? '')) .slice(0, RECENT_LIMIT) .map(p => ({ projectId: p.id, projectName: p.name, lastActivityAt: p.lastActivityAt })); return { active, queueOpen, recent }; } //# sourceMappingURL=overview.js.map