claude-code-session-manager 0.35.4 → 0.35.5
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-CVFG9ufh.js → TiptapBody-CUO_twO8.js} +1 -1
- package/dist/assets/index-BHyeZfve.css +32 -0
- package/dist/assets/{index-DASEOqMP.js → index-DIVOtBGO.js} +22 -22
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +14 -5
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +51 -0
- package/src/main/browserView.cjs +2 -2
- package/src/main/chatRunner.cjs +158 -21
- package/src/main/index.cjs +14 -0
- package/src/main/ipcSchemas.cjs +7 -0
- package/src/main/scheduler.cjs +69 -44
- package/src/preload/api.d.ts +9 -0
- package/src/preload/index.cjs +6 -0
- package/dist/assets/index-DZgKJkf3.css +0 -32
package/dist/index.html
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-DIVOtBGO.js"></script>
|
|
11
11
|
<link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BHyeZfve.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="bg-bg text-fg font-sans antialiased">
|
|
16
16
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.5",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -38,10 +38,19 @@ the only step that runs on the cheaper executor.
|
|
|
38
38
|
## Standards (single source of truth)
|
|
39
39
|
|
|
40
40
|
The engineering standards (Performance, Debugging, API reuse / single source of truth, TDD,
|
|
41
|
-
and the executor-facing Execution discipline) live in **`standards.md`** beside this file
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
and the executor-facing Execution discipline) live in **`standards.md`** beside this file, in
|
|
42
|
+
the same skill directory (`.../skills/develop/standards.md` — NOT `~/.claude/skills/develop/`,
|
|
43
|
+
which is a different, non-existent path; resolve it relative to wherever this SKILL.md itself
|
|
44
|
+
was loaded from). **Re-read that file fresh with the Read tool immediately before pasting it
|
|
45
|
+
into each PRD (Phase 1 step 4) — never reuse a copy cached earlier in the same conversation.**
|
|
46
|
+
A long authoring session can span an edit to `standards.md` (including one autonomously applied
|
|
47
|
+
by a prior incident's fix-plan) without the model noticing; pasting a stale in-context copy
|
|
48
|
+
silently ships PRDs missing the latest execution-discipline rules. This is exactly the
|
|
49
|
+
single-source-of-truth violation the standards themselves warn against — don't let it happen to
|
|
50
|
+
the standards block itself. (Incident: PRDs 467/468 authored late in a long session carried a
|
|
51
|
+
standards.md snapshot from before the "You ARE the executor" guard was added earlier that same
|
|
52
|
+
session, so PRD 467's headless run repeated the exact anti-pattern the guard exists to prevent.)
|
|
53
|
+
Never restate or fork its content — one concept, one implementation.
|
|
45
54
|
|
|
46
55
|
For interactive dev work, also apply the `test-driven-development` and `systematic-debugging`
|
|
47
56
|
skills; the headless PRDs get the distilled core from `standards.md` instead, since they
|
|
@@ -195,7 +204,7 @@ single definition of "tracked to done" for both entry paths.
|
|
|
195
204
|
## References (reuse, don't duplicate)
|
|
196
205
|
|
|
197
206
|
- `~/.claude/session-manager/scheduled-plans/PRD_AUTHORING.md` — the §1–§10 safety rules.
|
|
198
|
-
-
|
|
207
|
+
- `standards.md` beside this file — the engineering + execution-discipline rules inlined into every PRD. Re-read fresh each time (see "Standards" above) — don't reuse a cached copy.
|
|
199
208
|
- `test-driven-development`, `systematic-debugging` — interactive dev sessions.
|
|
200
209
|
- `requesting-code-review` — the Phase-2 review gate.
|
|
201
210
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-investigation-prompt.test.cjs — unit tests for buildInvestigationPrompt.
|
|
3
|
+
*
|
|
4
|
+
* Run: timeout 120 node --test src/main/__tests__/scheduler-investigation-prompt.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { test } = require('node:test');
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const { buildInvestigationPrompt } = require('../scheduler.cjs');
|
|
12
|
+
|
|
13
|
+
function makeArgs(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
failedJob: { slug: '05-my-feature', title: 'My feature', exitCode: 1 },
|
|
16
|
+
cwd: '/home/bilko/Projects/session-manager',
|
|
17
|
+
failedLogPath: '/tmp/05-my-feature.log',
|
|
18
|
+
originalBody: 'Original PRD body.',
|
|
19
|
+
logTail: 'some log tail output',
|
|
20
|
+
fixPath: '/tmp/05-fix-my-feature.md',
|
|
21
|
+
group: 5,
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('prompt references the canonical standards.md file', () => {
|
|
27
|
+
const prompt = buildInvestigationPrompt(makeArgs());
|
|
28
|
+
assert.match(prompt, /standards\.md/);
|
|
29
|
+
assert.match(prompt, /plugins\/session-manager-dev\/skills\/develop\/standards\.md/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('prompt instructs inlining the Execution discipline section under Engineering standards', () => {
|
|
33
|
+
const prompt = buildInvestigationPrompt(makeArgs());
|
|
34
|
+
assert.match(prompt, /Engineering standards/);
|
|
35
|
+
assert.match(prompt, /Execution discipline \(headless runs\)/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('prompt contains the named "delegated instead of executed" detection check', () => {
|
|
39
|
+
const prompt = buildInvestigationPrompt(makeArgs());
|
|
40
|
+
assert.match(prompt, /ScheduleWakeup/);
|
|
41
|
+
assert.match(prompt, /session-manager-dev:develop/);
|
|
42
|
+
assert.match(prompt, /never re-queue or self-schedule|delegated instead of executed/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('prompt includes the resolved job fields', () => {
|
|
46
|
+
const prompt = buildInvestigationPrompt(makeArgs());
|
|
47
|
+
assert.match(prompt, /05-my-feature/);
|
|
48
|
+
assert.match(prompt, /Original PRD body\./);
|
|
49
|
+
assert.match(prompt, /some log tail output/);
|
|
50
|
+
assert.match(prompt, /05-fix-my-feature\.md/);
|
|
51
|
+
});
|
package/src/main/browserView.cjs
CHANGED
|
@@ -18,7 +18,7 @@ const path = require('path');
|
|
|
18
18
|
const os = require('os');
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const crypto = require('crypto');
|
|
21
|
-
const { WebContentsView
|
|
21
|
+
const { WebContentsView } = require('electron');
|
|
22
22
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
23
23
|
const { readJson, writeJson } = require('./config.cjs');
|
|
24
24
|
|
|
@@ -181,7 +181,7 @@ function wireNavEvents(viewId, view) {
|
|
|
181
181
|
|
|
182
182
|
wc.setWindowOpenHandler(({ url }) => {
|
|
183
183
|
if (/^https?:\/\//i.test(url)) {
|
|
184
|
-
|
|
184
|
+
sendIfAlive(win, 'browser:open-tab-request', { url });
|
|
185
185
|
}
|
|
186
186
|
return { action: 'deny' };
|
|
187
187
|
});
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
* OOM that SIGKILLed a job on 2026-06-26.
|
|
13
13
|
*
|
|
14
14
|
* Public surface:
|
|
15
|
-
* run({ tabId, sessionId, prompt, cwd, resume }): void — fire-and-forget enqueue
|
|
15
|
+
* run({ tabId, sessionId, prompt, cwd, resume, silent }): void — fire-and-forget enqueue.
|
|
16
|
+
* `silent` (PRD 470) still goes through the same per-tab FIFO lane, but suppresses the
|
|
17
|
+
* six turn-affecting broadcasts + recordExchange (see probeContextUsage below).
|
|
16
18
|
* cancel(tabId): void
|
|
17
19
|
* parseStopSignal(finalText): { questions: string[] } | null — exported for reuse
|
|
20
|
+
* parseContextUsageMarkdown(text): { usedTokens, totalTokens, usedPct, categories } | null
|
|
21
|
+
* — pure parser for `/context`
|
|
22
|
+
* probeContextUsage({ tabId, sessionId, cwd }): void — fire-and-forget silent `/context` probe
|
|
18
23
|
* STOP_SENTINEL: string — exported for tests
|
|
19
24
|
* __setExecutor(fn): void — test seam
|
|
20
25
|
*
|
|
@@ -27,6 +32,8 @@
|
|
|
27
32
|
* chat:run:needs-input { tabId, sessionId, questions, raw }
|
|
28
33
|
* chat:run:error { tabId, sessionId, message }
|
|
29
34
|
* chat:run:notice { tabId, sessionId, message } — informational, not terminal
|
|
35
|
+
* chat:context-usage { tabId, sessionId, usedTokens, totalTokens, usedPct, categories }
|
|
36
|
+
* — result of a silent `/context` probe
|
|
30
37
|
*/
|
|
31
38
|
|
|
32
39
|
const { spawn } = require('node:child_process');
|
|
@@ -72,6 +79,119 @@ function parseStopSignal(finalText) {
|
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
// ─── `/context` probe (PRD 470) ────────────────────────────────────────────
|
|
83
|
+
// `claude -p "/context"` is answered entirely locally by the CLI (zero-cost,
|
|
84
|
+
// no real API call) with markdown containing a summary line and a per-category
|
|
85
|
+
// breakdown table. Parsed here so the app can mirror the CLI's own context-
|
|
86
|
+
// window accounting without re-implementing the estimate itself.
|
|
87
|
+
|
|
88
|
+
// "9k" -> 9000, "15.1k" -> 15100; bare "207" -> 207. Never throws — returns NaN
|
|
89
|
+
// for genuinely malformed input, which callers treat as a parse failure.
|
|
90
|
+
function parseTokenAmount(str) {
|
|
91
|
+
const suffixed = /^(\d+(?:\.\d+)?)k$/i.exec(str);
|
|
92
|
+
if (suffixed) return Math.round(parseFloat(suffixed[1]) * 1000);
|
|
93
|
+
return Number(str);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse the markdown produced by the CLI's own `/context` slash command into
|
|
98
|
+
* a structured summary. Pure, single line-by-line pass — O(n) over the lines
|
|
99
|
+
* of a small, bounded response, no nested scans.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} text
|
|
102
|
+
* @returns {{ usedTokens: number, totalTokens: number, usedPct: number, categories: Array<{ category: string, tokens: number, pct: number }> } | null}
|
|
103
|
+
*/
|
|
104
|
+
function parseContextUsageMarkdown(text) {
|
|
105
|
+
if (typeof text !== 'string' || !text) return null;
|
|
106
|
+
const lines = text.split('\n');
|
|
107
|
+
|
|
108
|
+
let usedTokens = null;
|
|
109
|
+
let totalTokens = null;
|
|
110
|
+
let usedPct = null;
|
|
111
|
+
const tokensLineRe = /\*\*Tokens:\*\*\s*(\S+)\s*\/\s*(\S+)\s*\(([\d.]+)%\)/;
|
|
112
|
+
|
|
113
|
+
let inTable = false;
|
|
114
|
+
let foundHeading = false;
|
|
115
|
+
const categories = [];
|
|
116
|
+
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (usedTokens === null) {
|
|
119
|
+
const m = tokensLineRe.exec(line);
|
|
120
|
+
if (m) {
|
|
121
|
+
usedTokens = parseTokenAmount(m[1]);
|
|
122
|
+
totalTokens = parseTokenAmount(m[2]);
|
|
123
|
+
usedPct = Number(m[3]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!inTable) {
|
|
128
|
+
if (line.trim() === '### Estimated usage by category') {
|
|
129
|
+
foundHeading = true;
|
|
130
|
+
inTable = true;
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
if (trimmed.startsWith('###')) { inTable = false; continue; }
|
|
136
|
+
if (!trimmed.startsWith('|')) continue;
|
|
137
|
+
|
|
138
|
+
const cells = trimmed.split('|').map((c) => c.trim()).filter((c) => c.length > 0);
|
|
139
|
+
if (cells.length !== 3) continue;
|
|
140
|
+
if (cells[0].toLowerCase() === 'category') continue; // header row
|
|
141
|
+
if (/^:?-+:?$/.test(cells[0])) continue; // separator row
|
|
142
|
+
|
|
143
|
+
const tokens = parseTokenAmount(cells[1]);
|
|
144
|
+
const pctMatch = /^([\d.]+)%$/.exec(cells[2]);
|
|
145
|
+
if (!pctMatch || Number.isNaN(tokens)) continue;
|
|
146
|
+
categories.push({ category: cells[0], tokens, pct: Number(pctMatch[1]) });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
usedTokens === null || totalTokens === null || usedPct === null ||
|
|
151
|
+
Number.isNaN(usedTokens) || Number.isNaN(totalTokens) || Number.isNaN(usedPct) ||
|
|
152
|
+
!foundHeading || categories.length === 0
|
|
153
|
+
) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { usedTokens, totalTokens, usedPct, categories };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Fire-and-forget silent probe of the resumed session's context usage. Reuses
|
|
162
|
+
* the normal `run()` queue/lane (resume: true, silent: true) so it can never
|
|
163
|
+
* race a real chat run against the same `--resume sessionId`. Broadcasts
|
|
164
|
+
* `chat:context-usage` on success; logs (never throws/broadcasts) on a parse
|
|
165
|
+
* failure.
|
|
166
|
+
*
|
|
167
|
+
* @param {{ tabId: string, sessionId: string, cwd: string }} params
|
|
168
|
+
*/
|
|
169
|
+
function probeContextUsage({ tabId, sessionId, cwd }) {
|
|
170
|
+
run({
|
|
171
|
+
tabId,
|
|
172
|
+
sessionId,
|
|
173
|
+
prompt: '/context',
|
|
174
|
+
cwd,
|
|
175
|
+
resume: true,
|
|
176
|
+
silent: true,
|
|
177
|
+
onSilentResult: (text) => {
|
|
178
|
+
const parsed = parseContextUsageMarkdown(text);
|
|
179
|
+
if (!parsed) {
|
|
180
|
+
console.error('[chatRunner] parseContextUsageMarkdown failed to parse /context probe output');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
broadcast('chat:context-usage', {
|
|
184
|
+
tabId,
|
|
185
|
+
sessionId,
|
|
186
|
+
usedTokens: parsed.usedTokens,
|
|
187
|
+
totalTokens: parsed.totalTokens,
|
|
188
|
+
usedPct: parsed.usedPct,
|
|
189
|
+
categories: parsed.categories,
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
75
195
|
// ─── MCP consent-denial detection ──────────────────────────────────────────
|
|
76
196
|
// Best-effort substring heuristic over known CLI phrasing — NOT a documented
|
|
77
197
|
// stream-json schema field. Confirmed by extracting strings from the compiled
|
|
@@ -121,7 +241,7 @@ const CONCURRENCY_CAP = Math.min(
|
|
|
121
241
|
|
|
122
242
|
// tabId → cancel() for every ACTIVE run; FIFO list of WAITING runs; live count.
|
|
123
243
|
const inFlight = new Map();
|
|
124
|
-
const waiting = []; // [{ tabId, sessionId, prompt, cwd, resume }]
|
|
244
|
+
const waiting = []; // [{ tabId, sessionId, prompt, cwd, resume, silent, onSilentResult }]
|
|
125
245
|
let activeCount = 0;
|
|
126
246
|
|
|
127
247
|
// Indirection so tests can stub the spawn without launching claude.
|
|
@@ -153,7 +273,7 @@ function broadcast(channel, payload) {
|
|
|
153
273
|
* queue and are announced via chat:run:queued. De-dupes a tab already in the
|
|
154
274
|
* pipeline (the UI disables input while running, but guard anyway).
|
|
155
275
|
*
|
|
156
|
-
* @param {{ tabId: string, sessionId: string, prompt: string, cwd: string, resume: boolean }} opts
|
|
276
|
+
* @param {{ tabId: string, sessionId: string, prompt: string, cwd: string, resume: boolean, silent?: boolean, onSilentResult?: (text: string) => void }} opts
|
|
157
277
|
*/
|
|
158
278
|
function run(opts) {
|
|
159
279
|
if (inFlight.has(opts.tabId) || waiting.some((w) => w.tabId === opts.tabId)) return;
|
|
@@ -186,10 +306,10 @@ function pump() {
|
|
|
186
306
|
* for the run's lifetime so cancel() can reach it. Never rejects — the queue
|
|
187
307
|
* pump relies on the returned promise always settling so the lane frees.
|
|
188
308
|
*
|
|
189
|
-
* @param {{ tabId: string, sessionId: string, prompt: string, cwd: string, resume: boolean }} opts
|
|
309
|
+
* @param {{ tabId: string, sessionId: string, prompt: string, cwd: string, resume: boolean, silent?: boolean, onSilentResult?: (text: string) => void }} opts
|
|
190
310
|
* @returns {Promise<void>}
|
|
191
311
|
*/
|
|
192
|
-
function executeRun({ tabId, sessionId, prompt, cwd, resume }) {
|
|
312
|
+
function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentResult }) {
|
|
193
313
|
return new Promise((resolve) => {
|
|
194
314
|
let settled = false;
|
|
195
315
|
// Frees the lane exactly once: drops the cancel fn and resolves the promise
|
|
@@ -212,6 +332,9 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume }) {
|
|
|
212
332
|
const emitTerminal = (channel, payload) => {
|
|
213
333
|
if (terminalSent) return;
|
|
214
334
|
terminalSent = true;
|
|
335
|
+
// Silent (probe) runs still need this bookkeeping so the lane frees
|
|
336
|
+
// correctly, but must never surface on the six turn-affecting channels.
|
|
337
|
+
if (silent) return;
|
|
215
338
|
broadcast(channel, payload);
|
|
216
339
|
};
|
|
217
340
|
|
|
@@ -254,7 +377,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume }) {
|
|
|
254
377
|
args.push('--session-id', sessionId);
|
|
255
378
|
}
|
|
256
379
|
|
|
257
|
-
broadcast('chat:run:started', { tabId, sessionId });
|
|
380
|
+
if (!silent) broadcast('chat:run:started', { tabId, sessionId });
|
|
258
381
|
|
|
259
382
|
// Spawn with stdin closed (mirrors scheduler's 'ignore' — prevents the
|
|
260
383
|
// "claude -p stdin must be closed" gotcha from kg.cjs). stdout is piped for
|
|
@@ -324,10 +447,10 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume }) {
|
|
|
324
447
|
for (const block of content) {
|
|
325
448
|
if (block.type === 'text' && typeof block.text === 'string') {
|
|
326
449
|
finalAssistantText += block.text;
|
|
327
|
-
broadcast('chat:run:output', { tabId, delta: block.text });
|
|
450
|
+
if (!silent) broadcast('chat:run:output', { tabId, delta: block.text });
|
|
328
451
|
} else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
329
452
|
const classified = classifyToolUse(block);
|
|
330
|
-
broadcast('chat:run:tool-use', { tabId, id: block.id, ...classified });
|
|
453
|
+
if (!silent) broadcast('chat:run:tool-use', { tabId, id: block.id, ...classified });
|
|
331
454
|
}
|
|
332
455
|
}
|
|
333
456
|
} else if (event.type === 'user') {
|
|
@@ -351,20 +474,27 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume }) {
|
|
|
351
474
|
const text = typeof event.result === 'string' ? event.result : finalAssistantText;
|
|
352
475
|
|
|
353
476
|
if (event.subtype === 'success') {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
tabId,
|
|
358
|
-
sessionId,
|
|
359
|
-
questions: signal.questions,
|
|
360
|
-
raw: text,
|
|
361
|
-
});
|
|
362
|
-
} else {
|
|
477
|
+
if (silent) {
|
|
478
|
+
// Silent probe: no turn broadcast, no recordExchange — just hand
|
|
479
|
+
// the final text to whoever enqueued the probe (e.g. probeContextUsage).
|
|
363
480
|
emitTerminal('chat:run:complete', { tabId, sessionId, finalMessage: text });
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
481
|
+
if (typeof onSilentResult === 'function') onSilentResult(text);
|
|
482
|
+
} else {
|
|
483
|
+
const signal = parseStopSignal(text);
|
|
484
|
+
if (signal) {
|
|
485
|
+
emitTerminal('chat:run:needs-input', {
|
|
486
|
+
tabId,
|
|
487
|
+
sessionId,
|
|
488
|
+
questions: signal.questions,
|
|
489
|
+
raw: text,
|
|
490
|
+
});
|
|
491
|
+
} else {
|
|
492
|
+
emitTerminal('chat:run:complete', { tabId, sessionId, finalMessage: text });
|
|
493
|
+
// Record durable exchange off the hot path — UI must not wait on Haiku
|
|
494
|
+
recordExchange({ sessionId, cwd, prompt, result: text }).catch((err) => {
|
|
495
|
+
console.error('[chatRunner] recordExchange failed:', err?.message ?? err);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
368
498
|
}
|
|
369
499
|
} else {
|
|
370
500
|
emitTerminal('chat:run:error', {
|
|
@@ -470,6 +600,11 @@ function registerChatHandlers() {
|
|
|
470
600
|
try { tabId = schemas.chatCancel.parse(payload).tabId; } catch { return; }
|
|
471
601
|
cancel(tabId);
|
|
472
602
|
});
|
|
603
|
+
|
|
604
|
+
ipcMain.handle('chat:probe-context', validated(schemas.chatProbeContext, async ({ tabId, sessionId, cwd }) => {
|
|
605
|
+
probeContextUsage({ tabId, sessionId, cwd });
|
|
606
|
+
return { ok: true };
|
|
607
|
+
}));
|
|
473
608
|
}
|
|
474
609
|
|
|
475
610
|
module.exports = {
|
|
@@ -478,6 +613,8 @@ module.exports = {
|
|
|
478
613
|
attachWindow,
|
|
479
614
|
registerChatHandlers,
|
|
480
615
|
parseStopSignal,
|
|
616
|
+
parseContextUsageMarkdown,
|
|
617
|
+
probeContextUsage,
|
|
481
618
|
STOP_SENTINEL,
|
|
482
619
|
__setExecutor,
|
|
483
620
|
};
|
package/src/main/index.cjs
CHANGED
|
@@ -455,6 +455,20 @@ ipcMain.handle('clipboard:paste-image', async () => {
|
|
|
455
455
|
}
|
|
456
456
|
});
|
|
457
457
|
|
|
458
|
+
// Text paste — same rationale as clipboard:paste-image above: the renderer's
|
|
459
|
+
// navigator.clipboard.readText() requires the 'clipboard-read' permission,
|
|
460
|
+
// which setPermissionRequestHandler denies (MEDIA_PERMS only), so it always
|
|
461
|
+
// rejects. Reading via Electron's main-process clipboard.readText() sidesteps
|
|
462
|
+
// that permission gate entirely — no permission-handler changes needed.
|
|
463
|
+
ipcMain.handle('clipboard:paste-text', async () => {
|
|
464
|
+
try {
|
|
465
|
+
const text = clipboard.readText();
|
|
466
|
+
return { ok: true, text: text || '' };
|
|
467
|
+
} catch (e) {
|
|
468
|
+
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
458
472
|
// PRD 407 Capture panel — write side of paste-image's read. Writes a
|
|
459
473
|
// screenshot capture to the OS clipboard as an image.
|
|
460
474
|
ipcMain.handle('browser:copy-image', validated(schemas.browserCopyImage, ({ dataUrl }) => {
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -382,6 +382,12 @@ const chatCancel = z.object({
|
|
|
382
382
|
tabId: z.string().min(1).max(128),
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
+
const chatProbeContext = z.object({
|
|
386
|
+
tabId: z.string().min(1).max(128),
|
|
387
|
+
sessionId: z.string().min(1).max(128),
|
|
388
|
+
cwd: z.string().min(1).max(4096),
|
|
389
|
+
});
|
|
390
|
+
|
|
385
391
|
// ──────────────────────────────────────────── Web Remote
|
|
386
392
|
// OTP is 8 uppercase alphanumeric chars (case-insensitive entry, normalised to upper in handler).
|
|
387
393
|
const WEB_REMOTE_OTP_RE = /^[A-Z0-9]{8}$/i;
|
|
@@ -665,6 +671,7 @@ module.exports = {
|
|
|
665
671
|
watchersKillTab,
|
|
666
672
|
chatRun,
|
|
667
673
|
chatCancel,
|
|
674
|
+
chatProbeContext,
|
|
668
675
|
exchangesList,
|
|
669
676
|
},
|
|
670
677
|
validated,
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -1164,6 +1164,73 @@ function healTargetForFix(fixSlug, jobs) {
|
|
|
1164
1164
|
return jobs.find((x) => x.slug === originalSlug && isPromotableOriginal(x.status)) || null;
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
+
/**
|
|
1168
|
+
* Build the Opus investigation prompt. Pure/hermetic so its content can be
|
|
1169
|
+
* unit-tested (no spawn, no fs). Inputs are the already-resolved values that
|
|
1170
|
+
* spawnInvestigation computes.
|
|
1171
|
+
*/
|
|
1172
|
+
function buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group }) {
|
|
1173
|
+
return `You are investigating a failed scheduled job in the session-manager queue. Your ONLY job is to write a fix-plan PRD file. Do NOT attempt the fix yourself.
|
|
1174
|
+
|
|
1175
|
+
# Failed job
|
|
1176
|
+
- Slug: ${failedJob.slug}
|
|
1177
|
+
- Title: ${failedJob.title}
|
|
1178
|
+
- cwd: ${cwd}
|
|
1179
|
+
- Exit code: ${failedJob.exitCode}
|
|
1180
|
+
- Full failure log: ${failedLogPath}
|
|
1181
|
+
|
|
1182
|
+
# Original PRD body (this is what the job was trying to do)
|
|
1183
|
+
\`\`\`
|
|
1184
|
+
${originalBody}
|
|
1185
|
+
\`\`\`
|
|
1186
|
+
|
|
1187
|
+
# Last ~16KB of the failure log (stream-json format from \`claude -p\`)
|
|
1188
|
+
\`\`\`
|
|
1189
|
+
${logTail}
|
|
1190
|
+
\`\`\`
|
|
1191
|
+
|
|
1192
|
+
# Your task
|
|
1193
|
+
1. Read the full failure log at ${failedLogPath} if the tail above isn't sufficient.
|
|
1194
|
+
2. Read source files in ${cwd} as needed to understand the context.
|
|
1195
|
+
3. Identify the root cause of the failure.
|
|
1196
|
+
4. Write a NEW fix-plan PRD file at exactly this path:
|
|
1197
|
+
|
|
1198
|
+
${fixPath}
|
|
1199
|
+
|
|
1200
|
+
5. The frontmatter MUST be exactly this format (no extra keys):
|
|
1201
|
+
\`\`\`
|
|
1202
|
+
---
|
|
1203
|
+
title: Fix: <short summary of the fix>
|
|
1204
|
+
cwd: ${cwd}
|
|
1205
|
+
parallelGroup: ${group}
|
|
1206
|
+
estimateMinutes: <your time estimate>
|
|
1207
|
+
---
|
|
1208
|
+
\`\`\`
|
|
1209
|
+
6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
|
|
1210
|
+
- Root-cause analysis (what went wrong and why)
|
|
1211
|
+
- Concrete fix steps (specific files / commands / edits)
|
|
1212
|
+
- Verification command(s) the next agent should run to confirm the fix
|
|
1213
|
+
- Acceptance criteria
|
|
1214
|
+
- Before writing the fix-plan PRD body, read
|
|
1215
|
+
\`plugins/session-manager-dev/skills/develop/standards.md\` (resolve it relative to the failed
|
|
1216
|
+
job's repo root — for session-manager-authored PRDs that is this cwd). Inline its
|
|
1217
|
+
\`## Execution discipline (headless runs)\` section VERBATIM into the new fix-plan PRD under an
|
|
1218
|
+
\`## Engineering standards\` heading, exactly as the \`/develop\` skill's Phase 1 does for normal
|
|
1219
|
+
PRDs. Reuse that convention — do not invent a second, paraphrased set of warnings. The headless
|
|
1220
|
+
Sonnet executor that runs your fix-plan sees no skills and no conversation, so inlining these
|
|
1221
|
+
rules verbatim is the only way they reach it.
|
|
1222
|
+
- Known failure class — "delegated instead of executed": if the failure log tail above shows the
|
|
1223
|
+
failed run invoked the \`Skill\` tool with a \`session-manager-dev:develop\` or
|
|
1224
|
+
\`session-manager-dev:process-feedback\` argument, and/or called \`ScheduleWakeup\`, and then exited
|
|
1225
|
+
0 without producing the diff/tests its PRD demanded, recognize this as a self-delegation failure.
|
|
1226
|
+
In that case LEAD the fix-plan PRD by quoting VERBATIM the canonical rule from standards.md's
|
|
1227
|
+
Execution discipline section — "You ARE the executor — never re-queue or self-schedule" — rather
|
|
1228
|
+
than authoring new prose. Instruct the fix-plan's executor plainly that a queued PRD is the task,
|
|
1229
|
+
not evidence of completion, and that the deliverable is the code diff.
|
|
1230
|
+
|
|
1231
|
+
DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediately.`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1167
1234
|
/**
|
|
1168
1235
|
* Spawn an Opus investigation session for a failed job. The investigator's job
|
|
1169
1236
|
* is to read the failure log + original PRD, identify the root cause, and write
|
|
@@ -1236,49 +1303,7 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
1236
1303
|
console.warn(`[scheduler] investigation cwd missing (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
|
|
1237
1304
|
cwd = DEFAULT_PROJECT_CWD;
|
|
1238
1305
|
}
|
|
1239
|
-
const prompt =
|
|
1240
|
-
|
|
1241
|
-
# Failed job
|
|
1242
|
-
- Slug: ${failedJob.slug}
|
|
1243
|
-
- Title: ${failedJob.title}
|
|
1244
|
-
- cwd: ${cwd}
|
|
1245
|
-
- Exit code: ${failedJob.exitCode}
|
|
1246
|
-
- Full failure log: ${failedLogPath}
|
|
1247
|
-
|
|
1248
|
-
# Original PRD body (this is what the job was trying to do)
|
|
1249
|
-
\`\`\`
|
|
1250
|
-
${originalBody}
|
|
1251
|
-
\`\`\`
|
|
1252
|
-
|
|
1253
|
-
# Last ~16KB of the failure log (stream-json format from \`claude -p\`)
|
|
1254
|
-
\`\`\`
|
|
1255
|
-
${logTail}
|
|
1256
|
-
\`\`\`
|
|
1257
|
-
|
|
1258
|
-
# Your task
|
|
1259
|
-
1. Read the full failure log at ${failedLogPath} if the tail above isn't sufficient.
|
|
1260
|
-
2. Read source files in ${cwd} as needed to understand the context.
|
|
1261
|
-
3. Identify the root cause of the failure.
|
|
1262
|
-
4. Write a NEW fix-plan PRD file at exactly this path:
|
|
1263
|
-
|
|
1264
|
-
${fixPath}
|
|
1265
|
-
|
|
1266
|
-
5. The frontmatter MUST be exactly this format (no extra keys):
|
|
1267
|
-
\`\`\`
|
|
1268
|
-
---
|
|
1269
|
-
title: Fix: <short summary of the fix>
|
|
1270
|
-
cwd: ${cwd}
|
|
1271
|
-
parallelGroup: ${group}
|
|
1272
|
-
estimateMinutes: <your time estimate>
|
|
1273
|
-
---
|
|
1274
|
-
\`\`\`
|
|
1275
|
-
6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
|
|
1276
|
-
- Root-cause analysis (what went wrong and why)
|
|
1277
|
-
- Concrete fix steps (specific files / commands / edits)
|
|
1278
|
-
- Verification command(s) the next agent should run to confirm the fix
|
|
1279
|
-
- Acceptance criteria
|
|
1280
|
-
|
|
1281
|
-
DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediately.`;
|
|
1306
|
+
const prompt = buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group });
|
|
1282
1307
|
|
|
1283
1308
|
// Phase 1: open log fd for pre-spawn diagnostics.
|
|
1284
1309
|
const { fd, safeLog, closeFd } = openLog(investigationLogPath);
|
|
@@ -2633,4 +2658,4 @@ const remote = {
|
|
|
2633
2658
|
},
|
|
2634
2659
|
};
|
|
2635
2660
|
|
|
2636
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, resolveRunId, isUnresolvableNeedsReview, healTargetForFix };
|
|
2661
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -1079,6 +1079,8 @@ export interface SessionManagerAPI {
|
|
|
1079
1079
|
| { ok: true; mode: string; text: string; meta: { chunks?: number; tokens?: number } }
|
|
1080
1080
|
| { ok: false; error: string }
|
|
1081
1081
|
>;
|
|
1082
|
+
/** Fired when the embedded page tries to open a new window (target="_blank", window.open, ctrl/cmd-click) — the main process denies the native popup and forwards the URL here. */
|
|
1083
|
+
onOpenTabRequest: (handler: (payload: { url: string }) => void) => () => void;
|
|
1082
1084
|
};
|
|
1083
1085
|
transcripts: {
|
|
1084
1086
|
subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
|
|
@@ -1250,6 +1252,13 @@ export interface SessionManagerAPI {
|
|
|
1250
1252
|
| { ok: true; path: string; bytes: number }
|
|
1251
1253
|
| { ok: false; empty?: true; error?: string }
|
|
1252
1254
|
>;
|
|
1255
|
+
/** Ctrl+V text paste — reads OS clipboard text via Electron's native API
|
|
1256
|
+
* (renderer's navigator.clipboard.readText() is denied by the
|
|
1257
|
+
* permission-request handler, which only allows media permissions). */
|
|
1258
|
+
pasteText: () => Promise<
|
|
1259
|
+
| { ok: true; text: string }
|
|
1260
|
+
| { ok: false; error?: string }
|
|
1261
|
+
>;
|
|
1253
1262
|
/** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
|
|
1254
1263
|
copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1255
1264
|
};
|
package/src/preload/index.cjs
CHANGED
|
@@ -107,6 +107,11 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
107
107
|
return () => ipcRenderer.removeListener(channel, listener);
|
|
108
108
|
},
|
|
109
109
|
capture: (payload) => ipcRenderer.invoke('browser:capture', payload),
|
|
110
|
+
onOpenTabRequest: (handler) => {
|
|
111
|
+
const listener = (_e, payload) => handler(payload);
|
|
112
|
+
ipcRenderer.on('browser:open-tab-request', listener);
|
|
113
|
+
return () => ipcRenderer.removeListener('browser:open-tab-request', listener);
|
|
114
|
+
},
|
|
110
115
|
},
|
|
111
116
|
transcripts: {
|
|
112
117
|
subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
|
|
@@ -282,6 +287,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
282
287
|
},
|
|
283
288
|
clipboard: {
|
|
284
289
|
pasteImage: () => ipcRenderer.invoke('clipboard:paste-image'),
|
|
290
|
+
pasteText: () => ipcRenderer.invoke('clipboard:paste-text'),
|
|
285
291
|
copyImage: (dataUrl) => ipcRenderer.invoke('browser:copy-image', { dataUrl }),
|
|
286
292
|
},
|
|
287
293
|
memory: {
|