claude-code-session-manager 0.61.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 (38) hide show
  1. package/dist/assets/{TiptapBody-CCmz3PdX.js → TiptapBody-BwN4pNC0.js} +42 -42
  2. package/dist/assets/{index-CPMP2XZ_.css → index-CVCCMw5o.css} +1 -1
  3. package/dist/assets/index-ZjTDk5b7.js +3197 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -2
  6. package/src/main/__tests__/chat-preamble-anchors.test.cjs +100 -0
  7. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +215 -0
  8. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +42 -1
  9. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +54 -1
  10. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +6 -0
  11. package/src/main/__tests__/transcripts-paged-reads.test.cjs +6 -0
  12. package/src/main/__tests__/transcriptsUsageFor.test.cjs +6 -0
  13. package/src/main/chatRunner.cjs +4 -0
  14. package/src/main/config.cjs +30 -23
  15. package/src/main/files.cjs +20 -0
  16. package/src/main/index.cjs +1 -63
  17. package/src/main/ipcSchemas.cjs +11 -209
  18. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +87 -0
  19. package/src/main/lib/activeIndexMerge.cjs +9 -0
  20. package/src/main/lib/opsOwnership.cjs +0 -2
  21. package/src/main/lib/summarize.cjs +0 -1
  22. package/src/main/runVerify.cjs +55 -6
  23. package/src/main/scheduler.cjs +46 -7
  24. package/src/preload/api.d.ts +2 -197
  25. package/src/preload/index.cjs +1 -100
  26. package/dist/assets/index-B5994QOJ.js +0 -3222
  27. package/src/main/__tests__/browserAgentServer.test.cjs +0 -235
  28. package/src/main/__tests__/browserView-destroyed-handler.test.cjs +0 -85
  29. package/src/main/__tests__/browserView-oauth-popup.test.cjs +0 -126
  30. package/src/main/__tests__/web-remote-e2e-pinning.test.cjs +0 -181
  31. package/src/main/browserAgentServer.cjs +0 -224
  32. package/src/main/browserCapture.cjs +0 -728
  33. package/src/main/browserView.cjs +0 -916
  34. package/src/main/lib/__tests__/browserAgentActions.test.cjs +0 -76
  35. package/src/main/lib/browserAgentActions.cjs +0 -114
  36. package/src/main/lib/e2eStateMachine.cjs +0 -39
  37. package/src/main/webRemote.cjs +0 -1367
  38. package/src/preload/browserViewPreload.cjs +0 -118
