cli-wechat-bridge 1.1.6 → 1.1.8

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 (52) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +454 -475
  3. package/bin/_run-entry.mjs +23 -5
  4. package/bin/wecom-check-update.mjs +5 -0
  5. package/bin/wecom-claude.mjs +5 -0
  6. package/bin/wecom-codex.mjs +5 -0
  7. package/bin/wecom-daemon.mjs +5 -0
  8. package/bin/wecom-opencode.mjs +5 -0
  9. package/bin/wecom-pi.mjs +5 -0
  10. package/bin/wecom-setup.mjs +5 -0
  11. package/dist/bridge/adapter-control.js +58 -0
  12. package/dist/bridge/bridge-adapters.claude.js +155 -4
  13. package/dist/bridge/bridge-adapters.codex.js +112 -7
  14. package/dist/bridge/bridge-adapters.core.js +19 -1
  15. package/dist/bridge/bridge-adapters.opencode.js +150 -1
  16. package/dist/bridge/bridge-adapters.pi.js +39 -0
  17. package/dist/bridge/bridge-process-reaper.js +35 -2
  18. package/dist/bridge/bridge-state.js +10 -3
  19. package/dist/bridge/claude-native-controls.js +133 -0
  20. package/dist/bridge/native-terminal-control.js +253 -0
  21. package/dist/bridge/wechat-bridge.js +389 -87
  22. package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
  23. package/dist/channels/wecom/bridge-final-reply.js +26 -0
  24. package/dist/channels/wecom/setup-cli.js +6 -0
  25. package/dist/channels/wecom/setup.js +176 -0
  26. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  27. package/dist/channels/wecom/wecom-config.js +66 -0
  28. package/dist/channels/wecom/wecom-media.js +141 -0
  29. package/dist/channels/wecom/wecom-message.js +149 -0
  30. package/dist/channels/wecom/wecom-transport.js +308 -0
  31. package/dist/companion/codex-remote-client.js +24 -3
  32. package/dist/companion/codex-visible-client-link.js +101 -0
  33. package/dist/companion/local-companion-start.js +56 -11
  34. package/dist/companion/local-companion.js +19 -1
  35. package/dist/companion/pi-tui-bridge-extension.js +56 -3
  36. package/dist/core/bridge-control.js +8 -0
  37. package/dist/core/channel-types.js +3 -1
  38. package/dist/core/conversation-routing.js +48 -0
  39. package/dist/core/direct-run.js +17 -0
  40. package/dist/core/inbound-message-claims.js +61 -0
  41. package/dist/daemon/daemon-link.js +3 -0
  42. package/dist/daemon/wechat-daemon.js +335 -60
  43. package/dist/i18n/messages-en.js +3 -1
  44. package/dist/i18n/messages-zh.js +3 -1
  45. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  46. package/dist/runtime/runtime-types.js +3 -0
  47. package/dist/utils/doctor.js +30 -10
  48. package/dist/utils/version-checker.js +11 -11
  49. package/dist/wechat/setup.js +113 -13
  50. package/dist/wechat/wechat-transport.js +96 -58
  51. package/package.json +116 -104
  52. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -8,6 +8,8 @@ const BIN_DIR = path.dirname(fileURLToPath(import.meta.url));
8
8
  const PROJECT_DIR = path.resolve(BIN_DIR, "..");
9
9
  export const MIN_NODE_VERSION = Object.freeze([22, 13, 0]);
10
10
  export const MIN_NODE_VERSION_TEXT = MIN_NODE_VERSION.join(".");
11
+ export const MIN_PI_NODE_VERSION = Object.freeze([22, 19, 0]);
12
+ export const MIN_PI_NODE_VERSION_TEXT = MIN_PI_NODE_VERSION.join(".");
11
13
 
12
14
  export function isSupportedNodeVersion(version) {
13
15
  const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version);
@@ -24,19 +26,21 @@ export function isSupportedNodeVersion(version) {
24
26
  return true;
25
27
  }
26
28
 
