claude-code-session-manager 0.35.2 → 0.35.3
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-CIEmzy3k.js → TiptapBody-DmCekkUQ.js} +1 -1
- package/dist/assets/index-CHKMzzCM.js +3558 -0
- package/dist/assets/index-DVqmrWP3.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +3 -2
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +3 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +12 -0
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +83 -0
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +127 -0
- package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +10 -6
- package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +3 -2
- package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +12 -7
- package/src/main/__tests__/adminServer.test.cjs +175 -0
- package/src/main/__tests__/runVerify.test.cjs +74 -4
- package/src/main/adminServer.cjs +157 -0
- package/src/main/browserCapture.cjs +714 -0
- package/src/main/browserView.cjs +613 -0
- package/src/main/config.cjs +37 -1
- package/src/main/historyAggregator.cjs +92 -6
- package/src/main/index.cjs +32 -3
- package/src/main/ipcSchemas.cjs +106 -0
- package/src/main/lib/schedulerConfig.cjs +6 -2
- package/src/main/runVerify.cjs +14 -1
- package/src/main/scheduler.cjs +6 -1
- package/src/main/sessionsStore.cjs +4 -3
- package/src/preload/api.d.ts +129 -5
- package/src/preload/browserViewPreload.cjs +67 -0
- package/src/preload/index.cjs +55 -5
- package/dist/assets/index-2Om-ouz6.js +0 -3534
- package/dist/assets/index-DeIJ8SM5.css +0 -32
- package/src/main/docEditor.cjs +0 -92
|
@@ -11,6 +11,32 @@ const PARSE_BUDGET_MS = 2_000;
|
|
|
11
11
|
const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
12
12
|
const CACHE_MAX = 500;
|
|
13
13
|
|
|
14
|
+
// Dollars per million tokens (input / output / cache-read). Cache-read
|
|
15
|
+
// tokens are priced far below input because they're served from
|
|
16
|
+
// Anthropic's prompt cache, not re-processed — this is what makes the
|
|
17
|
+
// cache-savings figure in the dashboard real money, not a vanity stat.
|
|
18
|
+
const MODEL_PRICING = {
|
|
19
|
+
opus: { i: 15, o: 75, c: 1.5 },
|
|
20
|
+
sonnet: { i: 3, o: 15, c: 0.3 },
|
|
21
|
+
haiku: { i: 0.8, o: 4, c: 0.08 },
|
|
22
|
+
};
|
|
23
|
+
const DEFAULT_PRICING_KEY = 'sonnet'; // fallback for unrecognized model ids
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a raw model id (e.g. "claude-opus-4-8-20260115") to a pricing
|
|
27
|
+
* bucket key. Model ids aren't stable enough to match exactly, so this is a
|
|
28
|
+
* case-insensitive substring check. Unrecognized ids fall back to
|
|
29
|
+
* DEFAULT_PRICING_KEY and are flagged so callers can annotate them as
|
|
30
|
+
* estimated rather than exact.
|
|
31
|
+
*/
|
|
32
|
+
function resolvePricingKey(modelId) {
|
|
33
|
+
const id = String(modelId ?? '').toLowerCase();
|
|
34
|
+
if (id.includes('opus')) return { key: 'opus', estimated: false };
|
|
35
|
+
if (id.includes('sonnet')) return { key: 'sonnet', estimated: false };
|
|
36
|
+
if (id.includes('haiku')) return { key: 'haiku', estimated: false };
|
|
37
|
+
return { key: DEFAULT_PRICING_KEY, estimated: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
// ── LRU cache ─────────────────────────────────────────────────────────────────
|
|
15
41
|
// Backed by an insertion-order Map: delete+re-insert on access = O(1) LRU.
|
|
16
42
|
class LRUCache {
|
|
@@ -114,6 +140,19 @@ function scanAggrLines(lines, acc, captureFirst) {
|
|
|
114
140
|
if (typeof outT === 'number') acc.outputTokens += outT;
|
|
115
141
|
if (typeof cacheR === 'number') acc.cacheReadTokens += cacheR;
|
|
116
142
|
if (typeof cacheC === 'number') acc.cacheCreationTokens += cacheC;
|
|
143
|
+
|
|
144
|
+
// Only assistant usage lines carry model+usage together in practice;
|
|
145
|
+
// read defensively rather than assuming every usage line has a model.
|
|
146
|
+
const modelId = obj.message?.model;
|
|
147
|
+
if (typeof modelId === 'string' && modelId) {
|
|
148
|
+
const bucket = acc.byModel[modelId] ?? (acc.byModel[modelId] = {
|
|
149
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0,
|
|
150
|
+
});
|
|
151
|
+
if (typeof inT === 'number') bucket.inputTokens += inT;
|
|
152
|
+
if (typeof outT === 'number') bucket.outputTokens += outT;
|
|
153
|
+
if (typeof cacheR === 'number') bucket.cacheReadTokens += cacheR;
|
|
154
|
+
if (typeof cacheC === 'number') bucket.cacheCreationTokens += cacheC;
|
|
155
|
+
}
|
|
117
156
|
}
|
|
118
157
|
|
|
119
158
|
const content = obj.message?.content ?? obj.content;
|
|
@@ -157,6 +196,7 @@ async function parseJSONL(filePath, stat) {
|
|
|
157
196
|
cacheCreationTokens: 0,
|
|
158
197
|
toolCallCount: 0,
|
|
159
198
|
toolBreakdown: {},
|
|
199
|
+
byModel: {},
|
|
160
200
|
errorCount: 0,
|
|
161
201
|
sessionDate: null,
|
|
162
202
|
skipped: false,
|
|
@@ -195,6 +235,7 @@ async function parseJSONL(filePath, stat) {
|
|
|
195
235
|
cacheCreationTokens: prev.cacheCreationTokens + delta.cacheCreationTokens,
|
|
196
236
|
toolCallCount: prev.toolCallCount + delta.toolCallCount,
|
|
197
237
|
toolBreakdown: { ...prev.toolBreakdown },
|
|
238
|
+
byModel: {},
|
|
198
239
|
errorCount: prev.errorCount + delta.errorCount,
|
|
199
240
|
sessionDate: prev.sessionDate, // firstTs doesn't change on appends
|
|
200
241
|
skipped: false,
|
|
@@ -202,6 +243,18 @@ async function parseJSONL(filePath, stat) {
|
|
|
202
243
|
for (const [k, v] of Object.entries(delta.toolBreakdown)) {
|
|
203
244
|
merged.toolBreakdown[k] = (merged.toolBreakdown[k] ?? 0) + v;
|
|
204
245
|
}
|
|
246
|
+
for (const [modelId, srcBucket] of Object.entries(prev.byModel ?? {})) {
|
|
247
|
+
merged.byModel[modelId] = { ...srcBucket };
|
|
248
|
+
}
|
|
249
|
+
for (const [modelId, deltaBucket] of Object.entries(delta.byModel)) {
|
|
250
|
+
const b = merged.byModel[modelId] ?? (merged.byModel[modelId] = {
|
|
251
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0,
|
|
252
|
+
});
|
|
253
|
+
b.inputTokens += deltaBucket.inputTokens;
|
|
254
|
+
b.outputTokens += deltaBucket.outputTokens;
|
|
255
|
+
b.cacheReadTokens += deltaBucket.cacheReadTokens;
|
|
256
|
+
b.cacheCreationTokens += deltaBucket.cacheCreationTokens;
|
|
257
|
+
}
|
|
205
258
|
// readOffset advances to the last complete newline so the next tail
|
|
206
259
|
// always starts at a line boundary. size stays at stat.size so the
|
|
207
260
|
// exact-hit check works correctly on the next call.
|
|
@@ -307,6 +360,7 @@ async function aggregate(req) {
|
|
|
307
360
|
cacheCreationTokens: 0,
|
|
308
361
|
toolCallCount: 0,
|
|
309
362
|
toolBreakdown: {},
|
|
363
|
+
byModel: {},
|
|
310
364
|
sessionCount: 0,
|
|
311
365
|
errorCount: 0,
|
|
312
366
|
});
|
|
@@ -322,6 +376,15 @@ async function aggregate(req) {
|
|
|
322
376
|
for (const [tool, cnt] of Object.entries(parsed.toolBreakdown)) {
|
|
323
377
|
b.toolBreakdown[tool] = (b.toolBreakdown[tool] ?? 0) + cnt;
|
|
324
378
|
}
|
|
379
|
+
for (const [modelId, srcBucket] of Object.entries(parsed.byModel ?? {})) {
|
|
380
|
+
const dst = b.byModel[modelId] ?? (b.byModel[modelId] = {
|
|
381
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0,
|
|
382
|
+
});
|
|
383
|
+
dst.inputTokens += srcBucket.inputTokens;
|
|
384
|
+
dst.outputTokens += srcBucket.outputTokens;
|
|
385
|
+
dst.cacheReadTokens += srcBucket.cacheReadTokens;
|
|
386
|
+
dst.cacheCreationTokens += srcBucket.cacheCreationTokens;
|
|
387
|
+
}
|
|
325
388
|
b.sessionCount++;
|
|
326
389
|
b.errorCount += parsed.errorCount;
|
|
327
390
|
|
|
@@ -337,15 +400,30 @@ async function aggregate(req) {
|
|
|
337
400
|
}
|
|
338
401
|
}
|
|
339
402
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
403
|
+
let cacheSavingsUsd = 0;
|
|
404
|
+
const rows = Array.from(buckets.values()).map((b) => {
|
|
405
|
+
const byModel = {};
|
|
406
|
+
let estimatedCostUsd = 0;
|
|
407
|
+
for (const [modelId, bucket] of Object.entries(b.byModel)) {
|
|
408
|
+
const { key, estimated } = resolvePricingKey(modelId);
|
|
409
|
+
const pricing = MODEL_PRICING[key];
|
|
410
|
+
const costUsd =
|
|
411
|
+
(bucket.inputTokens + bucket.cacheCreationTokens) * pricing.i / 1e6 +
|
|
412
|
+
bucket.outputTokens * pricing.o / 1e6 +
|
|
413
|
+
bucket.cacheReadTokens * pricing.c / 1e6;
|
|
414
|
+
byModel[modelId] = { ...bucket, costUsd, ...(estimated ? { estimated: true } : {}) };
|
|
415
|
+
estimatedCostUsd += costUsd;
|
|
416
|
+
// What those cache-read tokens would have cost at the full input rate,
|
|
417
|
+
// minus what they actually cost at the cache rate.
|
|
418
|
+
cacheSavingsUsd += bucket.cacheReadTokens * (pricing.i - pricing.c) / 1e6;
|
|
419
|
+
}
|
|
420
|
+
return { ...b, byModel, estimatedCostUsd };
|
|
421
|
+
});
|
|
344
422
|
|
|
345
423
|
rows.sort((a, b) => a.date.localeCompare(b.date) || a.projectCwd.localeCompare(b.projectCwd));
|
|
346
424
|
|
|
347
425
|
const scannedMs = Date.now() - t0;
|
|
348
|
-
return { rows, partial: truncated, truncated, scannedMs, skippedLargeFiles };
|
|
426
|
+
return { rows, partial: truncated, truncated, scannedMs, skippedLargeFiles, cacheSavingsUsd };
|
|
349
427
|
}
|
|
350
428
|
|
|
351
429
|
// ── IPC registration ──────────────────────────────────────────────────────────
|
|
@@ -398,4 +476,12 @@ function registerHistoryAggregatorHandlers() {
|
|
|
398
476
|
|
|
399
477
|
const remote = { aggregate };
|
|
400
478
|
|
|
401
|
-
module.exports = {
|
|
479
|
+
module.exports = {
|
|
480
|
+
registerHistoryAggregatorHandlers,
|
|
481
|
+
remote,
|
|
482
|
+
MODEL_PRICING,
|
|
483
|
+
// exported for tests
|
|
484
|
+
scanAggrLines,
|
|
485
|
+
parseJSONL,
|
|
486
|
+
resolvePricingKey,
|
|
487
|
+
};
|
package/src/main/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, powerSaveBlocker, protocol } = require('electron');
|
|
1
|
+
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, nativeImage, powerSaveBlocker, protocol } = require('electron');
|
|
2
2
|
const { spawn, execFile, execFileSync } = require('node:child_process');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const fs = require('node:fs');
|
|
@@ -7,6 +7,8 @@ const os = require('node:os');
|
|
|
7
7
|
const { schemas, validated } = require('./ipcSchemas.cjs');
|
|
8
8
|
const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
9
9
|
const { manager: ptyManager, registerPtyHandlers } = require('./pty.cjs');
|
|
10
|
+
const browserView = require('./browserView.cjs');
|
|
11
|
+
const browserCapture = require('./browserCapture.cjs');
|
|
10
12
|
const configMgr = require('./config.cjs');
|
|
11
13
|
const transcripts = require('./transcripts.cjs');
|
|
12
14
|
const usageMatrix = require('./usageMatrix.cjs');
|
|
@@ -21,6 +23,8 @@ crashDiagnostics.startCrashReporter();
|
|
|
21
23
|
const voiceHotkey = require('./voiceHotkey.cjs');
|
|
22
24
|
const voiceWizard = require('./voiceWizard.cjs');
|
|
23
25
|
const scheduler = require('./scheduler.cjs');
|
|
26
|
+
const { createAdminServer } = require('./adminServer.cjs');
|
|
27
|
+
const adminServer = createAdminServer(scheduler.remote);
|
|
24
28
|
const supervisor = require('./supervisor.cjs');
|
|
25
29
|
const watchers = require('./watchers.cjs');
|
|
26
30
|
const teams = require('./teams.cjs');
|
|
@@ -33,7 +37,6 @@ const { registerHistoryAggregatorHandlers } = require('./historyAggregator.cjs')
|
|
|
33
37
|
const memoryTool = require('./memoryTool.cjs');
|
|
34
38
|
const { registerMemoryAggregateIpc } = require('./memoryAggregate.cjs');
|
|
35
39
|
const agentMemory = require('./agentMemory.cjs');
|
|
36
|
-
const { registerDocEditorHandlers } = require('./docEditor.cjs');
|
|
37
40
|
const git = require('./git.cjs');
|
|
38
41
|
const superagent = require('./superagent.cjs');
|
|
39
42
|
const filesIpc = require('./files.cjs');
|
|
@@ -265,6 +268,8 @@ async function rebootApp() {
|
|
|
265
268
|
configMgr.attachWindow(mainWindow);
|
|
266
269
|
transcripts.attachWindow(mainWindow);
|
|
267
270
|
usageMatrix.attachWindow(mainWindow);
|
|
271
|
+
browserView.attachWindow(mainWindow);
|
|
272
|
+
browserCapture.attachWindow(mainWindow);
|
|
268
273
|
voiceHotkey.init(mainWindow).catch((e) => {
|
|
269
274
|
logs.writeLine({ scope: 'voice-hotkey', level: 'error', message: 'reinit failed', meta: { error: e?.message } });
|
|
270
275
|
});
|
|
@@ -445,6 +450,19 @@ ipcMain.handle('clipboard:paste-image', async () => {
|
|
|
445
450
|
}
|
|
446
451
|
});
|
|
447
452
|
|
|
453
|
+
// PRD 407 Capture panel — write side of paste-image's read. Writes a
|
|
454
|
+
// screenshot capture to the OS clipboard as an image.
|
|
455
|
+
ipcMain.handle('browser:copy-image', validated(schemas.browserCopyImage, ({ dataUrl }) => {
|
|
456
|
+
try {
|
|
457
|
+
const img = nativeImage.createFromDataURL(dataUrl);
|
|
458
|
+
if (!img || img.isEmpty()) return { ok: false, error: 'empty image' };
|
|
459
|
+
clipboard.writeImage(img);
|
|
460
|
+
return { ok: true };
|
|
461
|
+
} catch (e) {
|
|
462
|
+
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
463
|
+
}
|
|
464
|
+
}));
|
|
465
|
+
|
|
448
466
|
// Hooks tab "Test fire": run a hook command with a fake event payload piped
|
|
449
467
|
// to stdin. shell:true is intentional — Claude Code's hook field is a shell
|
|
450
468
|
// string. Timeout is enforced via SIGKILL on a timer because spawn's built-in
|
|
@@ -673,7 +691,6 @@ pluginInstall.registerPluginInstallHandlers();
|
|
|
673
691
|
memoryTool.registerMemoryHandlers();
|
|
674
692
|
registerMemoryAggregateIpc();
|
|
675
693
|
agentMemory.registerAgentMemoryHandlers();
|
|
676
|
-
registerDocEditorHandlers();
|
|
677
694
|
git.register(ipcMain);
|
|
678
695
|
superagent.registerSuperAgentHandlers();
|
|
679
696
|
filesIpc.registerFilesHandlers();
|
|
@@ -774,6 +791,11 @@ app.on('web-contents-created', (_e, wc) => {
|
|
|
774
791
|
});
|
|
775
792
|
|
|
776
793
|
wc.on('will-navigate', (event, url) => {
|
|
794
|
+
// The embedded Browser tab's WebContentsView is a real browser — it must
|
|
795
|
+
// be able to navigate anywhere. Exempt ONLY webContents registered by
|
|
796
|
+
// browserView.cjs; the main window's lock below is unchanged.
|
|
797
|
+
if (browserView.isBrowserViewContents(wc.id)) return;
|
|
798
|
+
|
|
777
799
|
const allowed = useDevServer
|
|
778
800
|
? ['http://localhost:5173', 'http://127.0.0.1:5173']
|
|
779
801
|
: [];
|
|
@@ -971,6 +993,9 @@ app.whenReady().then(async () => {
|
|
|
971
993
|
configMgr.attachWindow(mainWindow);
|
|
972
994
|
transcripts.attachWindow(mainWindow);
|
|
973
995
|
usageMatrix.attachWindow(mainWindow);
|
|
996
|
+
browserView.registerBrowserView({ mainWindow, ipcMain });
|
|
997
|
+
browserCapture.attachWindow(mainWindow);
|
|
998
|
+
browserCapture.registerBrowserCapture({ ipcMain, getView: browserView.getView });
|
|
974
999
|
voiceHotkey.init(mainWindow).catch((e) => {
|
|
975
1000
|
logs.writeLine({ scope: 'voice-hotkey', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
976
1001
|
});
|
|
@@ -983,6 +1008,9 @@ app.whenReady().then(async () => {
|
|
|
983
1008
|
scheduler.init().catch((e) => {
|
|
984
1009
|
logs.writeLine({ scope: 'scheduler', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
985
1010
|
});
|
|
1011
|
+
adminServer.start().catch((e) => {
|
|
1012
|
+
logs.writeLine({ scope: 'admin-server', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
1013
|
+
});
|
|
986
1014
|
// First-boot default: install the bundled session-manager-dev plugin (its 10
|
|
987
1015
|
// dev skills) from the app's own marketplace. One-shot + idempotent; never
|
|
988
1016
|
// throws. SM_SEED_DEV_PLUGIN_DISABLE=1 to opt out.
|
|
@@ -1072,6 +1100,7 @@ app.on('before-quit', () => {
|
|
|
1072
1100
|
configMgr.closeAllWatchers();
|
|
1073
1101
|
transcripts.closeAll();
|
|
1074
1102
|
watchers.manager.killAll();
|
|
1103
|
+
adminServer.stop().catch(() => {});
|
|
1075
1104
|
// Best-effort flush of any pending OTEL spans. shutdown() has its own 2s
|
|
1076
1105
|
// ceiling so a wedged exporter can't hold quit.
|
|
1077
1106
|
otel.shutdown().catch(() => {});
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -45,6 +45,100 @@ const ptyResize = z.object({
|
|
|
45
45
|
rows: z.number().int().min(3).max(1000),
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// ──────────────────────────────────────────── Browser (WebContentsView embed)
|
|
49
|
+
// viewId is a renderer-generated identifier; restrict to a safe charset (no
|
|
50
|
+
// '/', no '.') since it keys the in-process Map<viewId, WebContentsView> —
|
|
51
|
+
// not used as a filesystem path, but kept consistent with tabId conventions.
|
|
52
|
+
const BROWSER_VIEW_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
53
|
+
const browserViewId = z.object({
|
|
54
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const browserCreate = z.object({
|
|
58
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
59
|
+
// Non-persistent partition string (PRD 400 run-mode isolation). No leading
|
|
60
|
+
// 'persist:' enforced here — callers choose persistence explicitly.
|
|
61
|
+
partition: z.string().min(1).max(256),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const BOUNDS_INT = z.number().int().min(0).max(100000);
|
|
65
|
+
const browserSetBounds = z.object({
|
|
66
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
67
|
+
x: BOUNDS_INT,
|
|
68
|
+
y: BOUNDS_INT,
|
|
69
|
+
width: BOUNDS_INT,
|
|
70
|
+
height: BOUNDS_INT,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const browserNavigate = z.object({
|
|
74
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
75
|
+
url: z.string().min(1).max(8192),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// PRD 407: DOM/text capture from the active browser sub-tab.
|
|
79
|
+
const browserCaptureDom = z.object({
|
|
80
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
81
|
+
kind: z.enum(['text', 'html']),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// PRD 404: filter -> prune -> summarize -> chunk capture of a picked
|
|
85
|
+
// selection (browser:capture). selectors comes from the PRD 403 picker.
|
|
86
|
+
const browserCaptureSelection = z.object({
|
|
87
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
88
|
+
selectors: z.array(z.string().min(1).max(2048)).min(1).max(50),
|
|
89
|
+
mode: z.enum(['agent', 'html', 'a11y', 'selector']),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// PRD 407: clipboard image write (browser:copy-image). dataUrl is a PNG data
|
|
93
|
+
// URL from webContents.capturePage() — capped well above any realistic
|
|
94
|
+
// screenshot so a malformed/huge payload can't wedge the IPC channel.
|
|
95
|
+
const browserCopyImage = z.object({
|
|
96
|
+
dataUrl: z.string().min(1).max(50_000_000),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// PRD 407: binary-safe atomic write (browser:save-binary) for screenshot
|
|
100
|
+
// captures — config:write-text is utf8-only.
|
|
101
|
+
const browserSaveBinary = z.object({
|
|
102
|
+
path: z.string().min(1).max(4096),
|
|
103
|
+
base64: z.string().min(1).max(50_000_000),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// PRD 410: replay a recorded step list against a live view. The renderer
|
|
107
|
+
// owns the step list (main never persists recorded steps), so every call is
|
|
108
|
+
// self-contained. `select` is accepted for forward-compat even though the
|
|
109
|
+
// live recorder engine doesn't emit it yet.
|
|
110
|
+
const browserReplayStep = z.object({
|
|
111
|
+
n: z.number().int().min(1),
|
|
112
|
+
verb: z.enum(['navigate', 'click', 'type', 'select', 'wait-for']),
|
|
113
|
+
target: z.string().max(2000),
|
|
114
|
+
value: z.string().max(2000).optional(),
|
|
115
|
+
variable: z.string().max(64).nullable().optional(),
|
|
116
|
+
kind: z.enum(['nav', 'assert']).optional(),
|
|
117
|
+
masked: z.boolean().optional(),
|
|
118
|
+
variableSuggestion: z.string().max(64).optional(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const browserReplay = z.object({
|
|
122
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
123
|
+
steps: z.array(browserReplayStep).max(500),
|
|
124
|
+
values: z.record(z.string().max(64), z.string().max(2000)).optional(),
|
|
125
|
+
continueOnError: z.boolean().optional(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// PRD 402: address-bar zoom control. factor is clamped again in
|
|
129
|
+
// browserView.cjs's setZoom — this just bounds the wire payload.
|
|
130
|
+
const browserSetZoom = z.object({
|
|
131
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
132
|
+
factor: z.number().min(0.1).max(10),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// PRD 402: Cmd/Ctrl+F find bar.
|
|
136
|
+
const browserFind = z.object({
|
|
137
|
+
viewId: z.string().min(1).max(128).regex(BROWSER_VIEW_ID_RE),
|
|
138
|
+
text: z.string().max(2000),
|
|
139
|
+
forward: z.boolean().optional(),
|
|
140
|
+
});
|
|
141
|
+
|
|
48
142
|
// ──────────────────────────────────────────── Transcripts
|
|
49
143
|
const SESSION_UUID_RE = /^[a-zA-Z0-9-]{1,64}$/;
|
|
50
144
|
|
|
@@ -90,6 +184,7 @@ const sessionsPayload = z.object({
|
|
|
90
184
|
tabs: z.array(z.object({
|
|
91
185
|
id: z.string().min(1).max(128),
|
|
92
186
|
claudeSessionId: z.string().min(1).max(128),
|
|
187
|
+
chatSessionId: z.string().min(1).max(128).optional(),
|
|
93
188
|
cwd: z.string().min(1).max(4096),
|
|
94
189
|
label: z.string().max(256),
|
|
95
190
|
presetId: z.string().max(128).nullable(),
|
|
@@ -512,6 +607,17 @@ module.exports = {
|
|
|
512
607
|
ptyWrite,
|
|
513
608
|
ptyResize,
|
|
514
609
|
sessionSubscribe,
|
|
610
|
+
browserViewId,
|
|
611
|
+
browserCreate,
|
|
612
|
+
browserSetBounds,
|
|
613
|
+
browserNavigate,
|
|
614
|
+
browserCaptureDom,
|
|
615
|
+
browserCaptureSelection,
|
|
616
|
+
browserCopyImage,
|
|
617
|
+
browserSaveBinary,
|
|
618
|
+
browserReplay,
|
|
619
|
+
browserSetZoom,
|
|
620
|
+
browserFind,
|
|
515
621
|
transcriptSubscribe,
|
|
516
622
|
transcriptTabId,
|
|
517
623
|
transcriptPath,
|
|
@@ -3,8 +3,12 @@ module.exports = {
|
|
|
3
3
|
// `when-available` policy notices the 5h window crossing the utilization
|
|
4
4
|
// threshold — i.e. when to stop (util ≥ threshold) and start (util < threshold)
|
|
5
5
|
// jobs around the 5-hour limit. Reset-time resume is scheduled exactly (not
|
|
6
|
-
// poll-bound), so
|
|
7
|
-
|
|
6
|
+
// poll-bound), so 1 min only bounds how late we react to utilization drift
|
|
7
|
+
// (e.g. freed host memory unblocking a pending job in another project).
|
|
8
|
+
// Tradeoff accepted: this raises billing.fetchUsage() calls from 6x/hour to
|
|
9
|
+
// 60x/hour against Anthropic's usage endpoint (src/main/usage.cjs) — not a
|
|
10
|
+
// job-execution cost, just a lighter-weight polling GET.
|
|
11
|
+
POLL_INTERVAL_MS: 60_000,
|
|
8
12
|
// Exponential backoff floor for polling retries after transient failures.
|
|
9
13
|
POLL_MIN_INTERVAL_MS: 90_000,
|
|
10
14
|
// Cadence for refreshing the AppStatusBar's 5h-usage chip (billing meter).
|
package/src/main/runVerify.cjs
CHANGED
|
@@ -616,6 +616,18 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
616
616
|
? { ...(annotations.length ? { annotations } : {}), ...sentinelFields }
|
|
617
617
|
: undefined;
|
|
618
618
|
|
|
619
|
+
// No pattern hits is not automatically "clean": a run that neither
|
|
620
|
+
// committed anything nor ever emitted the finish-protocol sentinel likely
|
|
621
|
+
// ended before doing real work (e.g. stopped on a clarifying question).
|
|
622
|
+
// Weaker evidence than a caught transcript error, but still not clean.
|
|
623
|
+
if (sentinel === null && !committedDuringRun) {
|
|
624
|
+
issues.push({
|
|
625
|
+
verdict: 'no_verdict_sentinel',
|
|
626
|
+
reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel — likely ended before the finish protocol (e.g. stopped on a clarifying question)',
|
|
627
|
+
priority: 1,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
619
631
|
if (issues.length === 0) {
|
|
620
632
|
const reason = annotations.length
|
|
621
633
|
? `no blocking issues (${annotations.length} annotation(s): ${annotations.map((a) => a.reason).join('; ')})`
|
|
@@ -623,7 +635,8 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
623
635
|
return conclude('clean', reason, null, extras);
|
|
624
636
|
}
|
|
625
637
|
|
|
626
|
-
// Pick highest-priority issue (transcript_errors > verify_unavailable
|
|
638
|
+
// Pick highest-priority issue (transcript_errors > verify_unavailable ==
|
|
639
|
+
// no_verdict_sentinel; ties keep the loop's original order via stable sort).
|
|
627
640
|
issues.sort((a, b) => b.priority - a.priority);
|
|
628
641
|
const top = issues[0];
|
|
629
642
|
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -2479,7 +2479,12 @@ const remote = {
|
|
|
2479
2479
|
});
|
|
2480
2480
|
if (!found) return { ok: false, error: 'not found' };
|
|
2481
2481
|
await broadcast();
|
|
2482
|
-
return { ok: true };
|
|
2482
|
+
return { ok: true, slug, status: 'pending' };
|
|
2483
|
+
},
|
|
2484
|
+
|
|
2485
|
+
async listJobs() {
|
|
2486
|
+
const state = await readQueue();
|
|
2487
|
+
return state.jobs.map((j) => ({ slug: j.slug, title: j.title, status: j.status, cwd: j.cwd }));
|
|
2483
2488
|
},
|
|
2484
2489
|
|
|
2485
2490
|
async runNow() {
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Storage: ~/.config/session-manager/tabs.json
|
|
6
6
|
* Shape: { tabs: PersistedTab[], activeTabId: string | null, savedAt: number }
|
|
7
7
|
*
|
|
8
|
-
* Only serializable, durable fields are persisted: id, claudeSessionId,
|
|
9
|
-
* label, presetId. Runtime-only fields (pid, status,
|
|
10
|
-
* exitCode) are recomputed on boot.
|
|
8
|
+
* Only serializable, durable fields are persisted: id, claudeSessionId,
|
|
9
|
+
* chatSessionId, cwd, label, presetId. Runtime-only fields (pid, status,
|
|
10
|
+
* startupCommand, exitCode) are recomputed on boot.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const fs = require('node:fs');
|
|
@@ -59,6 +59,7 @@ async function markFreshRestart() {
|
|
|
59
59
|
const freshTabs = tabs.map((t) => ({
|
|
60
60
|
...t,
|
|
61
61
|
claudeSessionId: crypto.randomUUID(),
|
|
62
|
+
chatSessionId: crypto.randomUUID(),
|
|
62
63
|
}));
|
|
63
64
|
await save({ tabs: freshTabs, activeTabId, freshStart: true });
|
|
64
65
|
}
|
package/src/preload/api.d.ts
CHANGED
|
@@ -15,6 +15,72 @@ export interface WriteErrorEvent {
|
|
|
15
15
|
reason: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface BrowserNavState {
|
|
19
|
+
url: string;
|
|
20
|
+
title: string;
|
|
21
|
+
canGoBack: boolean;
|
|
22
|
+
canGoForward: boolean;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
isSecure: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One captured recorder step (PRD 408 engine → PRD 409 panel). */
|
|
28
|
+
export interface RecordStep {
|
|
29
|
+
n: number;
|
|
30
|
+
/** `select` is accepted by replay/export (PRD 410) for forward-compat;
|
|
31
|
+
* the live engine does not emit it yet. */
|
|
32
|
+
verb: 'navigate' | 'click' | 'type' | 'select' | 'wait-for';
|
|
33
|
+
target: string;
|
|
34
|
+
kind?: 'nav' | 'assert';
|
|
35
|
+
/** True for `type` steps — the actual typed value is never captured. */
|
|
36
|
+
masked?: boolean;
|
|
37
|
+
/** Engine-suggested `{{var}}` name for a `type` step (e.g. field name). */
|
|
38
|
+
variableSuggestion?: string;
|
|
39
|
+
/** Renderer-owned: set once the user checks "parameterize as {{var}}". */
|
|
40
|
+
variable?: string | null;
|
|
41
|
+
/** `select` steps only — the option value to choose on replay/export. */
|
|
42
|
+
value?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Per-step replay outcome (PRD 410), streamed as `browser:replay-step:<viewId>`. */
|
|
46
|
+
export interface ReplayStepResult {
|
|
47
|
+
n: number;
|
|
48
|
+
status: 'pass' | 'fail';
|
|
49
|
+
detail?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** find-in-page result (PRD 402), streamed as `browser:find-result:<viewId>`. */
|
|
53
|
+
export interface FindResult {
|
|
54
|
+
requestId: number;
|
|
55
|
+
matches: number;
|
|
56
|
+
activeMatchOrdinal: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Element-picker event (PRD 403), streamed as `browser:picker-event:<viewId>`. */
|
|
60
|
+
export interface PickerEvent {
|
|
61
|
+
type: 'hover' | 'pick' | 'unpick' | 'exit';
|
|
62
|
+
selector?: string;
|
|
63
|
+
label?: string;
|
|
64
|
+
tag?: string;
|
|
65
|
+
rect?: { x: number; y: number; width: number; height: number };
|
|
66
|
+
/** Present on a 'pick' event fired by an unmodified click — the selection was replaced, not accumulated. */
|
|
67
|
+
replace?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One entry in `~/.claude/session-manager/browser/history.json` (PRD 402). */
|
|
71
|
+
export interface BrowserHistoryEntry {
|
|
72
|
+
url: string;
|
|
73
|
+
title: string;
|
|
74
|
+
ts: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One entry in `~/.claude/session-manager/browser/bookmarks.json` (PRD 402). */
|
|
78
|
+
export interface BrowserBookmark {
|
|
79
|
+
url: string;
|
|
80
|
+
title: string;
|
|
81
|
+
ts: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
18
84
|
export interface ReadJsonResult {
|
|
19
85
|
exists: boolean;
|
|
20
86
|
raw: string;
|
|
@@ -82,6 +148,8 @@ export interface SubscribeResult {
|
|
|
82
148
|
export interface PersistedTab {
|
|
83
149
|
id: string;
|
|
84
150
|
claudeSessionId: string;
|
|
151
|
+
/** Chat mode's own session id, decoupled from claudeSessionId. Optional for backwards-compat with tabs.json written before this field existed. */
|
|
152
|
+
chatSessionId?: string;
|
|
85
153
|
cwd: string;
|
|
86
154
|
label: string;
|
|
87
155
|
presetId: string | null;
|
|
@@ -435,12 +503,23 @@ export interface DayProjectRow {
|
|
|
435
503
|
sessionCount: number;
|
|
436
504
|
errorCount: number;
|
|
437
505
|
estimatedCostUsd: number;
|
|
506
|
+
byModel: Record<string, {
|
|
507
|
+
inputTokens: number;
|
|
508
|
+
outputTokens: number;
|
|
509
|
+
cacheReadTokens: number;
|
|
510
|
+
cacheCreationTokens: number;
|
|
511
|
+
costUsd: number;
|
|
512
|
+
/** Set when the model id didn't match opus/sonnet/haiku and was priced at Sonnet rates as a fallback. */
|
|
513
|
+
estimated?: boolean;
|
|
514
|
+
}>;
|
|
438
515
|
}
|
|
439
516
|
|
|
440
517
|
export interface HistoryAggregateResult {
|
|
441
518
|
rows: DayProjectRow[];
|
|
442
519
|
partial: boolean;
|
|
443
520
|
scannedMs: number;
|
|
521
|
+
/** Total $ saved across all rows from cache-read pricing vs. full input pricing. Global, not per-row. */
|
|
522
|
+
cacheSavingsUsd: number;
|
|
444
523
|
}
|
|
445
524
|
|
|
446
525
|
export interface SessionScanEntry {
|
|
@@ -932,6 +1011,54 @@ export interface SessionManagerAPI {
|
|
|
932
1011
|
onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
|
|
933
1012
|
onWriteError: (handler: (ev: WriteErrorEvent) => void) => () => void;
|
|
934
1013
|
};
|
|
1014
|
+
browser: {
|
|
1015
|
+
create: (payload: { viewId: string; partition: string }) => Promise<{ ok: boolean }>;
|
|
1016
|
+
setBounds: (payload: { viewId: string; x: number; y: number; width: number; height: number }) => Promise<{ ok: boolean }>;
|
|
1017
|
+
show: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1018
|
+
hide: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1019
|
+
destroy: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1020
|
+
navigate: (payload: { viewId: string; url: string }) => Promise<{ ok: boolean; error?: string }>;
|
|
1021
|
+
back: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1022
|
+
forward: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1023
|
+
reload: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1024
|
+
stop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1025
|
+
onNavState: (viewId: string, handler: (state: BrowserNavState) => void) => () => void;
|
|
1026
|
+
recordStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1027
|
+
recordStop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1028
|
+
onRecordStep: (viewId: string, handler: (step: RecordStep) => void) => () => void;
|
|
1029
|
+
captureDom: (payload: { viewId: string; kind: 'text' | 'html' }) => Promise<
|
|
1030
|
+
| { ok: true; url: string; title: string; text: string; truncated?: boolean }
|
|
1031
|
+
| { ok: false; error: string }
|
|
1032
|
+
>;
|
|
1033
|
+
captureShot: (viewId: string) => Promise<
|
|
1034
|
+
| { ok: true; url: string; title: string; dataUrl: string }
|
|
1035
|
+
| { ok: false; error: string }
|
|
1036
|
+
>;
|
|
1037
|
+
saveBinary: (path: string, base64: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1038
|
+
replay: (payload: {
|
|
1039
|
+
viewId: string;
|
|
1040
|
+
steps: RecordStep[];
|
|
1041
|
+
values?: Record<string, string>;
|
|
1042
|
+
continueOnError?: boolean;
|
|
1043
|
+
}) => Promise<{ ok: boolean; error?: string; stopped?: boolean; failedAt?: number }>;
|
|
1044
|
+
onReplayStep: (viewId: string, handler: (step: ReplayStepResult) => void) => () => void;
|
|
1045
|
+
setZoom: (payload: { viewId: string; factor: number }) => Promise<{ ok: boolean; error?: string; factor?: number }>;
|
|
1046
|
+
find: (payload: { viewId: string; text: string; forward?: boolean }) => Promise<{ ok: boolean; error?: string }>;
|
|
1047
|
+
stopFind: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1048
|
+
onFindResult: (viewId: string, handler: (result: FindResult) => void) => () => void;
|
|
1049
|
+
pickerStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1050
|
+
pickerStop: (viewId: string) => Promise<{ ok: boolean; error?: string; wasPicking?: boolean }>;
|
|
1051
|
+
onPickerEvent: (viewId: string, handler: (ev: PickerEvent) => void) => () => void;
|
|
1052
|
+
/** Selection-scoped capture pipeline (PRD 404): filter -> prune -> summarize -> chunk for 'agent', outerHTML for 'html', CDP AX-tree for 'a11y', fallback-chain for 'selector'. */
|
|
1053
|
+
capture: (payload: {
|
|
1054
|
+
viewId: string;
|
|
1055
|
+
selectors: string[];
|
|
1056
|
+
mode: 'agent' | 'html' | 'a11y' | 'selector';
|
|
1057
|
+
}) => Promise<
|
|
1058
|
+
| { ok: true; mode: string; text: string; meta: { chunks?: number; tokens?: number } }
|
|
1059
|
+
| { ok: false; error: string }
|
|
1060
|
+
>;
|
|
1061
|
+
};
|
|
935
1062
|
transcripts: {
|
|
936
1063
|
subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
|
|
937
1064
|
/** Release the sub back to the LRU cache (view-switch). Does not destroy the watcher. */
|
|
@@ -1099,6 +1226,8 @@ export interface SessionManagerAPI {
|
|
|
1099
1226
|
| { ok: true; path: string; bytes: number }
|
|
1100
1227
|
| { ok: false; empty?: true; error?: string }
|
|
1101
1228
|
>;
|
|
1229
|
+
/** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
|
|
1230
|
+
copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1102
1231
|
};
|
|
1103
1232
|
memory: {
|
|
1104
1233
|
/** List markdown memory entries for the given workspace (defaults to 'default'). */
|
|
@@ -1129,11 +1258,6 @@ export interface SessionManagerAPI {
|
|
|
1129
1258
|
/** Delete one entry. Removes the file outright when last entry is removed. */
|
|
1130
1259
|
delete: (agentId: string, entryId: string) => Promise<AgentMemoryMutationResult>;
|
|
1131
1260
|
};
|
|
1132
|
-
docEditor: {
|
|
1133
|
-
pickFile: (payload?: { lastDir?: string }) => Promise<{ path: string | null; error?: string }>;
|
|
1134
|
-
readFile: (path: string) => Promise<{ ok: boolean; text?: string; mtimeMs?: number; error?: string }>;
|
|
1135
|
-
writeFile: (path: string, text: string) => Promise<{ ok: boolean; mtimeMs?: number; error?: string }>;
|
|
1136
|
-
};
|
|
1137
1261
|
git: {
|
|
1138
1262
|
/** Full git status for `cwd`. Returns null when not a git repo, git is
|
|
1139
1263
|
* missing, or the call times out (5s ceiling). Cached per-cwd for 5s. */
|