claude-code-session-manager 0.120.0 → 0.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/assets/{AgentLibrary-LBg3mVL8.js → AgentLibrary-FO2ei2wB.js} +1 -1
  2. package/dist/assets/{Button-C4BN5ISR.js → Button-CeIOXUPU.js} +1 -1
  3. package/dist/assets/{DataModel-CHhjk61s.js → DataModel-D0p__Hil.js} +1 -1
  4. package/dist/assets/{History-CL-T6Lv3.js → History-DVqQ5azN.js} +1 -1
  5. package/dist/assets/{Hooks-ByWVgXIf.js → Hooks-DkTnRHyT.js} +1 -1
  6. package/dist/assets/{Library-QvYVAEmY.js → Library-l7KS6pkE.js} +1 -1
  7. package/dist/assets/{ListDetail-FvNe6fo5.js → ListDetail-DDw8fuRT.js} +1 -1
  8. package/dist/assets/{McpServers-C7EmPm8B.js → McpServers-UUNIxk_3.js} +1 -1
  9. package/dist/assets/{Memory-nyLR4Yw4.js → Memory-CNYUoQ8C.js} +1 -1
  10. package/dist/assets/{Panel-thElCbLX.js → Panel-Bb54soc2.js} +1 -1
  11. package/dist/assets/{Permissions-FQ9GM6Ca.js → Permissions-D-dNdV3f.js} +1 -1
  12. package/dist/assets/{Plugins-D9KE7XTI.js → Plugins-DhcIXRhQ.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-lAViG8yV.js → ProvenanceBadge-DEAkDNjA.js} +1 -1
  14. package/dist/assets/{RemoteMachines-AWpARk7N.js → RemoteMachines-xCEgvTGU.js} +1 -1
  15. package/dist/assets/{SaveBar-c15EEGB_.js → SaveBar-BSEs7ecA.js} +1 -1
  16. package/dist/assets/{Scheduler-FvduP1FG.js → Scheduler-BIKJIU-C.js} +1 -1
  17. package/dist/assets/{ScopeSwitcher-BLep1rtB.js → ScopeSwitcher-DcaFyAV-.js} +1 -1
  18. package/dist/assets/{Settings-D1kTUIDB.js → Settings-vcdWy_Jx.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-_40_f0t3.js → SkillReferenceGraph-CLrJWTPd.js} +1 -1
  20. package/dist/assets/{Skills-DaTkSOuU.js → Skills-CYM4rBEC.js} +1 -1
  21. package/dist/assets/{SystemPrompt-DxNf-soM.js → SystemPrompt-BWNIu-iY.js} +1 -1
  22. package/dist/assets/{TagLibrary-L6KpBV-x.js → TagLibrary-CYdwHK6m.js} +1 -1
  23. package/dist/assets/{TiptapBody-C_XjtJ82.js → TiptapBody-Kt90lHrD.js} +1 -1
  24. package/dist/assets/{Toggle-H0rERKNI.js → Toggle-D1OXWYOz.js} +1 -1
  25. package/dist/assets/{ViewTabs-ZUWphDat.js → ViewTabs-BeQR3CKh.js} +1 -1
  26. package/dist/assets/{historyTotals-BxFKOztb.js → historyTotals-B0qafrRo.js} +1 -1
  27. package/dist/assets/{index-B5wj0DEX.js → index-Bq9uH08R.js} +200 -200
  28. package/dist/assets/{settingsSchema-BRcYkXJN.js → settingsSchema-0ZksSAPB.js} +1 -1
  29. package/dist/assets/{skillFrontmatter-DFGp4DST.js → skillFrontmatter-C0nh8yfa.js} +1 -1
  30. package/dist/assets/{useAgentsDirWatch-VvrgalfC.js → useAgentsDirWatch-DPkoBybS.js} +1 -1
  31. package/dist/assets/{useRemotePeers-mvXKNEE3.js → useRemotePeers-BDJMUihy.js} +1 -1
  32. package/dist/index.html +1 -1
  33. package/package.json +1 -1
  34. package/src/main/build-info.json +4 -4
  35. package/src/main/chatRunner.cjs +43 -6
  36. package/src/main/ipcSchemas.cjs +15 -1
  37. package/src/main/lib/persona-macro/macroLibrary.cjs +75 -0
  38. package/src/main/lib/prd/prdCreate.cjs +25 -0
  39. package/src/main/lib/remoteTranscriptTail.cjs +69 -2
  40. package/src/main/webRemote.cjs +82 -91
  41. package/src/preload/api.d.ts +13 -0
  42. package/src/preload/index.cjs +2 -0
