From 697a43ebbc949dcac9958463d644805e4ef1a7ed Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Tue, 5 May 2026 20:13:55 +0530 Subject: [PATCH] initial commit --- apps/x/apps/main/src/ipc.ts | 5 + .../src/components/sidebar-content.tsx | 155 ++++++++++++++++++ .../core/src/knowledge/track/fileops.ts | 52 +++++- apps/x/packages/shared/src/ipc.ts | 9 + 4 files changed, 220 insertions(+), 1 deletion(-) diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index 056bb4c3..996aa6cf 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -52,6 +52,7 @@ import { getInstallationId } from '@x/core/dist/analytics/installation.js'; import { API_URL } from '@x/core/dist/config/env.js'; import { fetchYaml, + listNotesWithTracks, updateTrackBlock, replaceTrackBlockYaml, deleteTrackBlock, @@ -832,6 +833,10 @@ export function setupIpcHandlers() { return { success: false, error: err instanceof Error ? err.message : String(err) }; } }, + 'track:listNotes': async () => { + const notes = await listNotesWithTracks(); + return { notes }; + }, // Billing handler 'billing:getInfo': async () => { return await getBillingInfo(); diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 9c50c334..2558260c 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -89,6 +89,7 @@ import { } from "@/components/ui/context-menu" import { Input } from "@/components/ui/input" import { cn } from "@/lib/utils" +import { stripKnowledgePrefix, wikiLabel } from "@/lib/wiki-links" import { type ActiveSection, useSidebarSection } from "@/contexts/sidebar-context" import { ConnectorsPopover } from "@/components/connectors-popover" import { HelpPopover } from "@/components/help-popover" @@ -144,6 +145,11 @@ type BackgroundTaskItem = { lastRunAt?: string | null } +type TrackNoteItem = { + path: string + trackCount: number +} + type ServiceEventType = z.infer const MAX_SYNC_EVENTS = 1000 @@ -180,6 +186,151 @@ function collectServiceErrors(events: ServiceEventType[]): Map { return errors } +function isKnowledgeMarkdownPath(path: string | undefined): boolean { + return typeof path === "string" && path.startsWith("knowledge/") && path.endsWith(".md") +} + +function BackgroundAgentsShortcut({ + selectedPath, + onSelectFile, +}: { + selectedPath: string | null + onSelectFile: (path: string, kind: "file" | "dir") => void +}) { + const [open, setOpen] = useState(false) + const [notes, setNotes] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const reloadTimeoutRef = useRef | null>(null) + + const loadNotes = useCallback(async () => { + setLoading(true) + try { + const result = await window.ipc.invoke("track:listNotes", null) + setNotes(result.notes) + setError(null) + } catch (err) { + console.error("Failed to load background agent notes:", err) + setError("Couldn't load background agents.") + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + void loadNotes() + }, [loadNotes]) + + useEffect(() => { + const scheduleReload = () => { + if (reloadTimeoutRef.current) clearTimeout(reloadTimeoutRef.current) + reloadTimeoutRef.current = setTimeout(() => { + reloadTimeoutRef.current = null + void loadNotes() + }, 200) + } + + const cleanup = window.ipc.on("workspace:didChange", (event) => { + switch (event.type) { + case "created": + case "deleted": + case "changed": + if (isKnowledgeMarkdownPath(event.path)) scheduleReload() + break + case "moved": + if (isKnowledgeMarkdownPath(event.from) || isKnowledgeMarkdownPath(event.to)) { + scheduleReload() + } + break + case "bulkChanged": + if (!event.paths || event.paths.some(isKnowledgeMarkdownPath)) { + scheduleReload() + } + break + } + }) + + return () => { + cleanup() + if (reloadTimeoutRef.current) clearTimeout(reloadTimeoutRef.current) + } + }, [loadNotes]) + + return ( + + + + + +
+ {loading ? ( +
+ + Loading notes… +
+ ) : error ? ( +

{error}

+ ) : notes.length === 0 ? ( +

+ No notes with background agents yet. +

+ ) : ( + notes.map((note) => { + const relativePath = stripKnowledgePrefix(note.path) + const lastSlash = relativePath.lastIndexOf("/") + const folderPath = lastSlash >= 0 ? relativePath.slice(0, lastSlash) : "" + const isSelected = selectedPath === note.path + + return ( + + ) + }) + )} +
+
+
+ ) +} + type TasksActions = { onNewChat: () => void onSelectRun: (runId: string) => void @@ -679,6 +830,10 @@ export function SidebarContentPanel({ Suggested Topics )} + diff --git a/apps/x/packages/core/src/knowledge/track/fileops.ts b/apps/x/packages/core/src/knowledge/track/fileops.ts index bd731823..223b528c 100644 --- a/apps/x/packages/core/src/knowledge/track/fileops.ts +++ b/apps/x/packages/core/src/knowledge/track/fileops.ts @@ -70,6 +70,56 @@ export async function fetch(filePath: string, trackId: string): Promise b.track.trackId === trackId) ?? null; } +export async function listNotesWithTracks(): Promise> { + async function walk(relativeDir = ''): Promise { + const dirPath = absPath(relativeDir); + try { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + const files: string[] = []; + + for (const entry of entries) { + if (entry.name.startsWith('.')) continue; + + const childRelPath = relativeDir + ? path.posix.join(relativeDir, entry.name) + : entry.name; + + if (entry.isDirectory()) { + files.push(...await walk(childRelPath)); + continue; + } + + if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) { + files.push(childRelPath); + } + } + + return files; + } catch { + return []; + } + } + + 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, + }; + })); + + return notes + .filter((note): note is { path: string; trackCount: number } => note !== null) + .sort((a, b) => { + const aName = path.basename(a.path, '.md').toLowerCase(); + const bName = path.basename(b.path, '.md').toLowerCase(); + if (aName !== bName) return aName.localeCompare(bName); + return a.path.localeCompare(b.path); + }); +} + /** * 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 @@ -196,4 +246,4 @@ export async function deleteTrackBlock(filePath: string, trackId: string): Promi await fs.writeFile(absPath(filePath), lines.join('\n'), 'utf-8'); }); -} \ No newline at end of file +} diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 605b26d9..d2c2f6f6 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -662,6 +662,15 @@ const ipcSchemas = { error: z.string().optional(), }), }, + 'track:listNotes': { + req: z.null(), + res: z.object({ + notes: z.array(z.object({ + path: RelPath, + trackCount: z.number().int().positive(), + })), + }), + }, // Embedded browser (WebContentsView) channels 'browser:setBounds': { req: z.object({