@unlinearity/cli-wechat-bridge 1.0.3 → 1.0.4
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/README.md +221 -223
- package/dist/bridge/bridge-adapter-common.js +95 -0
- package/dist/bridge/bridge-adapters.claude.js +1 -1
- package/dist/bridge/bridge-adapters.codex.js +18 -3
- package/dist/bridge/bridge-adapters.core.js +5 -1
- package/dist/bridge/bridge-adapters.opencode.js +220 -22
- package/dist/bridge/bridge-adapters.shared.js +4 -97
- package/dist/bridge/bridge-final-reply.js +18 -3
- package/dist/bridge/bridge-state.js +9 -3
- package/dist/bridge/bridge-utils.js +38 -7
- package/dist/bridge/claude-hooks.js +2 -2
- package/dist/bridge/wechat-bridge.js +97 -27
- package/dist/companion/codex-remote-client.js +3 -0
- package/dist/companion/local-companion-start.js +6 -1
- package/dist/companion/local-companion.js +3 -0
- package/dist/utils/version-checker.js +1 -1
- package/dist/wechat/wechat-transport.js +46 -2
- package/package.json +10 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function getCodexRpcRequestId(value) {
|
|
5
|
+
return typeof value === "string" || typeof value === "number" ? value : null;
|
|
6
|
+
}
|
|
7
|
+
export function getNotificationThreadId(params) {
|
|
8
|
+
if (!isRecord(params)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (typeof params.threadId === "string") {
|
|
12
|
+
return params.threadId;
|
|
13
|
+
}
|
|
14
|
+
if (isRecord(params.thread) && typeof params.thread.id === "string") {
|
|
15
|
+
return params.thread.id;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export function getNotificationTurnId(params) {
|
|
20
|
+
if (!isRecord(params)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (typeof params.turnId === "string") {
|
|
24
|
+
return params.turnId;
|
|
25
|
+
}
|
|
26
|
+
if (isRecord(params.turn) && typeof params.turn.id === "string") {
|
|
27
|
+
return params.turn.id;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
export function describeUnknownError(error) {
|
|
32
|
+
if (error instanceof Error && error.message) {
|
|
33
|
+
return error.message;
|
|
34
|
+
}
|
|
35
|
+
return String(error);
|
|
36
|
+
}
|
|
37
|
+
export function normalizeCodexRpcError(error) {
|
|
38
|
+
if (isRecord(error)) {
|
|
39
|
+
const message = typeof error.message === "string"
|
|
40
|
+
? error.message
|
|
41
|
+
: typeof error.code === "number"
|
|
42
|
+
? `RPC error ${error.code}`
|
|
43
|
+
: "";
|
|
44
|
+
const data = typeof error.data === "string"
|
|
45
|
+
? error.data
|
|
46
|
+
: typeof error.details === "string"
|
|
47
|
+
? error.details
|
|
48
|
+
: "";
|
|
49
|
+
const combined = [message, data].filter(Boolean).join(": ");
|
|
50
|
+
if (combined) {
|
|
51
|
+
return combined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return describeUnknownError(error);
|
|
55
|
+
}
|
|
56
|
+
export function getLocalCompanionCommandName(kind) {
|
|
57
|
+
switch (kind) {
|
|
58
|
+
case "codex":
|
|
59
|
+
return "wechat-codex";
|
|
60
|
+
case "claude":
|
|
61
|
+
return "wechat-claude";
|
|
62
|
+
case "opencode":
|
|
63
|
+
return "wechat-opencode";
|
|
64
|
+
default:
|
|
65
|
+
return "local companion";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function getSharedSessionIdFromAdapterState(state) {
|
|
69
|
+
return state.sharedSessionId ?? state.sharedThreadId;
|
|
70
|
+
}
|
|
71
|
+
export function quoteWindowsCommandArg(value) {
|
|
72
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
73
|
+
}
|
|
74
|
+
export function quotePosixCommandArg(value) {
|
|
75
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
76
|
+
}
|
|
77
|
+
export function isRecentIsoTimestamp(timestamp, maxAgeMs) {
|
|
78
|
+
const parsedMs = Date.parse(timestamp);
|
|
79
|
+
if (!Number.isFinite(parsedMs)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return parsedMs >= Date.now() - maxAgeMs;
|
|
83
|
+
}
|
|
84
|
+
export function coerceWebSocketMessageData(data) {
|
|
85
|
+
if (typeof data === "string") {
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
if (data instanceof ArrayBuffer) {
|
|
89
|
+
return Buffer.from(data).toString("utf8");
|
|
90
|
+
}
|
|
91
|
+
if (ArrayBuffer.isView(data)) {
|
|
92
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -7,7 +7,7 @@ import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettin
|
|
|
7
7
|
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
8
8
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
9
9
|
import * as shared from "./bridge-adapters.shared.js";
|
|
10
|
-
const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, MODULE_DIR, buildClaudeCliArgs, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
|
|
10
|
+
const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
|
|
11
11
|
const CLAUDE_COMPACT_OUTPUT_LINE_RE = /^Compacted(?:\s*\(.*full summary.*\))?$/i;
|
|
12
12
|
const CLAUDE_COMPACT_FAILURE_RE = /Error:\s*Error during compaction:|(?:^|\b)API Error:|\b(?:compact|compaction)\s+failed\b|^Error:/i;
|
|
13
13
|
const CLAUDE_COMPACT_DEDUP_MS = 2_000;
|
|
@@ -469,6 +469,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
469
469
|
}
|
|
470
470
|
this.sessionReadOffset = 0;
|
|
471
471
|
this.sessionPartialLine = "";
|
|
472
|
+
this.seedSessionReplayCutoff(startedAtMs);
|
|
472
473
|
}
|
|
473
474
|
let content;
|
|
474
475
|
try {
|
|
@@ -495,6 +496,17 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
495
496
|
this.handleSessionLogLine(line);
|
|
496
497
|
}
|
|
497
498
|
}
|
|
499
|
+
seedSessionReplayCutoff(startedAtMs) {
|
|
500
|
+
if (this.sessionIgnoreBeforeMs !== null ||
|
|
501
|
+
this.pendingTurnStart ||
|
|
502
|
+
this.activeTurn ||
|
|
503
|
+
this.state.activeTurnId) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (Number.isFinite(startedAtMs)) {
|
|
507
|
+
this.sessionIgnoreBeforeMs = startedAtMs;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
498
510
|
maybeApplyRecentSessionFallback() {
|
|
499
511
|
if (!this.isNativePanelMode()) {
|
|
500
512
|
return;
|
|
@@ -538,6 +550,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
538
550
|
this.sessionReadOffset = 0;
|
|
539
551
|
this.sessionPartialLine = "";
|
|
540
552
|
this.sessionFinalText = null;
|
|
553
|
+
this.seedSessionReplayCutoff(startedAtMs);
|
|
541
554
|
}
|
|
542
555
|
}
|
|
543
556
|
handleSessionLogLine(line) {
|
|
@@ -790,7 +803,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
790
803
|
this.pendingTurnThreadId = null;
|
|
791
804
|
this.interruptPendingTurnStart = false;
|
|
792
805
|
this.state.activeTurnOrigin = undefined;
|
|
793
|
-
if (!this.activeTurn && this.
|
|
806
|
+
if (!this.activeTurn && this.getState().status === "busy") {
|
|
794
807
|
this.setStatus("idle");
|
|
795
808
|
}
|
|
796
809
|
throw error;
|
|
@@ -886,7 +899,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
886
899
|
catch (err) {
|
|
887
900
|
await this.stopAppServer();
|
|
888
901
|
const details = this.describeAppServerLog();
|
|
889
|
-
throw new Error(`Failed to start Codex app-server: ${String(err)}${details}
|
|
902
|
+
throw new Error(`Failed to start Codex app-server: ${String(err)}${details}`, {
|
|
903
|
+
cause: err,
|
|
904
|
+
});
|
|
890
905
|
}
|
|
891
906
|
}
|
|
892
907
|
async connectRpcClient() {
|
|
@@ -1982,7 +1997,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1982
1997
|
if (deltaFallback.length === 0) {
|
|
1983
1998
|
return null;
|
|
1984
1999
|
}
|
|
1985
|
-
return deltaFallback[deltaFallback.length - 1];
|
|
2000
|
+
return deltaFallback[deltaFallback.length - 1] ?? null;
|
|
1986
2001
|
}
|
|
1987
2002
|
recordTurnActivity(turnId, timestamp = Date.now()) {
|
|
1988
2003
|
const timestampMs = typeof timestamp === "number" ? timestamp : Date.parse(timestamp);
|
|
@@ -3,6 +3,7 @@ import { spawn as spawnPty } from "node-pty";
|
|
|
3
3
|
import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
4
4
|
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
5
5
|
import * as shared from "./bridge-adapters.shared.js";
|
|
6
|
+
import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
|
|
6
7
|
const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, } = shared;
|
|
7
8
|
export class LocalCompanionProxyAdapter {
|
|
8
9
|
options;
|
|
@@ -62,6 +63,8 @@ export class LocalCompanionProxyAdapter {
|
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
this.endpoint = {
|
|
66
|
+
protocolVersion: LOCAL_CLIENT_PROTOCOL_VERSION,
|
|
67
|
+
runtimeKind: "legacy_adapter",
|
|
65
68
|
instanceId: `${process.pid}-${Date.now().toString(36)}`,
|
|
66
69
|
kind: this.options.kind,
|
|
67
70
|
port: address.port,
|
|
@@ -74,7 +77,8 @@ export class LocalCompanionProxyAdapter {
|
|
|
74
77
|
transcriptPath: this.state.transcriptPath,
|
|
75
78
|
startedAt: nowIso(),
|
|
76
79
|
};
|
|
77
|
-
|
|
80
|
+
const endpoint = this.endpoint;
|
|
81
|
+
writeLocalCompanionEndpoint(endpoint);
|
|
78
82
|
resolve();
|
|
79
83
|
});
|
|
80
84
|
});
|
|
@@ -34,6 +34,9 @@ export class OpenCodeServerAdapter {
|
|
|
34
34
|
localPromptNoticeSent = false;
|
|
35
35
|
loggedUnknownEventTypes = new Set();
|
|
36
36
|
emittedTextByPartId = new Map();
|
|
37
|
+
partTypeByPartId = new Map();
|
|
38
|
+
visibleReplyPartsByPartId = new Map();
|
|
39
|
+
visibleReplyMessageIds = new Set();
|
|
37
40
|
observedOpenCodeMessages = new Map();
|
|
38
41
|
observedUserTextByPartId = new Map();
|
|
39
42
|
observedUserMessagePartIds = new Map();
|
|
@@ -71,7 +74,7 @@ export class OpenCodeServerAdapter {
|
|
|
71
74
|
this.setStatus("starting", "Starting OpenCode companion...");
|
|
72
75
|
try {
|
|
73
76
|
this.serverPort = await reserveLocalPort();
|
|
74
|
-
await this.startServerProcess();
|
|
77
|
+
const serverProcess = await this.startServerProcess();
|
|
75
78
|
await waitForTcpPort(OPENCODE_SERVER_HOST, this.serverPort, OPENCODE_SERVER_READY_TIMEOUT_MS);
|
|
76
79
|
await this.createSdkClient();
|
|
77
80
|
await this.checkHealth();
|
|
@@ -82,7 +85,7 @@ export class OpenCodeServerAdapter {
|
|
|
82
85
|
await this.syncVisibleSessionToShared({ force: true });
|
|
83
86
|
}
|
|
84
87
|
else {
|
|
85
|
-
this.state.pid =
|
|
88
|
+
this.state.pid = serverProcess.pid;
|
|
86
89
|
this.state.startedAt = nowIso();
|
|
87
90
|
}
|
|
88
91
|
this.setStatus("idle", "OpenCode adapter is ready.");
|
|
@@ -138,7 +141,9 @@ export class OpenCodeServerAdapter {
|
|
|
138
141
|
this.settleTurnState();
|
|
139
142
|
this.clearObservedMessageTracking();
|
|
140
143
|
this.setStatus("idle");
|
|
141
|
-
throw new Error(`Failed to send prompt: ${describeUnknownError(err)}
|
|
144
|
+
throw new Error(`Failed to send prompt: ${describeUnknownError(err)}`, {
|
|
145
|
+
cause: err,
|
|
146
|
+
});
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
async listResumeSessions(limit = 10) {
|
|
@@ -354,6 +359,7 @@ export class OpenCodeServerAdapter {
|
|
|
354
359
|
timestamp: nowIso(),
|
|
355
360
|
});
|
|
356
361
|
});
|
|
362
|
+
return server;
|
|
357
363
|
}
|
|
358
364
|
async startNativeClient() {
|
|
359
365
|
if (this.nativeProcess) {
|
|
@@ -395,7 +401,7 @@ export class OpenCodeServerAdapter {
|
|
|
395
401
|
const detail = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
|
|
396
402
|
this.emit({
|
|
397
403
|
type: "shutdown_requested",
|
|
398
|
-
reason: "
|
|
404
|
+
reason: "companion_closed",
|
|
399
405
|
message: `OpenCode companion exited (${detail}).`,
|
|
400
406
|
exitCode: typeof code === "number" ? code : 0,
|
|
401
407
|
timestamp: nowIso(),
|
|
@@ -421,7 +427,7 @@ export class OpenCodeServerAdapter {
|
|
|
421
427
|
});
|
|
422
428
|
}
|
|
423
429
|
catch (err) {
|
|
424
|
-
throw new Error(`Failed to load @opencode-ai/sdk. Make sure it is installed: ${describeUnknownError(err)}
|
|
430
|
+
throw new Error(`Failed to load @opencode-ai/sdk. Make sure it is installed: ${describeUnknownError(err)}`, { cause: err });
|
|
425
431
|
}
|
|
426
432
|
}
|
|
427
433
|
async checkHealth() {
|
|
@@ -730,15 +736,16 @@ export class OpenCodeServerAdapter {
|
|
|
730
736
|
this.state.activeTurnOrigin = undefined;
|
|
731
737
|
this.hasAcceptedInput = false;
|
|
732
738
|
const completedPreview = this.currentPreview;
|
|
739
|
+
const turnStartedAtMs = this.lastBusyAtMs;
|
|
733
740
|
void this.outputBatcher.flushNow()
|
|
734
741
|
.catch(() => undefined)
|
|
735
|
-
.then(() => {
|
|
736
|
-
const
|
|
742
|
+
.then(async () => {
|
|
743
|
+
const finalReplyText = await this.resolveFinalReplyText(sessionId, turnStartedAtMs);
|
|
737
744
|
this.setStatus("idle");
|
|
738
|
-
if (
|
|
745
|
+
if (finalReplyText) {
|
|
739
746
|
this.emit({
|
|
740
747
|
type: "final_reply",
|
|
741
|
-
text:
|
|
748
|
+
text: finalReplyText,
|
|
742
749
|
timestamp: nowIso(),
|
|
743
750
|
});
|
|
744
751
|
}
|
|
@@ -904,7 +911,7 @@ export class OpenCodeServerAdapter {
|
|
|
904
911
|
if (!messageId) {
|
|
905
912
|
return;
|
|
906
913
|
}
|
|
907
|
-
const observed = this.getOrCreateObservedOpenCodeMessage(messageId, sessionId);
|
|
914
|
+
const observed = this.getOrCreateObservedOpenCodeMessage(messageId, sessionId ?? undefined);
|
|
908
915
|
observed.updatedAtMs = Date.now();
|
|
909
916
|
observed.sessionId = sessionId ?? observed.sessionId;
|
|
910
917
|
observed.role = role;
|
|
@@ -919,11 +926,15 @@ export class OpenCodeServerAdapter {
|
|
|
919
926
|
return;
|
|
920
927
|
}
|
|
921
928
|
const part = isRecord(properties.part) ? properties.part : undefined;
|
|
929
|
+
const partId = this.extractPartId(properties, part);
|
|
930
|
+
if (partId && typeof part?.type === "string") {
|
|
931
|
+
this.partTypeByPartId.set(partId, part.type);
|
|
932
|
+
}
|
|
922
933
|
if (this.isVisibleTextPart(part)) {
|
|
923
934
|
this.trackObservedOpenCodeMessagePart({
|
|
924
935
|
messageId: part.messageID,
|
|
925
936
|
sessionId: part.sessionID,
|
|
926
|
-
partId: part.id,
|
|
937
|
+
partId: partId ?? part.id,
|
|
927
938
|
snapshotText: typeof part.text === "string" ? part.text : undefined,
|
|
928
939
|
deltaText: typeof properties.delta === "string" ? properties.delta : undefined,
|
|
929
940
|
});
|
|
@@ -940,7 +951,6 @@ export class OpenCodeServerAdapter {
|
|
|
940
951
|
if (!this.syncTrackedSessionFromEvent(part.sessionID)) {
|
|
941
952
|
return;
|
|
942
953
|
}
|
|
943
|
-
const partId = this.extractPartId(properties, part);
|
|
944
954
|
if (!partId) {
|
|
945
955
|
return;
|
|
946
956
|
}
|
|
@@ -958,11 +968,26 @@ export class OpenCodeServerAdapter {
|
|
|
958
968
|
const observedText = typeof part.messageID === "string"
|
|
959
969
|
? this.observedOpenCodeMessages.get(part.messageID)?.text
|
|
960
970
|
: undefined;
|
|
971
|
+
const observedRole = typeof part.messageID === "string"
|
|
972
|
+
? this.observedOpenCodeMessages.get(part.messageID)?.role
|
|
973
|
+
: undefined;
|
|
974
|
+
if (observedRole !== "assistant" &&
|
|
975
|
+
this.matchesRecentLocalPromptMirror(observedText || partText || delta || text, {
|
|
976
|
+
allowPrefix: true,
|
|
977
|
+
})) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
961
980
|
if (this.matchesRecentWechatPromptMirror(part.sessionID, observedText || partText || delta || text, {
|
|
962
981
|
allowPrefix: true,
|
|
963
982
|
})) {
|
|
964
983
|
return;
|
|
965
984
|
}
|
|
985
|
+
if (partText) {
|
|
986
|
+
this.recordVisibleReplyPartSnapshot(partId, part.sessionID, part.messageID, partText);
|
|
987
|
+
}
|
|
988
|
+
else if (text) {
|
|
989
|
+
this.recordVisibleReplyPartDelta(partId, part.sessionID, part.messageID, text);
|
|
990
|
+
}
|
|
966
991
|
this.pushVisibleOutput(text);
|
|
967
992
|
}
|
|
968
993
|
handleMessagePartDelta(properties) {
|
|
@@ -984,16 +1009,20 @@ export class OpenCodeServerAdapter {
|
|
|
984
1009
|
}
|
|
985
1010
|
return;
|
|
986
1011
|
}
|
|
1012
|
+
const sessionId = typeof properties.sessionID === "string"
|
|
1013
|
+
? properties.sessionID
|
|
1014
|
+
: undefined;
|
|
1015
|
+
const partId = this.extractPartId(properties);
|
|
1016
|
+
const knownPartType = partId ? this.partTypeByPartId.get(partId) : undefined;
|
|
1017
|
+
if (knownPartType && knownPartType !== "text") {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
987
1020
|
if (properties.field !== "text") {
|
|
988
1021
|
return;
|
|
989
1022
|
}
|
|
990
1023
|
const delta = typeof properties.delta === "string"
|
|
991
1024
|
? properties.delta
|
|
992
1025
|
: undefined;
|
|
993
|
-
const sessionId = typeof properties.sessionID === "string"
|
|
994
|
-
? properties.sessionID
|
|
995
|
-
: undefined;
|
|
996
|
-
const partId = this.extractPartId(properties);
|
|
997
1026
|
if (typeof properties.messageID === "string" && partId && delta) {
|
|
998
1027
|
this.trackObservedOpenCodeMessagePart({
|
|
999
1028
|
messageId: properties.messageID,
|
|
@@ -1012,11 +1041,21 @@ export class OpenCodeServerAdapter {
|
|
|
1012
1041
|
const observedText = typeof properties.messageID === "string"
|
|
1013
1042
|
? this.observedOpenCodeMessages.get(properties.messageID)?.text
|
|
1014
1043
|
: undefined;
|
|
1044
|
+
const observedRole = typeof properties.messageID === "string"
|
|
1045
|
+
? this.observedOpenCodeMessages.get(properties.messageID)?.role
|
|
1046
|
+
: undefined;
|
|
1047
|
+
if (observedRole !== "assistant" &&
|
|
1048
|
+
this.matchesRecentLocalPromptMirror(observedText || delta || text, {
|
|
1049
|
+
allowPrefix: true,
|
|
1050
|
+
})) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1015
1053
|
if (this.matchesRecentWechatPromptMirror(sessionId, observedText || delta || text, {
|
|
1016
1054
|
allowPrefix: true,
|
|
1017
1055
|
})) {
|
|
1018
1056
|
return;
|
|
1019
1057
|
}
|
|
1058
|
+
this.recordVisibleReplyPartDelta(partId, sessionId, properties.messageID, text);
|
|
1020
1059
|
this.pushVisibleOutput(text);
|
|
1021
1060
|
}
|
|
1022
1061
|
handleMessagePartRemoved(properties) {
|
|
@@ -1028,6 +1067,156 @@ export class OpenCodeServerAdapter {
|
|
|
1028
1067
|
return;
|
|
1029
1068
|
}
|
|
1030
1069
|
this.emittedTextByPartId.delete(partId);
|
|
1070
|
+
this.partTypeByPartId.delete(partId);
|
|
1071
|
+
this.visibleReplyPartsByPartId.delete(partId);
|
|
1072
|
+
}
|
|
1073
|
+
recordVisibleReplyPartSnapshot(partId, sessionId, messageId, text) {
|
|
1074
|
+
const normalizedText = normalizeOutput(text);
|
|
1075
|
+
if (!normalizedText) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const resolvedSessionId = sessionId ?? this.activeSessionId ?? undefined;
|
|
1079
|
+
if (!resolvedSessionId) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const resolvedMessageId = typeof messageId === "string" ? messageId : undefined;
|
|
1083
|
+
this.visibleReplyPartsByPartId.set(partId, {
|
|
1084
|
+
sessionId: resolvedSessionId,
|
|
1085
|
+
messageId: resolvedMessageId,
|
|
1086
|
+
text: normalizedText,
|
|
1087
|
+
});
|
|
1088
|
+
if (resolvedMessageId) {
|
|
1089
|
+
this.visibleReplyMessageIds.add(resolvedMessageId);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
recordVisibleReplyPartDelta(partId, sessionId, messageId, delta) {
|
|
1093
|
+
const normalizedDelta = normalizeOutput(delta);
|
|
1094
|
+
if (!normalizedDelta) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const previous = this.visibleReplyPartsByPartId.get(partId);
|
|
1098
|
+
this.recordVisibleReplyPartSnapshot(partId, sessionId ?? previous?.sessionId, messageId ?? previous?.messageId, `${previous?.text ?? ""}${normalizedDelta}`);
|
|
1099
|
+
}
|
|
1100
|
+
async resolveFinalReplyText(sessionId, turnStartedAtMs) {
|
|
1101
|
+
const sessionReply = sessionId
|
|
1102
|
+
? await this.fetchLatestAssistantVisibleReply(sessionId, turnStartedAtMs)
|
|
1103
|
+
: "";
|
|
1104
|
+
if (sessionReply.trim()) {
|
|
1105
|
+
return sessionReply.trim();
|
|
1106
|
+
}
|
|
1107
|
+
const streamedReply = this.getBufferedVisibleReplyText(sessionId ?? undefined);
|
|
1108
|
+
if (streamedReply.trim()) {
|
|
1109
|
+
return streamedReply.trim();
|
|
1110
|
+
}
|
|
1111
|
+
const summary = this.outputBatcher.getRecentSummary(500);
|
|
1112
|
+
return summary && summary !== "(no output)" ? summary : "";
|
|
1113
|
+
}
|
|
1114
|
+
getBufferedVisibleReplyText(sessionId) {
|
|
1115
|
+
const chunks = [];
|
|
1116
|
+
for (const part of this.visibleReplyPartsByPartId.values()) {
|
|
1117
|
+
if (sessionId && part.sessionId !== sessionId) {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (part.text) {
|
|
1121
|
+
chunks.push(part.text);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return normalizeOutput(chunks.join(""));
|
|
1125
|
+
}
|
|
1126
|
+
async fetchLatestAssistantVisibleReply(sessionId, turnStartedAtMs) {
|
|
1127
|
+
const sessionClient = this.client?.session;
|
|
1128
|
+
if (!sessionClient?.messages) {
|
|
1129
|
+
return "";
|
|
1130
|
+
}
|
|
1131
|
+
const queryVariants = this.activeWorkspaceId
|
|
1132
|
+
? [
|
|
1133
|
+
{
|
|
1134
|
+
sessionID: sessionId,
|
|
1135
|
+
directory: this.options.cwd,
|
|
1136
|
+
workspace: this.activeWorkspaceId,
|
|
1137
|
+
limit: 20,
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
sessionID: sessionId,
|
|
1141
|
+
directory: this.options.cwd,
|
|
1142
|
+
limit: 20,
|
|
1143
|
+
},
|
|
1144
|
+
]
|
|
1145
|
+
: [
|
|
1146
|
+
{
|
|
1147
|
+
sessionID: sessionId,
|
|
1148
|
+
directory: this.options.cwd,
|
|
1149
|
+
limit: 20,
|
|
1150
|
+
},
|
|
1151
|
+
];
|
|
1152
|
+
for (const query of queryVariants) {
|
|
1153
|
+
try {
|
|
1154
|
+
const result = await sessionClient.messages(query);
|
|
1155
|
+
if (result.error !== undefined || !Array.isArray(result.data)) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const text = this.extractLatestAssistantVisibleReply(result.data, turnStartedAtMs, this.visibleReplyMessageIds);
|
|
1159
|
+
if (text) {
|
|
1160
|
+
return text;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
catch {
|
|
1164
|
+
// Fall back to the streamed visible text buffer.
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return "";
|
|
1168
|
+
}
|
|
1169
|
+
extractLatestAssistantVisibleReply(messages, turnStartedAtMs, expectedMessageIds) {
|
|
1170
|
+
let best = null;
|
|
1171
|
+
const minTimeMs = turnStartedAtMs > 0 ? turnStartedAtMs - 5_000 : 0;
|
|
1172
|
+
for (const [index, message] of messages.entries()) {
|
|
1173
|
+
if (!isRecord(message) || !isRecord(message.info)) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (message.info.role !== "assistant") {
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
const messageId = typeof message.info.id === "string" ? message.info.id : "";
|
|
1180
|
+
if (expectedMessageIds.size > 0 && !expectedMessageIds.has(messageId)) {
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
const timeMs = this.extractMessageTimeMs(message.info);
|
|
1184
|
+
if (expectedMessageIds.size === 0 && (!timeMs || timeMs < minTimeMs)) {
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const text = this.extractVisibleTextFromParts(message.parts);
|
|
1188
|
+
if (!text.trim()) {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const candidate = { text, timeMs: timeMs ?? 0, index };
|
|
1192
|
+
if (!best ||
|
|
1193
|
+
candidate.timeMs > best.timeMs ||
|
|
1194
|
+
(candidate.timeMs === best.timeMs && candidate.index > best.index)) {
|
|
1195
|
+
best = candidate;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return best?.text ?? "";
|
|
1199
|
+
}
|
|
1200
|
+
extractVisibleTextFromParts(parts) {
|
|
1201
|
+
if (!Array.isArray(parts)) {
|
|
1202
|
+
return "";
|
|
1203
|
+
}
|
|
1204
|
+
return normalizeOutput(parts
|
|
1205
|
+
.filter((part) => this.isVisibleTextPart(isRecord(part) ? part : undefined))
|
|
1206
|
+
.map((part) => part.text ?? "")
|
|
1207
|
+
.join(""));
|
|
1208
|
+
}
|
|
1209
|
+
extractMessageTimeMs(info) {
|
|
1210
|
+
const time = isRecord(info.time) ? info.time : undefined;
|
|
1211
|
+
const rawTime = typeof time?.completed === "number"
|
|
1212
|
+
? time.completed
|
|
1213
|
+
: typeof time?.created === "number"
|
|
1214
|
+
? time.created
|
|
1215
|
+
: undefined;
|
|
1216
|
+
if (typeof rawTime !== "number" || !Number.isFinite(rawTime) || rawTime <= 0) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
return rawTime < 1_000_000_000_000 ? rawTime * 1_000 : rawTime;
|
|
1031
1220
|
}
|
|
1032
1221
|
handleTuiPromptAppend(properties) {
|
|
1033
1222
|
if (!isRecord(properties)) {
|
|
@@ -1284,7 +1473,10 @@ export class OpenCodeServerAdapter {
|
|
|
1284
1473
|
}
|
|
1285
1474
|
}
|
|
1286
1475
|
wasRecentlyMirroredLocalPrompt(text) {
|
|
1287
|
-
|
|
1476
|
+
return this.matchesRecentLocalPromptMirror(text);
|
|
1477
|
+
}
|
|
1478
|
+
matchesRecentLocalPromptMirror(text, options = {}) {
|
|
1479
|
+
const normalizedText = normalizeOutput(text ?? "").trim();
|
|
1288
1480
|
if (!normalizedText) {
|
|
1289
1481
|
return false;
|
|
1290
1482
|
}
|
|
@@ -1292,7 +1484,9 @@ export class OpenCodeServerAdapter {
|
|
|
1292
1484
|
this.hasAcceptedInput &&
|
|
1293
1485
|
this.lastMirroredLocalPrompt &&
|
|
1294
1486
|
this.lastMirroredLocalPrompt.createdAtMs >= Date.now() - OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS &&
|
|
1295
|
-
this.lastMirroredLocalPrompt.text === normalizedText
|
|
1487
|
+
(this.lastMirroredLocalPrompt.text === normalizedText ||
|
|
1488
|
+
(options.allowPrefix === true &&
|
|
1489
|
+
this.lastMirroredLocalPrompt.text.startsWith(normalizedText)))) {
|
|
1296
1490
|
return true;
|
|
1297
1491
|
}
|
|
1298
1492
|
return false;
|
|
@@ -1483,7 +1677,7 @@ export class OpenCodeServerAdapter {
|
|
|
1483
1677
|
return true;
|
|
1484
1678
|
}
|
|
1485
1679
|
if (options.allowLocalTurnFollow !== false && this.shouldFollowLocalTurnSession(sessionId)) {
|
|
1486
|
-
this.switchSharedSession(session, {
|
|
1680
|
+
this.switchSharedSession(session ?? sessionId, {
|
|
1487
1681
|
source: "local",
|
|
1488
1682
|
reason: "local_turn",
|
|
1489
1683
|
notify: true,
|
|
@@ -1497,7 +1691,7 @@ export class OpenCodeServerAdapter {
|
|
|
1497
1691
|
if (typeof properties.sessionID === "string" || typeof properties.sessionId === "string") {
|
|
1498
1692
|
const id = typeof properties.sessionID === "string"
|
|
1499
1693
|
? properties.sessionID
|
|
1500
|
-
: properties.sessionId;
|
|
1694
|
+
: String(properties.sessionId);
|
|
1501
1695
|
return {
|
|
1502
1696
|
id,
|
|
1503
1697
|
workspaceID: this.extractWorkspaceId(properties) ?? undefined,
|
|
@@ -1653,8 +1847,9 @@ export class OpenCodeServerAdapter {
|
|
|
1653
1847
|
}
|
|
1654
1848
|
matchesCurrentDirectoryEvent(event) {
|
|
1655
1849
|
const payload = this.extractEventPayload(event);
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1850
|
+
const eventRecord = event;
|
|
1851
|
+
const wrappedDirectory = typeof eventRecord.directory === "string"
|
|
1852
|
+
? eventRecord.directory
|
|
1658
1853
|
: undefined;
|
|
1659
1854
|
if (wrappedDirectory) {
|
|
1660
1855
|
const matchesWrappedDirectory = this.normalizeDirectory(wrappedDirectory) === this.normalizeDirectory(this.options.cwd);
|
|
@@ -1820,6 +2015,9 @@ export class OpenCodeServerAdapter {
|
|
|
1820
2015
|
}
|
|
1821
2016
|
clearStreamedPartState() {
|
|
1822
2017
|
this.emittedTextByPartId.clear();
|
|
2018
|
+
this.partTypeByPartId.clear();
|
|
2019
|
+
this.visibleReplyPartsByPartId.clear();
|
|
2020
|
+
this.visibleReplyMessageIds.clear();
|
|
1823
2021
|
}
|
|
1824
2022
|
cleanupObservedOpenCodeMessage(messageId) {
|
|
1825
2023
|
this.observedOpenCodeMessages.delete(messageId);
|