@@ -29,7 +29,6 @@ const logs = require('./logs.cjs');
29
29
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
30
30
  const { claudeHome } = require('./lib/scheduler/schedulerPaths.cjs');
31
31
  const { schemas } = require('./ipcSchemas.cjs');
32
- const { summarize: _sharedSummarize } = require('./lib/summarize.cjs');
33
32
  const { makeState, confirmSas: confirmSasLogic } = require('./lib/e2eStateMachine.cjs');
34
33
  const { createClientSessions, LEGACY_CLIENT_KEY } = require('./lib/feedback-remote/remoteClientSessions.cjs');
35
34
 
@@ -497,12 +496,11 @@ async function handleTokenRevoked(deviceId) {
497
496
  sendIfAlive(_window, 'webRemote:token-revoked', { deviceId });
498
497
  }
499
498
 
500
- // ─── v2 mobile: session live state + summary push ────────────────────────────
499
+ // ─── v2 mobile: session live state push ────────────────────────────
501
500
  //
502
501
  // For each subscribed tab the agent tails its transcript JSONL (reusing the
503
502
  // canonical classifyLine + transcriptPath from transcripts.cjs — single source of
504
503
  // truth), derives a coarse state, and pushes event:session:state on change.
505
- // The last completed assistant turn drives the Haiku summary (see maybeSummarize).
506
504
 
507
505
  const SESSION_POLL_MS = 1500;
508
506
  const SESSION_INIT_TAIL_BYTES = 512 * 1024; // bound the initial read
@@ -524,7 +522,7 @@ function pushEventTo(client, type, payload) {
524
522
  }
525
523
  }
526
524
 
527
- /** Authenticated clients subscribed to a tab's session state/summary. */
525
+ /** Authenticated clients subscribed to a tab's session state. */
528
526
  function subscribersOf(tabId) {
529
527
  return _clients.authenticated().filter((c) => c.subscriptions.has(tabId));
530
528
  }
@@ -551,15 +549,6 @@ function teardownAllClients() {
551
549
  return n;
552
550
  }
553
551
 
