claude-code-session-manager 0.123.1 → 0.124.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.
- package/dist/assets/{AgentLibrary-T14aM8ie.js → AgentLibrary-qmAgadiS.js} +1 -1
- package/dist/assets/{Button-Dz1kV8Z_.js → Button-CkuaU6cA.js} +1 -1
- package/dist/assets/{DataModel-DdYRn2Ec.js → DataModel-DEVHxW5I.js} +1 -1
- package/dist/assets/{History-zQPvqwE7.js → History-C-qw4Gx4.js} +1 -1
- package/dist/assets/{Hooks-BTrbT742.js → Hooks-DzAnCyjJ.js} +1 -1
- package/dist/assets/{Library-BwQ6GRHR.js → Library-DExZ29wM.js} +1 -1
- package/dist/assets/{ListDetail-CunxAhRC.js → ListDetail-DiQk-Hf9.js} +1 -1
- package/dist/assets/{McpServers-DpoyxXaO.js → McpServers-C0nqX36_.js} +1 -1
- package/dist/assets/{Memory-KDtGYMc3.js → Memory-D4KAWPAC.js} +1 -1
- package/dist/assets/{Panel-xXmUBo8l.js → Panel-xv9ToSjl.js} +1 -1
- package/dist/assets/{Permissions-lIzTUrW2.js → Permissions-0yEyqwZU.js} +1 -1
- package/dist/assets/{Plugins-BA1Zqk69.js → Plugins-CB28SGGJ.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CmeJG2Kw.js → ProvenanceBadge-9i4gXDvI.js} +1 -1
- package/dist/assets/{SaveBar-BTJg_9Uc.js → SaveBar-mSVRXpml.js} +1 -1
- package/dist/assets/{Scheduler-CISTDNMU.js → Scheduler-BoUetIC0.js} +1 -1
- package/dist/assets/{ScopeSwitcher-C6NWvzoj.js → ScopeSwitcher-B8tdQIKs.js} +1 -1
- package/dist/assets/{Settings-BBoGJ34v.js → Settings-D3jfKwaK.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-IJv60QAJ.js → SkillReferenceGraph-DJ4JJRt3.js} +1 -1
- package/dist/assets/{Skills-L6nGcNF1.js → Skills-P9HHxDCR.js} +1 -1
- package/dist/assets/{SystemPrompt-Cm7I2gJz.js → SystemPrompt-B_tnbBZ2.js} +1 -1
- package/dist/assets/{TagLibrary-BxPwoPE6.js → TagLibrary-CI8NHRKi.js} +1 -1
- package/dist/assets/{TiptapBody-CR-Zr3Pz.js → TiptapBody-9LktXPiD.js} +1 -1
- package/dist/assets/{Toggle-BFhlpZ-n.js → Toggle-BwsUsAA7.js} +1 -1
- package/dist/assets/{ViewTabs-CuGlZ9Ed.js → ViewTabs-CY5gMZ_N.js} +1 -1
- package/dist/assets/{historyTotals-Dxv9Fzrs.js → historyTotals-_dIaxmPX.js} +1 -1
- package/dist/assets/{index-Dx8owDeQ.js → index-d4i3X1x3.js} +419 -419
- package/dist/assets/{settingsSchema-DaqFSAQH.js → settingsSchema-Dg0Xa9RR.js} +1 -1
- package/dist/assets/{skillFrontmatter-BIJddxVB.js → skillFrontmatter-CpoFkSyj.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-1hjZ4knk.js → useAgentsDirWatch-CFPprfWG.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-web/assets/app.css +32 -0
- package/dist-web/assets/app.js +1479 -0
- package/dist-web/index.html +16 -0
- package/package.json +6 -4
- package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +28 -10
- package/plugins/session-manager-dev/skills/develop/SKILL.md +5 -6
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +68 -9
- package/src/main/config.cjs +23 -29
- package/src/main/docEdit.cjs +2 -5
- package/src/main/files.cjs +16 -4
- package/src/main/index.cjs +26 -4
- package/src/main/ipcSchemas.cjs +6 -2
- package/src/main/lib/epic/activeIndexMerge.cjs +3 -5
- package/src/main/lib/epic/epicReopen.cjs +161 -0
- package/src/main/lib/epic/epicSpawnPlan.cjs +9 -1
- package/src/main/lib/epic/epicTranscriptRestore.cjs +109 -0
- package/src/main/lib/epic/epicValidationHook.cjs +1 -1
- package/src/main/lib/epic/epicWorktreeMint.cjs +50 -3
- package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +2 -1
- package/src/main/lib/feedback-remote/remoteAssets.cjs +98 -0
- package/src/main/lib/feedback-remote/remoteIpcDispatch.cjs +216 -0
- package/src/main/lib/insideHome.cjs +102 -23
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +27 -0
- package/src/main/lib/prd/prdWorktreeReach.cjs +9 -1
- package/src/main/lib/scheduler/rcaReport.cjs +1 -13
- package/src/main/lib/scheduler/reaperHelpers.cjs +82 -77
- package/src/main/lib/scheduler/reviewNotice.cjs +0 -2
- package/src/main/lib/scheduler/scheduleJobTransitions.cjs +1 -1
- package/src/main/lib/scheduler/schedulerBatch.cjs +1 -1
- package/src/main/lib/sendToRenderer.cjs +22 -2
- package/src/main/lib/smfileHeaders.cjs +17 -1
- package/src/main/pluginInstall.cjs +1 -1
- package/src/main/promptSessionEvents.cjs +2 -5
- package/src/main/pty.cjs +5 -4
- package/src/main/runVerify.cjs +6 -41
- package/src/main/scheduler/budget.cjs +4 -4
- package/src/main/scheduler/prdParser.cjs +2 -3
- package/src/main/scheduler/reviewLadder.cjs +0 -9
- package/src/main/scheduler.cjs +72 -25
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/templates/PRD_AUTHORING.md +2 -2
- package/src/main/voiceHotkey.cjs +3 -2
- package/src/main/webRemote.cjs +161 -1
- package/src/preload/api.d.ts +8 -1
- package/src/preload/apiSurface.cjs +19 -0
- package/src/preload/apiSurface.json +1689 -0
- package/src/preload/index.cjs +2 -0
- package/src/seed/agents/architect.md +2 -2
- package/src/seed/agents/blender-artist.md +76 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Claude Session Manager</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
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/app.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="./assets/app.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body class="bg-bg text-fg font-sans antialiased">
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.124.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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dist/index.html",
|
|
30
30
|
"dist/assets/",
|
|
31
31
|
"dist/vad/",
|
|
32
|
+
"dist-web/",
|
|
32
33
|
"README.md",
|
|
33
34
|
"!src/**/__tests__/**"
|
|
34
35
|
],
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
"dev": "concurrently -k -n vite,electron -c magenta,cyan \"npm:dev:renderer\" \"npm:dev:electron\"",
|
|
37
38
|
"dev:renderer": "vite",
|
|
38
39
|
"dev:electron": "wait-on http://localhost:5173 && SM_DEV=1 electron .",
|
|
39
|
-
"build": "vite build",
|
|
40
|
+
"build": "vite build && vite build --config vite.web.config.ts",
|
|
41
|
+
"build:web": "vite build --config vite.web.config.ts",
|
|
40
42
|
"dist:stage": "npm run build && node scripts/package/stage.cjs",
|
|
41
43
|
"dist:mac": "npm run dist:stage && electron-builder --config electron-builder.yml --mac --publish never",
|
|
42
44
|
"dist:win": "npm run dist:stage && electron-builder --config electron-builder.yml --win --publish never",
|
|
@@ -45,7 +47,7 @@
|
|
|
45
47
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.main.json",
|
|
46
48
|
"start": "electron .",
|
|
47
49
|
"prepack": "node scripts/write-build-info.cjs",
|
|
48
|
-
"prepublishOnly": "node scripts/write-build-info.cjs &&
|
|
50
|
+
"prepublishOnly": "node scripts/write-build-info.cjs && npm run build",
|
|
49
51
|
"test:unit": "vitest run",
|
|
50
52
|
"test:unit:stability": "bash scripts/test-unit-stability.sh",
|
|
51
53
|
"bench": "node scripts/bench/main-bench.cjs",
|
|
@@ -138,7 +140,7 @@
|
|
|
138
140
|
"electron-updater": "^6.8.9",
|
|
139
141
|
"node-pty": "^1.2.0-beta.12",
|
|
140
142
|
"ws": "^8.18.0",
|
|
141
|
-
"zod": "^
|
|
143
|
+
"zod": "^4.6.7"
|
|
142
144
|
},
|
|
143
145
|
"devDependencies": {
|
|
144
146
|
"@huggingface/transformers": "^4.1.0",
|
|
@@ -40,24 +40,37 @@ to ask for reconfirmation once `builder:gate` has passed.
|
|
|
40
40
|
|
|
41
41
|
5. **Create the isolated worktree from that tag:**
|
|
42
42
|
```
|
|
43
|
-
|
|
43
|
+
mkdir -p "$HOME/.cache/sm-publish"
|
|
44
|
+
git worktree add "$HOME/.cache/sm-publish/v<version>" v<version>
|
|
44
45
|
```
|
|
46
|
+
**Never under `/tmp`** (or anywhere under `os.tmpdir()`): the test suite treats such paths as
|
|
47
|
+
test fixtures, so `src/main/__tests__/opsErrorLog.test.cjs` ("a real project root is
|
|
48
|
+
refused") fails inside a `/tmp` worktree.
|
|
45
49
|
|
|
46
|
-
6. **Move into the worktree and
|
|
50
|
+
6. **Move into the worktree, build clean, and run the in-tree gate:**
|
|
47
51
|
```
|
|
48
|
-
cd /
|
|
52
|
+
cd "$HOME/.cache/sm-publish/v<version>"
|
|
49
53
|
npm ci
|
|
50
54
|
npm run typecheck
|
|
55
|
+
npm run test:unit
|
|
51
56
|
```
|
|
52
57
|
`npm ci` gives a fully clean `node_modules` from the lockfile — no leftover local state
|
|
53
|
-
from the dirty main repo can leak into the published artifact.
|
|
58
|
+
from the dirty main repo can leak into the published artifact. Typecheck and unit tests
|
|
59
|
+
must both pass here; stop before `npm publish` on any failure.
|
|
60
|
+
|
|
61
|
+
After the build step (run the project's build, e.g. `npm run build` plus any web build that
|
|
62
|
+
produces `dist-web/`), check that every `files` entry exists:
|
|
63
|
+
```
|
|
64
|
+
test -f dist-web/index.html && test -f dist/index.html
|
|
65
|
+
```
|
|
66
|
+
If either is missing, stop before `npm publish` and report.
|
|
54
67
|
|
|
55
68
|
7. **Publish from inside the worktree:**
|
|
56
69
|
```
|
|
57
70
|
npm publish
|
|
58
71
|
```
|
|
59
72
|
This project's `prepublishOnly` script runs `vite build` — because step 6 already `cd`'d
|
|
60
|
-
into
|
|
73
|
+
into `$HOME/.cache/sm-publish/v<version>`, that build runs **inside the clean worktree**, never
|
|
61
74
|
touching (or being affected by) the live/dirty working directory back in the main repo.
|
|
62
75
|
This is the entire reason the worktree exists: it decouples "what's uncommitted in my
|
|
63
76
|
editor right now" from "what gets built and shipped."
|
|
@@ -81,16 +94,21 @@ to ask for reconfirmation once `builder:gate` has passed.
|
|
|
81
94
|
|
|
82
95
|
9. **Clean up the worktree:**
|
|
83
96
|
```
|
|
84
|
-
git worktree remove /
|
|
97
|
+
git worktree remove "$HOME/.cache/sm-publish/v<version>" --force
|
|
85
98
|
```
|
|
86
99
|
|
|
87
|
-
10. **Verify the publish actually landed on the registry
|
|
100
|
+
10. **Verify the publish actually landed on the registry.** `npm view` is cached and lags for
|
|
101
|
+
minutes after a real publish, so check a cache-busted packument and the tarball first:
|
|
88
102
|
```
|
|
103
|
+
curl -s "https://registry.npmjs.org/<packageName>?t=$(date +%s)" # dist-tags.latest + versions
|
|
104
|
+
curl -sI <tarball URL from versions["<version>"].dist.tarball> # expect HTTP 200
|
|
89
105
|
npm view <packageName> version
|
|
90
106
|
npm view <packageName> dist-tags
|
|
91
107
|
```
|
|
92
|
-
Confirm
|
|
93
|
-
project intentionally publishes to a different dist-tag).
|
|
108
|
+
Confirm `dist-tags.latest` is `<version>`, `<version>` is in `versions`, and the tarball
|
|
109
|
+
returns 200 (unless the project intentionally publishes to a different dist-tag). If the
|
|
110
|
+
packument is current, an unchanged `npm view` for a few minutes is normal lag, not a
|
|
111
|
+
failure — do not republish.
|
|
94
112
|
|
|
95
113
|
11. **Post-publish: verify the installer release.** Pushing `v<version>` (step 8) triggers
|
|
96
114
|
`.github/workflows/release.yml`. Confirm the tag push succeeded, then poll (bounded: at most
|
|
@@ -109,7 +127,7 @@ to ask for reconfirmation once `builder:gate` has passed.
|
|
|
109
127
|
Stop at that step. Do not proceed. Report which step failed and its output. If the worktree
|
|
110
128
|
was already created (step 5+), leave it in place if its state is relevant evidence for
|
|
111
129
|
diagnosing the failure; otherwise remove it (step 9) before reporting, so a retry doesn't
|
|
112
|
-
collide with a stale
|
|
130
|
+
collide with a stale `$HOME/.cache/sm-publish/v<version>` path.
|
|
113
131
|
|
|
114
132
|
## Output
|
|
115
133
|
|
|
@@ -181,9 +181,9 @@ the API rejects that.
|
|
|
181
181
|
or a gitignored path. Why: a dirtied tree fails the clean-tree check.
|
|
182
182
|
12. Never put a step only a human or the passage of time can complete (host cron install,
|
|
183
183
|
waiting for a live post, a live payment, an account dashboard action) in a gate or
|
|
184
|
-
criterion
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
criterion; keep them out of PRDs at authoring time. An executor that meets one ends
|
|
185
|
+
`SCHEDULER_VERDICT: FAIL` naming what is missing. Why: no executor can pass such a gate,
|
|
186
|
+
and queued work runs with no questions asked.
|
|
187
187
|
Live-run steps that need gitignored state follow PRD_AUTHORING.md §7 (absolute main-checkout
|
|
188
188
|
paths, artifact-only).
|
|
189
189
|
|
|
@@ -271,9 +271,8 @@ Rules:
|
|
|
271
271
|
`scheduler_archive_prd`. Archiving marks it completed and frees the PRDs that depend on
|
|
272
272
|
it.
|
|
273
273
|
A parked validator arrives this way too.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
on a step only the human can answer.
|
|
274
|
+
Never relay a question to the human: resolve every PRD through cases 1-4, making the
|
|
275
|
+
safest reasonable choice, and report it. Why: queued work runs with no questions asked.
|
|
277
276
|
2. **A PLAN SETTLED digest.** Read the validator records it names. A `REFUTED` PRD, or a
|
|
278
277
|
Critical or Important finding, means a fix wave: one `behavior` or `wire` PRD per finding
|
|
279
278
|
plus a new validate PRD. Never fix inline. A Not validated list means validate exactly
|
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.124.1",
|
|
3
|
+
"gitSha": "604bae39a0da6417ec4f1ba2f92c81f4661c2dfd",
|
|
4
|
+
"gitShortSha": "604bae39",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-26T07:38:06.859Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* chatRunner.cjs — headless `claude -p --output-format stream-json` runner for
|
|
@@ -49,6 +50,7 @@ const { killTree, detachedSpawnOpts } = require('./lib/process/killTree.cjs');
|
|
|
49
50
|
const agentModelResolve = require('./lib/claude/agentModelResolve.cjs');
|
|
50
51
|
const { resolveEpicEffort, effortArgs } = require('./lib/claude/agentEffortResolve.cjs');
|
|
51
52
|
const { planEpicSpawn } = require('./lib/epic/epicSpawnPlan.cjs');
|
|
53
|
+
const epicWorktreeMint = require('./lib/epic/epicWorktreeMint.cjs');
|
|
52
54
|
const logs = require('./logs.cjs');
|
|
53
55
|
const { createHeadTailBuffer } = require('./lib/util/headTailBuffer.cjs');
|
|
54
56
|
|
|
@@ -257,6 +259,33 @@ const STOP_SIGNAL_INSTRUCTION =
|
|
|
257
259
|
`guess on what's genuinely blocked, but always answer what you can first. ` +
|
|
258
260
|
`Otherwise complete the task and end with a concise summary of what you did.\n\n`;
|
|
259
261
|
|
|
262
|
+
// Replaces STOP_SIGNAL_INSTRUCTION for turns the scheduler itself starts: no human is
|
|
263
|
+
// watching, so a question would stall the Epic forever.
|
|
264
|
+
const SCHEDULER_TURN_INSTRUCTION =
|
|
265
|
+
`IMPORTANT: This turn was started by the scheduler and nobody is watching. ` +
|
|
266
|
+
`Never ask questions and never print ${STOP_SENTINEL}. Act on the message below with ` +
|
|
267
|
+
`the /develop skill's Phase 2 rules (reset, fix the spec, archive, or queue a fix wave). ` +
|
|
268
|
+
`Make the safest reasonable choice for anything left open, and end with a short report ` +
|
|
269
|
+
`of what you did.\n\n`;
|
|
270
|
+
|
|
271
|
+
// tab+prompt keys of scheduler-originated prompts awaiting their run() (consumed once).
|
|
272
|
+
const schedulerPrompts = new Map();
|
|
273
|
+
const SCHEDULER_PROMPT_TTL_MS = 6 * 60 * 60 * 1000;
|
|
274
|
+
function noteSchedulerPrompt(tabId, prompt) {
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
for (const [k, v] of schedulerPrompts) if (v.expires < now) schedulerPrompts.delete(k);
|
|
277
|
+
const key = `${tabId}\n${prompt}`;
|
|
278
|
+
const cur = schedulerPrompts.get(key);
|
|
279
|
+
schedulerPrompts.set(key, { count: (cur ? cur.count : 0) + 1, expires: now + SCHEDULER_PROMPT_TTL_MS });
|
|
280
|
+
}
|
|
281
|
+
function consumeSchedulerPrompt(tabId, prompt) {
|
|
282
|
+
const key = `${tabId}\n${prompt}`;
|
|
283
|
+
const cur = schedulerPrompts.get(key);
|
|
284
|
+
if (!cur || cur.expires < Date.now()) { schedulerPrompts.delete(key); return false; }
|
|
285
|
+
if (cur.count <= 1) schedulerPrompts.delete(key); else cur.count -= 1;
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
260
289
|
// ─── Hard wall-clock kill ceiling ────────────────────────────────────────
|
|
261
290
|
// Defined here (ahead of CHAT_MODE_TRUTH_INSTRUCTION) so the prompt's stated
|
|
262
291
|
// budget is always derived from this single constant — never a hand-written
|
|
@@ -311,6 +340,9 @@ const dispatching = new Set();
|
|
|
311
340
|
// "Session ID … is already in use", so pump() refuses to start a second run
|
|
312
341
|
// for a held sessionId (it waits in `waiting` like any other blocked run).
|
|
313
342
|
const liveSessions = new Set();
|
|
343
|
+
// Submissions parked while a New Epic's fire-and-forget worktree mint is still in
|
|
344
|
+
// flight (tabId → opts). Counted by run()'s tabBusy; cancel() can drop them.
|
|
345
|
+
const pendingStart = new Map();
|
|
314
346
|
|
|
315
347
|
// Indirection so tests can stub the spawn without launching claude.
|
|
316
348
|
let executor = executeRun;
|
|
@@ -327,6 +359,7 @@ function __resetQueueForTests() {
|
|
|
327
359
|
dispatching.clear();
|
|
328
360
|
liveSessions.clear();
|
|
329
361
|
waiting.length = 0;
|
|
362
|
+
pendingStart.clear();
|
|
330
363
|
sessionSlots.__resetForTests();
|
|
331
364
|
}
|
|
332
365
|
|
|
@@ -337,11 +370,7 @@ let mainWindow = null;
|
|
|
337
370
|
function attachWindow(win) { mainWindow = win; }
|
|
338
371
|
|
|
339
372
|
function broadcast(channel, payload) {
|
|
340
|
-
|
|
341
|
-
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
|
|
342
|
-
mainWindow.webContents.send(channel, payload);
|
|
343
|
-
}
|
|
344
|
-
} catch { /* render frame may be gone */ }
|
|
373
|
+
sendIfAlive(mainWindow, channel, payload);
|
|
345
374
|
}
|
|
346
375
|
|
|
347
376
|
// ─── Public queue entry ─────────────────────────────────────────────────────
|
|
@@ -358,6 +387,9 @@ function broadcast(channel, payload) {
|
|
|
358
387
|
* @param {{ tabId: string, sessionId: string, prompt: string, cwd: string, resume: boolean, silent?: boolean, onSilentResult?: (text: string) => void, onSilentError?: (message: string, code?: string) => void, promptId?: string }} opts
|
|
359
388
|
*/
|
|
360
389
|
function run(opts) {
|
|
390
|
+
// A scheduler-originated prompt (tagged by enqueueExternalPrompt) is re-identified
|
|
391
|
+
// here by tab + exact text, since it travels through the renderer's chat:run.
|
|
392
|
+
if (!opts.silent && consumeSchedulerPrompt(opts.tabId, opts.prompt)) opts = { ...opts, origin: 'scheduler' };
|
|
361
393
|
// Per-tab exclusivity still holds — two `claude -p --resume` against ONE
|
|
362
394
|
// sessionId must never overlap — but it is now enforced in pump() (which
|
|
363
395
|
// refuses to start a run for a tab that already has one live) rather than by
|
|
@@ -374,6 +406,7 @@ function run(opts) {
|
|
|
374
406
|
const tabBusy = inFlight.has(opts.tabId)
|
|
375
407
|
|| dispatching.has(opts.tabId)
|
|
376
408
|
|| liveSessions.has(opts.sessionId)
|
|
409
|
+
|| pendingStart.has(opts.tabId)
|
|
377
410
|
|| waiting.some((w) => w.tabId === opts.tabId);
|
|
378
411
|
if (tabBusy && opts.silent) {
|
|
379
412
|
// Silent probes stay best-effort and invisible: a probe that collides is
|
|
@@ -381,6 +414,21 @@ function run(opts) {
|
|
|
381
414
|
return { accepted: false, reason: 'tab-busy-silent' };
|
|
382
415
|
}
|
|
383
416
|
|
|
417
|
+
// A New Epic's turn 1 races the fire-and-forget worktree mint: only when a mint is
|
|
418
|
+
// actually in flight for this Epic, hold the submission (<=90s, never rejects) so the
|
|
419
|
+
// first spawn lands in the worktree. Every other run is untouched.
|
|
420
|
+
if (epicWorktreeMint.hasPendingEpicWorktree(opts.cwd, opts.tabId)) {
|
|
421
|
+
pendingStart.set(opts.tabId, opts);
|
|
422
|
+
epicWorktreeMint.awaitPendingEpicWorktree(opts.cwd, opts.tabId)
|
|
423
|
+
.then((wt) => wt, () => null)
|
|
424
|
+
.then((wt) => {
|
|
425
|
+
if (pendingStart.get(opts.tabId) !== opts) return; // cancelled meanwhile
|
|
426
|
+
pendingStart.delete(opts.tabId);
|
|
427
|
+
run({ ...opts, worktreeDirHint: wt && wt.dir ? wt.dir : undefined });
|
|
428
|
+
});
|
|
429
|
+
return { accepted: true, queued: tabBusy };
|
|
430
|
+
}
|
|
431
|
+
|
|
384
432
|
waiting.push(opts);
|
|
385
433
|
pump();
|
|
386
434
|
return { accepted: true, queued: tabBusy };
|
|
@@ -448,7 +496,7 @@ sessionSlots.subscribe(() => { try { pump(); } catch { /* defensive */ } });
|
|
|
448
496
|
* @param {{ tabId: string, sessionId: string, prompt: string, cwd: string, resume: boolean, silent?: boolean, onSilentResult?: (text: string) => void, onSilentError?: (message: string, code?: string) => void, promptId?: string }} opts
|
|
449
497
|
* @returns {Promise<void>}
|
|
450
498
|
*/
|
|
451
|
-
function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentResult, onSilentError, promptId }) {
|
|
499
|
+
function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentResult, onSilentError, promptId, origin, worktreeDirHint }) {
|
|
452
500
|
return new Promise((resolve) => {
|
|
453
501
|
const startedAt = Date.now();
|
|
454
502
|
// Last few tool_use blocks seen on the stream, oldest first — surfaced in
|
|
@@ -532,7 +580,8 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
532
580
|
|
|
533
581
|
// Prepend the stop-signal protocol instruction and the chat-mode truth
|
|
534
582
|
// instruction to every prompt
|
|
535
|
-
const fullPrompt =
|
|
583
|
+
const fullPrompt = (origin === 'scheduler' ? SCHEDULER_TURN_INSTRUCTION : STOP_SIGNAL_INSTRUCTION)
|
|
584
|
+
+ CHAT_MODE_TRUTH_INSTRUCTION + prompt;
|
|
536
585
|
|
|
537
586
|
// sessionId is the Epic's claudeSessionId for an Epic-backed tab (see
|
|
538
587
|
// comment above childEnv) — resolve that Epic's agentType persona model,
|
|
@@ -551,7 +600,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
551
600
|
// Main is authoritative about --resume vs --session-id AND the spawn cwd, and it decides
|
|
552
601
|
// them TOGETHER (epicSpawnPlan.cjs): a transcript under an encoding the spawn cwd cannot see
|
|
553
602
|
// must never yield a claimed flag. Unreachable / closed Epics are refused before any spawn.
|
|
554
|
-
const plan = planEpicSpawn({ cwd, claudeSessionId: sessionId, fallbackResume: !!resume });
|
|
603
|
+
const plan = planEpicSpawn({ cwd, claudeSessionId: sessionId, fallbackResume: !!resume, worktreeDirHint });
|
|
555
604
|
if (!plan.ok) {
|
|
556
605
|
emitTerminal('chat:run:error', { tabId, sessionId, code: plan.code, message: plan.message });
|
|
557
606
|
// emitTerminal is a no-op broadcast for silent runs, so hand the refusal to the caller
|
|
@@ -919,6 +968,14 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
919
968
|
* no-op cancel, since there is nothing further to await in those cases.
|
|
920
969
|
*/
|
|
921
970
|
function cancel(tabId) {
|
|
971
|
+
const parked = pendingStart.get(tabId);
|
|
972
|
+
if (parked) {
|
|
973
|
+
pendingStart.delete(tabId);
|
|
974
|
+
if (!parked.silent) {
|
|
975
|
+
broadcast('chat:run:error', { tabId: parked.tabId, sessionId: parked.sessionId, message: 'cancelled while queued' });
|
|
976
|
+
}
|
|
977
|
+
return Promise.resolve();
|
|
978
|
+
}
|
|
922
979
|
const entry = inFlight.get(tabId);
|
|
923
980
|
if (entry) {
|
|
924
981
|
entry.cancelFn(); // settle() (on child exit) deletes from inFlight + pumps the next run
|
|
@@ -959,8 +1016,9 @@ function hasLiveWindow() {
|
|
|
959
1016
|
* { disposition: 'unconfirmed', reason: 'timeout' | 'no-window' }. Never rejects.
|
|
960
1017
|
*/
|
|
961
1018
|
function enqueueExternalPrompt(tabId, prompt, extra = {}) {
|
|
962
|
-
const { sessionId, cwd } = extra ?? {};
|
|
1019
|
+
const { sessionId, cwd, origin } = extra ?? {};
|
|
963
1020
|
if (!hasLiveWindow()) return Promise.resolve({ disposition: 'unconfirmed', reason: 'no-window' });
|
|
1021
|
+
if (origin === 'scheduler') noteSchedulerPrompt(tabId, prompt);
|
|
964
1022
|
const requestId = require('node:crypto').randomUUID();
|
|
965
1023
|
return new Promise((resolve) => {
|
|
966
1024
|
const timer = setTimeout(() => {
|
|
@@ -1088,6 +1146,7 @@ module.exports = {
|
|
|
1088
1146
|
// (lib/promptPreamble.ts) can be drift-tested against the REAL text rather
|
|
1089
1147
|
// than a copy — see chat-preamble-anchors.test.cjs.
|
|
1090
1148
|
STOP_SIGNAL_INSTRUCTION,
|
|
1149
|
+
SCHEDULER_TURN_INSTRUCTION,
|
|
1091
1150
|
CHAT_MODE_TRUTH_INSTRUCTION,
|
|
1092
1151
|
KILL_CEILING_MS,
|
|
1093
1152
|
KILL_CEILING_MIN,
|
package/src/main/config.cjs
CHANGED
|
@@ -26,6 +26,13 @@ const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
|
26
26
|
const { expandHome } = require('./lib/util/expandHome.cjs');
|
|
27
27
|
const { listReferencedFiles } = require('./lib/importReferences.cjs');
|
|
28
28
|
const atomicFs = require('./lib/util/atomicFs.cjs');
|
|
29
|
+
const insideHome = require('./lib/insideHome.cjs');
|
|
30
|
+
|
|
31
|
+
// Platform + matching path module, read per call so tests can inject win32.
|
|
32
|
+
const platformNow = () => process.platform;
|
|
33
|
+
const pathFor = () => (platformNow() === 'win32' ? path.win32 : path);
|
|
34
|
+
const within = (real, root) => insideHome.isContainedIn(real, root, platformNow());
|
|
35
|
+
const canon = (p) => insideHome.realpath(p, { platform: platformNow() });
|
|
29
36
|
|
|
30
37
|
/** Map<watchKey, {watcher, refCount}> — one chokidar watcher per path (+ depth, see watchKeyOf). */
|
|
31
38
|
const watchers = new Map();
|
|
@@ -55,26 +62,7 @@ function attachWindow(w) {
|
|
|
55
62
|
* squarely inside the allowed root.
|
|
56
63
|
*/
|
|
57
64
|
function realResolve(abs) {
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
return fs.realpathSync(lex);
|
|
61
|
-
} catch (e) {
|
|
62
|
-
if (e.code !== 'ENOENT') throw e;
|
|
63
|
-
}
|
|
64
|
-
const tail = [path.basename(lex)];
|
|
65
|
-
let dir = path.dirname(lex);
|
|
66
|
-
for (;;) {
|
|
67
|
-
try {
|
|
68
|
-
const realDir = fs.realpathSync(dir);
|
|
69
|
-
return path.join(realDir, ...tail.reverse());
|
|
70
|
-
} catch (e) {
|
|
71
|
-
if (e.code !== 'ENOENT') throw e;
|
|
72
|
-
const parentDir = path.dirname(dir);
|
|
73
|
-
if (parentDir === dir) return lex; // hit the filesystem root; give up safely
|
|
74
|
-
tail.push(path.basename(dir));
|
|
75
|
-
dir = parentDir;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
65
|
+
return canon(expandHome(abs));
|
|
78
66
|
}
|
|
79
67
|
|
|
80
68
|
/**
|
|
@@ -87,7 +75,12 @@ function realResolve(abs) {
|
|
|
87
75
|
const allowedRoots = new Set([os.homedir()]);
|
|
88
76
|
|
|
89
77
|
function addAllowedRoot(dir) {
|
|
90
|
-
if (dir) allowedRoots.add(
|
|
78
|
+
if (dir) allowedRoots.add(pathFor().resolve(dir));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Snapshot of the registered roots (main-process-registered only). */
|
|
82
|
+
function getAllowedRoots() {
|
|
83
|
+
return [...allowedRoots];
|
|
91
84
|
}
|
|
92
85
|
|
|
93
86
|
// PromptSession archive writes (state/promptSessions.ts's markCompleted ->
|
|
@@ -119,11 +112,11 @@ function validatePath(abs) {
|
|
|
119
112
|
for (const root of allowedRoots) {
|
|
120
113
|
let realRoot;
|
|
121
114
|
try {
|
|
122
|
-
realRoot =
|
|
115
|
+
realRoot = canon(root);
|
|
123
116
|
} catch {
|
|
124
117
|
realRoot = root;
|
|
125
118
|
}
|
|
126
|
-
if (real
|
|
119
|
+
if (within(real, realRoot)) return real;
|
|
127
120
|
}
|
|
128
121
|
throw new Error(`Path outside allowed boundaries: ${real}`);
|
|
129
122
|
}
|
|
@@ -132,7 +125,7 @@ function validatePath(abs) {
|
|
|
132
125
|
// home because validateWrite compares against a realpath — on macOS a HOME
|
|
133
126
|
// under os.tmpdir() (/var → /private/var) otherwise never matches.
|
|
134
127
|
const REAL_HOME = (() => {
|
|
135
|
-
try { return
|
|
128
|
+
try { return canon(os.homedir()); } catch { return os.homedir(); }
|
|
136
129
|
})();
|
|
137
130
|
const WRITE_PREFIXES = [
|
|
138
131
|
path.join(REAL_HOME, '.claude'),
|
|
@@ -150,7 +143,7 @@ function validateWrite(realAbs) {
|
|
|
150
143
|
throw new Error(`Write to .credentials.json denied`);
|
|
151
144
|
}
|
|
152
145
|
const inWritePrefix = WRITE_PREFIXES.some(
|
|
153
|
-
(p) => realAbs
|
|
146
|
+
(p) => within(realAbs, p)
|
|
154
147
|
);
|
|
155
148
|
if (inWritePrefix) return;
|
|
156
149
|
// The app's own userData dir (update-check.json cache).
|
|
@@ -162,10 +155,10 @@ function validateWrite(realAbs) {
|
|
|
162
155
|
for (const root of allowedRoots) {
|
|
163
156
|
if (root === os.homedir()) continue;
|
|
164
157
|
let realRoot;
|
|
165
|
-
try { realRoot =
|
|
166
|
-
if (realAbs
|
|
158
|
+
try { realRoot = canon(root); } catch { realRoot = root; }
|
|
159
|
+
if (within(realAbs, realRoot)) {
|
|
167
160
|
const claudeSub = path.join(realRoot, '.claude');
|
|
168
|
-
if (realAbs
|
|
161
|
+
if (within(realAbs, claudeSub)) {
|
|
169
162
|
return;
|
|
170
163
|
}
|
|
171
164
|
// Writable ops namespaces under a registered project root — every
|
|
@@ -189,7 +182,7 @@ function validateWrite(realAbs) {
|
|
|
189
182
|
opsSubs = [];
|
|
190
183
|
}
|
|
191
184
|
for (const sub of opsSubs) {
|
|
192
|
-
if (realAbs
|
|
185
|
+
if (within(realAbs, sub)) return;
|
|
193
186
|
}
|
|
194
187
|
}
|
|
195
188
|
}
|
|
@@ -536,6 +529,7 @@ module.exports = {
|
|
|
536
529
|
listDir,
|
|
537
530
|
exists,
|
|
538
531
|
addAllowedRoot,
|
|
532
|
+
getAllowedRoots,
|
|
539
533
|
registerPromptSessionsRoot,
|
|
540
534
|
validatePath,
|
|
541
535
|
validateWrite,
|
package/src/main/docEdit.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* docEdit.cjs — Document Experience rewrite runner (PRD 638).
|
|
@@ -154,11 +155,7 @@ let mainWindow = null;
|
|
|
154
155
|
function attachWindow(win) { mainWindow = win; }
|
|
155
156
|
|
|
156
157
|
function broadcast(channel, payload) {
|
|
157
|
-
|
|
158
|
-
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
|
|
159
|
-
mainWindow.webContents.send(channel, payload);
|
|
160
|
-
}
|
|
161
|
-
} catch { /* render frame may be gone */ }
|
|
158
|
+
sendIfAlive(mainWindow, channel, payload);
|
|
162
159
|
}
|
|
163
160
|
|
|
164
161
|
// `chatRunner.run()`'s silent lane invokes `onSilentResult` on its success path and
|
package/src/main/files.cjs
CHANGED
|
@@ -20,7 +20,8 @@ const path = require('node:path');
|
|
|
20
20
|
const os = require('node:os');
|
|
21
21
|
|
|
22
22
|
const { z } = require('zod');
|
|
23
|
-
const {
|
|
23
|
+
const { assertInsideHomeOrRoots } = require('./lib/insideHome.cjs');
|
|
24
|
+
const { getAllowedRoots } = require('./config.cjs');
|
|
24
25
|
const { expandHome } = require('./lib/util/expandHome.cjs');
|
|
25
26
|
const atomicFs = require('./lib/util/atomicFs.cjs');
|
|
26
27
|
const { schemas } = require('./ipcSchemas.cjs');
|
|
@@ -32,7 +33,7 @@ const { schemas } = require('./ipcSchemas.cjs');
|
|
|
32
33
|
* ~ — but never escapes the home tree.
|
|
33
34
|
*/
|
|
34
35
|
function validateHomePath(abs) {
|
|
35
|
-
return
|
|
36
|
+
return assertInsideHomeOrRoots(expandHome(abs), getAllowedRoots()).realPath;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** Reject .credentials.json writes regardless of where they sit. */
|
|
@@ -282,14 +283,23 @@ async function duplicateEntry(filePath) {
|
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
|
|
285
|
-
const CRITICAL_PATHS =
|
|
286
|
+
const CRITICAL_PATHS = ['/', '/usr', '/bin', '/etc', '/var', '/System', '/Applications'];
|
|
287
|
+
|
|
288
|
+
/** Case-insensitive on win32 (`C:\\Users\\Me` == `c:\\users\\me`). */
|
|
289
|
+
function isCriticalPath(resolved, platform = process.platform, home = os.homedir()) {
|
|
290
|
+
const norm = platform === 'win32'
|
|
291
|
+
? (v) => path.win32.normalize(v).toLowerCase().replace(/[\\/]+$/, '')
|
|
292
|
+
: (v) => v;
|
|
293
|
+
const r = norm(resolved);
|
|
294
|
+
return [home, ...CRITICAL_PATHS].some((c) => norm(c) === r);
|
|
295
|
+
}
|
|
286
296
|
|
|
287
297
|
async function deleteEntry(filePath) {
|
|
288
298
|
let resolved;
|
|
289
299
|
try { resolved = validateHomePath(filePath); }
|
|
290
300
|
catch (e) { return { ok: false, error: e.message }; }
|
|
291
301
|
|
|
292
|
-
if (
|
|
302
|
+
if (isCriticalPath(resolved)) {
|
|
293
303
|
return { ok: false, error: 'Cannot delete system-critical paths' };
|
|
294
304
|
}
|
|
295
305
|
try { rejectCredentials(resolved); }
|
|
@@ -414,6 +424,8 @@ module.exports = {
|
|
|
414
424
|
showInFinder,
|
|
415
425
|
// exported for tests
|
|
416
426
|
rejectCredentials,
|
|
427
|
+
validateHomePath,
|
|
428
|
+
isCriticalPath,
|
|
417
429
|
listDir,
|
|
418
430
|
readFile,
|
|
419
431
|
writeFile,
|
package/src/main/index.cjs
CHANGED
|
@@ -16,6 +16,9 @@ const { PROC_NAMES, setProcessTitle, inhibitHolderShell } = require('./lib/proce
|
|
|
16
16
|
// `process.argv.includes('--simple')` read further down.
|
|
17
17
|
setProcessTitle(PROC_NAMES.main);
|
|
18
18
|
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, powerSaveBlocker, protocol } = require('electron');
|
|
19
|
+
// Capture every ipcMain.handle registration (before any module registers one) so the
|
|
20
|
+
// web-remote ipc:invoke dispatcher calls the exact handlers the renderer calls.
|
|
21
|
+
const capturedIpcHandlers = require('./lib/feedback-remote/remoteIpcDispatch.cjs').captureIpcHandlers(ipcMain);
|
|
19
22
|
const { spawn, execFile, execFileSync } = require('node:child_process');
|
|
20
23
|
const path = require('node:path');
|
|
21
24
|
const fs = require('node:fs');
|
|
@@ -62,7 +65,7 @@ const prdCreate = require('./lib/prd/prdCreate.cjs');
|
|
|
62
65
|
const prdAdminRoutes = require('./lib/prd/prdAdminRoutes.cjs');
|
|
63
66
|
const projectHomeAdminRoutes = require('./lib/feedback-remote/projectHomeAdminRoutes.cjs');
|
|
64
67
|
const macroAdminRoutes = require('./lib/feedback-remote/macroAdminRoutes.cjs');
|
|
65
|
-
const { smfileResponseHeaders, shouldBlockFrameNavigation } = require('./lib/smfileHeaders.cjs');
|
|
68
|
+
const { smfileResponseHeaders, shouldBlockFrameNavigation, smfileUrlToPath } = require('./lib/smfileHeaders.cjs');
|
|
66
69
|
const { appendAuditEvent } = require('./lib/telemetry/auditLog.cjs');
|
|
67
70
|
const chatRunner = require('./chatRunner.cjs');
|
|
68
71
|
const promptSessionEvents = require('./promptSessionEvents.cjs');
|
|
@@ -122,6 +125,24 @@ const heapSnapshot = require('./heapSnapshot.cjs');
|
|
|
122
125
|
const filesIpc = require('./files.cjs');
|
|
123
126
|
const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
|
|
124
127
|
const webRemote = require('./webRemote.cjs');
|
|
128
|
+
{
|
|
129
|
+
const { createRemoteIpcDispatcher } = require('./lib/feedback-remote/remoteIpcDispatch.cjs');
|
|
130
|
+
webRemote.setIpcDispatcher(createRemoteIpcDispatcher({
|
|
131
|
+
surface: require('./../preload/apiSurface.cjs'),
|
|
132
|
+
handlers: capturedIpcHandlers,
|
|
133
|
+
getConfig: () => webRemote.loadConfig(),
|
|
134
|
+
fenceCtx: {
|
|
135
|
+
homeDir: os.homedir(),
|
|
136
|
+
realpath: (p) => require('./lib/insideHome.cjs').realpath(p),
|
|
137
|
+
projectRoots: () => {
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(fs.readFileSync(sessionsStore.storePath(), 'utf8'));
|
|
140
|
+
return (data.tabs || []).map((t) => t && t.cwd).filter((c) => typeof c === 'string' && c);
|
|
141
|
+
} catch { return []; }
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
125
146
|
const { registerAgentsHandlers } = require('./agentsIpc.cjs');
|
|
126
147
|
const { registerTelemetryHandlers } = require('./telemetryIpc.cjs');
|
|
127
148
|
const { registerUpdaterIpc } = require('./updaterIpc.cjs');
|
|
@@ -1025,6 +1046,7 @@ require('./lib/epic/archivedTranscript.cjs').registerIpc(ipcMain);
|
|
|
1025
1046
|
customerFeedbackIpc.registerCustomerFeedbackHandlers();
|
|
1026
1047
|
epicWorktreeMint.registerEpicWorktreeMintHandlers();
|
|
1027
1048
|
epicWorktreeMerge.registerEpicWorktreeMergeHandlers();
|
|
1049
|
+
require('./lib/epic/epicReopen.cjs').registerEpicReopenHandlers();
|
|
1028
1050
|
epicWorktreeProjectConfig.registerEpicWorktreeProjectConfigHandlers();
|
|
1029
1051
|
registerMemoryAggregateIpc();
|
|
1030
1052
|
registerProjectBriefIpc();
|
|
@@ -1322,7 +1344,7 @@ app.whenReady().then(async () => {
|
|
|
1322
1344
|
protocol.handle('smfile', async (request) => {
|
|
1323
1345
|
try {
|
|
1324
1346
|
const url = new URL(request.url);
|
|
1325
|
-
const abs =
|
|
1347
|
+
const abs = smfileUrlToPath(url.pathname);
|
|
1326
1348
|
const { realPath } = assertInsideHome(abs); // throws if outside home
|
|
1327
1349
|
const st = await fs.promises.stat(realPath);
|
|
1328
1350
|
if (st.isDirectory()) return new Response('Not a file', { status: 404 });
|
|
@@ -1365,7 +1387,7 @@ app.whenReady().then(async () => {
|
|
|
1365
1387
|
accelerator: 'CmdOrCtrl+N',
|
|
1366
1388
|
click: () => {
|
|
1367
1389
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
1368
|
-
mainWindow
|
|
1390
|
+
sendIfAlive(mainWindow, 'app:new-session');
|
|
1369
1391
|
}
|
|
1370
1392
|
},
|
|
1371
1393
|
},
|
|
@@ -1379,7 +1401,7 @@ app.whenReady().then(async () => {
|
|
|
1379
1401
|
accelerator: 'CmdOrCtrl+Shift+S',
|
|
1380
1402
|
click: () => {
|
|
1381
1403
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
1382
|
-
mainWindow
|
|
1404
|
+
sendIfAlive(mainWindow, 'app:reboot-session');
|
|
1383
1405
|
}
|
|
1384
1406
|
},
|
|
1385
1407
|
},
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -232,6 +232,9 @@ const promptSessionsCreateWorktree = z.object({
|
|
|
232
232
|
epicId: z.string().regex(PROMPT_SESSION_ID_RE),
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
// Renderer entry point for reopening a completed Epic in place (lib/epic/epicReopen.cjs).
|
|
236
|
+
const promptSessionsReopen = z.object({ cwd: z.string().min(1).max(4096), epicId: z.string().regex(PROMPT_SESSION_ID_RE) }).strict();
|
|
237
|
+
|
|
235
238
|
// Renderer entry point for folding an Epic's isolated git worktree branch
|
|
236
239
|
// back into its owning project's main tree (lib/epic/epicWorktreeMerge.cjs) — PRD
|
|
237
240
|
// 1034, the ONLY point where the per-Epic isolation PRD 1033 wires resolves.
|
|
@@ -835,8 +838,8 @@ const projectBriefUpdate = z.object({
|
|
|
835
838
|
purpose: PROJECT_BRIEF_TEXT.optional(),
|
|
836
839
|
what: z.array(PROJECT_BRIEF_TEXT).max(50).optional(),
|
|
837
840
|
conventions: z.array(PROJECT_BRIEF_TEXT).max(100).optional(),
|
|
838
|
-
areas: z.array(z.record(z.unknown())).max(100).optional(),
|
|
839
|
-
scope: z.array(z.record(z.unknown())).max(200).optional(),
|
|
841
|
+
areas: z.array(z.record(z.string(), z.unknown())).max(100).optional(),
|
|
842
|
+
scope: z.array(z.record(z.string(), z.unknown())).max(200).optional(),
|
|
840
843
|
}).strict().refine((p) => Object.keys(p).length > 0, { message: 'patch is empty' }),
|
|
841
844
|
}).strict();
|
|
842
845
|
|
|
@@ -1357,6 +1360,7 @@ module.exports = {
|
|
|
1357
1360
|
promptSessionsMergeActiveIndex,
|
|
1358
1361
|
promptSessionsCreateEpic,
|
|
1359
1362
|
promptSessionsCreateWorktree,
|
|
1363
|
+
promptSessionsReopen,
|
|
1360
1364
|
promptSessionsMergeToMain,
|
|
1361
1365
|
promptSessionsGetWorktreeDisabled,
|
|
1362
1366
|
promptSessionsSetWorktreeDisabled,
|