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.
- package/LICENSE.txt +661 -661
- package/README.md +454 -475
- package/bin/_run-entry.mjs +23 -5
- package/bin/wecom-check-update.mjs +5 -0
- package/bin/wecom-claude.mjs +5 -0
- package/bin/wecom-codex.mjs +5 -0
- package/bin/wecom-daemon.mjs +5 -0
- package/bin/wecom-opencode.mjs +5 -0
- package/bin/wecom-pi.mjs +5 -0
- package/bin/wecom-setup.mjs +5 -0
- package/dist/bridge/adapter-control.js +58 -0
- package/dist/bridge/bridge-adapters.claude.js +155 -4
- package/dist/bridge/bridge-adapters.codex.js +112 -7
- package/dist/bridge/bridge-adapters.core.js +19 -1
- package/dist/bridge/bridge-adapters.opencode.js +150 -1
- package/dist/bridge/bridge-adapters.pi.js +39 -0
- package/dist/bridge/bridge-process-reaper.js +35 -2
- package/dist/bridge/bridge-state.js +10 -3
- package/dist/bridge/claude-native-controls.js +133 -0
- package/dist/bridge/native-terminal-control.js +253 -0
- package/dist/bridge/wechat-bridge.js +389 -87
- package/dist/channels/wechat/wechat-outbound-queue.js +10 -3
- package/dist/channels/wecom/bridge-final-reply.js +26 -0
- package/dist/channels/wecom/setup-cli.js +6 -0
- package/dist/channels/wecom/setup.js +176 -0
- package/dist/channels/wecom/wecom-channel-port.js +45 -0
- package/dist/channels/wecom/wecom-config.js +66 -0
- package/dist/channels/wecom/wecom-media.js +141 -0
- package/dist/channels/wecom/wecom-message.js +149 -0
- package/dist/channels/wecom/wecom-transport.js +308 -0
- package/dist/companion/codex-remote-client.js +24 -3
- package/dist/companion/codex-visible-client-link.js +101 -0
- package/dist/companion/local-companion-start.js +56 -11
- package/dist/companion/local-companion.js +19 -1
- package/dist/companion/pi-tui-bridge-extension.js +56 -3
- package/dist/core/bridge-control.js +8 -0
- package/dist/core/channel-types.js +3 -1
- package/dist/core/conversation-routing.js +48 -0
- package/dist/core/direct-run.js +17 -0
- package/dist/core/inbound-message-claims.js +61 -0
- package/dist/daemon/daemon-link.js +3 -0
- package/dist/daemon/wechat-daemon.js +335 -60
- package/dist/i18n/messages-en.js +3 -1
- package/dist/i18n/messages-zh.js +3 -1
- package/dist/runtime/legacy-adapter-runtime.js +15 -0
- package/dist/runtime/runtime-types.js +3 -0
- package/dist/utils/doctor.js +30 -10
- package/dist/utils/version-checker.js +11 -11
- package/dist/wechat/setup.js +113 -13
- package/dist/wechat/wechat-transport.js +96 -58
- package/package.json +116 -104
- package/scripts/ensure-node-pty-permissions.mjs +73 -73
package/bin/_run-entry.mjs
CHANGED
|
@@ -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 >= ${
|
|
39
|
-
`[cli-wechat-bridge] 需要 Node.js >= ${
|
|
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,
|
package/bin/wecom-pi.mjs
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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;
|