554
- /** Extract concatenated text from an assistant transcript line, or '' if none. */
555
- function extractAssistantText(raw) {
556
- const msg = raw?.message || raw;
557
- const content = msg?.content;
558
- if (typeof content === 'string') return content;
559
- if (!Array.isArray(content)) return '';
560
- return content.filter((b) => b?.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n').trim();
561
- }
562
-
563
552
  /** Map a classified transcript event to a coarse session state. */
564
553
  function deriveState(ev, raw) {
565
554
  // API/usage errors surface as a flagged message line.
@@ -613,6 +602,19 @@ async function tailLines(filePath, fromOffset) {
613
602
  }
614
603
 
615
604
  async function pollSessionWatcher(w) {
605
+ // The transcript may not exist yet (new session) or may live under another
606
+ // project/worktree encoding: re-resolve while the watched file is missing.
607
+ if (!fs.existsSync(w.filePath)) {
608
+ try {
609
+ const next = require('./transcripts.cjs').transcriptPath(w.cwd, w.sessionId);
610
+ if (next && next !== w.filePath) {
611
+ validatePath(next);
612
+ w.filePath = next;
613
+ w.offset = null;
614
+ w.inode = undefined;
615
+ }
616
+ } catch { /* keep the old path */ }
617
+ }
616
618
  let res;
617
619
  try {
618
620
  res = await tailLines(w.filePath, w.offset);
@@ -627,8 +629,6 @@ async function pollSessionWatcher(w) {
627
629
  w.inode = res.inode;
628
630
 
629
631
  let nextState = null;
630
- let newAssistantText = null;
631
- let newMsgId = null;
632
632
  for (const line of res.lines) {
633
633
  let obj;
634
634
  try { obj = JSON.parse(line); } catch { continue; }
@@ -636,10 +636,6 @@ async function pollSessionWatcher(w) {
636
636
  for (const ev of events) {
637
637
  const s = deriveState(ev, obj);
638
638
  if (s) nextState = s;
639
- if (ev.kind === 'assistant') {
640
- const text = extractAssistantText(obj);
641
- if (text) { newAssistantText = text; newMsgId = obj.uuid || obj.message?.id || `${w.tabId}:${res.size}`; }
642
- }
643
639
  }
644
640
  }
645
641
 
@@ -650,33 +646,20 @@ async function pollSessionWatcher(w) {
650
646
  pushEventTo(c, 'event:session:state', { tabId: w.tabId, state: w.state, since: Date.now() });
651
647
  }
652
648
  }
653
- if (newAssistantText && newMsgId !== w.lastMsgId) {
654
- w.lastAssistantText = newAssistantText;
655
- w.lastMsgId = newMsgId;
656
- }
657
- // Summarize only a COMPLETED turn (state idle) — not assistant text mid-turn that
658
- // is followed by a tool call. Cache by msgId so re-subscribe doesn't re-bill.
659
- if (w.state === 'idle' && w.lastAssistantText && w.lastMsgId !== w.summarizedMsgId) {
660
- w.summarizedMsgId = w.lastMsgId;
661
- maybeSummarize(w).catch(() => {});
662
- }
663
649
  }
664
650
 
665
- function startSessionWatch(tabId, cwd) {
651
+ function startSessionWatch(tabId, cwd, sessionId = tabId) {
666
652
  if (_sessionWatchers.has(tabId)) return;
667
- const filePath = require('./transcripts.cjs').transcriptPath(cwd, tabId);
653
+ const filePath = require('./transcripts.cjs').transcriptPath(cwd, sessionId);
668
654
  // Defense in depth: the schema restricts tabId charset, but re-validate the
669
655
  // FINAL joined path against the home-dir boundary (validatePath resolves
670
656
  // symlinks + rejects escapes) before any fs read. Throws → dispatch drops it.
671
657
  validatePath(filePath);
672
658
  const w = {
673
- tabId, cwd, filePath,
659
+ tabId, cwd, sessionId, filePath,
674
660
  offset: null, // null → first poll reads a bounded tail then tracks EOF
675
661
  inode: undefined,
676
662
  state: 'idle',
677
- lastAssistantText: null,
678
- lastMsgId: null,
679
- summarizedMsgId: null,
680
663
  timer: null,
681
664
  };
682
665
  _sessionWatchers.set(tabId, w);
@@ -732,44 +715,6 @@ function buildRemoteSessionList(tabs, readIndex) {
732
715
  return out;
733
716
  }
734
717
 
735
- // ─── SM-V2-03: mobile summary via Claude Haiku 4.5 ───────────────────────────
736
- // Summarization logic lives in lib/summarize.cjs (shared with exchanges.cjs).
737
-
738
- const SUMMARY_MIN_CHARS = 280; // below this, push raw — not worth an API call
739
-
740
- /**
741
- * Produce a mobile summary of the watcher's last completed assistant turn and push it.
742
- * Short turns are pushed raw (no API call). If no API key is configured, degrades to
743
- * the raw message so core remote control is never blocked. Cost: Haiku in+out per
744
- * completed turn per subscribed tab (~$1/$5 per 1M tokens).
745
- */
746
- async function maybeSummarize(w) {
747
- // Guard: summaries (session transcript content) go only to SAS-confirmed subscribers.
748
- if (subscribersOf(w.tabId).length === 0) return;
749
- const text = w.lastAssistantText;
750
- if (!text) return;
751
- const ofMessageId = w.lastMsgId;
752
-
753
- if (text.length < SUMMARY_MIN_CHARS) {
754
- for (const c of subscribersOf(w.tabId)) {
755
- pushEventTo(c, 'event:session:summary', { tabId: w.tabId, summary: text, ofMessageId, model: 'raw', ts: Date.now() });
756
- }
757
- return;
758
- }
759
-
760
- const { summary, model, degraded } = await _sharedSummarize(text);
761
- if (degraded) {
762
- logs.writeLine({ scope: 'webRemote', level: 'warn', message: `summary degraded: ${degraded}`, meta: {} });
763
- }
764
- for (const c of subscribersOf(w.tabId)) {
765
- pushEventTo(c, 'event:session:summary', {
766
- tabId: w.tabId, summary, ofMessageId, model,
767
- ...(degraded ? { degraded } : {}),
768
- ts: Date.now(),
769
- });
770
- }
771
- }
772
-
773
718
  /**
774
719
  * Shared finish-up for any path that transitions a client's e2e state into 'authenticated'
775
720
  * (manual SAS confirmation or pinned-key auto-trust): log, broadcast the new
@@ -1010,7 +955,7 @@ async function dispatchEnvelope(envelope, device, client) {
1010
955
  let result;
1011
956
  try {
1012
957
  result = await dispatchCommand(type, payload ?? {}, client);
1013
- await auditLog(ts, type, device.deviceId, id, 'ok', client);
958
+ await auditLog(ts, type, device.deviceId, id, auditResultFor(result), client);
1014
959
  } catch (e) {
1015
960
  const code = e?.name === 'ZodError' ? 'schema_invalid' : 'dispatch_error';
1016
961
  await auditLog(ts, type, device.deviceId, id, `error:${code}`, client);
@@ -1020,6 +965,14 @@ async function dispatchEnvelope(envelope, device, client) {
1020
965
  respond(id, result, undefined, client);
1021
966
  }
1022
967
 
968
+ /** Audit result string for a handler result: structured refusals log as refused[:reason]. */
969
+ function auditResultFor(result) {
970
+ if (result && result.ok === false && typeof result.disposition === 'string') {
971
+ return typeof result.reason === 'string' && result.reason ? `refused:${result.reason}` : 'refused';
972
+ }
973
+ return 'ok';
974
+ }
975
+
1023
976
  function respond(msgId, payload, typeOverride, client) {
1024
977
  if (!_ws || _ws.readyState !== WebSocket.OPEN) return;
1025
978
  const responseType = typeOverride || (msgId ? `resp:${msgId}` : undefined);
@@ -1102,26 +1055,34 @@ function getDispatchMap() {
1102
1055
  const parsed = schemas.remoteSessionTranscript.parse(payload);
1103
1056
  const { readActiveIndex } = require('./lib/epic/epicMint.cjs');
1104
1057
  const { resolveEpicTranscriptPath } = require('./lib/epic/epicTranscriptPath.cjs');
1105
- const { readRecentChatMessages } = require('./lib/remoteTranscriptTail.cjs');
1058
+ const { readRecentChatMessages, buildRemoteConversation } = require('./lib/remoteTranscriptTail.cjs');
1059
+ const { listExchanges } = require('./exchanges.cjs');
1106
1060
  const data = await sessionsStore.load();
1107
1061
  const tab = (data?.tabs ?? []).find((t) => t && t.cwd === parsed.cwd);
1108
1062
  if (!tab) throw new Error('cwd is not an open project');
1109
1063
  validatePath(parsed.cwd);
1064
+ const limit = parsed.limit ?? 10;
1065
+ const before = parsed.before ?? 0;
1110
1066
  let filePath = null;
1111
- if (parsed.sessionKey === (tab.id ?? tab.sessionId)) {
1112
- filePath = require('./transcripts.cjs').transcriptPath(parsed.cwd, parsed.sessionKey);
1067
+ let sessionId = null;
1068
+ if (parsed.sessionKey === tab.id || parsed.sessionKey === tab.sessionId) {
1069
+ sessionId = tab.sessionId || parsed.sessionKey;
1070
+ filePath = require('./transcripts.cjs').transcriptPath(parsed.cwd, sessionId);
1113
1071
  } else {
1114
1072
  const sessions = readActiveIndex(parsed.cwd)?.sessions ?? {};
1115
1073
  const epic = Object.values(sessions).find(
1116
1074
  (e) => e && (e.id === parsed.sessionKey || e.claudeSessionId === parsed.sessionKey),
1117
1075
  );
1118
1076
  if (epic && epic.claudeSessionId) {
1119
- filePath = resolveEpicTranscriptPath({ cwd: parsed.cwd, claudeSessionId: epic.claudeSessionId }).path;
1077
+ sessionId = epic.claudeSessionId;
1078
+ filePath = resolveEpicTranscriptPath({ cwd: parsed.cwd, claudeSessionId: sessionId }).path;
1120
1079
  }
1121
1080
  }
1122
- if (!filePath) return { messages: [] };
1081
+ if (!filePath || !sessionId) return { messages: [], hasMore: false, nextBefore: null };
1123
1082
  validatePath(filePath);
1124
- return { messages: await readRecentChatMessages(filePath, { limit: parsed.limit }) };
1083
+ const exchanges = await listExchanges({ cwd: parsed.cwd, sessionId, limit: limit + 1, offset: before });
1084
+ const tail = before === 0 ? await readRecentChatMessages(filePath, { limit: 30 }) : [];
1085
+ return buildRemoteConversation({ exchanges, tail, limit, before });
1125
1086
  },
1126
1087
 
1127
1088
  'cmd:session:prds': async (payload) => {
@@ -1209,12 +1170,15 @@ function getDispatchMap() {
1209
1170
  'cmd:app:info': async () =>
1210
1171
  ({ version: app.getVersion(), platform: process.platform }),
1211
1172
 
1212
- // v2 mobile: start/stop pushing live state + summary for a session.
1173
+ // v2 mobile: start/stop pushing live state for a session.
1213
1174
  'cmd:session:subscribe': async (payload, reqClient) => {
1214
1175
  const client = reqClient || _clients.getOrCreate(LEGACY_CLIENT_KEY);
1215
1176
  const parsed = schemas.sessionSubscribe.parse(payload);
1216
1177
  validatePath(parsed.cwd); // home-dir boundary before any fs access
1217
- startSessionWatch(parsed.tabId, parsed.cwd);
1178
+ // The phone's Main row passes the tab `id`, but the transcript is keyed by the tab's `sessionId`.
1179
+ const { tabs } = await require('./sessionsStore.cjs').load();
1180
+ const tab = (Array.isArray(tabs) ? tabs : []).find((t) => t && t.id === parsed.tabId && t.cwd === parsed.cwd);
1181
+ startSessionWatch(parsed.tabId, parsed.cwd, tab?.sessionId || parsed.tabId);
1218
1182
  client.subscriptions.add(parsed.tabId);
1219
1183
  return { ok: true };
1220
1184
  },
@@ -1236,23 +1200,28 @@ function getDispatchMap() {
1236
1200
  const data = await sessionsStore.load();
1237
1201
  const tabs = data?.tabs ?? [];
1238
1202
  // Resolve the target in main: an open tab, or an ACTIVE Epic (by id or
1239
- // claudeSessionId) of an open project. Anything else fails loudly.
1203
+ // claudeSessionId) of an open project. Anything else is a structured refusal.
1204
+ const outcome = (r) => ({
1205
+ ok: r.disposition !== 'refused',
1206
+ disposition: r.disposition,
1207
+ ...(r.reason ? { reason: r.reason } : {}),
1208
+ });
1240
1209
  if (tabs.some((t) => t.sessionId === parsed.tabId || t.id === parsed.tabId)) {
1241
- chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt);
1242
- return { ok: true };
1210
+ return outcome(await chatRunner.enqueueExternalPrompt(parsed.tabId, parsed.prompt));
1243
1211
  }
1244
1212
  for (const tab of tabs) {
1245
1213
  const sessions = Object.values(readActiveIndex(tab.cwd)?.sessions ?? {});
1246
1214
  const epic = sessions.find((e) => e && (e.id === parsed.tabId || e.claudeSessionId === parsed.tabId));
1247
1215
  if (!epic) continue;
1248
- if (epic.status !== 'active' || !epic.claudeSessionId) throw new Error('target is not an active session');
1249
- chatRunner.enqueueExternalPrompt(epic.id, parsed.prompt, {
1216
+ if (epic.status !== 'active' || !epic.claudeSessionId) {
1217
+ return { ok: false, disposition: 'refused', reason: epic.status === 'proposed' ? 'proposed' : 'not-active' };
1218
+ }
1219
+ return outcome(await chatRunner.enqueueExternalPrompt(epic.id, parsed.prompt, {
1250
1220
  sessionId: epic.claudeSessionId,
1251
1221
  cwd: epic.cwd || tab.cwd,
1252
- });
1253
- return { ok: true };
1222
+ }));
1254
1223
  }
1255
- throw new Error('unknown chat target');
1224
+ return { ok: false, disposition: 'refused', reason: 'unknown-target' };
1256
1225
  },
1257
1226
 
1258
1227
  'cmd:session:options': async () => {
@@ -1266,6 +1235,7 @@ function getDispatchMap() {
1266
1235
  agents: personas.map((a) => ({ name: a.name, description: a.description })),
1267
1236
  tags: EpicTagSchema.options,
1268
1237
  macros: await macroLibrary.listMacros(),
1238
+ macroInputs: true,
1269
1239
  };
1270
1240
  },
1271
1241
 
@@ -1278,7 +1248,23 @@ function getDispatchMap() {
1278
1248
  if (!tabs.some((t) => t.cwd === parsed.cwd)) throw new Error('cwd is not an open project');
1279
1249
  const personas = await agentLibrary.listPersonas();
1280
1250
  if (!personas.some((a) => a.name === parsed.agentName)) throw new Error('unknown agent');
1281
- chatRunner.requestExternalNewSession(parsed);
1251
+ let prompt = parsed.prompt;
1252
+ if (parsed.macroId) {
1253
+ const macroLibrary = require('./lib/persona-macro/macroLibrary.cjs');
1254
+ const macros = await macroLibrary.listMacros();
1255
+ const macro = macros.find((m) => m.id === parsed.macroId);
1256
+ if (!macro || !macroLibrary.macroVisibleIn(macro, parsed.cwd)) throw new Error('unknown macro');
1257
+ const result = macroLibrary.renderMacroPrompt(parsed.prompt, macro.inputs, parsed.inputs ?? {});
1258
+ if (!result.ok) throw new Error(result.error);
1259
+ prompt = result.prompt;
1260
+ }
1261
+ chatRunner.requestExternalNewSession({
1262
+ cwd: parsed.cwd,
1263
+ agentName: parsed.agentName,
1264
+ tag: parsed.tag,
1265
+ title: parsed.title,
1266
+ prompt,
1267
+ });
1282
1268
  return { ok: true };
1283
1269
  },
1284
1270
  };
@@ -1551,6 +1537,11 @@ module.exports = {
1551
1537
  },
1552
1538
  __test: {
1553
1539
  getDispatchMap,
1540
+ auditResultFor,
1541
+ startSessionWatch,
1542
+ pollSessionWatcher,
1543
+ getSessionWatcher: (tabId) => _sessionWatchers.get(tabId),
1544
+ stopAllSessionWatches,
1554
1545
  buildRemoteSessionList,
1555
1546
  setWs: (ws) => { _ws = ws; },
1556
1547
  connect,
@@ -1646,6 +1646,17 @@ export interface ChatExternalSendEvent {
1646
1646
  /** Epic's claudeSessionId + project cwd, resolved by main so the renderer can deliver before the Epic's project is loaded. */
1647
1647
  sessionId?: string;
1648
1648
  cwd?: string;
1649
+ /** Set by main's enqueueExternalPrompt on every push; the renderer must echo it back via `reportExternalSendResult`. */
1650
+ requestId?: string;
1651
+ }
1652
+
1653
+ export type ChatExternalSendDisposition = 'sent' | 'queued' | 'refused';
1654
+ export type ChatExternalSendRefusalReason = 'terminal-attached' | 'completed' | 'unknown-target' | 'empty-prompt';
1655
+ /** What the renderer did with a `ChatExternalSendEvent`, reported back to main. */
1656
+ export interface ChatExternalSendResult {
1657
+ requestId: string;
1658
+ disposition: ChatExternalSendDisposition;
1659
+ reason?: ChatExternalSendRefusalReason;
1649
1660
  }
1650
1661
 
1651
1662
  export type CreateProjectFolderResult =
@@ -2297,6 +2308,8 @@ export interface SessionManagerAPI {
2297
2308
  * tool) pushes a prompt into an open tab's queue from outside the
2298
2309
  * renderer (PRD 753). */
2299
2310
  onExternalSend: (handler: (e: ChatExternalSendEvent) => void) => () => void;
2311
+ /** Reports what the renderer did with an external send (echoing its requestId) back to main. */
2312
+ reportExternalSendResult: (result: ChatExternalSendResult) => Promise<void>;
2300
2313
  /** Fires when a paired phone asks main to start a new Session (payload pre-validated in main). */
2301
2314
  onExternalNewSession: (
2302
2315
  handler: (e: { cwd: string; agentName: string; tag: AgentPersonaTag; title: string; prompt: string }) => void,
@@ -536,6 +536,8 @@ contextBridge.exposeInMainWorld('api', {
536
536
  ipcRenderer.on('chat:external-send', listener);
537
537
  return () => ipcRenderer.removeListener('chat:external-send', listener);
538
538
  },
539
+ /** Reports the renderer's disposition for an external send back to main. */
540
+ reportExternalSendResult: (result) => ipcRenderer.invoke('chat:external-send-result', result),
539
541
  /** Fires when a paired phone asks main to start a new Session. */
540
542
  onExternalNewSession: (handler) => {
541
543
  const listener = (_e, payload) => handler(payload);