27
- function ensureSupportedNodeVersion() {
29
+ function ensureSupportedNodeVersion(extraArgs = []) {
28
30
  if (process.env.CLI_BRIDGE_SKIP_NODE_CHECK === "1") {
29
31
  return;
30
32
  }
31
33
 
34
+ const piRequested = extraArgs.some((arg, index) => arg === "pi" && extraArgs[index - 1] === "--adapter");
32
35
  if (isSupportedNodeVersion(process.versions.node)) {
33
- return;
36
+ if (!piRequested || compareNodeVersion(process.versions.node, MIN_PI_NODE_VERSION) >= 0) return;
34
37
  }
35
38
 
39
+ const minimumText = piRequested ? MIN_PI_NODE_VERSION_TEXT : MIN_NODE_VERSION_TEXT;
36
40
  process.stderr.write(
37
41
  [
38
- `[cli-wechat-bridge] Node.js >= ${MIN_NODE_VERSION_TEXT} is required, but you are running ${process.version}.`,
39
- `[cli-wechat-bridge] 需要 Node.js >= ${MIN_NODE_VERSION_TEXT},当前版本为 ${process.version}。`,
42
+ `[cli-wechat-bridge] Node.js >= ${minimumText} is required, but you are running ${process.version}.`,
43
+ `[cli-wechat-bridge] 需要 Node.js >= ${minimumText},当前版本为 ${process.version}。`,
40
44
  "Install the latest LTS from https://nodejs.org/ (or via nvm), then retry.",
41
45
  "Set CLI_BRIDGE_SKIP_NODE_CHECK=1 to bypass this check at your own risk.",
42
46
  "",
@@ -45,8 +49,22 @@ function ensureSupportedNodeVersion() {
45
49
  process.exit(1);
46
50
  }
47
51
 
52
+ function compareNodeVersion(version, minimum) {
53
+ const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version);
54
+ if (!match) return -1;
55
+ const current = [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)];
56
+ for (let index = 0; index < minimum.length; index += 1) {
57
+ if (current[index] !== minimum[index]) return current[index] > minimum[index] ? 1 : -1;
58
+ }
59
+ return 0;
60
+ }
61
+
62
+ export function isSupportedPiNodeVersion(version) {
63
+ return compareNodeVersion(version, MIN_PI_NODE_VERSION) >= 0;
64
+ }
65
+
48
66
  export function runJsEntry(relativeEntryPath, extraArgs = []) {
49
- ensureSupportedNodeVersion();
67
+ ensureSupportedNodeVersion([...extraArgs, ...process.argv.slice(2)]);
50
68
  const entryPath = path.join(PROJECT_DIR, relativeEntryPath);
51
69
  const child = spawn(
52
70
  process.execPath,
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/commands/check-update.js");
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "claude", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "codex", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/daemon/wechat-daemon.js", ["--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "opencode", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/companion/local-companion-start.js", ["--adapter", "pi", "--channel", "wecom"]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runJsEntry } from "./_run-entry.mjs";
4
+
5
+ runJsEntry("dist/channels/wecom/setup-cli.js");
@@ -0,0 +1,58 @@
1
+ const snapshots = new WeakMap();
2
+ export const MODEL_SNAPSHOT_TTL_MS = 5 * 60_000;
3
+ export function invalidateModelSnapshot(adapter) {
4
+ snapshots.delete(adapter);
5
+ }
6
+ function snapshotOwner(adapter, senderId) {
7
+ const state = adapter.getState();
8
+ return JSON.stringify([
9
+ senderId, state.kind, state.cwd, state.pid, state.startedAt,
10
+ state.resumeConversationId ?? state.sharedSessionId ?? state.sharedThreadId,
11
+ state.lastSessionSwitchAt ?? state.lastThreadSwitchAt,
12
+ ]);
13
+ }
14
+ export async function handleAdapterControl(adapter, senderId, command) {
15
+ const kind = adapter.getState().kind;
16
+ const name = kind === "claude" ? "Claude Code" : kind === "opencode" ? "OpenCode" : kind === "codex" ? "Codex" : "Pi";
17
+ try {
18
+ if (command.type === "plan") {
19
+ if (!adapter.setPlanMode)
20
+ throw new Error(`Plan mode is not available for ${name}.`);
21
+ const enabled = await adapter.setPlanMode(command.enabled);
22
+ return enabled
23
+ ? `${name} plan mode enabled. Send /plan off to restore the previous mode.`
24
+ : `${name} plan mode disabled.`;
25
+ }
26
+ if (!adapter.listModels || !adapter.selectModel)
27
+ throw new Error(`Model selection is not available for ${name}.`);
28
+ const owner = snapshotOwner(adapter, senderId);
29
+ if (!command.target) {
30
+ snapshots.delete(adapter);
31
+ const models = await adapter.listModels();
32
+ if (snapshotOwner(adapter, senderId) !== owner)
33
+ throw new Error("The active session changed. Send /model again.");
34
+ if (!models.length)
35
+ return `${name} did not return any available models.`;
36
+ snapshots.set(adapter, { owner, createdAtMs: Date.now(), models });
37
+ return [
38
+ `Available ${name} models:`, "",
39
+ ...models.map((model, index) => `${index + 1}. ${model.displayName}${model.isCurrent ? " (current)" : ""}`),
40
+ "", "Reply with /model <number> to switch. The list is valid for 5 minutes.",
41
+ ].join("\n");
42
+ }
43
+ const snapshot = snapshots.get(adapter);
44
+ if (!snapshot || snapshot.owner !== owner || Date.now() - snapshot.createdAtMs > MODEL_SNAPSHOT_TTL_MS) {
45
+ throw new Error("The model list has expired or belongs to another session. Send /model again.");
46
+ }
47
+ const index = Number(command.target);
48
+ if (!/^\d+$/.test(command.target) || !Number.isSafeInteger(index) || index < 1 || index > snapshot.models.length) {
49
+ throw new Error("Model selection is outside the displayed range. Send /model again to refresh the list.");
50
+ }
51
+ const model = await adapter.selectModel(snapshot.models[index - 1].id);
52
+ snapshots.delete(adapter);
53
+ return `${name} model switched to ${model.displayName}.`;
54
+ }
55
+ catch (error) {
56
+ return error instanceof Error ? error.message : String(error);
57
+ }
58
+ }
@@ -1,3 +1,5 @@
1
+ import { NativeTerminalControl, readClaudePermissionMode } from "./native-terminal-control.js";
2
+ import { ClaudeNativeControls } from "./claude-native-controls.js";
1
3
  import fs from "node:fs";
2
4
  import net from "node:net";
3
5
  import os from "node:os";
@@ -18,6 +20,8 @@ const CLAUDE_COMPACT_DEDUP_MS = 2_000;
18
20
  const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
19
21
  const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
20
22
  const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
23
+ const CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS = [200, 500, 1_000];
24
+ const CLAUDE_INTERACTIVE_READY_TIMEOUT_MS = 10_000;
21
25
  const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
22
26
  const CLAUDE_SESSION_METADATA_READ_BYTES = 64 * 1024;
23
27
  const CLAUDE_MAX_SANITIZED_PROJECT_PATH = 200;
@@ -337,6 +341,26 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
337
341
  polledTranscriptSessionId = null;
338
342
  pendingResume = null;
339
343
  localResumeSessionEndObserved = false;
344
+ interactiveReady = true;
345
+ interactiveReadyTimer = null;
346
+ interactiveReadyWaiters = new Set();
347
+ remoteSubmitRetryTimer = null;
348
+ remoteSubmitRetryIndex = 0;
349
+ remoteSubmitText = null;
350
+ terminalControl = new NativeTerminalControl({
351
+ write: (text) => this.writeToPty(text),
352
+ identity: () => JSON.stringify([this.state.pid, this.runtimeSessionId, this.resumeConversationId]),
353
+ isPromptEmpty: () => readClaudePermissionMode(this.terminalControl.text) !== null,
354
+ assertReady: () => {
355
+ if (!this.pty || this.state.status !== "idle" || this.pendingApproval || this.pendingResume) {
356
+ throw new Error("Claude must be idle with no pending approval before using remote controls.");
357
+ }
358
+ },
359
+ });
360
+ nativeControls = new ClaudeNativeControls(this.terminalControl, () => this.resumeConversationId ?? this.runtimeSessionId ?? "");
361
+ async listModels() { return this.nativeControls.listModels(); }
362
+ async selectModel(modelId) { return this.nativeControls.selectModel(modelId); }
363
+ async setPlanMode(enabled) { return this.nativeControls.setPlanMode(enabled); }
340
364
  constructor(options) {
341
365
  super(options);
342
366
  const shouldRestoreInitialSession = options.sessionStartMode !== "new";
@@ -364,6 +388,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
364
388
  if (this.pty) {
365
389
  return;
366
390
  }
391
+ this.beginInteractiveReadinessWait();
367
392
  this.startupOutputBuffer = "";
368
393
  this.hasAutoConfirmedWorkspaceTrustPrompt = false;
369
394
  ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
@@ -385,16 +410,22 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
385
410
  this.state.activeRuntimeSessionId = undefined;
386
411
  }
387
412
  }
388
- await this.startHookServer();
389
413
  try {
414
+ await this.startHookServer();
390
415
  await super.start();
416
+ if (!this.interactiveReady) {
417
+ this.setStatus("starting", "Waiting for Claude interactive session...");
418
+ }
391
419
  }
392
420
  catch (error) {
421
+ this.rejectInteractiveReadiness(error instanceof Error ? error : new Error(String(error)));
393
422
  await this.stopHookServer();
394
423
  throw error;
395
424
  }
396
425
  }
397
426
  async sendInput(text) {
427
+ if (this.terminalControl.active)
428
+ throw new Error("A Claude control operation is in progress.");
398
429
  if (!this.pty) {
399
430
  throw new Error("claude adapter is not running.");
400
431
  }
@@ -404,7 +435,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
404
435
  if (this.pendingApproval) {
405
436
  throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
406
437
  }
438
+ await this.waitForInteractiveReadiness();
439
+ if (!this.pty) {
440
+ throw new Error("claude adapter is not running.");
441
+ }
442
+ if (this.getState().status === "busy") {
443
+ throw new Error("claude is still working. Wait for the current reply or use /stop.");
444
+ }
445
+ if (this.pendingApproval) {
446
+ throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
447
+ }
407
448
  const normalizedText = normalizeOutput(text).trim();
449
+ this.clearRemoteSubmitRetry();
408
450
  this.pendingInjectedInputs.push({
409
451
  normalizedText,
410
452
  createdAtMs: Date.now(),
@@ -421,12 +463,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
421
463
  this.writeToPty(this.buildRemoteInputPayload(text));
422
464
  await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
423
465
  this.writeToPty("\r");
466
+ this.armRemoteSubmitRetry(normalizedText);
424
467
  this.armWechatWorkingNotice();
425
468
  }
426
469
  async listResumeSessions(limit = 10) {
427
470
  return listClaudeResumeSessions(this.options.cwd, limit);
428
471
  }
429
472
  async resumeSession(sessionId) {
473
+ if (this.terminalControl.active)
474
+ throw new Error("A CLI control operation is in progress. Wait for it to finish before switching sessions.");
430
475
  if (!this.pty) {
431
476
  throw new Error("Claude is not running yet.");
432
477
  }
@@ -523,6 +568,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
523
568
  return findClaudeResumeSessionFile(this.options.cwd, sessionId);
524
569
  }
525
570
  async interrupt() {
571
+ this.terminalControl.cancel("The remote control operation was interrupted.");
526
572
  if (!this.pty) {
527
573
  return false;
528
574
  }
@@ -530,6 +576,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
530
576
  return false;
531
577
  }
532
578
  this.clearWechatWorkingNotice(true);
579
+ this.clearRemoteSubmitRetry();
533
580
  this.pendingCliApprovalHints = null;
534
581
  this.flushPendingClaudeHookApprovals();
535
582
  if (this.pendingApproval) {
@@ -552,6 +599,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
552
599
  return true;
553
600
  }
554
601
  async reset() {
602
+ if (this.terminalControl.active)
603
+ throw new Error("A CLI control operation is in progress. Wait for it to finish before switching sessions.");
555
604
  this.rejectPendingResume(new Error("Claude was reset during session switching."));
556
605
  this.clearWechatWorkingNotice(true);
557
606
  this.pendingCliApprovalHints = null;
@@ -566,7 +615,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
566
615
  this.state.lastSessionSwitchAt = undefined;
567
616
  this.state.lastSessionSwitchSource = undefined;
568
617
  this.state.lastSessionSwitchReason = undefined;
569
- await super.reset();
618
+ await this.dispose();
619
+ await this.start();
570
620
  }
571
621
  async resolveApproval(action) {
572
622
  if (!this.pendingApproval) {
@@ -620,9 +670,12 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
620
670
  return ok ? 1 : 0;
621
671
  }
622
672
  async dispose() {
673
+ this.terminalControl.cancel("Claude is shutting down.");
623
674
  this.rejectPendingResume(new Error("Claude is shutting down during session switching."));
624
675
  this.detachLocalTerminal();
625
676
  this.clearWechatWorkingNotice(true);
677
+ this.clearRemoteSubmitRetry();
678
+ this.rejectInteractiveReadiness(new Error("Claude is shutting down."));
626
679
  this.pendingCliApprovalHints = null;
627
680
  this.flushPendingClaudeHookApprovals();
628
681
  this.stopTranscriptThinkingWatch();
@@ -647,6 +700,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
647
700
  this.startHookHealthCheck();
648
701
  }
649
702
  handleData(rawText) {
703
+ this.terminalControl.feed(rawText);
650
704
  this.renderLocalOutput(rawText);
651
705
  const text = normalizeOutput(rawText);
652
706
  if (!text) {
@@ -718,10 +772,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
718
772
  return true;
719
773
  }
720
774
  handleExit(exitCode) {
775
+ this.terminalControl.cancel("Claude exited during the control operation.");
721
776
  this.detachLocalTerminal();
722
777
  this.clearWechatWorkingNotice(true);
778
+ this.clearRemoteSubmitRetry();
723
779
  this.pendingCliApprovalHints = null;
724
780
  this.rejectPendingResume(new Error("Claude exited before the requested session switch completed."));
781
+ if (!this.recoveringInvalidResume) {
782
+ this.rejectInteractiveReadiness(new Error("Claude exited before its interactive session became ready."));
783
+ }
725
784
  void this.stopHookServer();
726
785
  if (this.recoveringInvalidResume && !this.shuttingDown) {
727
786
  this.clearCompletionTimer();
@@ -870,6 +929,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
870
929
  }
871
930
  this.localTerminalInputListener = (chunk) => {
872
931
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
932
+ this.terminalControl.localInput(text);
873
933
  this.writeToPty(text);
874
934
  };
875
935
  process.stdin.on("data", this.localTerminalInputListener);
@@ -898,10 +958,11 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
898
958
  }
899
959
  }
900
960
  resizePtyToTerminal() {
901
- if (!this.pty || !process.stdout.isTTY) {
961
+ if (!this.pty) {
902
962
  return;
903
963
  }
904
964
  try {
965
+ this.terminalControl.resize(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
905
966
  this.pty.resize?.(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
906
967
  }
907
968
  catch {
@@ -996,6 +1057,91 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
996
1057
  }
997
1058
  return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
998
1059
  }
1060
+ beginInteractiveReadinessWait() {
1061
+ this.clearInteractiveReadinessTimer();
1062
+ this.interactiveReady = false;
1063
+ this.interactiveReadyTimer = setTimeout(() => {
1064
+ this.interactiveReadyTimer = null;
1065
+ this.markInteractiveReady();
1066
+ }, CLAUDE_INTERACTIVE_READY_TIMEOUT_MS);
1067
+ this.interactiveReadyTimer.unref?.();
1068
+ }
1069
+ waitForInteractiveReadiness() {
1070
+ if (this.interactiveReady) {
1071
+ return Promise.resolve();
1072
+ }
1073
+ return new Promise((resolve, reject) => {
1074
+ this.interactiveReadyWaiters.add({ resolve, reject });
1075
+ });
1076
+ }
1077
+ markInteractiveReady() {
1078
+ if (this.interactiveReady) {
1079
+ return;
1080
+ }
1081
+ this.interactiveReady = true;
1082
+ this.clearInteractiveReadinessTimer();
1083
+ for (const waiter of this.interactiveReadyWaiters) {
1084
+ waiter.resolve();
1085
+ }
1086
+ this.interactiveReadyWaiters.clear();
1087
+ if (this.state.status === "starting" && !this.hasAcceptedInput) {
1088
+ this.setStatus("idle", "Claude interactive session is ready.");
1089
+ }
1090
+ }
1091
+ rejectInteractiveReadiness(error) {
1092
+ this.clearInteractiveReadinessTimer();
1093
+ for (const waiter of this.interactiveReadyWaiters) {
1094
+ waiter.reject(error);
1095
+ }
1096
+ this.interactiveReadyWaiters.clear();
1097
+ this.interactiveReady = true;
1098
+ }
1099
+ clearInteractiveReadinessTimer() {
1100
+ if (!this.interactiveReadyTimer) {
1101
+ return;
1102
+ }
1103
+ clearTimeout(this.interactiveReadyTimer);
1104
+ this.interactiveReadyTimer = null;
1105
+ }
1106
+ armRemoteSubmitRetry(normalizedText) {
1107
+ this.clearRemoteSubmitRetry();
1108
+ this.remoteSubmitText = normalizedText;
1109
+ this.remoteSubmitRetryIndex = 0;
1110
+ this.scheduleRemoteSubmitRetry();
1111
+ }
1112
+ scheduleRemoteSubmitRetry() {
1113
+ const delayMs = CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS[this.remoteSubmitRetryIndex];
1114
+ if (delayMs === undefined || !this.remoteSubmitText) {
1115
+ return;
1116
+ }
1117
+ this.remoteSubmitRetryTimer = setTimeout(() => {
1118
+ this.remoteSubmitRetryTimer = null;
1119
+ if (!this.remoteSubmitText ||
1120
+ !this.pty ||
1121
+ this.state.status !== "busy" ||
1122
+ !this.pendingInjectedInputs.some((input) => input.normalizedText === this.remoteSubmitText)) {
1123
+ this.clearRemoteSubmitRetry();
1124
+ return;
1125
+ }
1126
+ this.writeToPty("\r");
1127
+ this.remoteSubmitRetryIndex += 1;
1128
+ if (this.remoteSubmitRetryIndex >= CLAUDE_REMOTE_ENTER_RETRY_DELAYS_MS.length) {
1129
+ this.clearRemoteSubmitRetry();
1130
+ this.emitClaudeNotice("Claude did not confirm the WeChat message submission. Press Enter in the local Claude terminal to retry.", "warning");
1131
+ return;
1132
+ }
1133
+ this.scheduleRemoteSubmitRetry();
1134
+ }, delayMs);
1135
+ this.remoteSubmitRetryTimer.unref?.();
1136
+ }
1137
+ clearRemoteSubmitRetry() {
1138
+ if (this.remoteSubmitRetryTimer) {
1139
+ clearTimeout(this.remoteSubmitRetryTimer);
1140
+ this.remoteSubmitRetryTimer = null;
1141
+ }
1142
+ this.remoteSubmitRetryIndex = 0;
1143
+ this.remoteSubmitText = null;
1144
+ }
999
1145
  // Fallback heuristic for older Claude Code versions that lack PostCompact hooks.
1000
1146
  // The structured PostCompact hook event (handled in handleClaudeHookEnvelope) is
1001
1147
  // the reliable signal; this regex match serves as a best-effort fallback.
@@ -1044,6 +1190,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1044
1190
  }
1045
1191
  this.clearCompletionTimer();
1046
1192
  this.clearWechatWorkingNotice(true);
1193
+ this.clearRemoteSubmitRetry();
1047
1194
  this.pendingCliApprovalHints = null;
1048
1195
  this.flushPendingClaudeHookApprovals();
1049
1196
  this.pendingApproval = null;
@@ -1079,6 +1226,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1079
1226
  const completedPreview = this.currentPreview;
1080
1227
  this.clearCompletionTimer();
1081
1228
  this.clearWechatWorkingNotice(true);
1229
+ this.clearRemoteSubmitRetry();
1082
1230
  this.pendingCliApprovalHints = null;
1083
1231
  this.flushPendingClaudeHookApprovals();
1084
1232
  this.pendingApproval = null;
@@ -1168,6 +1316,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1168
1316
  }
1169
1317
  }
1170
1318
  handleClaudeSessionStart(payload) {
1319
+ this.markInteractiveReady();
1171
1320
  if (!payload.session_id) {
1172
1321
  return;
1173
1322
  }
@@ -1290,6 +1439,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1290
1439
  }
1291
1440
  const injectedIndex = findInjectedClaudePromptIndex(prompt, this.pendingInjectedInputs);
1292
1441
  if (injectedIndex >= 0) {
1442
+ this.clearRemoteSubmitRetry();
1293
1443
  this.pendingInjectedInputs.splice(injectedIndex, 1);
1294
1444
  return;
1295
1445
  }
@@ -1332,7 +1482,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1332
1482
  this.state.lastSessionSwitchReason = undefined;
1333
1483
  this.emitClaudeNotice(`Saved Claude conversation ${failedResumeConversationId} is no longer available. Starting a fresh Claude session.`, "warning");
1334
1484
  try {
1335
- await super.reset();
1485
+ await this.reset();
1336
1486
  }
1337
1487
  catch (error) {
1338
1488
  // recoverFromInvalidResume is invoked fire-and-forget (void) and there is
@@ -1441,6 +1591,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
1441
1591
  this.currentPreview === "(idle)") {
1442
1592
  return;
1443
1593
  }
1594
+ this.clearRemoteSubmitRetry();
1444
1595
  this.clearWechatWorkingNotice(true);
1445
1596
  this.pendingCliApprovalHints = null;
1446
1597
  this.lastAutoApprovedPayload = null;