claude-code-session-manager 0.62.0 → 0.62.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/assets/{TiptapBody-Bn-LMce4.js → TiptapBody-BwN4pNC0.js} +1 -1
  2. package/dist/assets/{index-DOXvph-Z.css → index-CVCCMw5o.css} +1 -1
  3. package/dist/assets/{index-BfiMRkln.js → index-ZjTDk5b7.js} +1083 -1111
  4. package/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +215 -0
  7. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +42 -1
  8. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +54 -1
  9. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +6 -0
  10. package/src/main/__tests__/transcripts-paged-reads.test.cjs +6 -0
  11. package/src/main/__tests__/transcriptsUsageFor.test.cjs +6 -0
  12. package/src/main/config.cjs +1 -22
  13. package/src/main/files.cjs +20 -0
  14. package/src/main/index.cjs +1 -63
  15. package/src/main/ipcSchemas.cjs +11 -209
  16. package/src/main/lib/opsOwnership.cjs +0 -2
  17. package/src/main/lib/summarize.cjs +0 -1
  18. package/src/main/runVerify.cjs +55 -6
  19. package/src/main/scheduler.cjs +46 -7
  20. package/src/preload/api.d.ts +2 -197
  21. package/src/preload/index.cjs +1 -100
  22. package/src/main/__tests__/browserAgentServer.test.cjs +0 -235
  23. package/src/main/__tests__/browserView-destroyed-handler.test.cjs +0 -85
  24. package/src/main/__tests__/browserView-oauth-popup.test.cjs +0 -126
  25. package/src/main/__tests__/web-remote-e2e-pinning.test.cjs +0 -181
  26. package/src/main/browserAgentServer.cjs +0 -224
  27. package/src/main/browserCapture.cjs +0 -728
  28. package/src/main/browserView.cjs +0 -916
  29. package/src/main/lib/__tests__/browserAgentActions.test.cjs +0 -76
  30. package/src/main/lib/browserAgentActions.cjs +0 -114
  31. package/src/main/lib/e2eStateMachine.cjs +0 -39
  32. package/src/main/webRemote.cjs +0 -1367
  33. package/src/preload/browserViewPreload.cjs +0 -118
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-BfiMRkln.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-ZjTDk5b7.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-DOXvph-Z.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-CVCCMw5o.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.62.0",
3
+ "version": "0.62.1",
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",
@@ -0,0 +1,215 @@
1
+ /**
2
+ * runVerify-transcript-commit-evidence.test.cjs — regression tests for the
3
+ * transcript-commit-evidence fallback added to runVerify.cjs.
4
+ *
5
+ * Root cause (99-fix-e2e-needs-review-test, 2026-08-07): a job whose cwd is
6
+ * not a git repo (e.g. an investigation fix-plan that inherited cwd: /tmp)
7
+ * can never produce committedDuringRun=true — gitHead()/committedInWindow()
8
+ * both fail closed there — so the PASS+commit sentinel override could never
9
+ * fire even when the harness's own transcript recorded a real commit. This
10
+ * file asserts the fallback fires ONLY when both conditions hold: the cwd is
11
+ * genuinely not a git repo, AND the transcript itself carries harness-emitted
12
+ * commit evidence (never inferred from prose the model could fabricate).
13
+ *
14
+ * Run standalone: timeout 120 npx vitest run src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs
15
+ */
16
+
17
+ import { test } from 'vitest';
18
+ const assert = require('node:assert/strict');
19
+ const os = require('node:os');
20
+ const fs = require('node:fs');
21
+ const path = require('node:path');
22
+ const { execFileSync } = require('node:child_process');
23
+ const { verifyRun } = require('../runVerify.cjs');
24
+
25
+ // ─── helpers (mirrors runVerify.test.cjs) ─────────────────────────────────────
26
+
27
+ function makeTmpDir() {
28
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-commit-evidence-test-'));
29
+ }
30
+
31
+ function rmdir(dir) {
32
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
33
+ }
34
+
35
+ function writeLog(dir, slug, events) {
36
+ const lines = ['[scheduler] starting ' + slug + ' at 2026-08-07T02:25:31.453Z'];
37
+ for (const ev of events) lines.push(JSON.stringify(ev));
38
+ lines.push('[scheduler] exit code=0 (raw code=0 signal=null) duration=47s');
39
+ fs.writeFileSync(path.join(dir, `${slug}.log`), lines.join('\n') + '\n');
40
+ }
41
+
42
+ function writePrd(dir, slug, body) {
43
+ const text = `---\ntitle: Test PRD\ncwd: /tmp\nestimateMinutes: 30\n---\n${body}\n`;
44
+ fs.writeFileSync(path.join(dir, `${slug}.md`), text);
45
+ return path.join(dir, `${slug}.md`);
46
+ }
47
+
48
+ function makeTmpGitRepo() {
49
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-commit-evidence-repo-'));
50
+ execFileSync('git', ['init', '-q'], { cwd: dir });
51
+ execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: dir });
52
+ execFileSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
53
+ fs.writeFileSync(path.join(dir, 'README.md'), 'init\n');
54
+ execFileSync('git', ['add', 'README.md'], { cwd: dir });
55
+ execFileSync('git', ['commit', '-q', '-m', 'init'], { cwd: dir });
56
+ return dir;
57
+ }
58
+
59
+ /**
60
+ * Build a run log with:
61
+ * - padding events so a late is_error lands in the final 20%
62
+ * - a late is_error tool_result (main-agent, no self-recovery)
63
+ * - optionally a harness-emitted commit-evidence line
64
+ * - a SCHEDULER_VERDICT: PASS result event
65
+ */
66
+ function buildRunEvents({ includeCommitEvidence, evidenceForm = 'vcs_state_changed' }) {
67
+ const events = [];
68
+ for (let k = 0; k < 8; k++) {
69
+ events.push({ type: 'assistant', message: { role: 'assistant', content: [
70
+ { type: 'tool_use', id: `t${k}`, name: 'Read', input: { description: `read ${k}` } }] } });
71
+ events.push({ type: 'user', message: { role: 'user', content: [
72
+ { type: 'tool_result', tool_use_id: `t${k}`, content: 'ok', is_error: false }] } });
73
+ }
74
+ events.push({ type: 'assistant', message: { role: 'assistant', content: [
75
+ { type: 'tool_use', id: 'tbad', name: 'Agent', input: { description: 'run code review agent' } }] } });
76
+ events.push({ type: 'user', message: { role: 'user', content: [
77
+ { type: 'tool_result', tool_use_id: 'tbad',
78
+ content: "Agent type 'code-reviewer' not found. Available agents: architect, …", is_error: true }] } });
79
+
80
+ if (includeCommitEvidence) {
81
+ if (evidenceForm === 'vcs_state_changed') {
82
+ events.push({ type: 'system', subtype: 'vcs_state_changed', kind: 'commit', cwd: '/tmp' });
83
+ } else {
84
+ events.push({
85
+ type: 'user',
86
+ message: { role: 'user', content: [
87
+ { type: 'tool_result', tool_use_id: 'tcommit', content: 'committed', is_error: false }] },
88
+ tool_use_result: { gitOperation: { commit: { sha: '6ecbd40', kind: 'committed' } } },
89
+ });
90
+ }
91
+ }
92
+
93
+ events.push({ type: 'result', subtype: 'success', result: 'All acceptance criteria verified.\nSCHEDULER_VERDICT: PASS' });
94
+ return events;
95
+ }
96
+
97
+ // ─── (1) regression case: non-git cwd + transcript evidence → clean ──────────
98
+
99
+ test('PASS + late is_error + transcript commit evidence + non-git cwd → clean (fallback fires)', async () => {
100
+ const tmp = makeTmpDir();
101
+ try {
102
+ const slug = '99-fix-e2e-needs-review-test';
103
+ writeLog(tmp, slug, buildRunEvents({ includeCommitEvidence: true }));
104
+ const prdPath = writePrd(tmp, slug, '# Fix scheduler skip guard');
105
+ const nonGitCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-nongit-cwd-'));
106
+ try {
107
+ const verdict = await verifyRun({
108
+ runDir: tmp,
109
+ prdPath,
110
+ queueEntry: { slug, status: 'running', cwd: nonGitCwd },
111
+ allJobs: [],
112
+ committedDuringRun: false,
113
+ });
114
+ assert.equal(verdict.verdict, 'clean', `expected clean via transcript fallback, got ${verdict.verdict}: ${verdict.reason}`);
115
+ assert.equal(verdict.downgradeTo, null);
116
+ const sidecar = JSON.parse(fs.readFileSync(path.join(tmp, `${slug}.verdicts.json`), 'utf8'));
117
+ assert.equal(sidecar.commitEvidenceSource, 'transcript', 'sidecar should record the fallback source');
118
+ } finally { rmdir(nonGitCwd); }
119
+ } finally { rmdir(tmp); }
120
+ });
121
+
122
+ test('PASS + late is_error + gitOperation.commit evidence + non-git cwd → clean (fallback fires)', async () => {
123
+ const tmp = makeTmpDir();
124
+ try {
125
+ const slug = '99-fix-e2e-needs-review-test-alt';
126
+ writeLog(tmp, slug, buildRunEvents({ includeCommitEvidence: true, evidenceForm: 'gitOperation' }));
127
+ const prdPath = writePrd(tmp, slug, '# Fix scheduler skip guard');
128
+ const nonGitCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-nongit-cwd-'));
129
+ try {
130
+ const verdict = await verifyRun({
131
+ runDir: tmp,
132
+ prdPath,
133
+ queueEntry: { slug, status: 'running', cwd: nonGitCwd },
134
+ allJobs: [],
135
+ committedDuringRun: false,
136
+ });
137
+ assert.equal(verdict.verdict, 'clean', `expected clean via transcript fallback, got ${verdict.verdict}: ${verdict.reason}`);
138
+ } finally { rmdir(nonGitCwd); }
139
+ } finally { rmdir(tmp); }
140
+ });
141
+
142
+ // ─── (2) no evidence lines → fallback must not fire, still transcript_errors ──
143
+
144
+ test('PASS + late is_error + NO transcript evidence + non-git cwd → transcript_errors (no fallback)', async () => {
145
+ const tmp = makeTmpDir();
146
+ try {
147
+ const slug = '99-fix-no-evidence';
148
+ writeLog(tmp, slug, buildRunEvents({ includeCommitEvidence: false }));
149
+ const prdPath = writePrd(tmp, slug, '# Fix scheduler skip guard');
150
+ const nonGitCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-nongit-cwd-'));
151
+ try {
152
+ const verdict = await verifyRun({
153
+ runDir: tmp,
154
+ prdPath,
155
+ queueEntry: { slug, status: 'running', cwd: nonGitCwd },
156
+ allJobs: [],
157
+ committedDuringRun: false,
158
+ });
159
+ assert.equal(verdict.verdict, 'transcript_errors', `expected transcript_errors without evidence, got ${verdict.verdict}: ${verdict.reason}`);
160
+ assert.equal(verdict.downgradeTo, 'needs_review');
161
+ } finally { rmdir(nonGitCwd); }
162
+ } finally { rmdir(tmp); }
163
+ });
164
+
165
+ // ─── (3) git-repo cwd + evidence + committedDuringRun:false → fallback must not apply ──
166
+
167
+ test('PASS + late is_error + transcript evidence + GIT-REPO cwd + committedDuringRun:false → transcript_errors (false-PASS guard)', async () => {
168
+ const tmp = makeTmpDir();
169
+ const repo = makeTmpGitRepo();
170
+ try {
171
+ const slug = '99-fix-git-repo-cwd';
172
+ writeLog(tmp, slug, buildRunEvents({ includeCommitEvidence: true }));
173
+ const prdPath = writePrd(tmp, slug, '# Fix scheduler skip guard');
174
+ const verdict = await verifyRun({
175
+ runDir: tmp,
176
+ prdPath,
177
+ queueEntry: { slug, status: 'running', cwd: repo },
178
+ allJobs: [],
179
+ committedDuringRun: false,
180
+ });
181
+ assert.equal(verdict.verdict, 'transcript_errors', `fallback must not apply when git ancestry is observable, got ${verdict.verdict}: ${verdict.reason}`);
182
+ assert.equal(verdict.downgradeTo, 'needs_review');
183
+ } finally { rmdir(tmp); rmdir(repo); }
184
+ });
185
+
186
+ // ─── (4) PASS + transcript evidence + non-git cwd must not be pass_no_commit ──
187
+
188
+ test('PASS (no error) + transcript evidence + non-git cwd → clean, not pass_no_commit', async () => {
189
+ const tmp = makeTmpDir();
190
+ try {
191
+ const slug = '99-fix-clean-pass';
192
+ const events = [
193
+ { type: 'assistant', message: { role: 'assistant', content: [
194
+ { type: 'tool_use', id: 't0', name: 'Bash', input: { description: 'run tests' } }] } },
195
+ { type: 'user', message: { role: 'user', content: [
196
+ { type: 'tool_result', tool_use_id: 't0', content: '535 passed', is_error: false }] } },
197
+ { type: 'system', subtype: 'vcs_state_changed', kind: 'commit', cwd: '/tmp' },
198
+ { type: 'result', subtype: 'success', result: 'All acceptance criteria verified.\nSCHEDULER_VERDICT: PASS' },
199
+ ];
200
+ writeLog(tmp, slug, events);
201
+ const prdPath = writePrd(tmp, slug, '# Fix scheduler skip guard');
202
+ const nonGitCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-nongit-cwd-'));
203
+ try {
204
+ const verdict = await verifyRun({
205
+ runDir: tmp,
206
+ prdPath,
207
+ queueEntry: { slug, status: 'running', cwd: nonGitCwd },
208
+ allJobs: [],
209
+ committedDuringRun: false,
210
+ });
211
+ assert.equal(verdict.verdict, 'clean', `expected clean, got ${verdict.verdict}: ${verdict.reason}`);
212
+ assert.notEqual(verdict.verdict, 'pass_no_commit');
213
+ } finally { rmdir(nonGitCwd); }
214
+ } finally { rmdir(tmp); }
215
+ });
@@ -8,7 +8,11 @@
8
8
 
