Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit d7c538b

Browse files
committed
FEA: Replace agent-monitor sidecar with in-process SQLite database + React renderer
- Replace agent-dashboard sidecar (external process) with in-process node:sqlite database - Create functional repository layer matching symphony-alpha patterns (sessions, agents, events, dashboard stores) - Add Vite + React 19 renderer with DesignSystemProvider shell and DS components - Add hook ingestion endpoint (POST /api/hooks/event) writing directly to local SQLite - Extend preload bridge with typed db IPC channels (db.getSessions, db.getAgents, db.getEvents, db.getDashboardSummary) - Update window.ts to load Vite-built renderer (dev server in dev, file in prod) - Vendor @closedloop-ai/design-system as file: dep with subpath component exports - Preserve legacy HTML renderer as legacy.html for strangler-fig migration via iframe - Add DashboardPage using DS Card/Badge/Button components with real-time summary queries Testing: pnpm build:renderer (Vite) and npx tsc (main process) both compile clean; all DB-layer errors resolved; pre-existing tsc errors (node-pty, ws, react) unchanged. Risks: Sidecar not yet disabled — this adds parallel in-process DB alongside existing agent-monitor; legacy HTML still works via iframe fallback.
1 parent d297752 commit d7c538b

38 files changed

Lines changed: 10892 additions & 10852 deletions

apps/desktop/package.json

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
"type": "module",
88
"main": "dist/main/index.js",
99
"scripts": {
10-
"dev": "pnpm build && ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" .",
10+
"dev": "pnpm build:renderer && pnpm build && ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" .",
1111
"start": "ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" .",
12-
"clean:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
12+
"clean:dist": "node -e \"require('fs').rmSync('dist/main',{recursive:true,force:true});require('fs').rmSync('dist/server',{recursive:true,force:true})\"",
1313
"clean:package": "node -e \"require('fs').rmSync('dist-dmg',{recursive:true,force:true})\"",
1414
"prebuild": "node -e \"const{execSync:e}=require('child_process'),{writeFileSync:w}=require('fs');const h=e('git rev-parse HEAD').toString().trim();w('src/shared/build-info.ts','// AUTO-GENERATED — do not edit\\nexport const BUILD_COMMIT_HASH = \\\"'+h+'\\\";\\n');\"",
15-
"build": "pnpm clean:dist && pnpm prebuild && tsc -p tsconfig.json && pnpm build:agent-monitor",
15+
"build": "pnpm build:renderer && pnpm clean:dist && pnpm prebuild && tsc -p tsconfig.json",
1616
"build:agent-monitor": "node scripts/build-agent-monitor.mjs",
17+
"build:renderer": "vite build --config vite.renderer.config.ts",
1718
"dashboard:reset": "node scripts/reset-dashboard-db.mjs",
1819
"dashboard:reset-packs": "node scripts/reset-dashboard-db.mjs --packs-only",
1920
"stage:package": "node scripts/stage-packaging-app.mjs",
@@ -24,29 +25,37 @@
2425
"release": "pnpm clean:package && pnpm build && pnpm stage:package && node scripts/run-electron-builder.mjs --publish always"
2526
},
2627
"dependencies": {
28+
"@closedloop-ai/design-system": "file:vendor/design-system",
2729
"@closedloop-ai/loops-api": "0.2.11",
2830
"agent-dashboard": "github:hoangsonww/Claude-Code-Agent-Monitor#840c518d7fa69231de049e41b893938228b67e40",
2931
"busboy": "^1.6.0",
3032
"electron-log": "^5.4.3",
3133
"electron-store": "^8.2.0",
3234
"electron-updater": "^6.8.3",
3335
"glob": "^11.0.1",
36+
"next-themes": "^0.4.6",
37+
"react": "^19.2.6",
38+
"react-day-picker": "^9.14.0",
39+
"react-dom": "^19.2.6",
40+
"react-hook-form": "^7.76.1",
41+
"react-resizable-panels": "^3.0.6",
3442
"socket.io-client": "^4.8.1",
3543
"zod": "^4.0.0"
3644
},
3745
"devDependencies": {
38-
"@vitejs/plugin-react": "4.3.4",
46+
"@tailwindcss/postcss": "^4.3.0",
3947
"@types/busboy": "^1.5.4",
4048
"@types/node": "^22.13.8",
4149
"@typescript-eslint/eslint-plugin": "^8.57.1",
4250
"@typescript-eslint/parser": "^8.57.1",
51+
"@vitejs/plugin-react": "4.3.4",
4352
"agent-dashboard-client": "github:hoangsonww/Claude-Code-Agent-Monitor#840c518d7fa69231de049e41b893938228b67e40&path:/client",
4453
"autoprefixer": "10.4.20",
4554
"electron": "^35.0.2",
4655
"electron-builder": "^26.8.1",
4756
"eslint": "^10.0.3",
48-
"postcss": "8.5.1",
49-
"tailwindcss": "3.4.17",
57+
"postcss": "8.5.15",
58+
"tailwindcss": "4.3.0",
5059
"tsx": "^4.19.3",
5160
"typescript": "^5.8.2",
5261
"typescript-eslint": "^8.57.1",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
plugins: {
3+
"@tailwindcss/postcss": {},
4+
},
5+
};

