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.

67 lines 4.3 kB
import { resolveAgentEventsPath } from '../store/index.js'; import { contextEventsSource, resolveProjectPath } from './context.js'; import { tailAgentEvents } from './events-tail.js'; import { forwardStream } from './stream-forward.js'; // The live event stream behind the dashboard (#405): the selected project's run, read straight // from the same `.the-framework/events.jsonl` the daemon writes. Each new JSONL line becomes one // `send(event)`, which the mount writes out as one SSE frame. Runs, docs, and the project log // come over the read-model RPCs (reads.ts). /** * The events file to tail, or undefined when the project is unknown. With a `agentId` this is * that agent's own log inside its worktree (#749): since #736 an agent appends there, not to the * project root, so streaming the project path would follow a file nothing writes to. Once the * run has ended and its worktree is gone, the agent's archived `<id>.jsonl` is that log (#1472) — * tailing the project root there would stream a foreign agent's journal. */ async function resolveEventsPath(projectId, agentId) { const cwd = await resolveProjectPath(projectId); return cwd ? resolveAgentEventsPath(cwd, agentId) : undefined; } /** * Follow one agent's events: `send` is called per event until the returned stop function runs. * Returns undefined when there is nothing to stream (an unknown project), which the mount ends as * a clean close — mirroring the read model's empty results rather than throwing at the client. * * Pass the `agentId` to follow that agent's own log (#749). A project has several concurrent agents * since #736, each writing inside its worktree, so the agent id is what makes the feed that agent's * rather than a mix — and without it the feed for a worktree agent is empty. Omitting it keeps the * pre-#736 behavior of tailing the project root. * * Two sources, chosen by what is wired. An in-memory stream wins: an agent the daemon is relaying * from a device (#1067). Otherwise the on-disk log. The daemon's source answers only for relayed * runs, so an ordinary local agent falls through to tailing the log. * * Only the on-disk tail sends the {@link StreamSync} marker: the in-memory sources have no replay * boundary to report, so a reconnecting client falls back to a grace deadline before swapping its * feed (see use-live-events in the dashboard). */ export async function streamAgentEvents(projectId, agentId, send, onDone) { const stream = contextEventsSource()(projectId, agentId); // An in-memory agent: replay + follow it, and end when it ends — a relayed agent that finished has // nothing more to say, and leaving the response open would read as a live feed gone quiet. if (stream) return forwardStream(stream, send, onDone); // Everywhere else: tail the agent's on-disk events.jsonl (undefined path -> nothing to stream). // The relocating tail, because the journal moves mid-subscription: teardown copies it into the // archive and removes the worktree, and a fixed-path tail whose fs.watch missed the final // appends went silent without the agent's `end`. On the move it re-resolves (the archive, #1472) // and carries its offset, so the feed gets exactly the lines the move would have swallowed. const path = await resolveEventsPath(projectId, agentId); if (!path) return undefined; // The one place a run-scoped feed must NOT relocate to: the project-root journal, which is // resolveAgentEventsPath's last-resort fallback once a Delete has removed worktree and archive // alike — it is another agent's feed (#1472). A deleted session's tab goes quiet instead. The // initial attach stays permissive: a fallback agent (non-git project) legitimately lives there. const rootJournal = agentId === undefined ? undefined : await resolveEventsPath(projectId, undefined); let initial = true; return tailAgentEvents(async () => { const next = await resolveEventsPath(projectId, agentId); if (initial) { initial = false; return next; } return rootJournal !== undefined && next === rootJournal ? undefined : next; }, send, () => send({ kind: 'stream-sync' })); } //# sourceMappingURL=events.js.map