claude-code-session-manager 0.38.1 → 0.38.2
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-DlPTesio.js → TiptapBody-UWL5KIJG.js} +1 -1
- package/dist/assets/{index-CBSyk1aI.js → index-CZPF8Qve.js} +35 -35
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +99 -0
- package/src/main/chatRunner.cjs +44 -0
- package/src/main/index.cjs +2 -1
- package/src/main/ipcSchemas.cjs +20 -0
- package/src/main/lib/prdCreate.cjs +4 -1
- package/src/main/scheduler/prdParser.cjs +5 -0
- package/src/main/scheduler.cjs +69 -1
- package/src/main/webRemote.cjs +10 -0
- package/src/preload/api.d.ts +11 -0
- package/src/preload/index.cjs +7 -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-CZPF8Qve.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-DyOGjslF.css">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-session-manager",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.2",
|
|
4
4
|
"description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main/index.cjs",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-notify-originating-tab.test.cjs — unit tests for
|
|
3
|
+
* notifyOriginatingTab (PRD 761): on a job's terminal completed/failed
|
|
4
|
+
* transition, push a short status prompt into the chat tab that originated
|
|
5
|
+
* it via enqueueExternalPrompt (PRD 753).
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-notify-originating-tab.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test, expect, vi } from 'vitest';
|
|
13
|
+
const { notifyOriginatingTab, isNotifiableTerminalStatus } = require('../scheduler.cjs');
|
|
14
|
+
|
|
15
|
+
test('isNotifiableTerminalStatus: completed and failed are notifiable', () => {
|
|
16
|
+
expect(isNotifiableTerminalStatus('completed')).toBe(true);
|
|
17
|
+
expect(isNotifiableTerminalStatus('failed')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('isNotifiableTerminalStatus: needs_review is not notifiable (may still auto-fix)', () => {
|
|
21
|
+
expect(isNotifiableTerminalStatus('needs_review')).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('isNotifiableTerminalStatus: a rateLimited exit never reaches an effectiveStatus (undefined) and is not notifiable', () => {
|
|
25
|
+
// spawnJob's treatAsPending branch (rateLimited || paused for rate_limit)
|
|
26
|
+
// resets the job to pending and returns before effectiveStatus is ever
|
|
27
|
+
// computed — so this hook is only ever evaluated with undefined/pending,
|
|
28
|
+
// never with a real status, on that path.
|
|
29
|
+
expect(isNotifiableTerminalStatus(undefined)).toBe(false);
|
|
30
|
+
expect(isNotifiableTerminalStatus('pending')).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('resolves via sourceTabId when present on the PRD frontmatter', async () => {
|
|
34
|
+
const sendPrompt = vi.fn();
|
|
35
|
+
const parsePrdRaw = vi.fn(async () => ({ sourceTabId: 'tab-from-frontmatter' }));
|
|
36
|
+
const loadSessions = vi.fn(async () => ({ tabs: [] }));
|
|
37
|
+
|
|
38
|
+
await notifyOriginatingTab(
|
|
39
|
+
{ slug: '761-notify', status: 'completed', cwd: '/some/cwd' },
|
|
40
|
+
{ parsePrdRaw, loadSessions, sendPrompt },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(loadSessions).not.toHaveBeenCalled();
|
|
44
|
+
expect(sendPrompt).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(sendPrompt).toHaveBeenCalledWith(
|
|
46
|
+
'tab-from-frontmatter',
|
|
47
|
+
expect.stringContaining('761-notify'),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('falls back to the first open tab whose cwd matches the job cwd', async () => {
|
|
52
|
+
const sendPrompt = vi.fn();
|
|
53
|
+
const parsePrdRaw = vi.fn(async () => ({ sourceTabId: null }));
|
|
54
|
+
const loadSessions = vi.fn(async () => ({
|
|
55
|
+
tabs: [
|
|
56
|
+
{ id: 'tab-other', cwd: '/other/cwd' },
|
|
57
|
+
{ id: 'tab-match', cwd: '/some/cwd' },
|
|
58
|
+
{ id: 'tab-second-match', cwd: '/some/cwd' },
|
|
59
|
+
],
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
await notifyOriginatingTab(
|
|
63
|
+
{ slug: '761-notify', status: 'failed', cwd: '/some/cwd' },
|
|
64
|
+
{ parsePrdRaw, loadSessions, sendPrompt },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(sendPrompt).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(sendPrompt).toHaveBeenCalledWith('tab-match', expect.stringContaining('761-notify'));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('no-ops without throwing when neither sourceTabId nor a cwd-matching tab resolves', async () => {
|
|
72
|
+
const sendPrompt = vi.fn();
|
|
73
|
+
const parsePrdRaw = vi.fn(async () => ({ sourceTabId: null }));
|
|
74
|
+
const loadSessions = vi.fn(async () => ({ tabs: [{ id: 'tab-other', cwd: '/unrelated' }] }));
|
|
75
|
+
|
|
76
|
+
await expect(
|
|
77
|
+
notifyOriginatingTab(
|
|
78
|
+
{ slug: '761-notify', status: 'completed', cwd: '/some/cwd' },
|
|
79
|
+
{ parsePrdRaw, loadSessions, sendPrompt },
|
|
80
|
+
),
|
|
81
|
+
).resolves.not.toThrow();
|
|
82
|
+
|
|
83
|
+
expect(sendPrompt).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('no-ops when parsing the PRD frontmatter throws', async () => {
|
|
87
|
+
const sendPrompt = vi.fn();
|
|
88
|
+
const parsePrdRaw = vi.fn(async () => { throw new Error('ENOENT'); });
|
|
89
|
+
const loadSessions = vi.fn(async () => ({ tabs: [] }));
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
notifyOriginatingTab(
|
|
93
|
+
{ slug: '761-notify', status: 'completed', cwd: '/some/cwd' },
|
|
94
|
+
{ parsePrdRaw, loadSessions, sendPrompt },
|
|
95
|
+
),
|
|
96
|
+
).resolves.not.toThrow();
|
|
97
|
+
|
|
98
|
+
expect(sendPrompt).not.toHaveBeenCalled();
|
|
99
|
+
});
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -677,6 +677,48 @@ function cancel(tabId) {
|
|
|
677
677
|
return Promise.resolve();
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
+
// ─── External-caller entry point (PRD 753) ─────────────────────────────────
|
|
681
|
+
// Pushes a prompt into an already-open tab's chat queue from outside the
|
|
682
|
+
// renderer — Web Remote, the admin HTTP route, or the scheduler MCP tool.
|
|
683
|
+
// Fire-and-forget over IPC: the renderer-side listener (chat.ts) resolves
|
|
684
|
+
// the tab's live sessionId/cwd from useSessions and calls useChat.send()
|
|
685
|
+
// directly, reusing send()'s existing queued-vs-immediate branching rather
|
|
686
|
+
// than duplicating it here.
|
|
687
|
+
|
|
688
|
+
function enqueueExternalPrompt(tabId, prompt) {
|
|
689
|
+
broadcast('chat:external-send', { tabId, prompt });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Registers the admin-HTTP entry point for enqueueExternalPrompt, mirroring
|
|
694
|
+
* prdCreate.cjs's registerAdminRoute pattern (auth is the injected transport's
|
|
695
|
+
* job; this only owns route logic + body parsing).
|
|
696
|
+
*/
|
|
697
|
+
function registerAdminRoute(adminHttp) {
|
|
698
|
+
const { readBody, sendJson } = require('./lib/localAdminHttp.cjs');
|
|
699
|
+
const { schemas } = require('./ipcSchemas.cjs');
|
|
700
|
+
|
|
701
|
+
adminHttp.registerRoute('POST', '/admin/chat/send-prompt', async (req, res) => {
|
|
702
|
+
const raw = await readBody(req);
|
|
703
|
+
let parsed;
|
|
704
|
+
try {
|
|
705
|
+
parsed = raw ? JSON.parse(raw) : {};
|
|
706
|
+
} catch {
|
|
707
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
let body;
|
|
711
|
+
try {
|
|
712
|
+
body = schemas.chatExternalSend.parse(parsed);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
sendJson(res, 400, { ok: false, error: e?.message ?? 'invalid body' });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
enqueueExternalPrompt(body.tabId, body.prompt);
|
|
718
|
+
sendJson(res, 200, { ok: true });
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
680
722
|
// ─── IPC handler registration ─────────────────────────────────────────────
|
|
681
723
|
|
|
682
724
|
function registerChatHandlers() {
|
|
@@ -718,4 +760,6 @@ module.exports = {
|
|
|
718
760
|
STOP_SENTINEL,
|
|
719
761
|
CHAT_MODE_TRUTH_INSTRUCTION,
|
|
720
762
|
__setExecutor,
|
|
763
|
+
enqueueExternalPrompt,
|
|
764
|
+
registerAdminRoute,
|
|
721
765
|
};
|
package/src/main/index.cjs
CHANGED
|
@@ -27,9 +27,11 @@ const voiceWizard = require('./voiceWizard.cjs');
|
|
|
27
27
|
const scheduler = require('./scheduler.cjs');
|
|
28
28
|
const { createAdminHttp } = require('./lib/localAdminHttp.cjs');
|
|
29
29
|
const prdCreate = require('./lib/prdCreate.cjs');
|
|
30
|
+
const chatRunner = require('./chatRunner.cjs');
|
|
30
31
|
const adminHttp = createAdminHttp();
|
|
31
32
|
scheduler.registerAdminRoutes(adminHttp);
|
|
32
33
|
prdCreate.registerAdminRoute(adminHttp, scheduler.remote);
|
|
34
|
+
chatRunner.registerAdminRoute(adminHttp);
|
|
33
35
|
const { createBrowserAgentServer } = require('./browserAgentServer.cjs');
|
|
34
36
|
const browserAgentServer = createBrowserAgentServer({
|
|
35
37
|
listTabs: () => browserView.listViews(),
|
|
@@ -60,7 +62,6 @@ const searchIpc = require('./search.cjs');
|
|
|
60
62
|
const repoAnalyzer = require('./repoAnalyzer.cjs');
|
|
61
63
|
const hivesIpc = require('./hives.cjs');
|
|
62
64
|
const webRemote = require('./webRemote.cjs');
|
|
63
|
-
const chatRunner = require('./chatRunner.cjs');
|
|
64
65
|
const { listExchanges } = require('./exchanges.cjs');
|
|
65
66
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
66
67
|
const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -257,6 +257,10 @@ const schedulerCreatePrd = z.object({
|
|
|
257
257
|
// prompt that spawned it. Same newline-injection guard as title/cwd since
|
|
258
258
|
// it also becomes a frontmatter value.
|
|
259
259
|
sourcePromptId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
260
|
+
// Originating tab id (PRD 761) — used at job completion to route a status
|
|
261
|
+
// prompt back into the chat tab that queued this PRD, via
|
|
262
|
+
// enqueueExternalPrompt (PRD 753). Same newline-injection guard.
|
|
263
|
+
sourceTabId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
260
264
|
});
|
|
261
265
|
|
|
262
266
|
// Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
|
|
@@ -476,6 +480,18 @@ const chatClassifyTicket = z.object({
|
|
|
476
480
|
),
|
|
477
481
|
});
|
|
478
482
|
|
|
483
|
+
// External-caller entry point (admin HTTP route + MCP tool + web-remote
|
|
484
|
+
// command, PRD 753) — pushes a prompt into an already-open tab's chat queue
|
|
485
|
+
// from outside the renderer. tabId only (no sessionId/cwd): those are
|
|
486
|
+
// resolved renderer-side from useSessions, same as the AC requires.
|
|
487
|
+
const chatExternalSend = z.object({
|
|
488
|
+
tabId: z.string().min(1).max(128),
|
|
489
|
+
prompt: z.string().min(1).refine(
|
|
490
|
+
(s) => Buffer.byteLength(s, 'utf8') <= CHAT_PROMPT_MAX_BYTES,
|
|
491
|
+
`prompt must be ≤ ${CHAT_PROMPT_MAX_BYTES} bytes`,
|
|
492
|
+
),
|
|
493
|
+
});
|
|
494
|
+
|
|
479
495
|
// ──────────────────────────────────────────── Web Remote
|
|
480
496
|
// OTP is 8 uppercase alphanumeric chars (case-insensitive entry, normalised to upper in handler).
|
|
481
497
|
const WEB_REMOTE_OTP_RE = /^[A-Z0-9]{8}$/i;
|
|
@@ -683,6 +699,9 @@ const MUTATE_COMMANDS = new Set([
|
|
|
683
699
|
'cmd:schedule:reset-job',
|
|
684
700
|
'cmd:schedule:run-now',
|
|
685
701
|
'cmd:schedule:set-config',
|
|
702
|
+
// Pushes a prompt into an open tab's chat queue — a real mutation of that
|
|
703
|
+
// tab's session, same tier as pty:write.
|
|
704
|
+
'cmd:chat:send',
|
|
686
705
|
]);
|
|
687
706
|
|
|
688
707
|
const ALLOWED_COMMANDS = new Set([...READ_COMMANDS, ...SAS_GATED_READS, ...MUTATE_COMMANDS]);
|
|
@@ -774,6 +793,7 @@ module.exports = {
|
|
|
774
793
|
chatCancel,
|
|
775
794
|
chatProbeContext,
|
|
776
795
|
chatClassifyTicket,
|
|
796
|
+
chatExternalSend,
|
|
777
797
|
exchangesList,
|
|
778
798
|
},
|
|
779
799
|
validated,
|
|
@@ -48,7 +48,7 @@ function deriveSlugFromTitle(title) {
|
|
|
48
48
|
function buildPrdBody(input) {
|
|
49
49
|
const {
|
|
50
50
|
title, cwd, estimateMinutes, goal, acceptanceCriteria,
|
|
51
|
-
implementationNotes, outOfScope, sourcePromptId,
|
|
51
|
+
implementationNotes, outOfScope, sourcePromptId, sourceTabId,
|
|
52
52
|
} = input;
|
|
53
53
|
|
|
54
54
|
// No `parallelGroup` frontmatter key by convention (SKILL.md) — the NN-
|
|
@@ -58,6 +58,9 @@ function buildPrdBody(input) {
|
|
|
58
58
|
// Optional, additive: traces this PRD back to the PromptTicket (PRD 748)
|
|
59
59
|
// that was classified 'develop' and spawned it (PRD 749).
|
|
60
60
|
if (sourcePromptId) fmLines.push(`sourcePromptId: ${sourcePromptId}`);
|
|
61
|
+
// Optional, additive: the tab that queued this PRD, read back by
|
|
62
|
+
// scheduler.cjs at job completion (PRD 761) to route a status prompt.
|
|
63
|
+
if (sourceTabId) fmLines.push(`sourceTabId: ${sourceTabId}`);
|
|
61
64
|
fmLines.push('---', '');
|
|
62
65
|
|
|
63
66
|
const acLines = acceptanceCriteria.map((line) => `- [ ] ${line}`).join('\n');
|
|
@@ -68,6 +68,11 @@ async function parsePrdRaw(filePath) {
|
|
|
68
68
|
// classified 'develop' and spawned this PRD (PRD 749). Additive — absent
|
|
69
69
|
// on every PRD authored before this field existed.
|
|
70
70
|
sourcePromptId: fm.sourcePromptId || null,
|
|
71
|
+
// Optional traceability back to the chat tab that queued this PRD (PRD
|
|
72
|
+
// 761) — read back at job completion to route a status prompt via
|
|
73
|
+
// enqueueExternalPrompt (PRD 753). Additive — absent on every PRD
|
|
74
|
+
// authored before this field existed.
|
|
75
|
+
sourceTabId: fm.sourceTabId || null,
|
|
71
76
|
body: body.trim(),
|
|
72
77
|
};
|
|
73
78
|
}
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -60,6 +60,8 @@ const { openLog, withChildAndLog } = require('./lib/childWithLog.cjs');
|
|
|
60
60
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
61
61
|
const { createBroadcastCoalescer } = require('./lib/broadcastCoalescer.cjs');
|
|
62
62
|
const prdParser = require('./scheduler/prdParser.cjs');
|
|
63
|
+
const sessionsStore = require('./sessionsStore.cjs');
|
|
64
|
+
const { enqueueExternalPrompt } = require('./chatRunner.cjs');
|
|
63
65
|
const { verifyRun } = require('./runVerify.cjs');
|
|
64
66
|
const logs = require('./logs.cjs');
|
|
65
67
|
const { schemas, validated } = require('./ipcSchemas.cjs');
|
|
@@ -1185,6 +1187,62 @@ function applyOrphanOutcome(job, outcome, killNote = '') {
|
|
|
1185
1187
|
}
|
|
1186
1188
|
}
|
|
1187
1189
|
|
|
1190
|
+
/**
|
|
1191
|
+
* isNotifiableTerminalStatus(effectiveStatus) → boolean
|
|
1192
|
+
*
|
|
1193
|
+
* Gates notifyOriginatingTab to true terminal transitions only: 'completed'
|
|
1194
|
+
* and 'failed'. Excludes 'needs_review' (not yet truly done — may still
|
|
1195
|
+
* auto-fix) and, implicitly, the rateLimited/paused-queue path, which never
|
|
1196
|
+
* reaches effectiveStatus computation at all (it takes the treatAsPending
|
|
1197
|
+
* branch in spawnJob and resets the job to pending instead).
|
|
1198
|
+
*/
|
|
1199
|
+
function isNotifiableTerminalStatus(effectiveStatus) {
|
|
1200
|
+
return effectiveStatus === 'completed' || effectiveStatus === 'failed';
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* notifyOriginatingTab(job) → void
|
|
1205
|
+
*
|
|
1206
|
+
* On a true terminal transition (completed/failed — never the benign
|
|
1207
|
+
* rateLimited auto-pause, which resets the job to pending instead), push a
|
|
1208
|
+
* short status prompt into the chat tab that queued this PRD via
|
|
1209
|
+
* enqueueExternalPrompt (PRD 753). Resolution order: (1) the PRD's own
|
|
1210
|
+
* `sourceTabId` frontmatter, captured at creation time; (2) the first open
|
|
1211
|
+
* tab (per sessionsStore's persisted tabs.json) whose cwd matches the job's
|
|
1212
|
+
* cwd — first match only, no fan-out to multiple matching tabs; (3) no-op.
|
|
1213
|
+
* Never throws to the caller (fire-and-forget from spawnJob). Deps are
|
|
1214
|
+
* injectable (mirrors partitionBootOrphans's isAlive param) so unit tests can
|
|
1215
|
+
* exercise the resolution logic without touching disk/electron.
|
|
1216
|
+
*/
|
|
1217
|
+
async function notifyOriginatingTab(job, {
|
|
1218
|
+
parsePrdRaw = prdParser.parsePrdRaw,
|
|
1219
|
+
loadSessions = sessionsStore.load,
|
|
1220
|
+
sendPrompt = enqueueExternalPrompt,
|
|
1221
|
+
} = {}) {
|
|
1222
|
+
try {
|
|
1223
|
+
const prdPath = path.join(PRDS_DIR, `${job.slug}.md`);
|
|
1224
|
+
const prd = await parsePrdRaw(prdPath).catch(() => null);
|
|
1225
|
+
|
|
1226
|
+
let targetTabId = prd?.sourceTabId || null;
|
|
1227
|
+
if (!targetTabId) {
|
|
1228
|
+
const jobCwd = job.cwd || null;
|
|
1229
|
+
if (jobCwd) {
|
|
1230
|
+
const { tabs } = await loadSessions();
|
|
1231
|
+
const match = (tabs || []).find((t) => t && t.cwd === jobCwd);
|
|
1232
|
+
targetTabId = match?.id || null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (!targetTabId) {
|
|
1236
|
+
console.log(`[scheduler] notifyOriginatingTab: no target tab for ${job.slug}, skipping`);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
sendPrompt(targetTabId, `PRD ${job.slug} finished: ${job.status}. Check Scheduler for details.`);
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
console.error('[scheduler] notifyOriginatingTab error', job?.slug, e);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1188
1246
|
/** Scan the tail of a job's log for the canonical rate-limit signal. We look
|
|
1189
1247
|
* at the last 16 KB — final result event always lands at the end.
|
|
1190
1248
|
* Uses readTail() so no raw fd lifecycle is needed here. */
|
|
@@ -1941,6 +1999,7 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
1941
1999
|
let needsInvestigationNow = false;
|
|
1942
2000
|
let investigationJobSnapshot = null;
|
|
1943
2001
|
let needsReviewRcaSnapshot = null;
|
|
2002
|
+
let terminalNotifySnapshot = null;
|
|
1944
2003
|
await mutate((s) => {
|
|
1945
2004
|
const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
|
|
1946
2005
|
if (i2 >= 0) {
|
|
@@ -1994,6 +2053,9 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
1994
2053
|
}
|
|
1995
2054
|
delete s.jobs[i2].runtime;
|
|
1996
2055
|
|
|
2056
|
+
if (isNotifiableTerminalStatus(effectiveStatus)) {
|
|
2057
|
+
terminalNotifySnapshot = { ...s.jobs[i2] };
|
|
2058
|
+
}
|
|
1997
2059
|
if (effectiveStatus === 'failed') {
|
|
1998
2060
|
actuallyFailed = true;
|
|
1999
2061
|
failedJobSnapshot = { ...s.jobs[i2] };
|
|
@@ -2052,6 +2114,12 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
2052
2114
|
});
|
|
2053
2115
|
await broadcast({ flush: true });
|
|
2054
2116
|
|
|
2117
|
+
if (terminalNotifySnapshot) {
|
|
2118
|
+
notifyOriginatingTab(terminalNotifySnapshot).catch((e) => {
|
|
2119
|
+
console.error('[scheduler] notifyOriginatingTab error', job.slug, e);
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2055
2123
|
if (needsReviewRcaSnapshot) {
|
|
2056
2124
|
// Fire-and-forget, mirroring the DoD drain hook: never blocks the status
|
|
2057
2125
|
// transition, never throws to this caller.
|
|
@@ -3386,4 +3454,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
3386
3454
|
});
|
|
3387
3455
|
}
|
|
3388
3456
|
|
|
3389
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, feedbackSweepDue, FEEDBACK_SWEEP_TICK_INTERVAL, sweepFeedback, registerAdminRoutes };
|
|
3457
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, feedbackSweepDue, FEEDBACK_SWEEP_TICK_INTERVAL, sweepFeedback, registerAdminRoutes, notifyOriginatingTab, isNotifiableTerminalStatus };
|
package/src/main/webRemote.cjs
CHANGED
|
@@ -1056,6 +1056,7 @@ function getDispatchMap() {
|
|
|
1056
1056
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
1057
1057
|
const scheduler = require('./scheduler.cjs');
|
|
1058
1058
|
const { remote: histRemote } = require('./historyAggregator.cjs');
|
|
1059
|
+
const chatRunner = require('./chatRunner.cjs');
|
|
1059
1060
|
|
|
1060
1061
|
_dispatchMap = {
|
|
1061
1062
|
'cmd:sessions:load': async () =>
|
|
@@ -1146,6 +1147,15 @@ function getDispatchMap() {
|
|
|
1146
1147
|
stopSessionWatch(parsed.tabId);
|
|
1147
1148
|
return { ok: true };
|
|
1148
1149
|
},
|
|
1150
|
+
|
|
1151
|
+
// Push a prompt into an open tab's chat queue from the paired phone
|
|
1152
|
+
// client — in-process call, no admin HTTP hop needed since webRemote
|
|
1153
|
+
// already runs inside the Electron main process (PRD 753).
|
|
1154
|
+
'cmd:chat:send': async (payload) => {
|
|
1155
|
+
const parsed = schemas.chatExternalSend.parse(payload);
|
|
1156
|
+
chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt);
|
|
1157
|
+
return { ok: true };
|
|
1158
|
+
},
|
|
1149
1159
|
};
|
|
1150
1160
|
|
|
1151
1161
|
return _dispatchMap;
|
package/src/preload/api.d.ts
CHANGED
|
@@ -1102,6 +1102,13 @@ export interface ChatRunNoticeEvent {
|
|
|
1102
1102
|
message: string;
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
+
/** Pushed by enqueueExternalPrompt (PRD 753) — Web Remote, the admin HTTP
|
|
1106
|
+
* route, or an MCP caller asking to enqueue a prompt into an open tab. */
|
|
1107
|
+
export interface ChatExternalSendEvent {
|
|
1108
|
+
tabId: string;
|
|
1109
|
+
prompt: string;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1105
1112
|
export interface SessionManagerAPI {
|
|
1106
1113
|
app: {
|
|
1107
1114
|
version: () => Promise<string>;
|
|
@@ -1475,6 +1482,10 @@ export interface SessionManagerAPI {
|
|
|
1475
1482
|
onComplete: (handler: (e: ChatRunCompleteEvent) => void) => () => void;
|
|
1476
1483
|
onError: (handler: (e: ChatRunErrorEvent) => void) => () => void;
|
|
1477
1484
|
onNotice: (handler: (e: ChatRunNoticeEvent) => void) => () => void;
|
|
1485
|
+
/** Fires when a main-process caller (Web Remote, admin HTTP route, MCP
|
|
1486
|
+
* tool) pushes a prompt into an open tab's queue from outside the
|
|
1487
|
+
* renderer (PRD 753). */
|
|
1488
|
+
onExternalSend: (handler: (e: ChatExternalSendEvent) => void) => () => void;
|
|
1478
1489
|
/** Classify a queued PromptTicket's text as 'inline' (run through
|
|
1479
1490
|
* chatRunner) or 'develop' (dispatch to /develop for PRD decomposition).
|
|
1480
1491
|
* A single bounded `claude -p` call, never a scheduler job. */
|
package/src/preload/index.cjs
CHANGED
|
@@ -436,6 +436,13 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
436
436
|
ipcRenderer.on('chat:run:notice', listener);
|
|
437
437
|
return () => ipcRenderer.removeListener('chat:run:notice', listener);
|
|
438
438
|
},
|
|
439
|
+
/** Fires when a main-process caller pushes a prompt into an open tab's
|
|
440
|
+
* queue from outside the renderer (Web Remote / admin HTTP / MCP). */
|
|
441
|
+
onExternalSend: (handler) => {
|
|
442
|
+
const listener = (_e, payload) => handler(payload);
|
|
443
|
+
ipcRenderer.on('chat:external-send', listener);
|
|
444
|
+
return () => ipcRenderer.removeListener('chat:external-send', listener);
|
|
445
|
+
},
|
|
439
446
|
/** Classify a queued PromptTicket's text as 'inline' or 'develop'. */
|
|
440
447
|
classifyTicket: (payload) => ipcRenderer.invoke('chat:classify-ticket', payload),
|
|
441
448
|
},
|