apps/desktop/src/main/app.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { SettingsStore, type SavedConfigManagedPatch } from "./settings-store.js
6565
import { DesktopTray } from "./tray.js";
6666
import { DesktopWindow } from "./window.js";
6767
import { AgentMonitorSidecar } from "./agent-monitor-sidecar.js";
68+
import { openAgentDatabase } from "./database/index.js";
6869
import { AgentSessionSyncService } from "./agent-session-sync-service.js";
6970
import {
7071
isAgentMonitorHooksEnabled,
@@ -196,6 +197,7 @@ export class DesktopApplication {
196197
private readonly cloudSocket: CloudSocketService;
197198
private readonly commandExecutor: CloudCommandExecutor;
198199
private readonly agentMonitor: AgentMonitorSidecar;
200+
private readonly agentDatabase: ReturnType<typeof openAgentDatabase>;
199201
private readonly agentSessionSync: AgentSessionSyncService;
200202
private readonly activityLog: ActivityLogStore;
201203
private readonly approvalStore: ApprovalStore;
@@ -284,6 +286,9 @@ export class DesktopApplication {
284286
this.tray = new DesktopTray();
285287
this.desktopWindow = new DesktopWindow();
286288
this.agentMonitor = new AgentMonitorSidecar();
289+
this.agentDatabase = openAgentDatabase(
290+
path.join(app.getPath("userData"), "agent-dashboard.sqlite"),
291+
);
287292
this.activityLog = new ActivityLogStore();
288293
this.jobStore = new JobStore();
289294
this.approvalStore = new ApprovalStore({
@@ -3190,6 +3195,59 @@ export class DesktopApplication {
31903195
this.restartCloudSocket();
31913196
return appliedConfig;
31923197
});
3198+
3199+
ipcMain.handle("desktop:db:get-sessions", () => {
3200+
return this.agentDatabase.sessions.getAll();
3201+
});
3202+
3203+
ipcMain.handle("desktop:db:get-session", (_event, id: string) => {
3204+
return this.agentDatabase.sessions.getById(id);
3205+
});
3206+
3207+
ipcMain.handle("desktop:db:get-agents", (_event, sessionId: string) => {
3208+
return this.agentDatabase.agents.getBySession(sessionId);
3209+
});
3210+
3211+
ipcMain.handle(
3212+
"desktop:db:get-events",
3213+
(_event, sessionId: string, agentId?: string) => {
3214+
if (agentId) {
3215+
return this.agentDatabase.events.getBySessionAndAgent(
3216+
sessionId,
3217+
agentId,
3218+
);
3219+
}
3220+
return this.agentDatabase.events.getBySession(sessionId);
3221+
},
3222+
);
3223+
3224+
ipcMain.handle("desktop:db:get-dashboard-summary", () => {
3225+
return this.agentDatabase.getSummary();
3226+
});
3227+
3228+
ipcMain.handle("desktop:db:get-sessions-with-details", () => {
3229+
return this.agentDatabase.sessions.getAllWithDetails();
3230+
});
3231+
3232+
ipcMain.handle("desktop:db:get-event-feed", () => {
3233+
return this.agentDatabase.events.getAll();
3234+
});
3235+
3236+
ipcMain.handle("desktop:db:get-events-with-session", (_event, sessionId: string) => {
3237+
return this.agentDatabase.events.getWithSession(sessionId);
3238+
});
3239+
3240+
ipcMain.handle("desktop:db:get-event-count-by-type", () => {
3241+
return this.agentDatabase.events.getCountByType();
3242+
});
3243+
3244+
ipcMain.handle("desktop:db:get-token-analytics", () => {
3245+
return this.agentDatabase.dashboard.getTokenAnalytics();
3246+
});
3247+
3248+
ipcMain.handle("desktop:db:get-agent-hierarchy", (_event, sessionId: string) => {
3249+
return this.agentDatabase.agents.getBySessionWithChildren(sessionId, this.agentDatabase.events);
3250+
});
31933251
}
31943252

31953253
private signDesktopRequest(
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { DatabaseSync } from "node:sqlite";
2+
import type { AgentRow, AgentHierarchyNode, HookEventPayload } from "./types.js";
3+
4+
export function createAgentStore(db: DatabaseSync) {
5+
const insertStmt = db.prepare(`
6+
INSERT INTO agents (id, session_id, name, type, subagent_type, status, task, current_tool, started_at, updated_at, parent_agent_id, metadata)
7+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8+
`);
9+
10+
const updateStatusStmt = db.prepare(`
11+
UPDATE agents SET status = ?, updated_at = ?, ended_at = ? WHERE id = ?
12+
`);
13+
14+
const updateStmt = db.prepare(`
15+
UPDATE agents SET name = ?, task = ?, current_tool = ?, updated_at = ? WHERE id = ?
16+
`);
17+
18+
const getBySessionStmt = db.prepare(
19+
"SELECT * FROM agents WHERE session_id = ? ORDER BY started_at ASC",
20+
);
21+
const getByIdStmt = db.prepare("SELECT * FROM agents WHERE id = ?");
22+
23+
const getBySessionWithChildrenStmt = db.prepare(`
24+
SELECT a.*,
25+
(SELECT COUNT(*) FROM agents child WHERE child.parent_agent_id = a.id) as children_count
26+
FROM agents a WHERE a.session_id = ? ORDER BY a.started_at ASC
27+
`);
28+
29+
function toRow(raw: Record<string, unknown> | undefined): AgentRow | undefined {
30+
if (!raw) return undefined;
31+
return {
32+
id: raw.id as string,
33+
sessionId: raw.session_id as string,
34+
name: (raw.name as string) ?? null,
35+
type: (raw.type as string) ?? null,
36+
subagentType: (raw.subagent_type as string) ?? null,
37+
status: raw.status as string,
38+
task: (raw.task as string) ?? null,
39+
currentTool: (raw.current_tool as string) ?? null,
40+
startedAt: (raw.started_at as string) ?? null,
41+
updatedAt: (raw.updated_at as string) ?? null,
42+
endedAt: (raw.ended_at as string) ?? null,
43+
awaitingInputSince: (raw.awaiting_input_since as string) ?? null,
44+
parentAgentId: (raw.parent_agent_id as string) ?? null,
45+
metadata: (raw.metadata as string) ?? null,
46+
};
47+
}
48+
49+
function rowsToList(raws: Record<string, unknown>[]): AgentRow[] {
50+
return raws.map(toRow).filter(Boolean) as AgentRow[];
51+
}
52+
53+
return {
54+
upsert(payload: HookEventPayload): AgentRow {
55+
if (!payload.agentId || !payload.sessionId) {
56+
throw new Error("agentId and sessionId are required");
57+
}
58+
59+
const existing = toRow(getByIdStmt.get(payload.agentId) as Record<string, unknown> | undefined);
60+
const now = new Date().toISOString();
61+
62+
if (existing) {
63+
if (payload.status) {
64+
const endedAt = ["completed", "failed", "stopped"].includes(payload.status) ? now : null;
65+
updateStatusStmt.run(payload.status, now, endedAt, payload.agentId);
66+
}
67+
if (payload.name || payload.task || payload.toolName) {
68+
updateStmt.run(
69+
payload.name ?? existing.name,
70+
payload.task ?? existing.task,
71+
payload.toolName ?? existing.currentTool,
72+
now,
73+
payload.agentId,
74+
);
75+
}
76+
return toRow(getByIdStmt.get(payload.agentId) as Record<string, unknown>)!;
77+
}
78+
79+
insertStmt.run(
80+
payload.agentId,
81+
payload.sessionId,
82+
payload.name ?? null,
83+
payload.type ?? null,
84+
payload.subagentType ?? null,
85+
payload.status ?? "running",
86+
payload.task ?? null,
87+
payload.toolName ?? null,
88+
now,
89+
now,
90+
payload.parentAgentId ?? null,
91+
payload.metadata ? JSON.stringify(payload.metadata) : null,
92+
);
93+
94+
return toRow(getByIdStmt.get(payload.agentId) as Record<string, unknown>)!;
95+
},
96+
97+
getBySession(sessionId: string): AgentRow[] {
98+
return rowsToList(getBySessionStmt.all(sessionId) as Record<string, unknown>[]);
99+
},
100+
101+
getBySessionWithChildren(sessionId: string, eventStore: { getBySession: (sid: string) => { agentId: string | null; eventType: string; toolName: string | null; summary: string | null; createdAt: string | null }[] }): AgentHierarchyNode[] {
102+
const raws = getBySessionWithChildrenStmt.all(sessionId) as Record<string, unknown>[];
103+
const allAgents = raws.map((r) => ({
104+
...toRow(r)!,
105+
childrenCount: r.children_count as number,
106+
}));
107+
108+
const agentMap = new Map<string, AgentHierarchyNode>();
109+
const roots: AgentHierarchyNode[] = [];
110+
111+
for (const agent of allAgents) {
112+
const events = eventStore.getBySession(sessionId)
113+
.filter((e) => e.agentId === agent.id)
114+
.map((e) => ({
115+
eventType: e.eventType,
116+
toolName: e.toolName,
117+
summary: e.summary,
118+
createdAt: e.createdAt,
119+
}));
120+
121+
const node: AgentHierarchyNode = {
122+
agentId: agent.id,
123+
name: agent.name,
124+
type: agent.type,
125+
subagentType: agent.subagentType,
126+
status: agent.status,
127+
task: agent.task,
128+
currentTool: agent.currentTool,
129+
children: [],
130+
events,
131+
};
132+
agentMap.set(agent.id, node);
133+
}
134+
135+
for (const agent of allAgents) {
136+
const node = agentMap.get(agent.id)!;
137+
if (agent.parentAgentId && agentMap.has(agent.parentAgentId)) {
138+
agentMap.get(agent.parentAgentId)!.children.push(node);
139+
} else {
140+
roots.push(node);
141+
}
142+
}
143+
144+
return roots;
145+
},
146+
};
147+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { DatabaseSync } from "node:sqlite";
2+
import type { DashboardSummary, TokenAnalytics } from "./types.js";
3+
4+
export function createDashboardQueries(db: DatabaseSync) {
5+
const totalSessionsStmt = db.prepare("SELECT COUNT(*) as count FROM sessions");
6+
const activeSessionsStmt = db.prepare(
7+
"SELECT COUNT(*) as count FROM sessions WHERE status NOT IN ('completed', 'failed', 'stopped')",
8+
);
9+
const totalAgentsStmt = db.prepare("SELECT COUNT(*) as count FROM agents");
10+
const totalEventsStmt = db.prepare("SELECT COUNT(*) as count FROM events");
11+
const totalTokensStmt = db.prepare(`
12+
SELECT COALESCE(SUM(input_tokens + output_tokens), 0) as total
13+
FROM token_usage
14+
`);
15+
const recentSessionsStmt = db.prepare(
16+
"SELECT id, name, status, model, cwd, started_at FROM sessions ORDER BY started_at DESC LIMIT 10",
17+
);
18+
19+
const tokenAnalyticsStmt = db.prepare(`
20+
SELECT COALESCE(SUM(input_tokens), 0) as totalInput,
21+
COALESCE(SUM(output_tokens), 0) as totalOutput,
22+
COALESCE(SUM(cache_read_tokens), 0) as totalCacheRead,
23+
COALESCE(SUM(cache_write_tokens), 0) as totalCacheWrite
24+
FROM token_usage
25+
`);
26+
27+
const tokenByModelStmt = db.prepare(`
28+
SELECT model,
29+
SUM(input_tokens) as inputTokens,
30+
SUM(output_tokens) as outputTokens,
31+
COUNT(DISTINCT session_id) as sessions
32+
FROM token_usage
33+
WHERE model IS NOT NULL
34+
GROUP BY model
35+
ORDER BY SUM(input_tokens + output_tokens) DESC
36+
`);
37+
38+
const tokenByDayStmt = db.prepare(`
39+
SELECT DATE(t.created_at) as day,
40+
SUM(t.input_tokens) as inputTokens,
41+
SUM(t.output_tokens) as outputTokens
42+
FROM token_usage t
43+
WHERE t.created_at IS NOT NULL
44+
GROUP BY DATE(t.created_at)
45+
ORDER BY day DESC
46+
LIMIT 30
47+
`);
48+
49+
return {
50+
getSummary(): DashboardSummary {
51+
const totalSessions = (totalSessionsStmt.get() as { count: number }).count;
52+
const activeSessions = (activeSessionsStmt.get() as { count: number }).count;
53+
const totalAgents = (totalAgentsStmt.get() as { count: number }).count;
54+
const totalEvents = (totalEventsStmt.get() as { count: number }).count;
55+
const totalTokens = (totalTokensStmt.get() as { total: number }).total;
56+
const recentSessions = recentSessionsStmt.all() as Array<{
57+
id: string;
58+
name: string | null;
59+
status: string;
60+
model: string | null;
61+
cwd: string | null;
62+
started_at: string | null;
63+
}>;
64+
65+
return {
66+
totalSessions,
67+
activeSessions,
68+
totalAgents,
69+
totalEvents,
70+
totalTokens,
71+
recentSessions: recentSessions.map((s) => ({
72+
id: s.id,
73+
name: s.name,
74+
status: s.status,
75+
model: s.model,
76+
cwd: s.cwd,
77+
startedAt: s.started_at,
78+
})),
79+
};
80+
},
81+
82+
getTokenAnalytics(): TokenAnalytics {
83+
const totals = tokenAnalyticsStmt.get() as { totalInput: number; totalOutput: number; totalCacheRead: number; totalCacheWrite: number };
84+
const byModel = tokenByModelStmt.all() as Array<{ model: string; inputTokens: number; outputTokens: number; sessions: number }>;
85+
const byDay = tokenByDayStmt.all() as Array<{ day: string; inputTokens: number; outputTokens: number }>;
86+
87+
return {
88+
totalInputTokens: totals.totalInput,
89+
totalOutputTokens: totals.totalOutput,
90+
totalCacheReadTokens: totals.totalCacheRead,
91+
totalCacheWriteTokens: totals.totalCacheWrite,
92+
byModel,
93+
byDay,
94+
};
95+
},
96+
};
97+
}

0 commit comments

Comments
 (0)