Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a6427ce
FEA: Replace agent-monitor sidecar with in-process SQLite database + …
mikeangstadt Jun 1, 2026
de59efd
FEA: Address PR #264 review feedback + feature parity + performance
mikeangstadt Jun 1, 2026
132c2d3
FEA: Hoist prepared statements, fix nav labels, reorder nav
mikeangstadt Jun 1, 2026
6d4b957
FEA: Fix query perf on large databases (2.7M events)
mikeangstadt Jun 1, 2026
cd526af
FEA: Fix rowid index crash + add manual chunk splitting
mikeangstadt Jun 1, 2026
bfd25a0
FEA: Add covering indexes for 2.8M event table + decouple dashboard load
mikeangstadt Jun 1, 2026
9272a7e
FEA: Consolidate nav — merge Analytics+Workflows into Agents, Activit…
mikeangstadt Jun 1, 2026
d623034
FEA-1497: Merge main + fix build (Phase 0)
thadeusb Jun 2, 2026
5342e2b
FEA-1497: Green the test suite after main-merge (Phase 0)
thadeusb Jun 2, 2026
3911766
FEA-1497: Re-merge main (#265, #266) to clear PR conflict (Phase 0)
thadeusb Jun 2, 2026
09e9df4
FEA-1497: Fix pre-existing lint errors blocking CI (Phase 0)
thadeusb Jun 2, 2026
617b9a7
FEA-1497: In-process hook listener + write engine (Phase 1 S1+S2)
thadeusb Jun 2, 2026
e9972e7
FEA-1497: Codex hook ingestion opt-in (Phase 1 S3)
thadeusb Jun 2, 2026
5165ecf
FEA-1497: Renderer typed IPC + session drill-down + live updates (Pha…
thadeusb Jun 2, 2026
1b6230d
FEA-1497: Atomic sidecar cutover + relay/recon repoint + boot migrati…
thadeusb Jun 2, 2026
a0a7b2b
FEA-1497: Address Phase 1 review findings (migration FK abort + harde…
thadeusb Jun 2, 2026
9f357c4
FEA-1497: Reference breaking-change ticket IDs (FEA-1500, FEA-1501) i…
thadeusb Jun 2, 2026
cd8e6d5
FEA-1497: Address PR #264 review findings (validation, packaging, typ…
thadeusb Jun 2, 2026
26259a7
FEA: consume published design-system package
mikeangstadt Jun 3, 2026
d589c41
FEA: avoid broken design-system root provider
mikeangstadt Jun 3, 2026
e366306
FEA: patch published design-system bundle in vite
mikeangstadt Jun 3, 2026
fd3c992
FEA: switch desktop to published design-system package
mikeangstadt Jun 3, 2026
0280eed
FEA: avoid analytics fetch on dashboard load
mikeangstadt Jun 3, 2026
846bb40
FEA: tighten sessions dashboard layout
mikeangstadt Jun 3, 2026
5c47688
FEA: defer to design-system renderer globals
mikeangstadt Jun 3, 2026
3a20dad
FEA: cache historical sessions and preserve views
mikeangstadt Jun 3, 2026
eb8157a
FEA: defer heavy analytics sections
mikeangstadt Jun 3, 2026
a876313
FEA-1503: In-process multi-harness collection + vendor removal
thadeusb Jun 3, 2026
ca3855c
Merge remote-tracking branch 'origin/feat/in-process-agent-database' …
thadeusb Jun 3, 2026
1da1146
FEA-1503: Fix CI 403 on @closedloop-ai/design-system via org-scoped a…
thadeusb Jun 3, 2026
6847025
FEA-1503: Revert CI app-token plumbing; grant-based package access
thadeusb Jun 3, 2026
b4ba906
FEA-1497: Address PR #264 review findings (DS pinning, Topbar labels,…
mikeangstadt Jun 4, 2026
f40b4d1
FEA-1497: Address remaining PR #264 review findings (settings parity,…
shafty023 Jun 4, 2026
143dc11
Merge remote-tracking branch 'origin/feat/in-process-agent-database' …
shafty023 Jun 4, 2026
0372f26
FEA-1504: Finalize gated agent dashboard
shafty023 Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
dist
!apps/desktop/vendor/design-system/dist/
.DS_Store
.idea
.vscode
Expand Down
56 changes: 36 additions & 20 deletions apps/desktop/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,26 +136,42 @@ Typical log locations:

The Diagnostics tab shows the current in-memory gateway log plus a bounded previous-session tail read from `main.log` at startup. First-run or unreadable log files must not block boot; return an empty previous-session tail and continue.

## Agent Monitor Sidecar

The desktop app bundles the MIT-licensed `Claude-Code-Agent-Monitor`
(`agent-dashboard` + `agent-dashboard-client`, pinned in
`apps/desktop/package.json`) and runs a generated runtime tree as a managed
localhost **sidecar** for local Claude Code session/agent observability. It is
the single embedded observability tool. It powers the **Dashboard** and the
agent nav items (Sessions, Kanban, Activity Feed, etc.) in the desktop left
sidebar. The feature is gated by the persisted `agentMonitorEnabled` desktop
setting, which **defaults ON**; when disabled, the agent nav items are hidden
and only the Gateway section remains.

- **Process model:** `src/main/agent-monitor-sidecar.ts` spawns the generated
`server/index.js` from `apps/desktop/.generated/agent-monitor/` (packaged:
unpacked `extraResources/agent-monitor`) using the Electron binary as Node
(`ELECTRON_RUN_AS_NODE=1`, `process.execPath`) — a packaged app ships no
standalone `node`. Started fire-and-forget from `boot()` **only when
`agentMonitorEnabled` is true**, and still before the gateway-start try-block
so a gateway-start failure never prevents it from running and a sidecar
failure never blocks or fails app boot.
## Agent Monitor (in-process)

> **Status (FEA-1497 Phase 1):** the runtime is now FIRST-PARTY and IN-PROCESS.
> The vendor sidecar is no longer spawned: `src/main/agent-monitor-listener.ts`
> (`AgentHookListener`) owns `127.0.0.1:4820` in the main process, writing
> through the `node:sqlite` repository layer (`src/main/database/`) via the hook
> lifecycle state machine (`database/lifecycle.ts`). The renderer is a
> first-party React app (`src/renderer/`) — there is NO iframe. The cloud relay
> and cost-reconciliation worker read the same in-process DB through the shared
> connection. The vendor `agent-monitor-sidecar.ts` / `build-agent-monitor.mjs`
> / `scripts/agent-monitor-*` source survives DORMANT (still builds the hook
> handler scripts) and is physically deleted in Phase 3E. The bullets below that
> describe the spawned sidecar/iframe runtime are historical until then.

The desktop app provides local Claude Code (and opt-in Codex) session/agent
observability. It powers the **Dashboard** and the agent nav items (Sessions,
Activity, Analytics, Workflows, Kanban) in the desktop left sidebar. The feature
is gated by the persisted `agentMonitorEnabled` desktop setting, which
**defaults ON**; when disabled, the agent nav items are hidden and only the
Gateway section remains.

- **Process model (current):** `src/main/agent-monitor-listener.ts` binds
`127.0.0.1:4820` in the main process and accepts the unchanged hook payload
(`POST /api/hooks/event`, `GET /api/health`). Each event is gated by the
FEA-1407 sandbox check, harness-stamped from `__provider`, and applied by the
lifecycle state machine in one `BEGIN IMMEDIATE` transaction. Started from
`startAgentCapture()` (boot + the enable path) **only when
`agentMonitorEnabled` is true**, before the gateway-start try-block, and a
bind failure (EADDRINUSE) degrades to "no monitor" rather than blocking boot.
A one-time boot migration carries the historical vendor `dashboard.db` into
the in-process DB and renames it `dashboard.db.migrated` (downgrade-safe).
- **Historical (dormant until Phase 3E):** `src/main/agent-monitor-sidecar.ts`
spawned the generated `server/index.js` from
`apps/desktop/.generated/agent-monitor/` (packaged: unpacked
`extraResources/agent-monitor`) using the Electron binary as Node
(`ELECTRON_RUN_AS_NODE=1`, `process.execPath`).
- **Fixed port (differs from the gateway):** `127.0.0.1:4820`
(`AGENT_MONITOR_PORT` in `src/shared/contracts.ts`), passed via
`DASHBOARD_PORT`. It MUST be fixed — Claude Code hooks bake a port at install
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export default tseslint.config(
...tseslint.configs.recommended,
{
files: ["src/**/*.ts"],
// src/renderer is built by Vite and is excluded from tsconfig.json, so it
// is not part of the type-aware TypeScript program. Exclude it from this
// type-checked block too (otherwise its .ts/.d.ts files error with
// "parserOptions.project ... file not found"); renderer files are still
// linted by the recommended (syntactic) config above.
ignores: ["src/renderer/**"],
languageOptions: {
parserOptions: {
project: "./tsconfig.json",
Expand Down
34 changes: 25 additions & 9 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
{
"name": "desktop",
"version": "0.15.106",
"version": "0.15.109",
"description": "ClosedLoop Desktop",
"author": "ClosedLoop AI <support@closedloop.ai>",
"private": true,
"type": "module",
"main": "dist/main/index.js",
"scripts": {
"dev": "pnpm build && ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" .",
"start": "pnpm build:agent-monitor && ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" .",
"clean:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
"start": "ELECTRON_BIN=$(bash scripts/patch-electron-plist.sh | tail -1) && \"$ELECTRON_BIN\" .",
"clean:dist": "node -e \"require('fs').rmSync('dist/main',{recursive:true,force:true});require('fs').rmSync('dist/server',{recursive:true,force:true})\"",
"clean:package": "node -e \"require('fs').rmSync('dist-dmg',{recursive:true,force:true})\"",
"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');\"",
"build": "pnpm clean:dist && pnpm prebuild && tsc -p tsconfig.json && pnpm build:agent-monitor",
"build": "pnpm clean:dist && pnpm prebuild && tsc -p tsconfig.json && pnpm build:renderer",
Comment thread
thadeusb marked this conversation as resolved.
Outdated
"build:agent-monitor": "node scripts/build-agent-monitor.mjs",
"build:renderer": "node scripts/patch-vendor-ds-react.mjs && vite build --config vite.renderer.config.ts",
"dashboard:reset": "node scripts/reset-dashboard-db.mjs",
"dashboard:reset-packs": "node scripts/reset-dashboard-db.mjs --packs-only",
"stage:package": "node scripts/stage-packaging-app.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
"typecheck": "tsc -p tsconfig.json --noEmit && pnpm typecheck:renderer",
"typecheck:renderer": "tsc -p tsconfig.renderer.json --noEmit",
"lint": "eslint src/",
"test": "tsx --test test/*.test.ts && node --test \"scripts/agent-monitor-packs/__tests__/*.test.js\" \"scripts/agent-monitor-pull-requests/__tests__/*.test.js\"",
"pretest:contract": "pnpm build:agent-monitor",
Expand All @@ -33,10 +35,11 @@
"test:audit:ui": "playwright test --config test-e2e/agent-monitor/playwright.audit.config.ts",
"preaudit:report": "pnpm build:agent-monitor",
"audit:report": "node test-e2e/agent-monitor/inventory/run-report.mjs",
"package": "pnpm clean:package && pnpm build && pnpm stage:package && node scripts/run-electron-builder.mjs",
"release": "pnpm clean:package && pnpm build && pnpm stage:package && node scripts/run-electron-builder.mjs --publish always"
"package": "pnpm clean:package && pnpm build && pnpm build:agent-monitor && pnpm stage:package && node scripts/run-electron-builder.mjs",
"release": "pnpm clean:package && pnpm build && pnpm build:agent-monitor && pnpm stage:package && node scripts/run-electron-builder.mjs --publish always"
},
"dependencies": {
"@closedloop-ai/design-system": "file:vendor/design-system",
Comment thread
thadeusb marked this conversation as resolved.
Outdated
"@closedloop-ai/loops-api": "0.2.11",
"@pydantic/genai-prices": "0.0.62",
"agent-dashboard": "github:hoangsonww/Claude-Code-Agent-Monitor#840c518d7fa69231de049e41b893938228b67e40",
Expand All @@ -45,13 +48,25 @@
"electron-store": "^8.2.0",
"electron-updater": "^6.8.3",
"glob": "^11.0.1",
"lucide-react": "^0.556.0",
"next-themes": "^0.4.6",
"react": "^19.2.6",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.6",
"react-hook-form": "^7.76.1",
"react-resizable-panels": "^3.0.6",
"socket.io-client": "^4.8.1",
"zod": "^4.0.0"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@tailwindcss/postcss": "^4.3.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.3.0",
"@types/busboy": "^1.5.4",
"@types/node": "^22.13.8",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1",
"@vitejs/plugin-react": "4.3.4",
Expand All @@ -60,8 +75,9 @@
"electron": "^35.0.2",
"electron-builder": "^26.8.1",
"eslint": "^10.0.3",
"postcss": "8.5.1",
"tailwindcss": "3.4.17",
"postcss": "8.5.15",
"tailwindcss": "4.3.0",
"tw-animate-css": "^1.4.0",
"tsx": "^4.19.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.57.1",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/postcss.renderer.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
80 changes: 80 additions & 0 deletions apps/desktop/scripts/agent-monitor-codex/codex-hook-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node

/**
* @file codex-hook-handler.js
* @description Codex CLI hook handler. Mirrors the upstream Claude
* `hook-handler.js` (provider-agnostic, POSTs to `/api/hooks/event` on the
* fixed agent-monitor sidecar port 4820) but injects `__provider: "codex"`
* into the forwarded payload so the sidecar can stamp the session row with
* `harness='codex'` via the existing `setSessionHarness` statement.
*
* Zero-dep, plain JS, fail-silent — same constraints as the upstream Claude
* handler so a hook never blocks a Codex turn. Codex calls this once per
* lifecycle event (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse,
* Stop) with the event name as the single argv arg.
*
* Part of FEA-1444 (opt-in Codex hook ingestion).
*/

const http = require("http");

const hookType = process.argv[2] || "unknown";
const port = parseInt(process.env.CLAUDE_DASHBOARD_PORT || "4820", 10);

let input = "";

process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => (input += chunk));
process.stdin.on("end", () => {
let parsedData;
try {
parsedData = JSON.parse(input);
} catch {
parsedData = { raw: input };
}

// Mark the payload as Codex-sourced so the sidecar's hooks-route patch
// (build-agent-monitor.mjs `patchHooksRouteCodexHarness`) can stamp the
// session's `harness` column. Field name is dunder-prefixed to make it
// obvious this is a transport hint, not a Codex-native field.
const enrichedData =
parsedData && typeof parsedData === "object" && !Array.isArray(parsedData)
? { ...parsedData, __provider: "codex" }
: { raw: parsedData, __provider: "codex" };

const payload = JSON.stringify({
hook_type: hookType,
data: enrichedData,
});

const req = http.request(
{
hostname: "127.0.0.1",
port,
path: "/api/hooks/event",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
},
timeout: 3000,
},
(res) => {
res.resume();
process.exit(0);
},
);

req.on("error", () => process.exit(0));
req.on("timeout", () => {
req.destroy();
process.exit(0);
});

req.write(payload);
req.end();
});

// Safety net timeout — Codex's default hook timeout is around 5s; never let
// this process linger longer than that.
setTimeout(() => process.exit(0), 5000);
9 changes: 9 additions & 0 deletions apps/desktop/scripts/build-agent-monitor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,14 @@ function materializeRuntimeTree() {
path.join(generatedRootDir, "scripts"),
{ recursive: true },
);
// FEA-1444 (carried into FEA-1497): the Codex wrapper hook handler ships
// in-repo (not vendored from upstream agent-dashboard) and is copied into the
// generated scripts/ tree so installCodexHooks resolves the SAME scriptsDir
// as the upstream Claude handler, for both dev and packaged builds.
cpSync(
path.join(codexModulesDir, "codex-hook-handler.js"),
path.join(generatedRootDir, "scripts", "codex-hook-handler.js"),
);
// The plans HTTP route lives in the generated server/routes (server/ was
// copied above); import-history.js (just copied with scripts/) is patched to
// persist captured plans on the shared import sink — both harnesses, history,
Expand Down Expand Up @@ -3653,6 +3661,7 @@ function assertGeneratedTree() {
generatedClientIndex,
path.join(generatedRootDir, "scripts", "install-hooks.js"),
path.join(generatedRootDir, "scripts", "hook-handler.js"),
path.join(generatedRootDir, "scripts", "codex-hook-handler.js"),
generatedUninstallHooks,
]) {
if (!existsSync(required)) {
Expand Down
103 changes: 103 additions & 0 deletions apps/desktop/scripts/patch-vendor-ds-react.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Patches vendored @closedloop-ai/design-system dist files that reference
// `React` as a bare global (missing/incorrect import/require statements).
// Run after pnpm install or by build:renderer.

import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
import { join, extname } from "node:path";

const VENDOR_DIST = new URL(
"../vendor/design-system/dist",
import.meta.url,
).pathname;

const REACT_CJS_LINE = 'var React = require("react");\n';
const REACT_ESM_LINE = 'import React from "react";\n';

function hasScopeImport(code) {
return (
/\bvar\s+React\b\s*=/.test(code) ||
/import\s+(?:\*\s+as\s+)?React\s+from\s/.test(code) ||
/\bimport\s+\{[^}]*\bReact\b[^}]*\}\s+from\s/.test(code)
);
}

function walk(dir) {
const entries = readdirSync(dir);
for (const entry of entries) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
walk(full);
} else if (entry.endsWith(".js") || entry.endsWith(".cjs") || entry.endsWith(".mjs")) {
patch(full);
}
}
}

function patch(file) {
let code = readFileSync(file, "utf-8");
const ext = extname(file);

// --- Step 1: Deduplicate stale patches from earlier tool runs ---
// If our patch line is present AND there's already a proper React import,
// remove the stale patch line.
const lines = code.split("\n");
let changed = false;

// For ESM: check if line 1 is our patch and another React import exists
if (
ext === ".mjs" &&
(lines[0] === 'import React from "react"' || lines[0] === 'import React from "react";') &&
hasScopeImport(code.slice(code.indexOf("\n") + 1))
) {
code = lines.slice(1).join("\n");
changed = true;
}

// For CJS: check if line 2 (after "use strict") is our patch
if (
ext !== ".mjs" &&
lines[0] === '"use strict"' &&
(lines[1] === 'var React = require("react")' || lines[1] === 'var React = require("react");') &&
hasScopeImport(code.slice(code.indexOf("\n", code.indexOf("\n") + 1) + 1))
) {
code = '"use strict";\n' + lines.slice(2).join("\n");
changed = true;
}

// --- Step 2: Re-check if already patched (dedup may have just cleaned it) ---
if (code.includes(REACT_ESM_LINE) || code.includes(REACT_CJS_LINE)) {
if (changed) {
writeFileSync(file, code);
console.log(` dedup: ${file.replace(VENDOR_DIST, "")}`);
}
return;
}

// If dedup removed our patch but the file already has its own React import, we're done.
if (changed && hasScopeImport(code)) {
writeFileSync(file, code);
console.log(` dedup: ${file.replace(VENDOR_DIST, "")}`);
return;
}

// --- Step 3: Check if file needs patching ---
if (!/\bReact\s*[\.\(]/.test(code)) {
return;
}

// Skip if React is already properly in scope
if (hasScopeImport(code)) {
return;
}

const line = ext === ".mjs" ? REACT_ESM_LINE : REACT_CJS_LINE;
if (code.startsWith('"use strict";\n')) {
code = code.replace('"use strict";\n', '"use strict";\n' + line);
} else {
code = line + code;
}
writeFileSync(file, code);
console.log(` patched: ${file.replace(VENDOR_DIST, "")}`);
}

walk(VENDOR_DIST);
Loading
Loading