9
9
  const { test } = require('node:test');
10
10
  const assert = require('node:assert/strict');
11
- const { buildInvestigationPrompt } = require('../scheduler.cjs');
11
+ const fs = require('node:fs');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+ const { execFileSync } = require('node:child_process');
15
+ const { buildInvestigationPrompt, isGitRepoSync } = require('../scheduler.cjs');
12
16
 
13
17
  function makeArgs(overrides = {}) {
14
18
  return {
@@ -49,3 +53,40 @@ test('prompt includes the resolved job fields', () => {
49
53
  assert.match(prompt, /some log tail output/);
50
54
  assert.match(prompt, /05-fix-my-feature\.md/);
51
55
  });
56
+
57
+ // ─── cwd-must-be-git-repo-root guidance (99-fix-e2e-needs-review-test) ────────
58
+
59
+ test('prompt instructs that cwd must be the git repo root where the fix will land', () => {
60
+ const prompt = buildInvestigationPrompt(makeArgs());
61
+ assert.match(prompt, /cwd.*must be the git repo root/);
62
+ assert.match(prompt, /needs_review/);
63
+ });
64
+
65
+ test('prompt frontmatter block still declares exactly the four expected keys', () => {
66
+ const prompt = buildInvestigationPrompt(makeArgs());
67
+ const fenceMatch = prompt.match(/```\s*\n\s*---\n([\s\S]*?)\n\s*---\n\s*```/);
68
+ assert.ok(fenceMatch, 'expected a fenced frontmatter block in the prompt');
69
+ const keys = fenceMatch[1]
70
+ .split('\n')
71
+ .filter((line) => line.trim().length > 0)
72
+ .map((line) => line.split(':')[0].trim());
73
+ assert.deepEqual(keys, ['title', 'cwd', 'parallelGroup', 'estimateMinutes']);
74
+ });
75
+
76
+ // ─── isGitRepoSync (scheduler.cjs) ─────────────────────────────────────────────
77
+
78
+ test('isGitRepoSync: true for a real git repo, false for a non-repo dir, false for missing/undefined', () => {
79
+ const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'scheduler-isgitrepo-repo-'));
80
+ const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'scheduler-isgitrepo-plain-'));
81
+ try {
82
+ execFileSync('git', ['init', '-q'], { cwd: repo });
83
+ assert.equal(isGitRepoSync(repo), true);
84
+ assert.equal(isGitRepoSync(nonRepo), false);
85
+ assert.equal(isGitRepoSync(undefined), false);
86
+ assert.equal(isGitRepoSync(''), false);
87
+ assert.equal(isGitRepoSync('/definitely/does/not/exist'), false);
88
+ } finally {
89
+ fs.rmSync(repo, { recursive: true, force: true });
90
+ fs.rmSync(nonRepo, { recursive: true, force: true });
91
+ }
92
+ });
@@ -23,6 +23,7 @@ const path = require('node:path');
23
23
  let tmpHome;
