claude-code-session-manager 0.10.5 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/assets/{TiptapBody-BroECZ_z.js → TiptapBody-CAJSNRPs.js} +1 -1
  2. package/dist/assets/{cssMode-Crq-Rykh.js → cssMode-o7rZCrm4.js} +1 -1
  3. package/dist/assets/{freemarker2-B6CC21Ql.js → freemarker2-CgmCS5Wh.js} +1 -1
  4. package/dist/assets/{handlebars-BLgR-12n.js → handlebars-BcPLqhPv.js} +1 -1
  5. package/dist/assets/{html-CiQkt_KY.js → html-CC9xWnC3.js} +1 -1
  6. package/dist/assets/{htmlMode-Cy8mc91p.js → htmlMode-DEgCqH7k.js} +1 -1
  7. package/dist/assets/{index-DU-o-LEm.js → index-C7ljEoqc.js} +1161 -1130
  8. package/dist/assets/{index-DW-tvyin.css → index-CH3K1pkS.css} +1 -1
  9. package/dist/assets/{javascript-CHNCN8qj.js → javascript-CjwqkQrn.js} +1 -1
  10. package/dist/assets/{jsonMode-BSN7mvBT.js → jsonMode-BYTLu76d.js} +1 -1
  11. package/dist/assets/{liquid-B0kmZauA.js → liquid-wbQUuJwT.js} +1 -1
  12. package/dist/assets/{lspLanguageFeatures-DI_RToRa.js → lspLanguageFeatures-BJGMI7Xu.js} +1 -1
  13. package/dist/assets/{mdx-BSF-fsyJ.js → mdx-DcDstgPF.js} +1 -1
  14. package/dist/assets/{python-DUl3Fmgk.js → python-B96yyM_5.js} +1 -1
  15. package/dist/assets/{razor-Df7WxBjo.js → razor-C7aRIxIE.js} +1 -1
  16. package/dist/assets/{tsMode-qccVs0_G.js → tsMode-B3UYlGaL.js} +1 -1
  17. package/dist/assets/{typescript-BEwM5qbq.js → typescript-CV587TvC.js} +1 -1
  18. package/dist/assets/{xml-CCtx-_Kw.js → xml-PWUJecBf.js} +1 -1
  19. package/dist/assets/{yaml-B66nOkCW.js → yaml-D8bBNHE4.js} +1 -1
  20. package/dist/index.html +2 -2
  21. package/package.json +1 -1
  22. package/src/main/agentMemory.cjs +267 -0
  23. package/src/main/files.cjs +346 -0
  24. package/src/main/git.cjs +333 -0
  25. package/src/main/historyAggregator.cjs +70 -0
  26. package/src/main/index.cjs +12 -0
  27. package/src/main/ipcSchemas.cjs +62 -0
  28. package/src/main/projectSkills.cjs +124 -0
  29. package/src/main/superagent.cjs +202 -0
  30. package/src/preload/api.d.ts +203 -0
  31. package/src/preload/index.cjs +47 -0
@@ -105,6 +105,34 @@ async function parseJSONL(filePath, stat) {
105
105
  return acc;
106
106
  }
107
107
 