@@ -157,6 +157,7 @@ function parseLog(logPath) {
157
157
  const events = [];
158
158
  let resultEvent = null;
159
159
  let seq = 0;
160
+ let transcriptCommitLanded = false;
160
161
 
161
162
  for (const line of text.split('\n')) {
162
163
  const trimmed = line.trim();
@@ -166,6 +167,18 @@ function parseLog(logPath) {
166
167
  try { obj = JSON.parse(trimmed); } catch { continue; }
167
168
  if (!obj || typeof obj !== 'object') continue;
168
169
 
170
+ // Harness-emitted commit evidence: git itself produced these lines (a
171
+ // `vcs_state_changed` system event, or a tool_use_result carrying a
172
+ // gitOperation), so the model cannot fabricate them. Detected here as a
173
+ // plain boolean scan — never pushed into `events`, so the index-based
174
+ // final-20% math elsewhere is unaffected.
175
+ if (obj.type === 'system' && obj.subtype === 'vcs_state_changed' && obj.kind === 'commit') {
176
+ transcriptCommitLanded = true;
177
+ }
178
+ if (obj.tool_use_result?.gitOperation?.commit?.kind === 'committed') {
179
+ transcriptCommitLanded = true;
180
+ }
181
+
169
182
  // Final result event.
170
183
  if (obj.type === 'result') {
171
184
  const ev = {
@@ -215,7 +228,7 @@ function parseLog(logPath) {
215
228
  }
216
229
  }
217
230
 
218
- return { events, resultEvent, error: null };
231
+ return { events, resultEvent, error: null, transcriptCommitLanded };
219
232
  }
220
233
 
221
234
  // ─── self-recovery helpers ────────────────────────────────────────────────────
@@ -591,6 +604,27 @@ function isAncestorCommit({ cwd, sha, timeoutMs = GH_CHECK_TIMEOUT_MS, execImpl
591
604
  }
592
605
  }
593
606
 
607
+ /**
608
+ * True when `cwd` is inside a git repository. Used to gate the transcript
609
+ * commit-evidence fallback below: a job whose cwd is not a git repo can never
610
+ * produce committedDuringRun=true (gitHead()/committedInWindow() fail closed
611
+ * there), so the fallback must only apply in that narrow case — never when
612
+ * git ancestry is actually observable.
613
+ */
614
+ function isGitRepo(cwd) {
615
+ if (!cwd) return false;
616
+ try {
617
+ execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
618
+ timeout: 10_000,
619
+ encoding: 'utf8',
620
+ stdio: ['ignore', 'pipe', 'pipe'],
621
+ });
622
+ return true;
623
+ } catch {
624
+ return false;
625
+ }
626
+ }
627
+
594
628
  // ─── already-shipped postcondition exemption (original PRD re-runs) ─────────
595
629
 
596
630
  const PRD_DELIVERABLE_PATH_RE = /(?:^|[`\s(])((?:src|scripts|session-manager-operations|test|tests|docs|bin)\/[A-Za-z0-9_./-]*[A-Za-z0-9_-]\.[A-Za-z0-9]+)\b/g;
@@ -743,12 +777,21 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
743
777
  }
744
778
 
745
779
  // ── 2. Parse log ───────────────────────────────────────────────────────
746
- const { events, resultEvent, error: parseError } = parseLog(logPath);
780
+ const { events, resultEvent, error: parseError, transcriptCommitLanded } = parseLog(logPath);
747
781
 
748
782
  if (parseError) {
749
783
  return conclude('verify_unavailable', `log unreadable: ${parseError}`, 'needs_review');
750
784
  }
751
785
 
786
+ // A job whose cwd is not a git repo (e.g. an investigation fix-plan that
787
+ // inherited cwd: /tmp) can never produce committedDuringRun=true —
788
+ // gitHead()/committedInWindow() both fail closed there. In that case
789
+ // ONLY, fall back to the harness-emitted commit evidence in the
790
+ // transcript, which git itself produced and the model cannot fabricate.
791
+ const cwdIsGitRepo = isGitRepo(queueEntry?.cwd);
792
+ const commitEvidence = committedDuringRun || (!cwdIsGitRepo && transcriptCommitLanded);
793
+ const commitEvidenceSource = (!committedDuringRun && commitEvidence) ? 'transcript' : null;
794
+
752
795
  // ── 3. HALT detection ─────────────────────────────────────────────────
753
796
  // Primary: check the final `{"type":"result"}` event's `result` text.
754
797
  if (resultEvent) {
@@ -865,7 +908,7 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
865
908
  // committed anything nor ever emitted the finish-protocol sentinel likely
866
909
  // ended before doing real work (e.g. stopped on a clarifying question).
867
910
  // Weaker evidence than a caught transcript error, but still not clean.
868
- if (sentinel === null && !committedDuringRun) {
911
+ if (sentinel === null && !commitEvidence) {
869
912
  issues.push({
870
913
  verdict: 'no_verdict_sentinel',
871
914
  reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel — likely ended before the finish protocol (e.g. stopped on a clarifying question)',
@@ -894,7 +937,7 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
894
937
  // bare PASS with no commit from one is still a strong silent-failure
895
938
  // signal and stays covered by this check.
896
939
  const isFixPlanJob = /^\d+-fix-/.test(queueEntry?.slug || '');
897
- if (sentinel === 'pass' && !committedDuringRun && !isFixPlanJob) {
940
+ if (sentinel === 'pass' && !commitEvidence && !isFixPlanJob) {
898
941
  // EXEMPTION: `-merge-main` PRDs (see isMergeMainSlug) can genuinely and
899
942
  // correctly find nothing left to do when an out-of-band actor (a human,
900
943
  // another agent, a sibling scheduler job) already merged/updated the
@@ -1015,13 +1058,18 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
1015
1058
  // MUST NOT apply to halt or deps_unmet — those keep their existing semantics.
1016
1059
  if (
1017
1060
  sentinel === 'pass'
1018
- && committedDuringRun
1061
+ && commitEvidence
1019
1062
  && (top.verdict === 'transcript_errors' || top.verdict === 'verify_unavailable')
1020
1063
  ) {
1021
1064
  return conclude('clean',
1022
1065
  `SCHEDULER_VERDICT: PASS + commit landed overrides ${top.verdict}`,
1023
1066
  null,
1024
- { ...(annotations.length ? { annotations } : {}), sentinel, sentinelOverride: top.verdict },
1067
+ {
1068
+ ...(annotations.length ? { annotations } : {}),
1069
+ sentinel,
1070
+ sentinelOverride: top.verdict,
1071
+ ...(commitEvidenceSource ? { commitEvidenceSource } : {}),
1072
+ },
1025
1073
  );
1026
1074
  }
1027
1075
 
@@ -1073,4 +1121,5 @@ module.exports = {
1073
1121
  extractPrdDeliverablePaths,
1074
1122
  allDeliverablesAlreadyTracked,
1075
1123
  isAncestorCommit,
1124
+ isGitRepo,
1076
1125
  };
@@ -271,6 +271,28 @@ function gitHead(cwd) {
271
271
  });
272
272
  }
273
273
 
274
+ // True when cwd is inside a git repository. Used to keep a non-git cwd (e.g.
275
+ // a scratch dir like /tmp) from ever being handed to an investigation's
276
+ // fix-plan as its cwd — the commit guard, worktree isolation, and
277
+ // runVerify's PASS+commit sentinel override all read git state from job.cwd,
278
+ // so a fix plan rooted at a non-repo path gets parked in needs_review even
279
+ // when its commit really landed (Incident: 99-fix-e2e-needs-review-test,
280
+ // 2026-08-07 — green, committed 6ecbd40, PASS sentinel, still needs_review).
281
+ // Bounded and never throws.
282
+ function isGitRepoSync(cwd) {
283
+ if (!cwd) return false;
284
+ try {
285
+ execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
286
+ timeout: 10_000,
287
+ windowsHide: true,
288
+ stdio: ['ignore', 'pipe', 'pipe'],
289
+ });
290
+ return true;
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+
274
296
  // Best-effort `git fetch --all --prune` in cwd, bounded and never throwing.
275
297
  // A PRD that does its work in a separate `git worktree add` checkout (per
276
298
  // standards.md's own recommended pattern for shared repos) commits and pushes
@@ -2666,6 +2688,10 @@ ${logTail}
2666
2688
  estimateMinutes: <your time estimate>
2667
2689
  ---
2668
2690
  \`\`\`
2691
+ \`cwd\` must be the git repo root where the fix will actually land. If the failed job's cwd is
2692
+ not that repo (e.g. a scratch dir like \`/tmp\`), set \`cwd:\` to the correct repo root instead —
2693
+ the scheduler's commit guard and post-run verifier read git state from this path, and a
2694
+ non-repo cwd parks even a fully green, committed run in \`needs_review\`.
2669
2695
  6. The PRD body MUST be self-contained — \`claude -p\` runs it on a fresh Sonnet session with NO conversation context. Include:
2670
2696
  - Root-cause analysis (what went wrong and why)
2671
2697
  - Concrete fix steps (specific files / commands / edits)
@@ -2803,6 +2829,16 @@ async function spawnInvestigation(failedJob, runDir) {
2803
2829
  console.warn(`[scheduler] investigation cwd missing (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
2804
2830
  cwd = DEFAULT_PROJECT_CWD;
2805
2831
  }
2832
+ // A non-git cwd is unusable as a fix-plan cwd: the commit guard, worktree
2833
+ // isolation, and runVerify's PASS+commit sentinel override all read git
2834
+ // state from job.cwd, so a fix plan rooted at e.g. /tmp gets parked in
2835
+ // needs_review even when its commit really landed. (Incident:
2836
+ // 99-fix-e2e-needs-review-test, 2026-08-07 — green, committed 6ecbd40,
2837
+ // PASS sentinel, still needs_review.)
2838
+ if (!isGitRepoSync(cwd)) {
2839
+ console.warn(`[scheduler] investigation cwd is not a git repo (${cwd}); falling back to ${DEFAULT_PROJECT_CWD}`);
2840
+ cwd = DEFAULT_PROJECT_CWD;
2841
+ }
2806
2842
  const prompt = buildInvestigationPrompt({ failedJob, cwd, failedLogPath, originalBody, logTail, fixPath, group });
2807
2843
 
2808
2844
  // Phase 1: open log fd for pre-spawn diagnostics.
@@ -3024,11 +3060,12 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
3024
3060
  await setPaused('rate_limit', resetIso);
3025
3061
  }
3026
3062
 
3027
- // Stale queue entry: the PRD already shipped and was archived before this
3028
- // run fired (see archivedTwinExists in executeJob). Treat it as a plain
3029
- // completion — no verify pass, no commit guard, no RCA feedback — since
3030
- // there is no real transcript/commit to check.
3031
- if (res.skipped === 'prd-archived') {
3063
+ // Stale queue entry: the PRD was archived (already shipped) or is gone
3064
+ // from disk entirely (deleted before dispatch). Both are retire-not-fail
3065
+ // results from prdArchivedSkipResult with exitCode 0 and no transcript —
3066
+ // treat any truthy skip reason as a plain completion: no verify pass, no
3067
+ // commit guard, no RCA feedback item.
3068
+ if (res.skipped) {
3032
3069
  await mutate((s) => {
3033
3070
  const idx = s.jobs.findIndex((x) => x.slug === job.slug);
3034
3071
  if (idx >= 0) {
@@ -4765,7 +4802,9 @@ async function init() {
4765
4802
  }
4766
4803
  }
4767
4804
 
4768
- // remote — callable from webRemote.cjs without going through IPC.
4805
+ // remote — in-process (non-IPC) scheduler accessors, used by prdCreate.cjs
4806
+ // and other main-process callers. (Named for the retired web-remote relay,
4807
+ // its original consumer; kept because it still has in-process callers.)
4769
4808
  const remote = {
4770
4809
  // `cwd` is optional: prdCreate.cjs's create flow passes the target
4771
4810
  // project's cwd explicitly (the file may not exist yet, so there's
@@ -4956,4 +4995,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
4956
4995
  });
4957
4996
  }
4958
4997
 
4959
- module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveNotifyPrd, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult };
4998
+ module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, healRefusalReason, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, isGitRepoSync, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, notifyNeedsReview, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, resolveVerifyPrdPath, resolveNotifyPrd, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields, executeJob, prdArchivedSkipResult, spawnJob };
@@ -29,82 +29,6 @@ export interface WriteErrorEvent {
29
29
  reason: string;
30
30
  }
31
31
 
32
- export interface BrowserNavState {
33
- url: string;
34
- title: string;
35
- canGoBack: boolean;
36
- canGoForward: boolean;
37
- loading: boolean;
38
- isSecure: boolean;
39
- }
40
-
41
- /** One captured recorder step (PRD 408 engine → PRD 409 panel). */
42
- export interface RecordStep {
43
- n: number;
44
- /** `select` is accepted by replay/export (PRD 410) for forward-compat;
45
- * the live engine does not emit it yet. */
46
- verb: 'navigate' | 'click' | 'type' | 'select' | 'wait-for' | 'drag';
47
- target: string;
48
- kind?: 'nav' | 'assert';
49
- /** True for `type` steps — the actual typed value is never captured. */
50
- masked?: boolean;
51
- /** Engine-suggested `{{var}}` name for a `type` step (e.g. field name). */
52
- variableSuggestion?: string;
53
- /** Renderer-owned: set once the user checks "parameterize as {{var}}". */
54
- variable?: string | null;
55
- /** `select` steps only — the option value to choose on replay/export. */
56
- value?: string;
57
- /** Click position, or drag-start position for `drag` steps. */
58
- x?: number;
59
- /** Click position, or drag-start position for `drag` steps. */
60
- y?: number;
61
- /** `drag` steps only — drag-end target selector. */
62
- endTarget?: string;
63
- /** `drag` steps only — drag-end x position. */
64
- endX?: number;
65
- /** `drag` steps only — drag-end y position. */
66
- endY?: number;
67
- }
68
-
69
- /** Per-step replay outcome (PRD 410), streamed as `browser:replay-step:<viewId>`. */
70
- export interface ReplayStepResult {
71
- n: number;
72
- status: 'pass' | 'fail';
73
- detail?: string;
74
- }
75
-
76
- /** find-in-page result (PRD 402), streamed as `browser:find-result:<viewId>`. */
77
- export interface FindResult {
78
- requestId: number;
79
- matches: number;
80
- activeMatchOrdinal: number;
81
- }
82
-
83
- /** Element-picker event (PRD 403), streamed as `browser:picker-event:<viewId>`. */
84
- export interface PickerEvent {
85
- type: 'hover' | 'pick' | 'unpick' | 'exit';
86
- selector?: string;
87
- label?: string;
88
- tag?: string;
89
- rect?: { x: number; y: number; width: number; height: number };
90
- /** Present on a 'pick' event fired by an unmodified click — the selection was replaced, not accumulated. */
91
- replace?: boolean;
92
- }
93
-
94
- /** One entry in `~/.claude/session-manager/browser/history.json` (PRD 402). */
95
- export interface BrowserHistoryEntry {
96
- url: string;
97
- title: string;
98
- ts: number;
99
- }
100
-
101
- /** One entry in `~/.claude/session-manager/browser/bookmarks.json` (PRD 402). */
102
- export interface BrowserBookmark {
103
- url: string;
104
- title: string;
105
- ts: number;
106
- }
107
-
108
32
  export interface ReadJsonResult {
109
33
  exists: boolean;
110
34
  raw: string;
@@ -133,7 +57,7 @@ export interface ImportRef {
133
57
  /** Declared owner ids for the single-writer law over a project's
134
58
  * session-manager-operations/ root. Mirrors OWNERS in
135
59
  * src/main/lib/opsOwnership.cjs — one writer per namespace; everyone reads. */
136
- export type OpsWriter = 'epics' | 'scheduler' | 'project-home' | 'browser';
60
+ export type OpsWriter = 'epics' | 'scheduler' | 'project-home';
137
61
 
138
62
  export interface WriteResult {
139
63
  ok: boolean;
@@ -1162,46 +1086,6 @@ export interface PluginInstallProgressEvent {
1162
1086
  line: string;
1163
1087
  }
1164
1088
 
1165
- // ────────────────────────────────────────────── Web Remote (PRD 08)
1166
-
1167
- export interface WebRemoteDevice {
1168
- deviceId: string;
1169
- deviceName: string;
1170
- issuedAt: string;
1171
- lastConnectedAt: string | null;
1172
- }
1173
-
1174
- export interface WebRemoteStatus {
1175
- enabled: boolean;
1176
- remoteControlEnabled: boolean;
1177
- connected: boolean;
1178
- e2eActive: boolean;
1179
- /** True once the user has confirmed the SAS on the desktop. */
1180
- e2eAuthenticated: boolean;
1181
- /** Explicit E2E session state for UI rendering. */
1182
- e2eState: 'idle' | 'pending_sas' | 'authenticated' | 'failed';
1183
- /** 6-digit Short Authentication String pending user confirmation, or null. */
1184
- pendingSas: string | null;
1185
- devices: WebRemoteDevice[];
1186
- }
1187
-
1188
- export interface WebRemotePairResult {
1189
- ok: boolean;
1190
- deviceId?: string;
1191
- error?: string;
1192
- }
1193
-
1194
- export interface WebRemoteMutationResult {
1195
- ok: boolean;
1196
- error?: string;
1197
- }
1198
-
1199
- export interface WebRemoteAuditTailResult {
1200
- ok: boolean;
1201
- lines?: string[];
1202
- error?: string;
1203
- }
1204
-
1205
1089
  // ────────────────────────────────────────────── Chat runner (PRD 319)
1206
1090
 
1207
1091
  export interface ChatRunPayload {
@@ -1344,63 +1228,6 @@ export interface SessionManagerAPI {
1344
1228
  onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
1345
1229
  onWriteError: (handler: (ev: WriteErrorEvent) => void) => () => void;
1346
1230
  };
1347
- browser: {
1348
- create: (payload: { viewId: string; partition: string }) => Promise<{ ok: boolean }>;
1349
- setBounds: (payload: { viewId: string; x: number; y: number; width: number; height: number }) => Promise<{ ok: boolean }>;
1350
- show: (viewId: string) => Promise<{ ok: boolean }>;
1351
- hide: (viewId: string) => Promise<{ ok: boolean }>;
1352
- destroy: (viewId: string) => Promise<{ ok: boolean }>;
1353
- navigate: (payload: { viewId: string; url: string }) => Promise<{ ok: boolean; error?: string }>;
1354
- back: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1355
- forward: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1356
- reload: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1357
- stop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1358
- onNavState: (viewId: string, handler: (state: BrowserNavState) => void) => () => void;
1359
- recordStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1360
- recordStop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1361
- onRecordStep: (viewId: string, handler: (step: RecordStep) => void) => () => void;
1362
- captureDom: (payload: { viewId: string; kind: 'text' | 'html' }) => Promise<
1363
- | { ok: true; url: string; title: string; text: string; truncated?: boolean }
1364
- | { ok: false; error: string }
1365
- >;
1366
- captureShot: (viewId: string) => Promise<
1367
- | { ok: true; url: string; title: string; dataUrl: string }
1368
- | { ok: false; error: string }
1369
- >;
1370
- saveBinary: (path: string, base64: string, writer?: OpsWriter) => Promise<{ ok: boolean; error?: string }>;
1371
- /** Opens a native "Save As" dialog and writes `text` to the chosen path directly
1372
- * (bypasses config.cjs's write-boundary check — the path is user-chosen via OS dialog). */
1373
- saveRecording: (payload: { defaultName: string; text: string }) => Promise<
1374
- | { ok: true; path: string }
1375
- | { ok: false; canceled: true }
1376
- | { ok: false; error: string }
1377
- >;
1378
- replay: (payload: {
1379
- viewId: string;
1380
- steps: RecordStep[];
1381
- values?: Record<string, string>;
1382
- continueOnError?: boolean;
1383
- }) => Promise<{ ok: boolean; error?: string; stopped?: boolean; failedAt?: number }>;
1384
- onReplayStep: (viewId: string, handler: (step: ReplayStepResult) => void) => () => void;
1385
- setZoom: (payload: { viewId: string; factor: number }) => Promise<{ ok: boolean; error?: string; factor?: number }>;
1386
- find: (payload: { viewId: string; text: string; forward?: boolean }) => Promise<{ ok: boolean; error?: string }>;
1387
- stopFind: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1388
- onFindResult: (viewId: string, handler: (result: FindResult) => void) => () => void;
1389
- pickerStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
1390
- pickerStop: (viewId: string) => Promise<{ ok: boolean; error?: string; wasPicking?: boolean }>;
1391
- onPickerEvent: (viewId: string, handler: (ev: PickerEvent) => void) => () => void;
1392
- /** Selection-scoped capture pipeline (PRD 404): filter -> prune -> summarize -> chunk for 'agent', outerHTML for 'html', CDP AX-tree for 'a11y', fallback-chain for 'selector'. */
1393
- capture: (payload: {
1394
- viewId: string;
1395
- selectors: string[];
1396
- mode: 'agent' | 'html' | 'a11y' | 'selector';
1397
- }) => Promise<
1398
- | { ok: true; mode: string; text: string; meta: { chunks?: number; tokens?: number } }
1399
- | { ok: false; error: string }
1400
- >;
1401
- /** 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. */
1402
- onOpenTabRequest: (handler: (payload: { url: string }) => void) => () => void;
1403
- };
1404
1231
  transcripts: {
1405
1232
  subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
1406
1233
  /** Release the sub back to the LRU cache (view-switch). Does not destroy the watcher. */
@@ -1507,6 +1334,7 @@ export interface SessionManagerAPI {
1507
1334
  rename: (path: string, newName: string) => Promise<FilesRenameResult>;
1508
1335
  delete: (path: string) => Promise<FilesDeleteResult>;
1509
1336
  duplicate: (path: string) => Promise<FilesDuplicateResult>;
1337
+ saveBinary: (path: string, base64: string, writer?: OpsWriter) => Promise<{ ok: boolean; error?: string }>;
1510
1338
  };
1511
1339
  docEdit: {
1512
1340
  run: (payload: { path: string; before: string; instruction: string; documentText?: string }) => Promise<DocEditResult>;
@@ -1616,8 +1444,6 @@ export interface SessionManagerAPI {
1616
1444
  | { ok: true; text: string }
1617
1445
  | { ok: false; error?: string }
1618
1446
  >;
1619
- /** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
1620
- copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
1621
1447
  /** Write side — copies arbitrary text (e.g. a recorded flow export) to the OS clipboard. */
1622
1448
  writeText: (text: string) => Promise<{ ok: boolean; error?: string }>;
1623
1449
  };
@@ -1704,27 +1530,6 @@ export interface SessionManagerAPI {
1704
1530
  * renderer needs per-row badges without a separate git call per file. */
1705
1531
  fileStatus: (cwd: string) => Promise<GitFileStatusMap>;
1706
1532
  };
1707
- webRemote: {
1708
- getStatus: () => Promise<WebRemoteStatus>;
1709
- enable: () => Promise<WebRemoteMutationResult>;
1710
- disable: () => Promise<WebRemoteMutationResult>;
1711
- /** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
1712
- enableControl: () => Promise<WebRemoteMutationResult>;
1713
- /** Block MUTATE-tier commands — mobile becomes read-only mirror. */
1714
- disableControl: () => Promise<WebRemoteMutationResult>;
1715
- pair: (otp: string) => Promise<WebRemotePairResult>;
1716
- revokeDevice: (deviceId: string) => Promise<WebRemoteMutationResult>;
1717
- auditTail: (lines?: number) => Promise<WebRemoteAuditTailResult>;
1718
- onStatus: (handler: (status: WebRemoteStatus) => void) => () => void;
1719
- onTokenRevoked: (handler: (ev: { deviceId: string }) => void) => () => void;
1720
- /** Revoke ALL paired devices and tear down every session immediately (panic). */
1721
- revokeAll: () => Promise<WebRemoteMutationResult>;
1722
- /** Subscribe to the completion event broadcast after revokeAll. */
1723
- onRevokedAll: (handler: (ev: { revokedCount: number }) => void) => () => void;
1724
- /** Confirm that the SAS shown on the desktop matches the browser — marks the
1725
- * E2E session as authenticated and unblocks MUTATE-tier commands. */
1726
- confirmSas: () => Promise<WebRemoteMutationResult>;
1727
- };
1728
1533
  chat: {
1729
1534
  /** Spawn a headless `claude -p` run for a tab. Results arrive via the on* listeners. */
1730
1535
  run: (payload: ChatRunPayload) => Promise<{ ok: boolean }>;
@@ -56,66 +56,6 @@ contextBridge.exposeInMainWorld('api', {
56
56
  return () => ipcRenderer.removeListener('pty:write-error', listener);
57
57
  },
58
58
  },
59
- browser: {
60
- create: (payload) => ipcRenderer.invoke('browser:create', payload),
61
- setBounds: (payload) => ipcRenderer.invoke('browser:set-bounds', payload),
62
- show: (viewId) => ipcRenderer.invoke('browser:show', { viewId }),
63
- hide: (viewId) => ipcRenderer.invoke('browser:hide', { viewId }),
64
- destroy: (viewId) => ipcRenderer.invoke('browser:destroy', { viewId }),
65
- navigate: (payload) => ipcRenderer.invoke('browser:navigate', payload),
66
- back: (viewId) => ipcRenderer.invoke('browser:back', { viewId }),
67
- forward: (viewId) => ipcRenderer.invoke('browser:forward', { viewId }),
68
- reload: (viewId) => ipcRenderer.invoke('browser:reload', { viewId }),
69
- stop: (viewId) => ipcRenderer.invoke('browser:stop', { viewId }),
70
- onNavState: (viewId, handler) => {
71
- const channel = `browser:nav-state:${viewId}`;
72
- const listener = (_e, state) => handler(state);
73
- ipcRenderer.on(channel, listener);
74
- return () => ipcRenderer.removeListener(channel, listener);
75
- },
76
- recordStart: (viewId) => ipcRenderer.invoke('browser:record-start', { viewId }),
77
- recordStop: (viewId) => ipcRenderer.invoke('browser:record-stop', { viewId }),
78
- onRecordStep: (viewId, handler) => {
79
- const channel = `browser:record-step:${viewId}`;
80
- const listener = (_e, step) => handler(step);
81
- ipcRenderer.on(channel, listener);
82
- return () => ipcRenderer.removeListener(channel, listener);
83
- },
84
- captureDom: (payload) => ipcRenderer.invoke('browser:capture-dom', payload),
85
- captureShot: (viewId) => ipcRenderer.invoke('browser:capture-shot', { viewId }),
86
- saveBinary: (path, base64, writer) => ipcRenderer.invoke('browser:save-binary', { path, base64, writer }),
87
- saveRecording: (payload) => ipcRenderer.invoke('browser:save-recording', payload),
88
- replay: (payload) => ipcRenderer.invoke('browser:replay', payload),
89
- onReplayStep: (viewId, handler) => {
90
- const channel = `browser:replay-step:${viewId}`;
91
- const listener = (_e, step) => handler(step);
92
- ipcRenderer.on(channel, listener);
93
- return () => ipcRenderer.removeListener(channel, listener);
94
- },
95
- setZoom: (payload) => ipcRenderer.invoke('browser:set-zoom', payload),
96
- find: (payload) => ipcRenderer.invoke('browser:find', payload),
97
- stopFind: (viewId) => ipcRenderer.invoke('browser:stop-find', { viewId }),
98
- onFindResult: (viewId, handler) => {
99
- const channel = `browser:find-result:${viewId}`;
100
- const listener = (_e, result) => handler(result);
101
- ipcRenderer.on(channel, listener);
102
- return () => ipcRenderer.removeListener(channel, listener);
103
- },
104
- pickerStart: (viewId) => ipcRenderer.invoke('browser:picker-start', { viewId }),
105
- pickerStop: (viewId) => ipcRenderer.invoke('browser:picker-stop', { viewId }),
106
- onPickerEvent: (viewId, handler) => {
107
- const channel = `browser:picker-event:${viewId}`;
108
- const listener = (_e, ev) => handler(ev);
109
- ipcRenderer.on(channel, listener);
110
- return () => ipcRenderer.removeListener(channel, listener);
111
- },
112
- capture: (payload) => ipcRenderer.invoke('browser:capture', payload),
113
- onOpenTabRequest: (handler) => {
114
- const listener = (_e, payload) => handler(payload);
115
- ipcRenderer.on('browser:open-tab-request', listener);
116
- return () => ipcRenderer.removeListener('browser:open-tab-request', listener);
117
- },
118
- },
119
59
  transcripts: {
120
60
  subscribe: (payload) => ipcRenderer.invoke('transcript:subscribe', payload),
121
61
  unsubscribe: (tabId) => ipcRenderer.invoke('transcript:unsubscribe', { tabId }),
@@ -246,6 +186,7 @@ contextBridge.exposeInMainWorld('api', {
246
186
  rename: (path, newName) => ipcRenderer.invoke('files:rename', { path, newName }),
247
187
  delete: (path) => ipcRenderer.invoke('files:delete', { path }),
248
188
  duplicate: (path) => ipcRenderer.invoke('files:duplicate', { path }),
189
+ saveBinary: (path, base64, writer) => ipcRenderer.invoke('files:save-binary', { path, base64, writer }),
249
190
  },
250
191
  docEdit: {
251
192
  run: (payload) => ipcRenderer.invoke('docedit:run', payload),
@@ -309,7 +250,6 @@ contextBridge.exposeInMainWorld('api', {
309
250
  clipboard: {
310
251
  pasteImage: () => ipcRenderer.invoke('clipboard:paste-image'),
311
252
  pasteText: () => ipcRenderer.invoke('clipboard:paste-text'),
312
- copyImage: (dataUrl) => ipcRenderer.invoke('browser:copy-image', { dataUrl }),
313
253
  writeText: (text) => ipcRenderer.invoke('clipboard:write-text', { text }),
314
254
  },
315
255
  memory: {
@@ -372,45 +312,6 @@ contextBridge.exposeInMainWorld('api', {
372
312
  status: (cwd) => ipcRenderer.invoke('git:status', { cwd }),
373
313
  fileStatus: (cwd) => ipcRenderer.invoke('git:file-status', { cwd }),
374
314
  },
375
- webRemote: {
376
- /** Current connection state (no token values). */
377
- getStatus: () => ipcRenderer.invoke('webRemote:get-status'),
378
- /** Turn remote control on. Initiates relay connection if a device is paired. */
379
- enable: () => ipcRenderer.invoke('webRemote:enable'),
380
- /** Turn remote control off. Immediately drops relay connection. */
381
- disable: () => ipcRenderer.invoke('webRemote:disable'),
382
- /** Allow MUTATE-tier commands (pty spawn/write, scheduler writes). Default off. */
383
- enableControl: () => ipcRenderer.invoke('webRemote:enable-control'),
384
- /** Block MUTATE-tier commands — mobile becomes read-only mirror. */
385
- disableControl: () => ipcRenderer.invoke('webRemote:disable-control'),
386
- /** Pair a new device using the 8-character OTP shown in the web UI. */
387
- pair: (otp) => ipcRenderer.invoke('webRemote:pair', { otp }),
388
- /** Revoke a paired device by its deviceId. */
389
- revokeDevice: (deviceId) => ipcRenderer.invoke('webRemote:revoke-device', { deviceId }),
390
- /** Read the last N lines of today's audit log (default 50). */
391
- auditTail: (lines) => ipcRenderer.invoke('webRemote:audit-tail', lines ? { lines } : {}),
392
- /** Push event from main when connection status changes. */
393
- onStatus: (handler) => {
394
- const listener = (_e, payload) => handler(payload);
395
- ipcRenderer.on('webRemote:status', listener);
396
- return () => ipcRenderer.removeListener('webRemote:status', listener);
397
- },
398
- /** Push event when the relay revokes this device's token. */
399
- onTokenRevoked: (handler) => {
400
- const listener = (_e, payload) => handler(payload);
401
- ipcRenderer.on('webRemote:token-revoked', listener);
402
- return () => ipcRenderer.removeListener('webRemote:token-revoked', listener);
403
- },
404
- /** Revoke ALL paired devices and tear down every session immediately. */
405
- revokeAll: () => ipcRenderer.invoke('webRemote:revoke-all'),
406
- confirmSas: () => ipcRenderer.invoke('webRemote:confirm-sas'),
407
- /** Push event when revokeAll completes (main broadcasts webRemote:revoked-all). */
408
- onRevokedAll: (handler) => {
409
- const listener = (_e, payload) => handler(payload);
410
- ipcRenderer.on('webRemote:revoked-all', listener);
411
- return () => ipcRenderer.removeListener('webRemote:revoked-all', listener);
412
- },
413
- },
414
315
  chat: {
415
316
  /** Spawn a headless claude -p job. Results arrive via the on* listeners. */
416
317
  run: (payload) => ipcRenderer.invoke('chat:run', payload),