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.
- package/dist/assets/{TiptapBody-BroECZ_z.js → TiptapBody-CAJSNRPs.js} +1 -1
- package/dist/assets/{cssMode-Crq-Rykh.js → cssMode-o7rZCrm4.js} +1 -1
- package/dist/assets/{freemarker2-B6CC21Ql.js → freemarker2-CgmCS5Wh.js} +1 -1
- package/dist/assets/{handlebars-BLgR-12n.js → handlebars-BcPLqhPv.js} +1 -1
- package/dist/assets/{html-CiQkt_KY.js → html-CC9xWnC3.js} +1 -1
- package/dist/assets/{htmlMode-Cy8mc91p.js → htmlMode-DEgCqH7k.js} +1 -1
- package/dist/assets/{index-DU-o-LEm.js → index-C7ljEoqc.js} +1161 -1130
- package/dist/assets/{index-DW-tvyin.css → index-CH3K1pkS.css} +1 -1
- package/dist/assets/{javascript-CHNCN8qj.js → javascript-CjwqkQrn.js} +1 -1
- package/dist/assets/{jsonMode-BSN7mvBT.js → jsonMode-BYTLu76d.js} +1 -1
- package/dist/assets/{liquid-B0kmZauA.js → liquid-wbQUuJwT.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DI_RToRa.js → lspLanguageFeatures-BJGMI7Xu.js} +1 -1
- package/dist/assets/{mdx-BSF-fsyJ.js → mdx-DcDstgPF.js} +1 -1
- package/dist/assets/{python-DUl3Fmgk.js → python-B96yyM_5.js} +1 -1
- package/dist/assets/{razor-Df7WxBjo.js → razor-C7aRIxIE.js} +1 -1
- package/dist/assets/{tsMode-qccVs0_G.js → tsMode-B3UYlGaL.js} +1 -1
- package/dist/assets/{typescript-BEwM5qbq.js → typescript-CV587TvC.js} +1 -1
- package/dist/assets/{xml-CCtx-_Kw.js → xml-PWUJecBf.js} +1 -1
- package/dist/assets/{yaml-B66nOkCW.js → yaml-D8bBNHE4.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/agentMemory.cjs +267 -0
- package/src/main/files.cjs +346 -0
- package/src/main/git.cjs +333 -0
- package/src/main/historyAggregator.cjs +70 -0
- package/src/main/index.cjs +12 -0
- package/src/main/ipcSchemas.cjs +62 -0
- package/src/main/projectSkills.cjs +124 -0
- package/src/main/superagent.cjs +202 -0
- package/src/preload/api.d.ts +203 -0
- 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 };
|
package/src/main/index.cjs
CHANGED
|
@@ -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
|
});
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -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
|
+
};
|