108
+ /** Lightweight per-file meta: { firstTs, lastTs, inputTokens, outputTokens, skipped }.
109
+ * Powers the `history:list-conversations` IPC used by the Overview detailed-
110
+ * stats panel. Single-pass O(L) scan, only honors ts + usage blocks. */
111
+ async function parseConversationMeta(filePath, stat) {
112
+ const meta = { firstTs: null, lastTs: null, inputTokens: 0, outputTokens: 0, skipped: false };
113
+ if (stat.size > MAX_FILE_BYTES) { meta.skipped = true; return meta; }
114
+ let text;
115
+ try { text = await fsp.readFile(filePath, 'utf8'); } catch { return meta; }
116
+ const lines = text.split('\n');
117
+ for (const raw of lines) {
118
+ const line = raw.trim();
119
+ if (!line) continue;
120
+ let obj;
121
+ try { obj = JSON.parse(line); } catch { continue; }
122
+ const ts = obj.ts ?? obj.timestamp;
123
+ if (ts) {
124
+ if (meta.firstTs === null) meta.firstTs = ts;
125
+ meta.lastTs = ts;
126
+ }
127
+ const usage = obj.usage ?? obj.message?.usage;
128
+ if (usage && typeof usage === 'object') {
129
+ if (typeof usage.inputTokens === 'number') meta.inputTokens += usage.inputTokens;
130
+ if (typeof usage.outputTokens === 'number') meta.outputTokens += usage.outputTokens;
131
+ }
132
+ }
133
+ return meta;
134
+ }
135
+
108
136
  function registerHistoryAggregatorHandlers() {
109
137
  ipcMain.handle('history:aggregate', async (_e, rawReq) => {
110
138
  // Wire the historyAggregate schema (previously defined but never used).
@@ -208,6 +236,48 @@ function registerHistoryAggregatorHandlers() {
208
236
  const scannedMs = Date.now() - t0;
209
237
  return { rows, partial, scannedMs, skippedLargeFiles };
210
238
  });
239
+
240
+ /** Per-conversation metadata: one row per JSONL with derived duration +
241
+ * token totals. Used by the Overview detailed-stats panel to compute
242
+ * hourly/daily distribution + top-projects. */
243
+ ipcMain.handle('history:list-conversations', async () => {
244
+ const t0 = Date.now();
245
+ const conversations = [];
246
+ let projectEntries;
247
+ try {
248
+ projectEntries = await fsp.readdir(PROJECTS_DIR, { withFileTypes: true });
249
+ } catch {
250
+ return { conversations: [], scannedMs: Date.now() - t0 };
251
+ }
252
+ for (const ent of projectEntries) {
253
+ if (!ent.isDirectory()) continue;
254
+ const projectDir = path.join(PROJECTS_DIR, ent.name);
255
+ const projectFolder = '/' + ent.name.replace(/-/g, '/');
256
+ let files;
257
+ try { files = await fsp.readdir(projectDir, { withFileTypes: true }); } catch { continue; }
258
+ for (const f of files) {
259
+ if (!f.isFile() || !f.name.endsWith('.jsonl')) continue;
260
+ const filePath = path.join(projectDir, f.name);
261
+ let stat;
262
+ try { stat = await fsp.stat(filePath); } catch { continue; }
263
+ const meta = await parseConversationMeta(filePath, stat);
264
+ const firstTs = meta.firstTs || new Date(stat.mtimeMs).toISOString();
265
+ const duration =
266
+ meta.firstTs && meta.lastTs
267
+ ? Math.max(0, Date.parse(meta.lastTs) - Date.parse(meta.firstTs))
268
+ : undefined;
269
+ conversations.push({
270
+ timestamp: firstTs,
271
+ projectFolder,
272
+ stats: {
273
+ ...(duration !== undefined ? { duration } : {}),
274
+ estimatedTokens: meta.inputTokens + meta.outputTokens,
275
+ },
276
+ });
277
+ }
278
+ }
279
+ return { conversations, scannedMs: Date.now() - t0 };
280
+ });
211
281
  }
212
282
 
213
283
  module.exports = { registerHistoryAggregatorHandlers };
@@ -24,7 +24,12 @@ const otel = require('./otel.cjs');
24
24
  const otelSettings = require('./otelSettings.cjs');
25
25
  const { registerHistoryAggregatorHandlers } = require('./historyAggregator.cjs');
26
26
  const memoryTool = require('./memoryTool.cjs');
27
+ const agentMemory = require('./agentMemory.cjs');
27
28
  const { registerDocEditorHandlers } = require('./docEditor.cjs');
29
+ const git = require('./git.cjs');
30
+ const superagent = require('./superagent.cjs');
31
+ const { registerProjectSkillsHandlers } = require('./projectSkills.cjs');
32
+ const filesIpc = require('./files.cjs');
28
33
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
29
34
  const { assertCwdInsideHome } = require('./lib/insideHome.cjs');
30
35
 
@@ -184,6 +189,7 @@ async function rebootApp() {
184
189
  scheduler.attachWindow(mainWindow);
185
190
  watchers.attachWindow(mainWindow);
186
191
  pluginInstall.attachWindow(mainWindow);
192
+ superagent.attachWindow(mainWindow);
187
193
  rebooting = false;
188
194
  return;
189
195
  }
@@ -630,7 +636,12 @@ queueOps.registerQueueOpsHandlers();
630
636
  registerHistoryAggregatorHandlers();
631
637
  pluginInstall.registerPluginInstallHandlers();
632
638
  memoryTool.registerMemoryHandlers();
639
+ agentMemory.registerAgentMemoryHandlers();
633
640
  registerDocEditorHandlers();
641
+ git.register(ipcMain);
642
+ superagent.registerSuperAgentHandlers();
643
+ registerProjectSkillsHandlers();
644
+ filesIpc.registerFilesHandlers();
634
645
 
635
646
  // OTEL telemetry export (opt-in via ~/.config/session-manager/otel.json).
636
647
  ipcMain.handle('otel:get-config', async () => otelSettings.load());
@@ -842,6 +853,7 @@ app.whenReady().then(async () => {
842
853
  scheduler.attachWindow(mainWindow);
843
854
  watchers.attachWindow(mainWindow);
844
855
  pluginInstall.attachWindow(mainWindow);
856
+ superagent.attachWindow(mainWindow);
845
857
  scheduler.init().catch((e) => {
846
858
  logs.writeLine({ scope: 'scheduler', level: 'error', message: 'init failed', meta: { error: e?.message } });
847
859
  });
@@ -212,6 +212,36 @@ const memoryCreate = z.object({
212
212
  description: z.string().max(2048).optional(),
213
213
  }).strict();
214
214
 
215
+ // ──────────────────────────────────────────── Per-subagent memory
216
+ // Distinct from the workspace-scoped Memory tool: agentMemory is keyed by
217
+ // subagent name (the .md filename in ~/.claude/agents/, e.g. "code-reviewer"),
218
+ // not by cwd. Storage lives at ~/.claude/session-manager/agent-memory/<agentId>.json.
219
+ // Regex caps must stay in lockstep with agentMemory.cjs AGENT_ID_RE / ENTRY_ID_RE.
220
+ const AGENT_MEMORY_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
221
+ const AGENT_MEMORY_CATEGORY = z.enum(['command', 'preference', 'pattern', 'failure', 'workflow']);
222
+ const AGENT_MEMORY_MAX_BODY = 1024 * 1024; // 1 MiB — must match MAX_BODY_BYTES in agentMemory.cjs
223
+
224
+ const agentMemoryList = z.object({
225
+ agentId: z.string().regex(AGENT_MEMORY_ID_RE),
226
+ }).strict();
227
+
228
+ const agentMemoryGet = z.object({
229
+ agentId: z.string().regex(AGENT_MEMORY_ID_RE),
230
+ entryId: z.string().regex(AGENT_MEMORY_ID_RE),
231
+ }).strict();
232
+
233
+ const agentMemorySet = z.object({
234
+ agentId: z.string().regex(AGENT_MEMORY_ID_RE),
235
+ entryId: z.string().regex(AGENT_MEMORY_ID_RE),
236
+ body: z.string().max(AGENT_MEMORY_MAX_BODY),
237
+ category: AGENT_MEMORY_CATEGORY.optional(),
238
+ }).strict();
239
+
240
+ const agentMemoryDelete = z.object({
241
+ agentId: z.string().regex(AGENT_MEMORY_ID_RE),
242
+ entryId: z.string().regex(AGENT_MEMORY_ID_RE),
243
+ }).strict();
244
+
215
245
  // ──────────────────────────────────────────── History
216
246
  const DATE_YYYY_MM_DD = /^\d{4}-\d{2}-\d{2}$/;
217
247
 
@@ -283,6 +313,16 @@ const appGitBranch = z.object({
283
313
  cwd: z.string().min(1).max(4096),
284
314
  }).passthrough();
285
315
 
316
+ // git:status / git:file-status — see src/main/git.cjs. cwd is validatePath'd
317
+ // inside the handler (allowedRoots = home), so the schema only enforces shape.
318
+ const gitStatus = z.object({
319
+ cwd: z.string().min(1).max(4096),
320
+ }).passthrough();
321
+
322
+ const gitFileStatus = z.object({
323
+ cwd: z.string().min(1).max(4096),
324
+ }).passthrough();
325
+
286
326
  // Plugin install: mirrors pluginInstall.cjs SLUG_RE + length cap. Defense in
287
327
  // depth — install() re-checks; the schema rejects earlier.
288
328
  const PLUGIN_SLUG_RE = /^[a-z0-9\-/]+$/;
@@ -290,6 +330,20 @@ const pluginsInstall = z.object({
290
330
  slug: z.string().regex(PLUGIN_SLUG_RE).min(1).max(128),
291
331
  }).passthrough();
292
332
 
333
+ // SuperAgent — "boss" run that writes a structured prompt to the active
334
+ // tab's PTY. Bounds match the inline schemas in superagent.cjs; centralizing
335
+ // here so the schema is the boundary fence rather than each handler.
336
+ const superagentStart = z.object({
337
+ tabId: z.string().min(1).max(128),
338
+ prompt: z.string().min(1).max(8 * 1024),
339
+ specialistCount: z.number().int().min(1).max(8),
340
+ depth: z.enum(['quick', 'standard', 'deep']),
341
+ }).strict();
342
+
343
+ const superagentTabId = z.object({
344
+ tabId: z.string().min(1).max(128),
345
+ }).strict();
346
+
293
347
  /**
294
348
  * Wrap an IPC handler with schema validation. Returns a new handler that
295
349
  * parses the payload before calling the original. On invalid payload throws
@@ -342,12 +396,20 @@ module.exports = {
342
396
  voiceSetRecording,
343
397
  appTestFireHook,
344
398
  appGitBranch,
399
+ gitStatus,
400
+ gitFileStatus,
345
401
  pluginsInstall,
402
+ superagentStart,
403
+ superagentTabId,
346
404
  memoryList,
347
405
  memoryRead,
348
406
  memoryWrite,
349
407
  memoryDelete,
350
408
  memoryCreate,
409
+ agentMemoryList,
410
+ agentMemoryGet,
411
+ agentMemorySet,
412
+ agentMemoryDelete,
351
413
  watchersAdd,
352
414
  watchersList,
353
415
  watchersRemove,
@@ -0,0 +1,124 @@
1
+ /**
2
+ * ProjectSkills — per-project skill enable/disable state.
3
+ *
4
+ * Storage: <cwd>/.claude/project-skills.json
5
+ * Format: { skills: Array<{ skillId: string; enabled: boolean }>, schemaVersion: 1 }
6
+ *
7
+ * Reads enumerate all skills under <cwd>/.claude/skills/ and <home>/.claude/skills/
8
+ * and merge their enable state from the project-local config. Skills not listed in
9
+ * the JSON default to `enabled: true` (i.e., opt-out per project).
10
+ *
11
+ * IPC:
12
+ * - project-skills:get(cwd) -> SkillState[]
13
+ * - project-skills:set(cwd, skillId, enabled) -> { ok: boolean }
14
+ *
15
+ * Atomic writes go through config.cjs::writeJson. cwd is validated via
16
+ * validatePath which constrains it to allowedRoots (home + registered project
17
+ * dirs).
18
+ */
19
+
20
+ const { ipcMain } = require('electron');
21
+ const path = require('node:path');
22
+ const { z } = require('zod');
23
+ const { readJson, writeJson, addAllowedRoot } = require('./config.cjs');
24
+
25
+ const SCHEMA_VERSION = 1;
26
+
27
+ function projectSkillsPath(cwd) {
28
+ return path.join(cwd, '.claude', 'project-skills.json');
29
+ }
30
+
31
+ /**
32
+ * Load the project-skills.json record for a cwd. Missing file => empty record.
33
+ * Returns { skills: Array<{skillId, enabled}>, schemaVersion }.
34
+ */
35
+ async function loadRecord(cwd) {
36
+ const filePath = projectSkillsPath(cwd);
37
+ const r = await readJson(filePath);
38
+ if (!r.exists || !r.data || typeof r.data !== 'object') {
39
+ return { skills: [], schemaVersion: SCHEMA_VERSION };
40
+ }
41
+ const data = r.data;
42
+ const skills = Array.isArray(data.skills) ? data.skills : [];
43
+ // Filter for well-formed entries; tolerate corruption silently.
44
+ const clean = [];
45
+ const seen = new Set();
46
+ for (const s of skills) {
47
+ if (!s || typeof s.skillId !== 'string' || typeof s.enabled !== 'boolean') continue;
48
+ if (seen.has(s.skillId)) continue;
49
+ seen.add(s.skillId);
50
+ clean.push({ skillId: s.skillId, enabled: s.enabled });
51
+ }
52
+ return { skills: clean, schemaVersion: SCHEMA_VERSION };
53
+ }
54
+
55
+ async function saveRecord(cwd, record) {
56
+ const filePath = projectSkillsPath(cwd);
57
+ const payload = {
58
+ schemaVersion: SCHEMA_VERSION,
59
+ skills: record.skills,
60
+ savedAt: Date.now(),
61
+ };
62
+ return writeJson(filePath, payload);
63
+ }
64
+
65
+ /** Return the array of skill enable-states for a project cwd. */
66
+ async function getProjectSkills(cwd) {
67
+ // Register cwd so writeJson is permitted under <cwd>/.claude.
68
+ addAllowedRoot(cwd);
69
+ const record = await loadRecord(cwd);
70
+ return record.skills;
71
+ }
72
+
73
+ /** Upsert a single skillId's enabled flag. */
74
+ async function setProjectSkill(cwd, skillId, enabled) {
75
+ addAllowedRoot(cwd);
76
+ const record = await loadRecord(cwd);
77
+ const idx = record.skills.findIndex((s) => s.skillId === skillId);
78
+ if (idx >= 0) {
79
+ record.skills[idx] = { skillId, enabled };
80
+ } else {
81
+ record.skills.push({ skillId, enabled });
82
+ }
83
+ await saveRecord(cwd, record);
84
+ return { ok: true };
85
+ }
86
+
87
+ // ──────────────────────────────────────────── IPC schemas
88
+ const projectSkillsCwd = z.object({
89
+ cwd: z.string().min(1).max(4096),
90
+ });
91
+
92
+ const projectSkillsSet = z.object({
93
+ cwd: z.string().min(1).max(4096),
94
+ skillId: z.string().min(1).max(256),
95
+ enabled: z.boolean(),
96
+ });
97
+
98
+ function validated(schema, handler) {
99
+ return (_event, payload) => {
100
+ const parsed = schema.parse(payload);
101
+ return handler(parsed);
102
+ };
103
+ }
104
+
105
+ function registerProjectSkillsHandlers() {
106
+ ipcMain.handle(
107
+ 'project-skills:get',
108
+ validated(projectSkillsCwd, ({ cwd }) => getProjectSkills(cwd)),
109
+ );
110
+ ipcMain.handle(
111
+ 'project-skills:set',
112
+ validated(projectSkillsSet, ({ cwd, skillId, enabled }) =>
113
+ setProjectSkill(cwd, skillId, enabled),
114
+ ),
115
+ );
116
+ }
117
+
118
+ module.exports = {
119
+ registerProjectSkillsHandlers,
120
+ // Exported for tests / direct use.
121
+ getProjectSkills,
122
+ setProjectSkill,
123
+ projectSkillsPath,
124
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * SuperAgent — "boss" runner that dispatches specialist subagents.
3
+ *
4
+ * Ported (in MVP form) from ClaudeCodeUnleashed's SuperAgent feature.
5
+ * Differs significantly from upstream:
6
+ * - We don't call Groq/OpenAI. The boss IS Claude — we send a structured
7
+ * prompt to the active PTY tab asking Claude to act as the boss, pick
8
+ * specialists, and report progress in a parseable format.
9
+ * - No autonomous output-parsing / fast-path / WAIT logic. State machine
10
+ * is renderer-driven via transcript events (see live.ts pattern).
11
+ * - Per-tab run state lives here so the renderer can poll status across
12
+ * navigation, and so a future supervisor probe can act on stuck runs.
13
+ *
14
+ * Channels:
15
+ * superagent:start({ tabId, prompt, specialistCount, depth })
16
+ * → { ok, error? } and broadcasts the boss prompt to the tab's PTY.
17
+ * superagent:status(tabId) → SuperAgentRunState | null
18
+ * superagent:stop(tabId) → { ok }
19
+ *
20
+ * Broadcasts:
21
+ * superagent:state-changed → { tabId, state } whenever a run starts/stops.
22
+ *
23
+ * The PTY write is intentionally done via the existing ptyManager.write
24
+ * indirection, NOT a fresh spawn — SuperAgent runs inside the user's current
25
+ * Claude session.
26
+ */
27
+
28
+ const { ipcMain } = require('electron');
29
+ const { schemas } = require('./ipcSchemas.cjs');
30
+ const { manager: ptyManager } = require('./pty.cjs');
31
+ const logs = require('./logs.cjs');
32
+
33
+ // Per-tab run state. Single live run per tab — starting a new run on a tab
34
+ // that's already running stops the existing one first.
35
+ //
36
+ // Shape (also exposed on the renderer via api.d.ts SuperAgentRunState):
37
+ // { status: 'idle' | 'running' | 'done' | 'error',
38
+ // prompt, specialistCount, depth,
39
+ // startedAt: number | null, finishedAt: number | null,
40
+ // error?: string }
41
+ const runs = new Map();
42
+
43
+ let mainWindow = null;
44
+
45
+ function attachWindow(win) {
46
+ mainWindow = win;
47
+ }
48
+
49
+ function broadcast(tabId) {
50
+ if (!mainWindow || mainWindow.isDestroyed()) return;
51
+ const state = runs.get(tabId) ?? null;
52
+ try {
53
+ mainWindow.webContents.send('superagent:state-changed', { tabId, state });
54
+ } catch { /* renderer gone */ }
55
+ }
56
+
57
+ /**
58
+ * Build the prompt we send to Claude to act as the boss. Structured so the
59
+ * renderer can (in a follow-up) pattern-match progress markers off the
60
+ * transcript without us having to parse PTY output ourselves.
61
+ *
62
+ * O(1) string concat — the prompt is bounded by the zod max(8KiB).
63
+ */
64
+ function buildBossPrompt({ prompt, specialistCount, depth }) {
65
+ const depthLine = depth === 'quick'
66
+ ? 'Quick pass — surface-level analysis, single-shot specialist work.'
67
+ : depth === 'deep'
68
+ ? 'Deep pass — multi-step plans per specialist, recurse on findings.'
69
+ : 'Standard pass — moderate detail, one or two iterations per specialist.';
70
+
71
+ return [
72
+ '## SuperAgent boss mode',
73
+ '',
74
+ `You are coordinating ${specialistCount} specialist subagent(s) for the user's task.`,
75
+ depthLine,
76
+ '',
77
+ 'Workflow:',
78
+ ` 1. Pick ${specialistCount} specialist subagent type(s) that best match the task.`,
79
+ ' Report them as: [SUPERAGENT] specialists: <type1>, <type2>, ...',
80
+ ' 2. Dispatch them via the Task tool, one at a time or in parallel as appropriate.',
81
+ ' 3. After each specialist returns, report: [SUPERAGENT] progress: <specialist> done',
82
+ ' 4. When the task is complete, summarize results and report: [SUPERAGENT] complete',
83
+ '',
84
+ 'User task:',
85
+ prompt,
86
+ ].join('\n');
87
+ }
88
+
89
+ function startRun(payload) {
90
+ const { tabId, prompt, specialistCount, depth } = payload;
91
+
92
+ // Stop any in-flight run on this tab — single run-per-tab invariant.
93
+ const existing = runs.get(tabId);
94
+ if (existing && existing.status === 'running') {
95
+ runs.set(tabId, {
96
+ ...existing,
97
+ status: 'done',
98
+ finishedAt: Date.now(),
99
+ });
100
+ }
101
+
102
+ const fullPrompt = buildBossPrompt({ prompt, specialistCount, depth });
103
+
104
+ // PTY write — exactly the pattern docstring'd at the top of the task brief.
105
+ // The trailing carriage-return submits the prompt in Claude's TUI.
106
+ let writeErr = null;
107
+ try {
108
+ ptyManager.write({ tabId, data: fullPrompt + '\r' });
109
+ } catch (e) {
110
+ writeErr = e?.message ?? String(e);
111
+ }
112
+
113
+ if (writeErr) {
114
+ const errState = {
115
+ status: 'error',
116
+ prompt,
117
+ specialistCount,
118
+ depth,
119
+ startedAt: Date.now(),
120
+ finishedAt: Date.now(),
121
+ error: writeErr,
122
+ };
123
+ runs.set(tabId, errState);
124
+ broadcast(tabId);
125
+ logs.writeLine({
126
+ scope: 'superagent',
127
+ level: 'error',
128
+ message: 'start failed',
129
+ meta: { tabId, error: writeErr },
130
+ });
131
+ return { ok: false, error: writeErr };
132
+ }
133
+
134
+ const state = {
135
+ status: 'running',
136
+ prompt,
137
+ specialistCount,
138
+ depth,
139
+ startedAt: Date.now(),
140
+ finishedAt: null,
141
+ };
142
+ runs.set(tabId, state);
143
+ broadcast(tabId);
144
+
145
+ logs.writeLine({
146
+ scope: 'superagent',
147
+ level: 'info',
148
+ message: 'start',
149
+ meta: { tabId, specialistCount, depth, promptBytes: prompt.length },
150
+ });
151
+
152
+ return { ok: true };
153
+ }
154
+
155
+ function stopRun(tabId) {
156
+ const existing = runs.get(tabId);
157
+ if (!existing || existing.status !== 'running') {
158
+ return { ok: true };
159
+ }
160
+ runs.set(tabId, {
161
+ ...existing,
162
+ status: 'done',
163
+ finishedAt: Date.now(),
164
+ });
165
+ broadcast(tabId);
166
+ logs.writeLine({
167
+ scope: 'superagent',
168
+ level: 'info',
169
+ message: 'stop',
170
+ meta: { tabId },
171
+ });
172
+ return { ok: true };
173
+ }
174
+
175
+ function getStatus(tabId) {
176
+ return runs.get(tabId) ?? null;
177
+ }
178
+
179
+ function registerSuperAgentHandlers() {
180
+ ipcMain.handle('superagent:start', (_e, payload) => {
181
+ const parsed = schemas.superagentStart.parse(payload);
182
+ return startRun(parsed);
183
+ });
184
+
185
+ ipcMain.handle('superagent:status', (_e, payload) => {
186
+ const { tabId } = schemas.superagentTabId.parse(payload);
187
+ return getStatus(tabId);
188
+ });
189
+
190
+ ipcMain.handle('superagent:stop', (_e, payload) => {
191
+ const { tabId } = schemas.superagentTabId.parse(payload);
192
+ return stopRun(tabId);
193
+ });
194
+ }
195
+
196
+ module.exports = {
197
+ attachWindow,
198
+ registerSuperAgentHandlers,
199
+ // Exposed for tests.
200
+ buildBossPrompt,
201
+ _runs: runs,
202
+ };