claude-code-session-manager 0.35.8 → 0.35.10
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-DI6Dl7gv.js → TiptapBody-CxSS_K2x.js} +1 -1
- package/dist/assets/{index-CCqF0xvC.js → index-B1hI9l9p.js} +175 -177
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/main/__tests__/runVerify.test.cjs +19 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +74 -1
- package/src/main/index.cjs +29 -0
- package/src/main/ipcSchemas.cjs +15 -0
- package/src/main/runVerify.cjs +14 -1
- package/src/main/scheduler.cjs +49 -8
- package/src/preload/api.d.ts +9 -0
- package/src/preload/index.cjs +2 -0
package/dist/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
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-B1hI9l9p.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
13
|
<link rel="stylesheet" crossorigin href="./assets/index-C7NyYuXu.css">
|
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.10",
|
|
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",
|
|
@@ -903,6 +903,25 @@ test('pass_no_commit: SCHEDULER_VERDICT: PASS but no commit landed → needs_rev
|
|
|
903
903
|
}
|
|
904
904
|
});
|
|
905
905
|
|
|
906
|
+
test('pass_no_commit: fix-plan job (slug ^\\d+-fix-) with PASS but no commit is exempt → clean', async () => {
|
|
907
|
+
const tmp = makeTmpDir();
|
|
908
|
+
try {
|
|
909
|
+
const slug = '523-fix-bounded-fix-plan-retry';
|
|
910
|
+
writeLog(tmp, slug, noOpRunEvents('Verification complete — no code gap exists.\nSCHEDULER_VERDICT: PASS'));
|
|
911
|
+
const prdPath = writePrd(tmp, slug, '# Fix: bounded fix-plan retry');
|
|
912
|
+
const verdict = await verifyRun({
|
|
913
|
+
runDir: tmp,
|
|
914
|
+
prdPath,
|
|
915
|
+
queueEntry: { slug, status: 'running' },
|
|
916
|
+
allJobs: [],
|
|
917
|
+
committedDuringRun: false,
|
|
918
|
+
});
|
|
919
|
+
assert.equal(verdict.verdict, 'clean', `expected clean (fix-plan exemption), got ${verdict.verdict}: ${verdict.reason}`);
|
|
920
|
+
} finally {
|
|
921
|
+
rmdir(tmp);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
|
|
906
925
|
// (e) halt + sentinel PASS → still halt (override must not apply to halt)
|
|
907
926
|
test('halt result + sentinel PASS + committedDuringRun:true → still halt', async () => {
|
|
908
927
|
const tmp = makeTmpDir();
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
const { test } = require('node:test');
|
|
10
10
|
const assert = require('node:assert/strict');
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
selectAutoFixTargets, isUnresolvableNeedsReview, isRescanCandidate,
|
|
13
|
+
healTargetForFix, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH,
|
|
14
|
+
} = require('../scheduler.cjs');
|
|
12
15
|
|
|
13
16
|
const noSiblingOnDisk = () => false;
|
|
14
17
|
const noRunDir = () => null;
|
|
@@ -140,4 +143,74 @@ test('defaults parallelGroup to 99 when absent', () => {
|
|
|
140
143
|
assert.strictEqual(seen[0], '99-fix-my-feature');
|
|
141
144
|
});
|
|
142
145
|
|
|
146
|
+
test('isRescanCandidate: needs_review + runId + no_verdict_sentinel → true (RESCANNABLE_VERDICTS includes it)', () => {
|
|
147
|
+
const job = makeJob({ verifierVerdict: 'no_verdict_sentinel' });
|
|
148
|
+
assert.strictEqual(isRescanCandidate(job), true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('isRescanCandidate: needs_review + runId + uncommitted_changes → false (git commit-guard verdict stays excluded)', () => {
|
|
152
|
+
const job = makeJob({ verifierVerdict: 'uncommitted_changes' });
|
|
153
|
+
assert.strictEqual(isRescanCandidate(job), false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('isRescanCandidate: needs_review + runId + pass_no_commit → true (fix-plan exemption can flip the verdict on rescan)', () => {
|
|
157
|
+
const job = makeJob({ verifierVerdict: 'pass_no_commit' });
|
|
158
|
+
assert.strictEqual(isRescanCandidate(job), true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---- investigationDepth bound (fix-plan-of-a-fix-plan recursion cap) ----
|
|
162
|
+
|
|
163
|
+
test('depth-1 (ordinary, no investigationDepth field) needs_review job is eligible', () => {
|
|
164
|
+
const jobs = [makeJob({ slug: '05-my-feature' })];
|
|
165
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
166
|
+
assert.strictEqual(result.length, 1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('fix-plan job with investigationDepth: 2 is now ALSO eligible', () => {
|
|
170
|
+
const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 2 })];
|
|
171
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
172
|
+
assert.strictEqual(result.length, 1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('fix-plan job with investigationDepth: 3 is NOT eligible (bound holds)', () => {
|
|
176
|
+
const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 3 })];
|
|
177
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
178
|
+
assert.strictEqual(result.length, 0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('legacy fix-plan job with no investigationDepth field defaults to excluded (back-compat)', () => {
|
|
182
|
+
const jobs = [makeJob({ slug: '05-fix-foo' })];
|
|
183
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
184
|
+
assert.strictEqual(result.length, 0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('isFixPlanBeyondDepthCap: non-fix-plan slug is never capped regardless of depth', () => {
|
|
188
|
+
assert.strictEqual(isFixPlanBeyondDepthCap('05-my-feature', 99), false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('isFixPlanBeyondDepthCap: fix-plan at depth 1 or 2 is not capped, depth 3 is', () => {
|
|
192
|
+
assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', 1), false);
|
|
193
|
+
assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', MAX_INVESTIGATION_DEPTH), false);
|
|
194
|
+
assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', MAX_INVESTIGATION_DEPTH + 1), true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('healTargetForFix: depth-2 fix-of-a-fix slug resolves back to the depth-1 fix job (521-fix-recorder-export-footer-redesign)', () => {
|
|
198
|
+
// Depth-1 fix job: originally authored to fix job 521, itself in needs_review.
|
|
199
|
+
const depth1FixJob = {
|
|
200
|
+
slug: '521-fix-recorder-export-footer-redesign',
|
|
201
|
+
status: 'needs_review',
|
|
202
|
+
parallelGroup: 521,
|
|
203
|
+
investigationDepth: 2,
|
|
204
|
+
};
|
|
205
|
+
const jobs = [depth1FixJob];
|
|
206
|
+
// spawnInvestigation's fix-slug formula: baseSlug strips only the leading
|
|
207
|
+
// numeric prefix (not "fix-"), so investigating this depth-1 fix job
|
|
208
|
+
// produces a depth-2 fix slug of the form "<group>-fix-fix-<rest>".
|
|
209
|
+
const baseSlug = depth1FixJob.slug.replace(/^\d+-/, '');
|
|
210
|
+
const depth2FixSlug = `${String(depth1FixJob.parallelGroup).padStart(2, '0')}-fix-${baseSlug}`;
|
|
211
|
+
assert.strictEqual(depth2FixSlug, '521-fix-fix-recorder-export-footer-redesign');
|
|
212
|
+
const resolved = healTargetForFix(depth2FixSlug, jobs);
|
|
213
|
+
assert.strictEqual(resolved, depth1FixJob);
|
|
214
|
+
});
|
|
215
|
+
|
|
143
216
|
console.log('scheduler-autofix-select tests: PASS');
|
package/src/main/index.cjs
CHANGED
|
@@ -434,6 +434,25 @@ ipcMain.handle('app:pick-directory', async () => {
|
|
|
434
434
|
|
|
435
435
|
ipcMain.on('app:reboot-app', () => rebootApp());
|
|
436
436
|
|
|
437
|
+
// Recorder export "Save to file…" (PRD 412) — native Save As dialog, written
|
|
438
|
+
// directly since the path is user-chosen, not renderer input.
|
|
439
|
+
ipcMain.handle('browser:save-recording', validated(schemas.browserSaveRecording, async ({ defaultName, text }) => {
|
|
440
|
+
try {
|
|
441
|
+
const result = await dialog.showSaveDialog(mainWindow, {
|
|
442
|
+
defaultPath: defaultName,
|
|
443
|
+
filters: [
|
|
444
|
+
{ name: 'Markdown', extensions: ['md'] },
|
|
445
|
+
{ name: 'All Files', extensions: ['*'] },
|
|
446
|
+
],
|
|
447
|
+
});
|
|
448
|
+
if (result.canceled || !result.filePath) return { ok: false, canceled: true };
|
|
449
|
+
await fsp.writeFile(result.filePath, text, 'utf8');
|
|
450
|
+
return { ok: true, path: result.filePath };
|
|
451
|
+
} catch (e) {
|
|
452
|
+
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
453
|
+
}
|
|
454
|
+
}));
|
|
455
|
+
|
|
437
456
|
// Image paste — Ctrl+V in the Terminal pane. Reads the OS clipboard via
|
|
438
457
|
// Electron's native API (renderer's navigator.clipboard.read() doesn't expose
|
|
439
458
|
// raw image MIME types under contextIsolation), saves the bitmap to a temp
|
|
@@ -469,6 +488,16 @@ ipcMain.handle('clipboard:paste-text', async () => {
|
|
|
469
488
|
}
|
|
470
489
|
});
|
|
471
490
|
|
|
491
|
+
// Recorder export "Copy to clipboard" (PRD 412) — write side.
|
|
492
|
+
ipcMain.handle('clipboard:write-text', validated(schemas.clipboardWriteText, ({ text }) => {
|
|
493
|
+
try {
|
|
494
|
+
clipboard.writeText(text);
|
|
495
|
+
return { ok: true };
|
|
496
|
+
} catch (e) {
|
|
497
|
+
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
498
|
+
}
|
|
499
|
+
}));
|
|
500
|
+
|
|
472
501
|
// PRD 407 Capture panel — write side of paste-image's read. Writes a
|
|
473
502
|
// screenshot capture to the OS clipboard as an image.
|
|
474
503
|
ipcMain.handle('browser:copy-image', validated(schemas.browserCopyImage, ({ dataUrl }) => {
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -96,6 +96,19 @@ const browserCopyImage = z.object({
|
|
|
96
96
|
dataUrl: z.string().min(1).max(50_000_000),
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
// Recorder export (PRD 412): write arbitrary recorded-flow text to the OS
|
|
100
|
+
// clipboard, separate from the image-only browserCopyImage above.
|
|
101
|
+
const clipboardWriteText = z.object({
|
|
102
|
+
text: z.string().max(1_000_000),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Recorder export (PRD 412): native "Save As" dialog write, bypassing the
|
|
106
|
+
// config.cjs write-boundary since the path is user-chosen via OS dialog.
|
|
107
|
+
const browserSaveRecording = z.object({
|
|
108
|
+
defaultName: z.string().min(1).max(255),
|
|
109
|
+
text: z.string().max(1_000_000),
|
|
110
|
+
});
|
|
111
|
+
|
|
99
112
|
// PRD 407: binary-safe atomic write (browser:save-binary) for screenshot
|
|
100
113
|
// captures — config:write-text is utf8-only.
|
|
101
114
|
const browserSaveBinary = z.object({
|
|
@@ -621,6 +634,8 @@ module.exports = {
|
|
|
621
634
|
browserCaptureSelection,
|
|
622
635
|
browserCopyImage,
|
|
623
636
|
browserSaveBinary,
|
|
637
|
+
clipboardWriteText,
|
|
638
|
+
browserSaveRecording,
|
|
624
639
|
browserReplay,
|
|
625
640
|
browserSetZoom,
|
|
626
641
|
browserFind,
|
package/src/main/runVerify.cjs
CHANGED
|
@@ -636,7 +636,20 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
636
636
|
// silently accepting a bare sentinel as proof of work done. Mutually
|
|
637
637
|
// exclusive with the no_verdict_sentinel case above (sentinel === null
|
|
638
638
|
// vs sentinel === 'pass'), kept as a separate sibling check for clarity.
|
|
639
|
-
|
|
639
|
+
//
|
|
640
|
+
// EXEMPTION: fix-plan jobs (slug matches ^\d+-fix-) are investigations —
|
|
641
|
+
// "I checked, the original work already landed correctly, nothing to
|
|
642
|
+
// change" is a legitimate, correct, common outcome for them, not a
|
|
643
|
+
// silent no-op bug. Applying this check to fix-plan jobs the same way as
|
|
644
|
+
// original feature/bugfix PRDs produced a false-positive cascade
|
|
645
|
+
// (2026-07-12: 523-fix-bounded-fix-plan-retry re-verified PRD 523, found
|
|
646
|
+
// it already fully implemented and committed, correctly declined to make
|
|
647
|
+
// a no-op commit, and printed a truthful PASS — but still got flagged
|
|
648
|
+
// needs_review). Original PRDs are expected to build something new, so a
|
|
649
|
+
// bare PASS with no commit from one is still a strong silent-failure
|
|
650
|
+
// signal and stays covered by this check.
|
|
651
|
+
const isFixPlanJob = /^\d+-fix-/.test(queueEntry?.slug || '');
|
|
652
|
+
if (sentinel === 'pass' && !committedDuringRun && !isFixPlanJob) {
|
|
640
653
|
issues.push({
|
|
641
654
|
verdict: 'pass_no_commit',
|
|
642
655
|
reason: 'SCHEDULER_VERDICT: PASS but no commit landed during the run window — the run claims success but produced no code change',
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -616,7 +616,7 @@ async function reconcile(state) {
|
|
|
616
616
|
}
|
|
617
617
|
for (const [slug, p] of onDisk) {
|
|
618
618
|
if (seen.has(slug)) continue;
|
|
619
|
-
|
|
619
|
+
const entry = {
|
|
620
620
|
slug,
|
|
621
621
|
title: p.title,
|
|
622
622
|
cwd: p.cwd,
|
|
@@ -629,7 +629,16 @@ async function reconcile(state) {
|
|
|
629
629
|
finishedAt: null,
|
|
630
630
|
exitCode: null,
|
|
631
631
|
error: null,
|
|
632
|
-
}
|
|
632
|
+
};
|
|
633
|
+
// Newly-discovered fix-plan PRD: stamp its investigationDepth relative to
|
|
634
|
+
// the original job it heals, so selectAutoFixTargets/spawnInvestigation
|
|
635
|
+
// can bound the fix-of-a-fix recursion (see MAX_INVESTIGATION_DEPTH).
|
|
636
|
+
// Non-fix-plan jobs get no explicit field — they read as depth 1 via `?? 1`.
|
|
637
|
+
if (isFixPlanSlug(slug)) {
|
|
638
|
+
const parent = healTargetForFix(slug, state.jobs);
|
|
639
|
+
entry.investigationDepth = parent ? (parent.investigationDepth ?? 1) + 1 : 2;
|
|
640
|
+
}
|
|
641
|
+
next.push(entry);
|
|
633
642
|
}
|
|
634
643
|
state.jobs = next.sort((a, b) => b.slug.localeCompare(a.slug));
|
|
635
644
|
return state;
|
|
@@ -1244,8 +1253,8 @@ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediat
|
|
|
1244
1253
|
* Skipped if the failed job is itself a fix-plan (avoids infinite recursion).
|
|
1245
1254
|
*/
|
|
1246
1255
|
async function spawnInvestigation(failedJob, runDir) {
|
|
1247
|
-
if (
|
|
1248
|
-
console.log(`[scheduler] skip investigation: ${failedJob.slug} is
|
|
1256
|
+
if (isFixPlanBeyondDepthCap(failedJob.slug, failedJob.investigationDepth)) {
|
|
1257
|
+
console.log(`[scheduler] skip investigation: ${failedJob.slug} is a fix plan at/beyond depth cap (depth=${failedJob.investigationDepth ?? 'none'})`);
|
|
1249
1258
|
return { deferred: false };
|
|
1250
1259
|
}
|
|
1251
1260
|
if (investigationsInFlight >= MAX_CONCURRENT_INVESTIGATIONS) {
|
|
@@ -1924,8 +1933,40 @@ function selectHistoryJobs(jobs, limit) {
|
|
|
1924
1933
|
// Transcript-scan verdicts that re-running verifyRun can re-evaluate. NOT
|
|
1925
1934
|
// 'uncommitted_changes' — that comes from the git commit-guard, which verifyRun
|
|
1926
1935
|
// does not inspect, so re-scanning it would always return 'clean' and wrongly
|
|
1927
|
-
// heal a genuinely-unfinished job.
|
|
1928
|
-
|
|
1936
|
+
// heal a genuinely-unfinished job. 'no_verdict_sentinel' is included because
|
|
1937
|
+
// its raising condition (sentinel === null && !committedDuringRun) depends on
|
|
1938
|
+
// commit-detection, which committedInWindow() can fix retroactively (e.g. the
|
|
1939
|
+
// git-log --all scan added for missed non-HEAD commits) — rescanning lets a
|
|
1940
|
+
// job whose commit is now correctly detected clear on its own instead of
|
|
1941
|
+
// staying stuck in needs_review forever. 'pass_no_commit' is included because
|
|
1942
|
+
// verifyRun now exempts fix-plan jobs (slug ^\d+-fix-) from that check — a
|
|
1943
|
+
// fix-plan job flagged before that exemption shipped gets a genuinely
|
|
1944
|
+
// different verdict on rescan (2026-07-12: false-positive cascade where
|
|
1945
|
+
// investigation jobs correctly found "nothing to fix" but were flagged
|
|
1946
|
+
// anyway). For non-fix-plan jobs the exemption never applies, so rescanning
|
|
1947
|
+
// their pass_no_commit verdict is a harmless no-op (same facts, same verdict).
|
|
1948
|
+
const RESCANNABLE_VERDICTS = new Set(['transcript_errors', 'verify_unavailable', 'no_verdict_sentinel', 'pass_no_commit']);
|
|
1949
|
+
|
|
1950
|
+
// Bounds fix-plan recursion: depth 1 = the original job, depth 2 = its fix
|
|
1951
|
+
// (gets exactly one follow-up investigation if it also lands in
|
|
1952
|
+
// needs_review), depth 3+ (a fix-of-a-fix-of-a-fix) is excluded. Shared by
|
|
1953
|
+
// selectAutoFixTargets and spawnInvestigation so both call sites agree on
|
|
1954
|
+
// one threshold.
|
|
1955
|
+
const MAX_INVESTIGATION_DEPTH = 2;
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* True when a fix-plan job's investigationDepth is at or past the recursion
|
|
1959
|
+
* cap and it must be excluded from auto-fix eligibility. A fix-plan job with
|
|
1960
|
+
* no recorded investigationDepth (a job already in the queue before this
|
|
1961
|
+
* depth tracking shipped) is treated as excluded too, preserving the
|
|
1962
|
+
* pre-existing blanket-exclusion behavior for legacy jobs — no retroactive
|
|
1963
|
+
* migration. Non-fix-plan slugs are never capped here. Exported for tests.
|
|
1964
|
+
*/
|
|
1965
|
+
function isFixPlanBeyondDepthCap(slug, investigationDepth) {
|
|
1966
|
+
if (!isFixPlanSlug(slug)) return false;
|
|
1967
|
+
if (investigationDepth == null) return true;
|
|
1968
|
+
return investigationDepth >= MAX_INVESTIGATION_DEPTH + 1;
|
|
1969
|
+
}
|
|
1929
1970
|
|
|
1930
1971
|
/**
|
|
1931
1972
|
* Backfill a job's missing runId by scanning RUNS_DIR for a run directory
|
|
@@ -2014,7 +2055,7 @@ function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRu
|
|
|
2014
2055
|
if (job.status !== 'needs_review') return false;
|
|
2015
2056
|
const runId = job.runId || resolveJobRunId(job);
|
|
2016
2057
|
if (!runId) return false;
|
|
2017
|
-
if (
|
|
2058
|
+
if (isFixPlanBeyondDepthCap(job.slug, job.investigationDepth)) return false;
|
|
2018
2059
|
if (job.autoFixAttempted) {
|
|
2019
2060
|
if (job.autoFixOutcome !== 'no-plan') return false;
|
|
2020
2061
|
if ((job.autoFixRetries ?? 0) >= 1) return false;
|
|
@@ -2660,4 +2701,4 @@ const remote = {
|
|
|
2660
2701
|
},
|
|
2661
2702
|
};
|
|
2662
2703
|
|
|
2663
|
-
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, committedInWindow };
|
|
2704
|
+
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, committedInWindow, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -1066,6 +1066,13 @@ export interface SessionManagerAPI {
|
|
|
1066
1066
|
| { ok: false; error: string }
|
|
1067
1067
|
>;
|
|
1068
1068
|
saveBinary: (path: string, base64: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1069
|
+
/** Opens a native "Save As" dialog and writes `text` to the chosen path directly
|
|
1070
|
+
* (bypasses config.cjs's write-boundary check — the path is user-chosen via OS dialog). */
|
|
1071
|
+
saveRecording: (payload: { defaultName: string; text: string }) => Promise<
|
|
1072
|
+
| { ok: true; path: string }
|
|
1073
|
+
| { ok: false; canceled: true }
|
|
1074
|
+
| { ok: false; error: string }
|
|
1075
|
+
>;
|
|
1069
1076
|
replay: (payload: {
|
|
1070
1077
|
viewId: string;
|
|
1071
1078
|
steps: RecordStep[];
|
|
@@ -1271,6 +1278,8 @@ export interface SessionManagerAPI {
|
|
|
1271
1278
|
>;
|
|
1272
1279
|
/** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
|
|
1273
1280
|
copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1281
|
+
/** Write side — copies arbitrary text (e.g. a recorded flow export) to the OS clipboard. */
|
|
1282
|
+
writeText: (text: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1274
1283
|
};
|
|
1275
1284
|
memory: {
|
|
1276
1285
|
/** List markdown memory entries for the given workspace (defaults to 'default'). */
|
package/src/preload/index.cjs
CHANGED
|
@@ -82,6 +82,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
82
82
|
captureDom: (payload) => ipcRenderer.invoke('browser:capture-dom', payload),
|
|
83
83
|
captureShot: (viewId) => ipcRenderer.invoke('browser:capture-shot', { viewId }),
|
|
84
84
|
saveBinary: (path, base64) => ipcRenderer.invoke('browser:save-binary', { path, base64 }),
|
|
85
|
+
saveRecording: (payload) => ipcRenderer.invoke('browser:save-recording', payload),
|
|
85
86
|
replay: (payload) => ipcRenderer.invoke('browser:replay', payload),
|
|
86
87
|
onReplayStep: (viewId, handler) => {
|
|
87
88
|
const channel = `browser:replay-step:${viewId}`;
|
|
@@ -289,6 +290,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
289
290
|
pasteImage: () => ipcRenderer.invoke('clipboard:paste-image'),
|
|
290
291
|
pasteText: () => ipcRenderer.invoke('clipboard:paste-text'),
|
|
291
292
|
copyImage: (dataUrl) => ipcRenderer.invoke('browser:copy-image', { dataUrl }),
|
|
293
|
+
writeText: (text) => ipcRenderer.invoke('clipboard:write-text', { text }),
|
|
292
294
|
},
|
|
293
295
|
memory: {
|
|
294
296
|
list: (workspace) => ipcRenderer.invoke('memory:list', workspace ? { workspace } : {}),
|