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/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-CBSyk1aI.js"></script>
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.1",
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
+ });
@@ -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
  };
@@ -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');
@@ -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
  }
@@ -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 };
@@ -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;
@@ -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. */
@@ -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
  },