cli-wechat-bridge 1.0.9 → 1.1.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.
@@ -3,9 +3,10 @@ import net from "node:net";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
6
+ import { t } from "../i18n/index.js";
6
7
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
7
8
  import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
8
- import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
9
+ import { detectCliApproval, isThinkingForwardEnabled, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
9
10
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
10
11
  import * as shared from "./bridge-adapters.shared.js";
11
12
  const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, delay, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
@@ -90,6 +91,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
90
91
  localTerminalInputListener = null;
91
92
  resizeListener = null;
92
93
  settingsFilePath = null;
94
+ hookErrorLogPath = null;
95
+ hookReceivedCount = 0;
96
+ hookHealthCheckTimer = null;
93
97
  pendingHookApprovals = new Map();
94
98
  recoveringInvalidResume = false;
95
99
  workingNoticeTimer = null;
@@ -98,6 +102,10 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
98
102
  lastCompactCompletionAtMs = 0;
99
103
  startupOutputBuffer = "";
100
104
  hasAutoConfirmedWorkspaceTrustPrompt = false;
105
+ lastAutoApprovedPayload = null;
106
+ transcriptPollTimer = null;
107
+ transcriptTailOffset = 0;
108
+ polledTranscriptSessionId = null;
101
109
  constructor(options) {
102
110
  super(options);
103
111
  const shouldRestoreInitialSession = options.sessionStartMode !== "new";
@@ -163,7 +171,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
163
171
  throw new Error("claude is still working. Wait for the current reply or use /stop.");
164
172
  }
165
173
  if (this.pendingApproval) {
166
- throw new Error("A Claude approval request is pending. Reply with /confirm <code> or /deny.");
174
+ throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
167
175
  }
168
176
  const normalizedText = normalizeOutput(text).trim();
169
177
  this.pendingInjectedInputs.push({
@@ -178,6 +186,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
178
186
  this.pendingCliApprovalHints = null;
179
187
  this.clearWechatWorkingNotice(true);
180
188
  this.setStatus("busy");
189
+ this.startTranscriptThinkingWatch();
181
190
  this.writeToPty(this.buildRemoteInputPayload(text));
182
191
  await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
183
192
  this.writeToPty("\r");
@@ -248,11 +257,34 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
248
257
  this.writeToPty(input);
249
258
  return true;
250
259
  }
260
+ async resolveAllApprovals(action) {
261
+ let count = 0;
262
+ for (const requestId of Array.from(this.pendingHookApprovals.keys())) {
263
+ const pending = this.pendingHookApprovals.get(requestId);
264
+ if (pending) {
265
+ this.pendingHookApprovals.delete(requestId);
266
+ this.respondToClaudeHook(pending.socket, requestId, buildClaudePermissionDecisionHookOutput(action));
267
+ count++;
268
+ }
269
+ }
270
+ if (count > 0) {
271
+ this.clearWechatWorkingNotice();
272
+ this.pendingCliApprovalHints = null;
273
+ this.pendingApproval = null;
274
+ this.state.pendingApproval = null;
275
+ this.state.pendingApprovalOrigin = undefined;
276
+ this.setStatus("busy");
277
+ return count;
278
+ }
279
+ const ok = await this.resolveApproval(action);
280
+ return ok ? 1 : 0;
281
+ }
251
282
  async dispose() {
252
283
  this.detachLocalTerminal();
253
284
  this.clearWechatWorkingNotice(true);
254
285
  this.pendingCliApprovalHints = null;
255
286
  this.flushPendingClaudeHookApprovals();
287
+ this.stopTranscriptThinkingWatch();
256
288
  await super.dispose();
257
289
  await this.stopHookServer();
258
290
  }
@@ -271,6 +303,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
271
303
  afterStart() {
272
304
  this.attachLocalTerminal();
273
305
  this.resizePtyToTerminal();
306
+ this.startHookHealthCheck();
274
307
  }
275
308
  handleData(rawText) {
276
309
  this.renderLocalOutput(rawText);
@@ -447,12 +480,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
447
480
  const hookEntryPath = fs.existsSync(sourceHookEntryPath)
448
481
  ? sourceHookEntryPath
449
482
  : path.join(MODULE_DIR, "claude-hook.js");
483
+ const hookErrorLogPath = path.join(runtimeDir, "hook-error.log");
484
+ this.hookErrorLogPath = hookErrorLogPath;
450
485
  fs.writeFileSync(hookScriptPath, buildClaudeHookScript({
451
486
  platform: process.platform,
452
487
  runtimeExecPath: process.execPath,
453
488
  hookEntryPath,
454
489
  hookPort: this.hookPort,
455
490
  hookToken: this.hookToken,
491
+ hookErrorLogPath,
456
492
  }), "utf8");
457
493
  if (process.platform !== "win32") {
458
494
  fs.chmodSync(hookScriptPath, 0o755);
@@ -501,7 +537,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
501
537
  return;
502
538
  }
503
539
  try {
504
- this.pty.resize(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
540
+ this.pty.resize?.(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
505
541
  }
506
542
  catch {
507
543
  // Best effort resize sync.
@@ -547,6 +583,34 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
547
583
  this.workingNoticeSent = false;
548
584
  }
549
585
  }
586
+ startHookHealthCheck() {
587
+ this.hookReceivedCount = 0;
588
+ this.clearHookHealthCheck();
589
+ this.hookHealthCheckTimer = setTimeout(() => {
590
+ this.hookHealthCheckTimer = null;
591
+ if (this.hookReceivedCount === 0 && !this.shuttingDown) {
592
+ const logHint = this.hookErrorLogPath
593
+ ? t("hook.healthCheck.logHint", { logPath: this.hookErrorLogPath })
594
+ : "";
595
+ this.emit({
596
+ type: "stdout",
597
+ text: [
598
+ t("hook.healthCheck.warning"),
599
+ logHint,
600
+ t("hook.healthCheck.fixes"),
601
+ ].filter(Boolean).join("\n"),
602
+ timestamp: nowIso(),
603
+ });
604
+ }
605
+ }, 15_000);
606
+ this.hookHealthCheckTimer.unref?.();
607
+ }
608
+ clearHookHealthCheck() {
609
+ if (this.hookHealthCheckTimer) {
610
+ clearTimeout(this.hookHealthCheckTimer);
611
+ this.hookHealthCheckTimer = null;
612
+ }
613
+ }
550
614
  emitClaudeNotice(text, level = "info") {
551
615
  const normalized = normalizeOutput(text).trim();
552
616
  if (!normalized) {
@@ -668,6 +732,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
668
732
  this.currentPreview = "(idle)";
669
733
  }
670
734
  handleClaudeHookEnvelope(params) {
735
+ this.hookReceivedCount++;
736
+ this.clearHookHealthCheck();
671
737
  const payload = parseClaudeHookPayload(params.rawPayload);
672
738
  if (!payload?.hook_event_name) {
673
739
  this.respondToClaudeHook(params.socket, params.requestId);
@@ -686,8 +752,32 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
686
752
  this.handleClaudePermissionRequest(params.requestId, payload, params.socket);
687
753
  return;
688
754
  case "Notification":
689
- if (payload.notification_type === "permission_prompt" && this.pendingApproval) {
690
- this.setStatus("awaiting_approval", "Claude approval is required.");
755
+ if (payload.notification_type === "permission_prompt") {
756
+ if (this.pendingApproval) {
757
+ this.setStatus("awaiting_approval", "Claude approval is required.");
758
+ }
759
+ else if (this.lastAutoApprovedPayload) {
760
+ // Auto-approval response failed to reach Claude Code (socket closed before delivery).
761
+ // Fall back to forwarding the approval request to WeChat.
762
+ const fallbackPayload = this.lastAutoApprovedPayload;
763
+ this.lastAutoApprovedPayload = null;
764
+ const request = buildClaudePermissionApprovalRequest(fallbackPayload);
765
+ this.pendingApproval = {
766
+ ...request,
767
+ requestId: undefined,
768
+ confirmInput: this.pendingCliApprovalHints?.confirmInput,
769
+ denyInput: this.pendingCliApprovalHints?.denyInput,
770
+ };
771
+ this.pendingCliApprovalHints = null;
772
+ this.state.pendingApproval = this.pendingApproval;
773
+ this.state.pendingApprovalOrigin = this.state.activeTurnOrigin;
774
+ this.setStatus("awaiting_approval", "Claude auto-approval failed, forwarding to WeChat.");
775
+ this.emit({
776
+ type: "approval_required",
777
+ request: this.pendingApproval,
778
+ timestamp: nowIso(),
779
+ });
780
+ }
691
781
  }
692
782
  this.respondToClaudeHook(params.socket, params.requestId);
693
783
  return;
@@ -740,6 +830,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
740
830
  this.state.resumeConversationId = nextResumeConversationId ?? undefined;
741
831
  this.transcriptPath = nextTranscriptPath;
742
832
  this.state.transcriptPath = nextTranscriptPath ?? undefined;
833
+ this.startTranscriptThinkingWatch();
743
834
  if (previousRuntimeSessionId === payload.session_id) {
744
835
  return;
745
836
  }
@@ -778,6 +869,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
778
869
  this.pendingCliApprovalHints = null;
779
870
  this.clearWechatWorkingNotice(true);
780
871
  this.setStatus("busy");
872
+ this.startTranscriptThinkingWatch();
781
873
  this.emit({
782
874
  type: "mirrored_user_input",
783
875
  text: prompt,
@@ -817,6 +909,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
817
909
  }
818
910
  handleClaudePermissionRequest(requestId, payload, socket) {
819
911
  this.clearWechatWorkingNotice();
912
+ this.lastAutoApprovedPayload = null;
820
913
  const denyMessage = getClaudeWechatOutboundAttachmentDenyMessage(payload);
821
914
  if (denyMessage) {
822
915
  this.pendingApproval = null;
@@ -833,6 +926,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
833
926
  if (!this.pendingApproval && !this.state.pendingApproval) {
834
927
  this.setStatus("busy", `Claude approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
835
928
  }
929
+ this.lastAutoApprovedPayload = payload;
836
930
  this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput(autoResponse.action));
837
931
  return;
838
932
  }
@@ -898,6 +992,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
898
992
  handleClaudeStop(payload) {
899
993
  this.clearWechatWorkingNotice(true);
900
994
  this.pendingCliApprovalHints = null;
995
+ this.lastAutoApprovedPayload = null;
901
996
  this.flushPendingClaudeHookApprovals();
902
997
  this.pendingApproval = null;
903
998
  this.state.pendingApproval = null;
@@ -915,11 +1010,97 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
915
1010
  summary: this.currentPreview,
916
1011
  timestamp: nowIso(),
917
1012
  });
1013
+ this.stopTranscriptThinkingWatch();
918
1014
  this.currentPreview = "(idle)";
919
1015
  }
920
1016
  handleClaudeStopFailure(payload) {
1017
+ this.lastAutoApprovedPayload = null;
1018
+ this.stopTranscriptThinkingWatch();
921
1019
  this.failClaudeTurn(buildClaudeFailureMessage(payload));
922
1020
  }
1021
+ startTranscriptThinkingWatch() {
1022
+ this.stopTranscriptThinkingWatch();
1023
+ if (!this.transcriptPath) {
1024
+ return;
1025
+ }
1026
+ if (!isThinkingForwardEnabled()) {
1027
+ return;
1028
+ }
1029
+ this.transcriptTailOffset = 0;
1030
+ try {
1031
+ const stat = fs.statSync(this.transcriptPath);
1032
+ this.transcriptTailOffset = stat.size;
1033
+ }
1034
+ catch {
1035
+ return;
1036
+ }
1037
+ const watchPath = this.transcriptPath;
1038
+ const sessionId = this.runtimeSessionId;
1039
+ this.polledTranscriptSessionId = sessionId;
1040
+ this.transcriptPollTimer = setInterval(() => {
1041
+ if (this.shuttingDown) {
1042
+ this.stopTranscriptThinkingWatch();
1043
+ return;
1044
+ }
1045
+ if (this.polledTranscriptSessionId !== this.runtimeSessionId) {
1046
+ this.stopTranscriptThinkingWatch();
1047
+ return;
1048
+ }
1049
+ try {
1050
+ const stat = fs.statSync(watchPath);
1051
+ if (stat.size <= this.transcriptTailOffset) {
1052
+ return;
1053
+ }
1054
+ const fd = fs.openSync(watchPath, "r");
1055
+ const buf = Buffer.alloc(stat.size - this.transcriptTailOffset);
1056
+ fs.readSync(fd, buf, 0, buf.length, this.transcriptTailOffset);
1057
+ fs.closeSync(fd);
1058
+ this.transcriptTailOffset = stat.size;
1059
+ const newText = buf.toString("utf8");
1060
+ const lines = newText.split(/\r?\n/).filter(Boolean);
1061
+ for (const line of lines) {
1062
+ let parsed;
1063
+ try {
1064
+ parsed = JSON.parse(line);
1065
+ }
1066
+ catch {
1067
+ continue;
1068
+ }
1069
+ if (!parsed ||
1070
+ parsed.type !== "assistant" ||
1071
+ !Array.isArray(parsed.message?.content)) {
1072
+ continue;
1073
+ }
1074
+ for (const block of parsed.message.content) {
1075
+ if (block.type === "thinking" &&
1076
+ typeof block.thinking === "string" &&
1077
+ block.thinking.trim()) {
1078
+ const thinking = normalizeOutput(block.thinking).trim();
1079
+ if (thinking) {
1080
+ this.emit({
1081
+ type: "thinking",
1082
+ text: thinking,
1083
+ timestamp: nowIso(),
1084
+ });
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ catch {
1091
+ // File may be temporarily locked or deleted; silently retry next poll.
1092
+ }
1093
+ }, 800);
1094
+ this.transcriptPollTimer.unref?.();
1095
+ }
1096
+ stopTranscriptThinkingWatch() {
1097
+ if (this.transcriptPollTimer) {
1098
+ clearInterval(this.transcriptPollTimer);
1099
+ this.transcriptPollTimer = null;
1100
+ }
1101
+ this.transcriptTailOffset = 0;
1102
+ this.polledTranscriptSessionId = null;
1103
+ }
923
1104
  respondToClaudeHook(socket, requestId, stdout) {
924
1105
  try {
925
1106
  socket.end(`${JSON.stringify({ requestId, stdout })}\n`);
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
4
4
  import { spawn as spawnChild } from "node:child_process";
5
5
  import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
6
6
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
7
+ import { killProcessTreeSync } from "./bridge-process-reaper.js";
7
8
  import * as shared from "./bridge-adapters.shared.js";
8
9
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
10
  import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
@@ -35,7 +36,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
35
36
  pendingTurnThreadId = null;
36
37
  interruptPendingTurnStart = false;
37
38
  pendingThreadFollowId = null;
38
- pendingApprovalRequest = null;
39
+ pendingApprovalRequests = [];
39
40
  pendingUserInputRequest = null;
40
41
  queuedTurnNotifications = [];
41
42
  queuedTurnServerRequests = [];
@@ -137,7 +138,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
137
138
  throw new Error("codex is still working. Wait for the current reply or use /stop.");
138
139
  }
139
140
  if (this.pendingApproval) {
140
- throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
141
+ throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
141
142
  }
142
143
  if (this.startupBlocker) {
143
144
  throw new Error("Codex is waiting for local terminal input before the session can continue.");
@@ -185,18 +186,35 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
185
186
  return true;
186
187
  }
187
188
  async resolveApproval(action) {
188
- if (!this.pendingApproval) {
189
+ if (!this.pendingApproval && this.pendingApprovalRequests.length === 0) {
189
190
  return false;
190
191
  }
191
- if (this.pendingApprovalRequest && this.rpcSocket) {
192
- const request = this.pendingApprovalRequest;
193
- await this.respondToApprovalRequest(request, action);
192
+ if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
193
+ for (const request of this.pendingApprovalRequests) {
194
+ await this.respondToApprovalRequest(request, action);
195
+ }
194
196
  this.clearPendingApprovalState();
195
197
  this.setStatus("busy");
196
198
  return true;
197
199
  }
198
200
  return await super.resolveApproval(action);
199
201
  }
202
+ async resolveAllApprovals(action) {
203
+ if (this.pendingApprovalRequests.length === 0 && !this.pendingApproval) {
204
+ return 0;
205
+ }
206
+ if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
207
+ const count = this.pendingApprovalRequests.length;
208
+ for (const request of this.pendingApprovalRequests) {
209
+ await this.respondToApprovalRequest(request, action);
210
+ }
211
+ this.clearPendingApprovalState();
212
+ this.setStatus("busy");
213
+ return count;
214
+ }
215
+ const ok = await super.resolveApproval(action);
216
+ return ok ? 1 : 0;
217
+ }
200
218
  async submitUserInput(answers) {
201
219
  if (!this.pendingUserInputRequest) {
202
220
  return false;
@@ -450,7 +468,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
450
468
  };
451
469
  child.once("exit", () => finish());
452
470
  try {
453
- child.kill();
471
+ if (child.pid) {
472
+ killProcessTreeSync(child.pid);
473
+ }
474
+ else {
475
+ child.kill();
476
+ }
454
477
  }
455
478
  catch {
456
479
  finish();
@@ -783,7 +806,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
783
806
  this.recoverStaleBusyStateIfNeeded();
784
807
  this.recoverStaleActiveTurnStateIfNeeded();
785
808
  if (this.pendingApproval) {
786
- throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
809
+ throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
787
810
  }
788
811
  if (this.pendingUserInputRequest || this.state.pendingUserInput) {
789
812
  throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
@@ -1247,7 +1270,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1247
1270
  throw new Error("A thread id is required to resume a Codex thread.");
1248
1271
  }
1249
1272
  if (this.pendingApproval) {
1250
- throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
1273
+ throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1251
1274
  }
1252
1275
  if (!options.startup &&
1253
1276
  (this.pendingTurnStart ||
@@ -1348,7 +1371,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1348
1371
  status: this.state.status,
1349
1372
  pendingTurnStart: this.pendingTurnStart,
1350
1373
  hasActiveTurn: Boolean(this.activeTurn),
1351
- hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequest),
1374
+ hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequests.length),
1352
1375
  hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
1353
1376
  activeTurnId: this.state.activeTurnId,
1354
1377
  })) {
@@ -1366,7 +1389,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1366
1389
  if (!this.activeTurn ||
1367
1390
  this.pendingTurnStart ||
1368
1391
  this.pendingApproval ||
1369
- this.pendingApprovalRequest ||
1392
+ this.pendingApprovalRequests.length ||
1370
1393
  this.state.status === "busy" ||
1371
1394
  this.state.status === "awaiting_approval" ||
1372
1395
  this.state.activeTurnId) {
@@ -1586,7 +1609,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1586
1609
  return true;
1587
1610
  }
1588
1611
  clearPendingApprovalState() {
1589
- this.pendingApprovalRequest = null;
1612
+ this.pendingApprovalRequests = [];
1590
1613
  this.pendingApproval = null;
1591
1614
  this.state.pendingApproval = null;
1592
1615
  this.state.pendingApprovalOrigin = undefined;
@@ -1850,10 +1873,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1850
1873
  case "serverRequest/resolved": {
1851
1874
  const requestId = getCodexRpcRequestId(params.requestId);
1852
1875
  if (requestId !== null &&
1853
- this.pendingApprovalRequest &&
1854
- requestId === this.pendingApprovalRequest.requestId &&
1855
- trackedTurn.turnId === this.pendingApprovalRequest.turnId) {
1856
- this.clearPendingApprovalState();
1876
+ this.pendingApprovalRequests.some((r) => r.requestId === requestId && r.turnId === trackedTurn.turnId)) {
1877
+ this.pendingApprovalRequests = this.pendingApprovalRequests.filter((r) => r.requestId !== requestId);
1878
+ if (this.pendingApprovalRequests.length === 0) {
1879
+ this.clearPendingApprovalState();
1880
+ }
1857
1881
  if (this.state.status === "awaiting_approval") {
1858
1882
  this.setStatus("busy", "Codex approval resolved.");
1859
1883
  }
@@ -1975,14 +1999,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1975
1999
  });
1976
2000
  return;
1977
2001
  }
1978
- this.pendingApprovalRequest = {
2002
+ this.pendingApprovalRequests.push({
1979
2003
  requestId,
1980
2004
  method,
1981
2005
  threadId: trackedTurn.threadId,
1982
2006
  turnId: trackedTurn.turnId,
1983
2007
  origin: trackedTurn.origin,
1984
2008
  params,
1985
- };
2009
+ });
1986
2010
  this.pendingApproval = request;
1987
2011
  this.state.pendingApproval = request;
1988
2012
  this.state.pendingApprovalOrigin = trackedTurn.origin;
@@ -2146,8 +2170,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2146
2170
  : completedTrackedTurn.origin === "local"
2147
2171
  ? "Local terminal turn completed."
2148
2172
  : this.currentPreview;
2149
- if (this.pendingApprovalRequest &&
2150
- this.pendingApprovalRequest.turnId === trackedTurn.turnId) {
2173
+ if (this.pendingApprovalRequests.length > 0 &&
2174
+ this.pendingApprovalRequests.some((r) => r.turnId === trackedTurn.turnId)) {
2151
2175
  this.clearPendingApprovalState();
2152
2176
  }
2153
2177
  if (this.pendingUserInputRequest &&
@@ -2241,7 +2265,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2241
2265
  this.activeTurn.origin !== "wechat" ||
2242
2266
  this.pendingTurnStart ||
2243
2267
  this.pendingApproval ||
2244
- this.pendingApprovalRequest ||
2268
+ this.pendingApprovalRequests.length ||
2245
2269
  this.pendingUserInputRequest ||
2246
2270
  this.state.pendingUserInput ||
2247
2271
  !this.collectTurnOutput(turnId)) {
@@ -2259,7 +2283,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2259
2283
  const activeTurn = this.activeTurn;
2260
2284
  const finalText = this.collectTurnOutput(turnId);
2261
2285
  const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
2262
- const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequest);
2286
+ const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequests.length);
2263
2287
  const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
2264
2288
  const nowMs = Date.now();
2265
2289
  if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
@@ -2339,7 +2363,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2339
2363
  };
2340
2364
  child.once("exit", () => finish());
2341
2365
  try {
2342
- child.kill();
2366
+ if (child.pid) {
2367
+ killProcessTreeSync(child.pid);
2368
+ }
2369
+ else {
2370
+ child.kill();
2371
+ }
2343
2372
  }
2344
2373
  catch {
2345
2374
  finish();
@@ -2370,7 +2399,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2370
2399
  }
2371
2400
  if (this.nativeProcess) {
2372
2401
  try {
2373
- this.nativeProcess.kill();
2402
+ if (this.nativeProcess.pid) {
2403
+ killProcessTreeSync(this.nativeProcess.pid);
2404
+ }
2405
+ else {
2406
+ this.nativeProcess.kill();
2407
+ }
2374
2408
  }
2375
2409
  catch {
2376
2410
  // Best effort cleanup after panel client failure.
@@ -1,10 +1,11 @@
1
1
  import net from "node:net";
2
2
  import { spawn as spawnPty } from "node-pty";
3
+ import { t } from "../i18n/index.js";
3
4
  import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
4
5
  import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
5
6
  import * as shared from "./bridge-adapters.shared.js";
6
7
  import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
7
- const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, } = shared;
8
+ const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, buildSpawnDiagnostic, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, spawnFallbackProcess, } = shared;
8
9
  export class LocalCompanionProxyAdapter {
9
10
  options;
10
11
  state;
@@ -136,6 +137,10 @@ export class LocalCompanionProxyAdapter {
136
137
  });
137
138
  return Boolean(result);
138
139
  }
140
+ async resolveAllApprovals(action) {
141
+ const ok = await this.resolveApproval(action);
142
+ return ok ? 1 : 0;
143
+ }
139
144
  async submitUserInput(_answers) {
140
145
  return false;
141
146
  }
@@ -299,6 +304,7 @@ export class LocalCompanionProxyAdapter {
299
304
  this.detachMessageListener = null;
300
305
  if (this.socket) {
301
306
  this.socket.removeAllListeners();
307
+ this.socket.on("error", () => { });
302
308
  this.socket.destroy();
303
309
  this.socket = null;
304
310
  }
@@ -509,10 +515,27 @@ export class AbstractPtyAdapter {
509
515
  try {
510
516
  spawnTarget = resolveSpawnTarget(this.options.command, this.options.kind);
511
517
  const env = this.buildEnv();
512
- const ptyProcess = spawnPty(spawnTarget.file, [...spawnTarget.args, ...this.buildSpawnArgs()], buildPtySpawnOptions({
513
- cwd: this.options.cwd,
514
- env,
515
- }));
518
+ const spawnArgs = [...spawnTarget.args, ...this.buildSpawnArgs()];
519
+ let ptyProcess;
520
+ let usedFallback = false;
521
+ try {
522
+ ptyProcess = spawnPty(spawnTarget.file, spawnArgs, buildPtySpawnOptions({
523
+ cwd: this.options.cwd,
524
+ env,
525
+ }));
526
+ }
527
+ catch (ptyErr) {
528
+ usedFallback = true;
529
+ try {
530
+ ptyProcess = spawnFallbackProcess(spawnTarget.file, spawnArgs, {
531
+ cwd: this.options.cwd,
532
+ env,
533
+ });
534
+ }
535
+ catch (fallbackErr) {
536
+ throw new Error(buildSpawnDiagnostic(ptyErr, spawnTarget), { cause: fallbackErr });
537
+ }
538
+ }
516
539
  this.pty = ptyProcess;
517
540
  this.shuttingDown = false;
518
541
  this.hasAcceptedInput = false;
@@ -523,6 +546,13 @@ export class AbstractPtyAdapter {
523
546
  ptyProcess.onData((data) => this.handleData(data));
524
547
  ptyProcess.onExit(({ exitCode }) => this.handleExit(exitCode));
525
548
  this.afterStart();
549
+ if (usedFallback) {
550
+ this.emit({
551
+ type: "stdout",
552
+ text: t("pty.fallback.warning"),
553
+ timestamp: nowIso(),
554
+ });
555
+ }
526
556
  this.setStatus("idle", `${this.options.kind} adapter is ready.`);
527
557
  }
528
558
  catch (err) {
@@ -584,6 +614,10 @@ export class AbstractPtyAdapter {
584
614
  this.state.pendingApproval = null;
585
615
  return true;
586
616
  }
617
+ async resolveAllApprovals(action) {
618
+ const ok = await this.resolveApproval(action);
619
+ return ok ? 1 : 0;
620
+ }
587
621
  async submitUserInput(_answers) {
588
622
  return false;
589
623
  }
@@ -110,7 +110,7 @@ export class OpenCodeServerAdapter {
110
110
  throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
111
111
  }
112
112
  if (this.pendingPermission) {
113
- throw new Error("An OpenCode approval request is pending. Reply with /confirm <code> or /deny.");
113
+ throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
114
114
  }
115
115
  const normalized = normalizeOutput(text).trim();
116
116
  if (!normalized) {
@@ -163,7 +163,7 @@ export class OpenCodeServerAdapter {
163
163
  throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
164
164
  }
165
165
  if (this.pendingPermission) {
166
- throw new Error("An OpenCode approval request is pending. Reply with /confirm <code> or /deny.");
166
+ throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
167
167
  }
168
168
  this.outputBatcher.clear();
169
169
  this.clearStreamedPartState();
@@ -254,6 +254,10 @@ export class OpenCodeServerAdapter {
254
254
  this.setStatus("busy");
255
255
  return true;
256
256
  }
257
+ async resolveAllApprovals(action) {
258
+ const ok = await this.resolveApproval(action);
259
+ return ok ? 1 : 0;
260
+ }
257
261
  async submitUserInput(_answers) {
258
262
  return false;
259
263
  }