move to a page for background agents

This commit is contained in:
Arjun 2026-05-05 20:30:36 +05:30
parent 697a43ebbc
commit d8e33afcc8
6 changed files with 507 additions and 191 deletions

View file

@ -70,7 +70,43 @@ export async function fetch(filePath: string, trackId: string): Promise<z.infer<
return blocks.find(b => b.track.trackId === trackId) ?? null;
}
export async function listNotesWithTracks(): Promise<Array<{ path: string; trackCount: number }>> {
type TrackNoteSummary = {
path: string;
trackCount: number;
createdAt: string | null;
lastRunAt: string | null;
isActive: boolean;
};
async function summarizeTrackNote(
filePath: string,
tracks: z.infer<typeof TrackStateSchema>[],
): Promise<TrackNoteSummary | null> {
if (tracks.length === 0) return null;
const stats = await fs.stat(absPath(filePath));
const createdMs = stats.birthtimeMs > 0 ? stats.birthtimeMs : stats.ctimeMs;
let latestRunAt: string | null = null;
let latestRunMs = -1;
for (const { track } of tracks) {
if (!track.lastRunAt) continue;
const candidateMs = Date.parse(track.lastRunAt);
if (Number.isNaN(candidateMs) || candidateMs <= latestRunMs) continue;
latestRunMs = candidateMs;
latestRunAt = track.lastRunAt;
}
return {
path: `knowledge/${filePath}`,
trackCount: tracks.length,
createdAt: createdMs > 0 ? new Date(createdMs).toISOString() : null,
lastRunAt: latestRunAt,
isActive: tracks.every(({ track }) => track.active !== false),
};
}
export async function listNotesWithTracks(): Promise<TrackNoteSummary[]> {
async function walk(relativeDir = ''): Promise<string[]> {
const dirPath = absPath(relativeDir);
try {
@ -102,16 +138,16 @@ export async function listNotesWithTracks(): Promise<Array<{ path: string; track
const markdownFiles = await walk();
const notes = await Promise.all(markdownFiles.map(async (relativePath) => {
const tracks = await fetchAll(relativePath);
if (tracks.length === 0) return null;
return {
path: `knowledge/${relativePath}`,
trackCount: tracks.length,
};
try {
const tracks = await fetchAll(relativePath);
return await summarizeTrackNote(relativePath, tracks);
} catch {
return null;
}
}));
return notes
.filter((note): note is { path: string; trackCount: number } => note !== null)
.filter((note): note is TrackNoteSummary => note !== null)
.sort((a, b) => {
const aName = path.basename(a.path, '.md').toLowerCase();
const bName = path.basename(b.path, '.md').toLowerCase();
@ -120,6 +156,36 @@ export async function listNotesWithTracks(): Promise<Array<{ path: string; track
});
}
export async function setNoteTracksActive(filePath: string, active: boolean): Promise<TrackNoteSummary | null> {
return withFileLock(absPath(filePath), async () => {
const blocks = await fetchAll(filePath);
if (blocks.length === 0) return null;
const alreadyMatches = blocks.every(({ track }) => (track.active !== false) === active);
if (alreadyMatches) {
return summarizeTrackNote(filePath, blocks);
}
const content = await fs.readFile(absPath(filePath), 'utf-8');
const lines = content.split('\n');
const updatedBlocks = blocks
.map((block) => ({
...block,
track: { ...block.track, active },
}))
.sort((a, b) => b.fenceStart - a.fenceStart);
for (const block of updatedBlocks) {
const yaml = stringifyYaml(block.track).trimEnd();
const yamlLines = yaml ? yaml.split('\n') : [];
lines.splice(block.fenceStart, block.fenceEnd - block.fenceStart + 1, '```track', ...yamlLines, '```');
}
await fs.writeFile(absPath(filePath), lines.join('\n'), 'utf-8');
return summarizeTrackNote(filePath, updatedBlocks);
});
}
/**
* Fetch a track block and return its canonical YAML string (or null if not found).
* Useful for IPC handlers that need to return the fresh YAML without taking a