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
JavaScript
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