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.
81 lines • 4.38 kB
JavaScript
import { readAllAgents } from '../store/index.js';
import { postDiscordWebhook } from './discord-webhook.js';
// The identity + diff live in the leaf `keys.ts` so the dashboard can share them (they are pure);
// re-exported here so this stays the import site for anything that already reads them from the
// module that defines `Activity`.
export { activityKey } from './keys.js';
// The "New activity" feed (#627): the cross-project stream of run lifecycle transitions that
// do NOT need the human — an agent started, an agent finished. It is the default-off notification
// category, the counterpart to the interventions queue (which is the always-on "needs you"
// half). Same shape and same "only genuinely new" diff as interventions.ts, so the browser
// hook and the Discord watcher fold it into a baseline on first look and then notify once per
// transition — you hear an agent kick off and an agent land, nothing when the page/daemon starts.
/** How many recent agents per project to consider. Bounds the finished-set — older agents rolled off
* long ago and were already baselined, so they never fire. A running agent is always newest (live
* meta is prepended), so it is always in range. */
const RECENT_RUNS = 20;
/** Map one agent to its current activity item: `started` while running, else `finished`. */
function activityFor(project, agent) {
const kind = agent.status === 'running' ? 'started' : 'finished';
return {
projectId: project.id,
projectName: project.name,
agentId: agent.id,
kind,
...(agent.intent ? { title: agent.intent } : {}),
...(kind === 'finished' ? { status: agent.status } : {}),
...(agent.updatedAt ? { updatedAt: agent.updatedAt } : {}),
};
}
/**
* Build the cross-project activity feed: for each registered project's most recent agents, one
* item per agent reflecting where it is now (`started` while it runs, `finished` once it lands),
* newest first. Forgiving — a project whose agents cannot be read simply contributes nothing, and
* comes back named as one that was *not* read whole (#1623), since "nothing happened there" and
* "I could not look" are the same empty list to everyone but the notification watcher.
*
* The `started` and `finished` items for one agent carry distinct keys ({@link activityKey}), so a
* run that is still going notifies once (started) and again when it lands (finished). An agent that
* both starts and finishes between two polls is only ever seen terminal, so it notifies once
* (finished) — one quick agent, one line.
*/
export async function buildActivity(projects, deps = {}) {
const readAgents = deps.readAgents ?? readAllAgents;
const items = [];
const whole = [];
for (const project of projects) {
const read = await readAgents(project.path).catch(() => undefined);
if (!read)
continue;
whole.push(project.id);
for (const agent of read.slice(0, RECENT_RUNS))
items.push(activityFor(project, agent));
}
items.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
return { items, whole };
}
/**
* How one activity item reads on Discord: a started agent, or a finished one tagged by its outcome.
* Beside {@link Activity} for the same reason {@link interventionLine} sits beside `Intervention`
* — it switches on the kind, so it belongs with the type that declares the kinds.
*/
export function activityLine(item) {
const what = item.title ?? 'a session';
if (item.kind === 'started')
return `▶️ started: ${what}`;
const mark = item.status === 'failed' ? '❌' : item.status === 'stopped' ? '⏹️' : '✅';
return `${mark} finished: ${what}`;
}
/**
* Post the given activity items to a Discord webhook as one message, resolving whether Discord
* accepted it (#940). `fetch` is injectable for tests.
*/
export async function postActivityDiscord(webhook, items, fetchImpl = fetch) {
if (items.length === 0)
return true;
const content = items.length === 1
? `📣 Activity (${items[0].projectName}): ${activityLine(items[0])}`
: `📣 ${items.length} session updates:\n${items.map(i => `• ${i.projectName}: ${activityLine(i)}`).join('\n')}`;
return postDiscordWebhook(webhook, content, fetchImpl);
}
//# sourceMappingURL=activity.js.map