24
24
  let originalHome;
25
25
  let executeJob;
26
+ let spawnJob;
26
27
  let PRDS_DIR;
27
28
 
28
29
  beforeAll(() => {
@@ -30,7 +31,7 @@ beforeAll(() => {
30
31
  tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-prd-missing-skip-'));
31
32
  process.env.HOME = tmpHome;
32
33
 
33
- ({ executeJob, PRDS_DIR } = require('../scheduler.cjs'));
34
+ ({ executeJob, spawnJob, PRDS_DIR } = require('../scheduler.cjs'));
34
35
 
35
36
  if (!PRDS_DIR.startsWith(tmpHome)) {
36
37
  throw new Error(`refusing to run: PRDS_DIR (${PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
@@ -87,3 +88,55 @@ test('a job whose PRD source exists but is unreadable for a non-ENOENT reason st
87
88
  fs.rmSync(runDir, { recursive: true, force: true });
88
89
  }
89
90
  });
91
+
92
+ function registerActiveProject(cwd) {
93
+ const projectsDir = path.join(tmpHome, '.claude', 'projects');
94
+ const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
95
+ fs.mkdirSync(slugDir, { recursive: true });
96
+ fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
97
+ }
98
+
99
+ function writeProjectQueue(cwd, jobs) {
100
+ const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
101
+ fs.mkdirSync(stateDir, { recursive: true });
102
+ fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
103
+ return path.join(stateDir, 'queue.json');
104
+ }
105
+
106
+ // This is the actual regression case: PRD 812's fix only covered executeJob's
107
+ // own return value. The bug lived one layer up, in spawnJob's post-run
108
+ // handler, which only short-circuited on skipped === 'prd-archived' and let
109
+ // 'prd-missing' fall through into the verify path meant for real runs —
110
+ // downgrading a stale-queue-entry retire into a needs_review with a bogus RCA
111
+ // feedback item. Drive spawnJob itself (not just executeJob) so this covers
112
+ // the actual defect, not just the classification it depends on.
113
+ test("spawnJob's post-run handler retires a prd-missing skip as a plain completion, without running verify", async () => {
114
+ const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-prd-missing-spawnjob-project-'));
115
+ registerActiveProject(projectCwd);
116
+ const runDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'sm-prd-missing-spawnjob-run-'));
117
+ try {
118
+ const slug = `812-test-spawnjob-missing-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
119
+ const queuePath = writeProjectQueue(projectCwd, [
120
+ { slug, status: 'pending', cwd: projectCwd },
121
+ ]);
122
+
123
+ const job = { slug, cwd: projectCwd };
124
+ await spawnJob(job, 'run-spawnjob-missing', runDir, projectCwd);
125
+
126
+ const state = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
127
+ const row = state.jobs.find((j) => j.slug === slug);
128
+ expect(row).toBeDefined();
129
+ expect(row.status).toBe('completed');
130
+ expect(row.exitCode).toBe(0);
131
+ expect(row.error).toBeNull();
132
+ // No verify pass ran and no RCA feedback fired: a verified run always
133
+ // stamps a verify-derived status ('needs_review') or a landedCommit/verify
134
+ // field onto the row. Neither is present here — the row was completed by
135
+ // the early-return skip branch, not the verification block further down.
136
+ expect(row.status).not.toBe('needs_review');
137
+ expect(row.landedCommit).toBeUndefined();
138
+ } finally {
139
+ fs.rmSync(projectCwd, { recursive: true, force: true });
140
+ fs.rmSync(runDir, { recursive: true, force: true });
141
+ }
142
+ });
@@ -29,6 +29,12 @@ beforeEach(() => {
29
29
 
30
30
  afterEach(() => {
31
31
  fs.rmSync(cwd, { recursive: true, force: true });
32
+ // writeTranscript mkdir -p's this run's folder under the REAL
33
+ // ~/.claude/projects (that's where transcriptPath resolves to), so leaving
34
+ // it behind accumulates one phantom project directory per test run — 1760
35
+ // of them had piled up before this cleanup existed, and Home counted every
36
+ // one as a project. Remove it with the tmp cwd it mirrors.
37
+ fs.rmSync(path.dirname(transcriptPath(cwd, 'x')), { recursive: true, force: true });
32
38
  });
33
39
 
34
40
  function writeTranscript(cwdArg, sessionId, lines) {
@@ -36,6 +36,12 @@ beforeEach(() => {
36
36
 
37
37
  afterEach(() => {
38
38
  fs.rmSync(cwd, { recursive: true, force: true });
39
+ // writeTranscript mkdir -p's this run's folder under the REAL
40
+ // ~/.claude/projects (that's where transcriptPath resolves to), so leaving
41
+ // it behind accumulates one phantom project directory per test run. Thousands
42
+ // had piled up before this cleanup existed, and Home counted every one as a
43
+ // project. Remove it with the tmp cwd it mirrors.
44
+ fs.rmSync(path.dirname(transcriptPath(cwd, 'x')), { recursive: true, force: true });
39
45
  });
40
46
 
41
47
  function writeTranscript(cwdArg, sessionId, lines) {
@@ -24,6 +24,12 @@ beforeEach(() => {
24
24
 
25
25
  afterEach(() => {
26
26
  fs.rmSync(cwd, { recursive: true, force: true });
27
+ // writeTranscript mkdir -p's this run's folder under the REAL
28
+ // ~/.claude/projects (that's where transcriptPath resolves to), so leaving
29
+ // it behind accumulates one phantom project directory per test run. Thousands
30
+ // had piled up before this cleanup existed, and Home counted every one as a
31
+ // project. Remove it with the tmp cwd it mirrors.
32
+ fs.rmSync(path.dirname(transcriptPath(cwd, 'x')), { recursive: true, force: true });
27
33
  });
28
34
 
29
35
  function writeTranscript(cwdArg, sessionId, lines) {
@@ -129,10 +129,7 @@ function validateWrite(realAbs) {
129
129
  (p) => realAbs === p || realAbs.startsWith(p + path.sep)
130
130
  );
131
131
  if (inWritePrefix) return;
132
- // Also allowed inside a registered project root's .claude/ subtree, OR its
133
- // tests/fixtures/browser-capture/ subtree (PRD 407 "PRD fixture" capture
134
- // destination — narrowly scoped to that one path segment, not a general
135
- // project-root write grant).
132
+ // Also allowed inside a registered project root's .claude/ subtree.
136
133
  for (const root of allowedRoots) {
137
134
  if (root === os.homedir()) continue;
138
135
  let realRoot;
@@ -142,24 +139,6 @@ function validateWrite(realAbs) {
142
139
  if (realAbs === claudeSub || realAbs.startsWith(claudeSub + path.sep)) {
143
140
  return;
144
141
  }
145
- const fixturesSub = path.join(realRoot, 'tests', 'fixtures', 'browser-capture');
146
- if (realAbs === fixturesSub || realAbs.startsWith(fixturesSub + path.sep)) {
147
- return;
148
- }
149
- // PRD 410 Recorder → Playwright-spec export: narrowly scoped to
150
- // tests/e2e/ (matches the repo's existing e2e spec location), not a
151
- // general project-root write grant.
152
- const e2eSub = path.join(realRoot, 'tests', 'e2e');
153
- if (realAbs === e2eSub || realAbs.startsWith(e2eSub + path.sep)) {
154
- return;
155
- }
156
- // Browser tab scratch saves (DOM captures, screenshots, recorded
157
- // flows): narrowly scoped to session-manager-operations/browser/,
158
- // this repo's existing per-project artifact-store convention.
159
- const browserSub = path.join(realRoot, 'session-manager-operations', 'browser');
160
- if (realAbs === browserSub || realAbs.startsWith(browserSub + path.sep)) {
161
- return;
162
- }
163
142
  // PromptSession persistence (active-index.json + per-session archives,
164
143
  // promptSessions.ts) and the scheduler's own read-modify-write of that
165
144
  // same active index (promptSessionEvents.cjs, PRD 814) — narrowly
@@ -318,6 +318,21 @@ async function deleteEntry(filePath) {
318
318
  }
319
319
  }
320
320
 
321
+ /**
322
+ * Binary-safe atomic write (formerly browser:save-binary). Routes through
323
+ * config.cjs's writeBinaryAtomic so the single-writer law still applies to
324
+ * ops-root destinations; `writer` is the renderer's declared owner id.
325
+ */
326
+ async function saveBinary(filePath, base64, writer) {
327
+ const { writeBinaryAtomic } = require('./config.cjs');
328
+ try {
329
+ await writeBinaryAtomic(filePath, Buffer.from(base64, 'base64'), { writer });
330
+ return { ok: true };
331
+ } catch (e) {
332
+ return { ok: false, error: e && e.message ? e.message : String(e) };
333
+ }
334
+ }
335
+
321
336
  async function openExternal(filePath) {
322
337
  let resolved;
323
338
  try { resolved = validateHomePath(filePath); }
@@ -385,6 +400,10 @@ function registerFilesHandlers() {
385
400
  const { path: p } = filesPath.parse(payload);
386
401
  return deleteEntry(p);
387
402
  });
403
+ ipcMain.handle('files:save-binary', (_e, payload) => {
404
+ const { path: p, base64, writer } = schemas.filesSaveBinary.parse(payload);
405
+ return saveBinary(p, base64, writer);
406
+ });
388
407
  // files:open-external / files:show-in-finder consolidated into shell:open
389
408
  // (index.cjs, as: 'openPath' / 'revealPath'). The openExternal/showInFinder
390
409
  // functions are exported for that handler to call.
@@ -406,4 +425,5 @@ module.exports = {
406
425
  deleteEntry,
407
426
  duplicateEntry,
408
427
  duplicateNameFor,
428
+ saveBinary,
409
429
  };
@@ -1,4 +1,4 @@
1
- const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, nativeImage, powerSaveBlocker, protocol } = require('electron');
1
+ const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, 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');
@@ -9,8 +9,6 @@ const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
9
9
  const { terminateLosingInstance } = require('./lib/singleInstanceGuard.cjs');
10
10
  const { acquireSchedulerOwnership, releaseSchedulerOwnership } = require('./lib/instanceLock.cjs');
11
11
  const { manager: ptyManager, registerPtyHandlers } = require('./pty.cjs');
12
- const browserView = require('./browserView.cjs');
13
- const browserCapture = require('./browserCapture.cjs');
14
12
  const configMgr = require('./config.cjs');
15
13
  const transcripts = require('./transcripts.cjs');
16
14
  const sessionsStore = require('./sessionsStore.cjs');
@@ -40,13 +38,6 @@ const adminHttp = createAdminHttp();
40
38
  scheduler.registerAdminRoutes(adminHttp);
41
39
  prdCreate.registerAdminRoute(adminHttp, scheduler.remote);
42
40
  chatRunner.registerAdminRoute(adminHttp);
43
- const { createBrowserAgentServer } = require('./browserAgentServer.cjs');
44
- const browserAgentServer = createBrowserAgentServer({
45
- listTabs: () => browserView.listViews(),
46
- screenshot: ({ viewId }) => browserView.captureShotForAgent({ viewId }),
47
- action: (params) => browserView.dispatchViewAction(params),
48
- dom: ({ viewId }) => browserView.captureAgentDom({ viewId }),
49
- });
50
41
  const supervisor = require('./supervisor.cjs');
51
42
  const watchers = require('./watchers.cjs');
52
43
  const teams = require('./teams.cjs');
@@ -69,7 +60,6 @@ const agentMemory = require('./agentMemory.cjs');
69
60
  const git = require('./git.cjs');
70
61
  const filesIpc = require('./files.cjs');
71
62
  const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
72
- const webRemote = require('./webRemote.cjs');
73
63
  const { listExchanges } = require('./exchanges.cjs');
74
64
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
75
65
  const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
@@ -306,8 +296,6 @@ async function rebootApp() {
306
296
  ptyManager.attachWindow(mainWindow);
307
297
  configMgr.attachWindow(mainWindow);
308
298
  transcripts.attachWindow(mainWindow);
309
- browserView.attachWindow(mainWindow);
310
- browserCapture.attachWindow(mainWindow);
311
299
  voiceHotkey.init(mainWindow).catch((e) => {
312
300
  logs.writeLine({ scope: 'voice-hotkey', level: 'error', message: 'reinit failed', meta: { error: e?.message } });
313
301
  });
@@ -506,25 +494,6 @@ ipcMain.handle('app:pick-directory', async () => {
506
494
 
507
495
  ipcMain.on('app:reboot-app', () => rebootApp());
508
496
 
509
- // Recorder export "Save to file…" (PRD 412) — native Save As dialog, written
510
- // directly since the path is user-chosen, not renderer input.
511
- ipcMain.handle('browser:save-recording', validated(schemas.browserSaveRecording, async ({ defaultName, text }) => {
512
- try {
513
- const result = await dialog.showSaveDialog(mainWindow, {
514
- defaultPath: defaultName,
515
- filters: [
516
- { name: 'Markdown', extensions: ['md'] },
517
- { name: 'All Files', extensions: ['*'] },
518
- ],
519
- });
520
- if (result.canceled || !result.filePath) return { ok: false, canceled: true };
521
- await fsp.writeFile(result.filePath, text, 'utf8');
522
- return { ok: true, path: result.filePath };
523
- } catch (e) {
524
- return { ok: false, error: e && e.message ? e.message : String(e) };
525
- }
526
- }));
527
-
528
497
  // Image paste — Ctrl+V in the Terminal pane. Reads the OS clipboard via
529
498
  // Electron's native API (renderer's navigator.clipboard.read() doesn't expose
530
499
  // raw image MIME types under contextIsolation), saves the bitmap to a temp
@@ -570,19 +539,6 @@ ipcMain.handle('clipboard:write-text', validated(schemas.clipboardWriteText, ({
570
539
  }
571
540
  }));
572
541
 
573
- // PRD 407 Capture panel — write side of paste-image's read. Writes a
574
- // screenshot capture to the OS clipboard as an image.
575
- ipcMain.handle('browser:copy-image', validated(schemas.browserCopyImage, ({ dataUrl }) => {
576
- try {
577
- const img = nativeImage.createFromDataURL(dataUrl);
578
- if (!img || img.isEmpty()) return { ok: false, error: 'empty image' };
579
- clipboard.writeImage(img);
580
- return { ok: true };
581
- } catch (e) {
582
- return { ok: false, error: e && e.message ? e.message : String(e) };
583
- }
584
- }));
585
-
586
542
  // Hooks tab "Test fire": run a hook command with a fake event payload piped
587
543
  // to stdin. shell:true is intentional — Claude Code's hook field is a shell
588
544
  // string. Timeout is enforced via SIGKILL on a timer because spawn's built-in
@@ -841,7 +797,6 @@ agentMemory.registerAgentMemoryHandlers();
841
797
  git.register(ipcMain);
842
798
  filesIpc.registerFilesHandlers();
843
799
  registerDocEditHandlers();
844
- webRemote.registerRemoteHandlers();
845
800
  chatRunner.registerChatHandlers();
846
801
 
847
802
  // Direct in-process PRD authoring for a queued PromptTicket classified
@@ -943,11 +898,6 @@ app.on('web-contents-created', (_e, wc) => {
943
898
  });
944
899
 
945
900
  wc.on('will-navigate', (event, url) => {
946
- // The embedded Browser tab's WebContentsView is a real browser — it must
947
- // be able to navigate anywhere. Exempt ONLY webContents registered by
948
- // browserView.cjs; the main window's lock below is unchanged.
949
- if (browserView.isBrowserViewContents(wc.id)) return;
950
-
951
901
  const allowed = useDevServer
952
902
  ? ['http://localhost:5173', 'http://127.0.0.1:5173']
953
903
  : [];
@@ -1144,16 +1094,12 @@ app.whenReady().then(async () => {
1144
1094
  ptyManager.attachWindow(mainWindow);
1145
1095
  configMgr.attachWindow(mainWindow);
1146
1096
  transcripts.attachWindow(mainWindow);
1147
- browserView.registerBrowserView({ mainWindow, ipcMain });
1148
- browserCapture.attachWindow(mainWindow);
1149
- browserCapture.registerBrowserCapture({ ipcMain, getView: browserView.getView });
1150
1097
  voiceHotkey.init(mainWindow).catch((e) => {
1151
1098
  logs.writeLine({ scope: 'voice-hotkey', level: 'error', message: 'init failed', meta: { error: e?.message } });
1152
1099
  });
1153
1100
  scheduler.attachWindow(mainWindow);
1154
1101
  watchers.attachWindow(mainWindow);
1155
1102
  pluginInstall.attachWindow(mainWindow);
1156
- webRemote.attachWindow(mainWindow);
1157
1103
  chatRunner.attachWindow(mainWindow);
1158
1104
  promptSessionEvents.attachWindow(mainWindow);
1159
1105
  attachDocEditWindow(mainWindow);
@@ -1177,18 +1123,12 @@ app.whenReady().then(async () => {
1177
1123
  console.log(`[main] scheduler ownership held by pid ${ownership.holderPid} — running scheduler-passive (no reconciliation, no admin server)`);
1178
1124
  logs.writeLine({ scope: 'scheduler', level: 'info', message: 'scheduler-passive: ownership held by another instance', meta: { holderPid: ownership.holderPid } });
1179
1125
  }
1180
- browserAgentServer.start().catch((e) => {
1181
- logs.writeLine({ scope: 'browser-agent-server', level: 'error', message: 'init failed', meta: { error: e?.message } });
1182
- });
1183
1126
  // First-boot default: install the bundled session-manager-dev plugin (its 10
1184
1127
  // dev skills) from the app's own marketplace. One-shot + idempotent; never
1185
1128
  // throws. SM_SEED_DEV_PLUGIN_DISABLE=1 to opt out.
1186
1129
  seedDevPlugin({ logger: console }).catch((e) => {
1187
1130
  logs.writeLine({ scope: 'seed-dev-plugin', level: 'error', message: 'seed failed', meta: { error: e?.message } });
1188
1131
  });
1189
- webRemote.init().catch((e) => {
1190
- logs.writeLine({ scope: 'webRemote', level: 'error', message: 'init failed', meta: { error: e?.message } });
1191
- });
1192
1132
  // History rollup finalize pass: deferred 30s past boot so it never competes
1193
1133
  // with first-paint, fire-and-forget (cron/offline refresh is PRD 651 — this
1194
1134
  // is just the one-shot in-app pass).
@@ -1249,7 +1189,6 @@ let teardownDone = false;
1249
1189
  function runShutdownCleanup() {
1250
1190
  if (teardownDone) return; // idempotent — will-quit may still fire after an app.exit path
1251
1191
  teardownDone = true;
1252
- webRemote.destroy();
1253
1192
  // Mark a clean exit so the next boot can distinguish a graceful quit from an
1254
1193
  // OOM-kill / native crash (which leaves the sentinel `open`).
1255
1194
  crashDiagnostics.markCleanShutdown();
@@ -1287,7 +1226,6 @@ app.on('before-quit', () => {
1287
1226
  transcripts.closeAll();
1288
1227
  watchers.manager.killAll();
1289
1228
  adminHttp.stop().catch(() => {});
1290
- browserAgentServer.stop().catch(() => {});
1291
1229
  // Best-effort flush of any pending OTEL spans. shutdown() has its own 2s
1292
1230
  // ceiling so a wedged exporter can't hold quit.
1293
1231
  otel.shutdown().catch(() => {});