@unlinearity/cli-wechat-bridge 1.0.2 → 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 +26 -4
- package/dist/bridge/bridge-adapters.core.js +10 -1
- package/dist/bridge/bridge-adapters.opencode.js +395 -43
- 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 +62 -8
- package/dist/bridge/claude-hooks.js +2 -2
- package/dist/bridge/wechat-bridge.js +111 -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 +11 -0
- package/dist/runtime/legacy-adapter-runtime.js +6 -0
- package/dist/utils/version-checker.js +1 -1
- package/dist/wechat/wechat-transport.js +46 -2
- package/package.json +10 -2
|
@@ -6,6 +6,7 @@ const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPEN
|
|
|
6
6
|
const OPENCODE_DUPLICATE_EVENT_TTL_MS = 150;
|
|
7
7
|
const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
|
|
8
8
|
const OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS = 10_000;
|
|
9
|
+
const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
|
|
9
10
|
/* ------------------------------------------------------------------ */
|
|
10
11
|
/* Adapter */
|
|
11
12
|
/* ------------------------------------------------------------------ */
|
|
@@ -33,13 +34,18 @@ export class OpenCodeServerAdapter {
|
|
|
33
34
|
localPromptNoticeSent = false;
|
|
34
35
|
loggedUnknownEventTypes = new Set();
|
|
35
36
|
emittedTextByPartId = new Map();
|
|
37
|
+
partTypeByPartId = new Map();
|
|
38
|
+
visibleReplyPartsByPartId = new Map();
|
|
39
|
+
visibleReplyMessageIds = new Set();
|
|
36
40
|
observedOpenCodeMessages = new Map();
|
|
37
41
|
observedUserTextByPartId = new Map();
|
|
38
42
|
observedUserMessagePartIds = new Map();
|
|
39
43
|
pendingWechatPromptMirrorSuppressions = [];
|
|
44
|
+
recentWechatPromptMirrorSuppressions = [];
|
|
40
45
|
recentSdkEventObservations = new Map();
|
|
41
46
|
suppressedTuiSessionSelectId = null;
|
|
42
47
|
lastMirroredLocalPrompt = null;
|
|
48
|
+
pendingLocalSessionCreateFollowUntilMs = 0;
|
|
43
49
|
pendingPermission = null;
|
|
44
50
|
constructor(options) {
|
|
45
51
|
this.options = options;
|
|
@@ -68,7 +74,7 @@ export class OpenCodeServerAdapter {
|
|
|
68
74
|
this.setStatus("starting", "Starting OpenCode companion...");
|
|
69
75
|
try {
|
|
70
76
|
this.serverPort = await reserveLocalPort();
|
|
71
|
-
await this.startServerProcess();
|
|
77
|
+
const serverProcess = await this.startServerProcess();
|
|
72
78
|
await waitForTcpPort(OPENCODE_SERVER_HOST, this.serverPort, OPENCODE_SERVER_READY_TIMEOUT_MS);
|
|
73
79
|
await this.createSdkClient();
|
|
74
80
|
await this.checkHealth();
|
|
@@ -79,7 +85,7 @@ export class OpenCodeServerAdapter {
|
|
|
79
85
|
await this.syncVisibleSessionToShared({ force: true });
|
|
80
86
|
}
|
|
81
87
|
else {
|
|
82
|
-
this.state.pid =
|
|
88
|
+
this.state.pid = serverProcess.pid;
|
|
83
89
|
this.state.startedAt = nowIso();
|
|
84
90
|
}
|
|
85
91
|
this.setStatus("idle", "OpenCode adapter is ready.");
|
|
@@ -118,6 +124,8 @@ export class OpenCodeServerAdapter {
|
|
|
118
124
|
syncVisible: true,
|
|
119
125
|
forceVisibleSync: true,
|
|
120
126
|
});
|
|
127
|
+
this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
|
|
128
|
+
this.beginTrackedTurn(normalized, "wechat");
|
|
121
129
|
try {
|
|
122
130
|
const result = await this.client.session.promptAsync({
|
|
123
131
|
sessionID: session.id,
|
|
@@ -130,10 +138,13 @@ export class OpenCodeServerAdapter {
|
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
catch (err) {
|
|
133
|
-
|
|
141
|
+
this.settleTurnState();
|
|
142
|
+
this.clearObservedMessageTracking();
|
|
143
|
+
this.setStatus("idle");
|
|
144
|
+
throw new Error(`Failed to send prompt: ${describeUnknownError(err)}`, {
|
|
145
|
+
cause: err,
|
|
146
|
+
});
|
|
134
147
|
}
|
|
135
|
-
this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
|
|
136
|
-
this.beginTrackedTurn(normalized, "wechat");
|
|
137
148
|
}
|
|
138
149
|
async listResumeSessions(limit = 10) {
|
|
139
150
|
void limit;
|
|
@@ -143,6 +154,31 @@ export class OpenCodeServerAdapter {
|
|
|
143
154
|
void sessionId;
|
|
144
155
|
throw new Error('WeChat /resume is disabled in opencode mode. Use /session directly inside "wechat-opencode"; WeChat will follow the active local session.');
|
|
145
156
|
}
|
|
157
|
+
async createSession() {
|
|
158
|
+
if (!this.client) {
|
|
159
|
+
throw new Error("OpenCode adapter is not running.");
|
|
160
|
+
}
|
|
161
|
+
if (this.state.status === "busy") {
|
|
162
|
+
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
163
|
+
}
|
|
164
|
+
if (this.pendingPermission) {
|
|
165
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm <code> or /deny.");
|
|
166
|
+
}
|
|
167
|
+
this.outputBatcher.clear();
|
|
168
|
+
this.clearStreamedPartState();
|
|
169
|
+
const session = this.unwrapOrThrow(await this.client.session.create({
|
|
170
|
+
directory: this.options.cwd,
|
|
171
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
172
|
+
}));
|
|
173
|
+
this.switchSharedSession(session, {
|
|
174
|
+
source: "wechat",
|
|
175
|
+
reason: "wechat_resume",
|
|
176
|
+
notify: true,
|
|
177
|
+
clearTrackedTurn: true,
|
|
178
|
+
syncVisible: true,
|
|
179
|
+
forceVisibleSync: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
146
182
|
async interrupt() {
|
|
147
183
|
if (!this.client || !this.activeSessionId) {
|
|
148
184
|
return false;
|
|
@@ -167,6 +203,7 @@ export class OpenCodeServerAdapter {
|
|
|
167
203
|
this.clearWechatWorkingNotice(true);
|
|
168
204
|
this.pendingLocalPrompt = "";
|
|
169
205
|
this.localPromptNoticeSent = false;
|
|
206
|
+
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
170
207
|
this.clearObservedMessageTracking();
|
|
171
208
|
this.recentSdkEventObservations.clear();
|
|
172
209
|
this.clearPendingPermissionState();
|
|
@@ -223,6 +260,7 @@ export class OpenCodeServerAdapter {
|
|
|
223
260
|
this.clearWechatWorkingNotice(true);
|
|
224
261
|
this.pendingLocalPrompt = "";
|
|
225
262
|
this.localPromptNoticeSent = false;
|
|
263
|
+
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
226
264
|
this.clearObservedMessageTracking();
|
|
227
265
|
this.recentSdkEventObservations.clear();
|
|
228
266
|
this.outputBatcher.clear();
|
|
@@ -321,6 +359,7 @@ export class OpenCodeServerAdapter {
|
|
|
321
359
|
timestamp: nowIso(),
|
|
322
360
|
});
|
|
323
361
|
});
|
|
362
|
+
return server;
|
|
324
363
|
}
|
|
325
364
|
async startNativeClient() {
|
|
326
365
|
if (this.nativeProcess) {
|
|
@@ -362,7 +401,7 @@ export class OpenCodeServerAdapter {
|
|
|
362
401
|
const detail = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
|
|
363
402
|
this.emit({
|
|
364
403
|
type: "shutdown_requested",
|
|
365
|
-
reason: "
|
|
404
|
+
reason: "companion_closed",
|
|
366
405
|
message: `OpenCode companion exited (${detail}).`,
|
|
367
406
|
exitCode: typeof code === "number" ? code : 0,
|
|
368
407
|
timestamp: nowIso(),
|
|
@@ -388,7 +427,7 @@ export class OpenCodeServerAdapter {
|
|
|
388
427
|
});
|
|
389
428
|
}
|
|
390
429
|
catch (err) {
|
|
391
|
-
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 });
|
|
392
431
|
}
|
|
393
432
|
}
|
|
394
433
|
async checkHealth() {
|
|
@@ -573,12 +612,15 @@ export class OpenCodeServerAdapter {
|
|
|
573
612
|
if (this.isImplicitLocalCompanionUiEvent(type)) {
|
|
574
613
|
return true;
|
|
575
614
|
}
|
|
576
|
-
return this.matchesCurrentDirectoryEvent(event)
|
|
615
|
+
return (this.matchesCurrentDirectoryEvent(event) ||
|
|
616
|
+
this.shouldAcceptUnscopedLocalSessionCreatedEvent(event));
|
|
577
617
|
}
|
|
578
618
|
if (type === "session.created" ||
|
|
579
619
|
type === "session.updated" ||
|
|
580
620
|
type === "session.deleted") {
|
|
581
|
-
return this.matchesCurrentDirectoryEvent(event)
|
|
621
|
+
return (this.matchesCurrentDirectoryEvent(event) ||
|
|
622
|
+
(type === "session.created" &&
|
|
623
|
+
this.shouldAcceptUnscopedLocalSessionCreatedEvent(event)));
|
|
582
624
|
}
|
|
583
625
|
return false;
|
|
584
626
|
}
|
|
@@ -694,15 +736,16 @@ export class OpenCodeServerAdapter {
|
|
|
694
736
|
this.state.activeTurnOrigin = undefined;
|
|
695
737
|
this.hasAcceptedInput = false;
|
|
696
738
|
const completedPreview = this.currentPreview;
|
|
739
|
+
const turnStartedAtMs = this.lastBusyAtMs;
|
|
697
740
|
void this.outputBatcher.flushNow()
|
|
698
741
|
.catch(() => undefined)
|
|
699
|
-
.then(() => {
|
|
700
|
-
const
|
|
742
|
+
.then(async () => {
|
|
743
|
+
const finalReplyText = await this.resolveFinalReplyText(sessionId, turnStartedAtMs);
|
|
701
744
|
this.setStatus("idle");
|
|
702
|
-
if (
|
|
745
|
+
if (finalReplyText) {
|
|
703
746
|
this.emit({
|
|
704
747
|
type: "final_reply",
|
|
705
|
-
text:
|
|
748
|
+
text: finalReplyText,
|
|
706
749
|
timestamp: nowIso(),
|
|
707
750
|
});
|
|
708
751
|
}
|
|
@@ -831,9 +874,21 @@ export class OpenCodeServerAdapter {
|
|
|
831
874
|
return;
|
|
832
875
|
}
|
|
833
876
|
const session = this.extractSessionReference(properties);
|
|
834
|
-
if (
|
|
877
|
+
if (this.syncTrackedSessionFromEvent(session)) {
|
|
835
878
|
return;
|
|
836
879
|
}
|
|
880
|
+
if (!this.shouldFollowCreatedLocalSession(session)) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
884
|
+
this.logDebug(`[opencode-adapter:local-follow] session.created ${session.id}`);
|
|
885
|
+
this.switchSharedSession(session, {
|
|
886
|
+
source: "local",
|
|
887
|
+
reason: "local_follow",
|
|
888
|
+
notify: true,
|
|
889
|
+
clearTrackedTurn: true,
|
|
890
|
+
syncVisible: true,
|
|
891
|
+
});
|
|
837
892
|
}
|
|
838
893
|
handleSessionUpdated(properties) {
|
|
839
894
|
if (!isRecord(properties)) {
|
|
@@ -856,7 +911,7 @@ export class OpenCodeServerAdapter {
|
|
|
856
911
|
if (!messageId) {
|
|
857
912
|
return;
|
|
858
913
|
}
|
|
859
|
-
const observed = this.getOrCreateObservedOpenCodeMessage(messageId, sessionId);
|
|
914
|
+
const observed = this.getOrCreateObservedOpenCodeMessage(messageId, sessionId ?? undefined);
|
|
860
915
|
observed.updatedAtMs = Date.now();
|
|
861
916
|
observed.sessionId = sessionId ?? observed.sessionId;
|
|
862
917
|
observed.role = role;
|
|
@@ -871,11 +926,15 @@ export class OpenCodeServerAdapter {
|
|
|
871
926
|
return;
|
|
872
927
|
}
|
|
873
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
|
+
}
|
|
874
933
|
if (this.isVisibleTextPart(part)) {
|
|
875
934
|
this.trackObservedOpenCodeMessagePart({
|
|
876
935
|
messageId: part.messageID,
|
|
877
936
|
sessionId: part.sessionID,
|
|
878
|
-
partId: part.id,
|
|
937
|
+
partId: partId ?? part.id,
|
|
879
938
|
snapshotText: typeof part.text === "string" ? part.text : undefined,
|
|
880
939
|
deltaText: typeof properties.delta === "string" ? properties.delta : undefined,
|
|
881
940
|
});
|
|
@@ -892,7 +951,6 @@ export class OpenCodeServerAdapter {
|
|
|
892
951
|
if (!this.syncTrackedSessionFromEvent(part.sessionID)) {
|
|
893
952
|
return;
|
|
894
953
|
}
|
|
895
|
-
const partId = this.extractPartId(properties, part);
|
|
896
954
|
if (!partId) {
|
|
897
955
|
return;
|
|
898
956
|
}
|
|
@@ -907,6 +965,29 @@ export class OpenCodeServerAdapter {
|
|
|
907
965
|
: delta
|
|
908
966
|
? this.consumeVisiblePartDelta(partId, delta)
|
|
909
967
|
: "";
|
|
968
|
+
const observedText = typeof part.messageID === "string"
|
|
969
|
+
? this.observedOpenCodeMessages.get(part.messageID)?.text
|
|
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
|
+
}
|
|
980
|
+
if (this.matchesRecentWechatPromptMirror(part.sessionID, observedText || partText || delta || text, {
|
|
981
|
+
allowPrefix: true,
|
|
982
|
+
})) {
|
|
983
|
+
return;
|
|
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
|
+
}
|
|
910
991
|
this.pushVisibleOutput(text);
|
|
911
992
|
}
|
|
912
993
|
handleMessagePartDelta(properties) {
|
|
@@ -928,16 +1009,20 @@ export class OpenCodeServerAdapter {
|
|
|
928
1009
|
}
|
|
929
1010
|
return;
|
|
930
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
|
+
}
|
|
931
1020
|
if (properties.field !== "text") {
|
|
932
1021
|
return;
|
|
933
1022
|
}
|
|
934
1023
|
const delta = typeof properties.delta === "string"
|
|
935
1024
|
? properties.delta
|
|
936
1025
|
: undefined;
|
|
937
|
-
const sessionId = typeof properties.sessionID === "string"
|
|
938
|
-
? properties.sessionID
|
|
939
|
-
: undefined;
|
|
940
|
-
const partId = this.extractPartId(properties);
|
|
941
1026
|
if (typeof properties.messageID === "string" && partId && delta) {
|
|
942
1027
|
this.trackObservedOpenCodeMessagePart({
|
|
943
1028
|
messageId: properties.messageID,
|
|
@@ -953,6 +1038,24 @@ export class OpenCodeServerAdapter {
|
|
|
953
1038
|
return;
|
|
954
1039
|
}
|
|
955
1040
|
const text = this.consumeVisiblePartDelta(partId, delta);
|
|
1041
|
+
const observedText = typeof properties.messageID === "string"
|
|
1042
|
+
? this.observedOpenCodeMessages.get(properties.messageID)?.text
|
|
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
|
+
}
|
|
1053
|
+
if (this.matchesRecentWechatPromptMirror(sessionId, observedText || delta || text, {
|
|
1054
|
+
allowPrefix: true,
|
|
1055
|
+
})) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
this.recordVisibleReplyPartDelta(partId, sessionId, properties.messageID, text);
|
|
956
1059
|
this.pushVisibleOutput(text);
|
|
957
1060
|
}
|
|
958
1061
|
handleMessagePartRemoved(properties) {
|
|
@@ -964,6 +1067,156 @@ export class OpenCodeServerAdapter {
|
|
|
964
1067
|
return;
|
|
965
1068
|
}
|
|
966
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;
|
|
967
1220
|
}
|
|
968
1221
|
handleTuiPromptAppend(properties) {
|
|
969
1222
|
if (!isRecord(properties)) {
|
|
@@ -993,6 +1246,9 @@ export class OpenCodeServerAdapter {
|
|
|
993
1246
|
this.handleLocalPromptSubmit();
|
|
994
1247
|
return;
|
|
995
1248
|
default:
|
|
1249
|
+
if (this.isLocalSessionNavigationCommand(command)) {
|
|
1250
|
+
this.markPendingLocalSessionCreateFollow();
|
|
1251
|
+
}
|
|
996
1252
|
return;
|
|
997
1253
|
}
|
|
998
1254
|
}
|
|
@@ -1000,7 +1256,7 @@ export class OpenCodeServerAdapter {
|
|
|
1000
1256
|
if (!isRecord(properties)) {
|
|
1001
1257
|
return;
|
|
1002
1258
|
}
|
|
1003
|
-
const sessionId =
|
|
1259
|
+
const sessionId = this.extractSessionId(properties) ?? undefined;
|
|
1004
1260
|
if (!sessionId) {
|
|
1005
1261
|
return;
|
|
1006
1262
|
}
|
|
@@ -1010,6 +1266,7 @@ export class OpenCodeServerAdapter {
|
|
|
1010
1266
|
}
|
|
1011
1267
|
this.pendingLocalPrompt = "";
|
|
1012
1268
|
this.localPromptNoticeSent = false;
|
|
1269
|
+
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
1013
1270
|
this.logDebug(`[opencode-adapter:local-follow] tui.session.select ${sessionId}`);
|
|
1014
1271
|
this.switchSharedSession({
|
|
1015
1272
|
id: sessionId,
|
|
@@ -1043,7 +1300,9 @@ export class OpenCodeServerAdapter {
|
|
|
1043
1300
|
clearTrackedTurn: true,
|
|
1044
1301
|
syncVisible: true,
|
|
1045
1302
|
});
|
|
1303
|
+
return;
|
|
1046
1304
|
}
|
|
1305
|
+
this.markPendingLocalSessionCreateFollow();
|
|
1047
1306
|
}
|
|
1048
1307
|
handleSessionError(properties) {
|
|
1049
1308
|
if (!isRecord(properties)) {
|
|
@@ -1138,7 +1397,8 @@ export class OpenCodeServerAdapter {
|
|
|
1138
1397
|
return;
|
|
1139
1398
|
}
|
|
1140
1399
|
observed.emitted = true;
|
|
1141
|
-
if (this.shouldSuppressWechatMirroredPrompt(observed.sessionId, text)
|
|
1400
|
+
if (this.shouldSuppressWechatMirroredPrompt(observed.sessionId, text) ||
|
|
1401
|
+
this.matchesRecentWechatPromptMirror(observed.sessionId, text)) {
|
|
1142
1402
|
this.cleanupObservedOpenCodeMessage(messageId);
|
|
1143
1403
|
return;
|
|
1144
1404
|
}
|
|
@@ -1159,30 +1419,23 @@ export class OpenCodeServerAdapter {
|
|
|
1159
1419
|
if (!sessionId || !normalizedText) {
|
|
1160
1420
|
return;
|
|
1161
1421
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if (this.pendingWechatPromptMirrorSuppressions[index].createdAtMs < cutoff) {
|
|
1165
|
-
this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
this.pendingWechatPromptMirrorSuppressions.push({
|
|
1422
|
+
this.pruneWechatPromptMirrorSuppressions();
|
|
1423
|
+
const suppression = {
|
|
1169
1424
|
sessionId,
|
|
1170
1425
|
text: normalizedText,
|
|
1171
1426
|
createdAtMs: Date.now(),
|
|
1172
|
-
}
|
|
1427
|
+
};
|
|
1428
|
+
this.pendingWechatPromptMirrorSuppressions.push(suppression);
|
|
1429
|
+
this.recentWechatPromptMirrorSuppressions.push(suppression);
|
|
1173
1430
|
}
|
|
1174
1431
|
shouldSuppressWechatMirroredPrompt(sessionId, text) {
|
|
1175
1432
|
const normalizedText = normalizeOutput(text).trim();
|
|
1176
1433
|
if (!sessionId || !normalizedText) {
|
|
1177
1434
|
return false;
|
|
1178
1435
|
}
|
|
1179
|
-
|
|
1436
|
+
this.pruneWechatPromptMirrorSuppressions();
|
|
1180
1437
|
for (let index = this.pendingWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
|
|
1181
1438
|
const pending = this.pendingWechatPromptMirrorSuppressions[index];
|
|
1182
|
-
if (pending.createdAtMs < cutoff) {
|
|
1183
|
-
this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
1439
|
if (pending.sessionId === sessionId && pending.text === normalizedText) {
|
|
1187
1440
|
this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
|
|
1188
1441
|
return true;
|
|
@@ -1190,8 +1443,40 @@ export class OpenCodeServerAdapter {
|
|
|
1190
1443
|
}
|
|
1191
1444
|
return false;
|
|
1192
1445
|
}
|
|
1446
|
+
matchesRecentWechatPromptMirror(sessionId, text, options = {}) {
|
|
1447
|
+
const normalizedText = normalizeOutput(text ?? "").trim();
|
|
1448
|
+
if (!sessionId || !normalizedText) {
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
this.pruneWechatPromptMirrorSuppressions();
|
|
1452
|
+
return this.recentWechatPromptMirrorSuppressions.some((pending) => {
|
|
1453
|
+
if (pending.sessionId !== sessionId) {
|
|
1454
|
+
return false;
|
|
1455
|
+
}
|
|
1456
|
+
if (pending.text === normalizedText) {
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
return options.allowPrefix === true && pending.text.startsWith(normalizedText);
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
pruneWechatPromptMirrorSuppressions() {
|
|
1463
|
+
const cutoff = Date.now() - OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS;
|
|
1464
|
+
for (let index = this.pendingWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
|
|
1465
|
+
if (this.pendingWechatPromptMirrorSuppressions[index].createdAtMs < cutoff) {
|
|
1466
|
+
this.pendingWechatPromptMirrorSuppressions.splice(index, 1);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
for (let index = this.recentWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
|
|
1470
|
+
if (this.recentWechatPromptMirrorSuppressions[index].createdAtMs < cutoff) {
|
|
1471
|
+
this.recentWechatPromptMirrorSuppressions.splice(index, 1);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1193
1475
|
wasRecentlyMirroredLocalPrompt(text) {
|
|
1194
|
-
|
|
1476
|
+
return this.matchesRecentLocalPromptMirror(text);
|
|
1477
|
+
}
|
|
1478
|
+
matchesRecentLocalPromptMirror(text, options = {}) {
|
|
1479
|
+
const normalizedText = normalizeOutput(text ?? "").trim();
|
|
1195
1480
|
if (!normalizedText) {
|
|
1196
1481
|
return false;
|
|
1197
1482
|
}
|
|
@@ -1199,7 +1484,9 @@ export class OpenCodeServerAdapter {
|
|
|
1199
1484
|
this.hasAcceptedInput &&
|
|
1200
1485
|
this.lastMirroredLocalPrompt &&
|
|
1201
1486
|
this.lastMirroredLocalPrompt.createdAtMs >= Date.now() - OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS &&
|
|
1202
|
-
this.lastMirroredLocalPrompt.text === normalizedText
|
|
1487
|
+
(this.lastMirroredLocalPrompt.text === normalizedText ||
|
|
1488
|
+
(options.allowPrefix === true &&
|
|
1489
|
+
this.lastMirroredLocalPrompt.text.startsWith(normalizedText)))) {
|
|
1203
1490
|
return true;
|
|
1204
1491
|
}
|
|
1205
1492
|
return false;
|
|
@@ -1276,6 +1563,7 @@ export class OpenCodeServerAdapter {
|
|
|
1276
1563
|
this.observedUserTextByPartId.clear();
|
|
1277
1564
|
this.observedUserMessagePartIds.clear();
|
|
1278
1565
|
this.pendingWechatPromptMirrorSuppressions.length = 0;
|
|
1566
|
+
this.recentWechatPromptMirrorSuppressions.length = 0;
|
|
1279
1567
|
this.lastMirroredLocalPrompt = null;
|
|
1280
1568
|
}
|
|
1281
1569
|
clearTrackedTurnForLocalSessionSwitch() {
|
|
@@ -1389,7 +1677,7 @@ export class OpenCodeServerAdapter {
|
|
|
1389
1677
|
return true;
|
|
1390
1678
|
}
|
|
1391
1679
|
if (options.allowLocalTurnFollow !== false && this.shouldFollowLocalTurnSession(sessionId)) {
|
|
1392
|
-
this.switchSharedSession(session, {
|
|
1680
|
+
this.switchSharedSession(session ?? sessionId, {
|
|
1393
1681
|
source: "local",
|
|
1394
1682
|
reason: "local_turn",
|
|
1395
1683
|
notify: true,
|
|
@@ -1400,12 +1688,22 @@ export class OpenCodeServerAdapter {
|
|
|
1400
1688
|
return false;
|
|
1401
1689
|
}
|
|
1402
1690
|
extractSessionReference(properties) {
|
|
1403
|
-
if (typeof properties.sessionID === "string") {
|
|
1691
|
+
if (typeof properties.sessionID === "string" || typeof properties.sessionId === "string") {
|
|
1692
|
+
const id = typeof properties.sessionID === "string"
|
|
1693
|
+
? properties.sessionID
|
|
1694
|
+
: String(properties.sessionId);
|
|
1404
1695
|
return {
|
|
1405
|
-
id
|
|
1696
|
+
id,
|
|
1406
1697
|
workspaceID: this.extractWorkspaceId(properties) ?? undefined,
|
|
1407
1698
|
};
|
|
1408
1699
|
}
|
|
1700
|
+
const session = isRecord(properties.session) ? properties.session : undefined;
|
|
1701
|
+
if (typeof session?.id === "string") {
|
|
1702
|
+
return {
|
|
1703
|
+
id: session.id,
|
|
1704
|
+
workspaceID: this.extractWorkspaceId(session) ?? this.extractWorkspaceId(properties) ?? undefined,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1409
1707
|
const info = isRecord(properties.info) ? properties.info : undefined;
|
|
1410
1708
|
if (typeof info?.id === "string") {
|
|
1411
1709
|
return {
|
|
@@ -1422,6 +1720,10 @@ export class OpenCodeServerAdapter {
|
|
|
1422
1720
|
if (typeof properties.workspaceID === "string") {
|
|
1423
1721
|
return properties.workspaceID;
|
|
1424
1722
|
}
|
|
1723
|
+
const session = isRecord(properties.session) ? properties.session : undefined;
|
|
1724
|
+
if (typeof session?.workspaceID === "string") {
|
|
1725
|
+
return session.workspaceID;
|
|
1726
|
+
}
|
|
1425
1727
|
const info = isRecord(properties.info) ? properties.info : undefined;
|
|
1426
1728
|
if (typeof info?.workspaceID === "string") {
|
|
1427
1729
|
return info.workspaceID;
|
|
@@ -1433,6 +1735,46 @@ export class OpenCodeServerAdapter {
|
|
|
1433
1735
|
this.state.activeTurnOrigin === "local" &&
|
|
1434
1736
|
this.hasTrackedTurnState());
|
|
1435
1737
|
}
|
|
1738
|
+
markPendingLocalSessionCreateFollow() {
|
|
1739
|
+
this.pendingLocalSessionCreateFollowUntilMs =
|
|
1740
|
+
Date.now() + OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS;
|
|
1741
|
+
}
|
|
1742
|
+
hasPendingLocalSessionCreateFollow() {
|
|
1743
|
+
return Date.now() <= this.pendingLocalSessionCreateFollowUntilMs;
|
|
1744
|
+
}
|
|
1745
|
+
shouldAcceptUnscopedLocalSessionCreatedEvent(event) {
|
|
1746
|
+
if (this.normalizeEventType(event.type) !== "session.created") {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
if (this.options.renderMode !== "companion") {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
const payload = this.extractEventPayload(event);
|
|
1753
|
+
if (!isRecord(payload)) {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
const session = this.extractSessionReference(payload);
|
|
1757
|
+
if (!session?.id || session.id === this.activeSessionId) {
|
|
1758
|
+
return false;
|
|
1759
|
+
}
|
|
1760
|
+
if (typeof event.directory === "string" ||
|
|
1761
|
+
this.extractSessionDirectory(payload) ||
|
|
1762
|
+
this.extractWorkspaceId(payload)) {
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
return this.hasPendingLocalSessionCreateFollow() || Boolean(this.activeSessionId);
|
|
1766
|
+
}
|
|
1767
|
+
shouldFollowCreatedLocalSession(session) {
|
|
1768
|
+
if (!session?.id || session.id === this.activeSessionId) {
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
if (this.hasPendingLocalSessionCreateFollow()) {
|
|
1772
|
+
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
this.pendingLocalSessionCreateFollowUntilMs = 0;
|
|
1776
|
+
return Boolean(this.activeSessionId);
|
|
1777
|
+
}
|
|
1436
1778
|
switchSharedSession(session, options) {
|
|
1437
1779
|
const sessionId = typeof session === "string" ? session : session.id;
|
|
1438
1780
|
if (!sessionId) {
|
|
@@ -1490,7 +1832,9 @@ export class OpenCodeServerAdapter {
|
|
|
1490
1832
|
}
|
|
1491
1833
|
const normalized = command.trim().toLowerCase();
|
|
1492
1834
|
return (normalized === "session" ||
|
|
1835
|
+
normalized === "new" ||
|
|
1493
1836
|
normalized === "/session" ||
|
|
1837
|
+
normalized === "/new" ||
|
|
1494
1838
|
normalized.startsWith("session.") ||
|
|
1495
1839
|
normalized.startsWith("/session "));
|
|
1496
1840
|
}
|
|
@@ -1503,8 +1847,9 @@ export class OpenCodeServerAdapter {
|
|
|
1503
1847
|
}
|
|
1504
1848
|
matchesCurrentDirectoryEvent(event) {
|
|
1505
1849
|
const payload = this.extractEventPayload(event);
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1850
|
+
const eventRecord = event;
|
|
1851
|
+
const wrappedDirectory = typeof eventRecord.directory === "string"
|
|
1852
|
+
? eventRecord.directory
|
|
1508
1853
|
: undefined;
|
|
1509
1854
|
if (wrappedDirectory) {
|
|
1510
1855
|
const matchesWrappedDirectory = this.normalizeDirectory(wrappedDirectory) === this.normalizeDirectory(this.options.cwd);
|
|
@@ -1538,6 +1883,10 @@ export class OpenCodeServerAdapter {
|
|
|
1538
1883
|
if (typeof properties.directory === "string") {
|
|
1539
1884
|
return properties.directory;
|
|
1540
1885
|
}
|
|
1886
|
+
const session = isRecord(properties.session) ? properties.session : undefined;
|
|
1887
|
+
if (typeof session?.directory === "string") {
|
|
1888
|
+
return session.directory;
|
|
1889
|
+
}
|
|
1541
1890
|
const info = isRecord(properties.info) ? properties.info : undefined;
|
|
1542
1891
|
if (typeof info?.directory === "string") {
|
|
1543
1892
|
return info.directory;
|
|
@@ -1666,6 +2015,9 @@ export class OpenCodeServerAdapter {
|
|
|
1666
2015
|
}
|
|
1667
2016
|
clearStreamedPartState() {
|
|
1668
2017
|
this.emittedTextByPartId.clear();
|
|
2018
|
+
this.partTypeByPartId.clear();
|
|
2019
|
+
this.visibleReplyPartsByPartId.clear();
|
|
2020
|
+
this.visibleReplyMessageIds.clear();
|
|
1669
2021
|
}
|
|
1670
2022
|
cleanupObservedOpenCodeMessage(messageId) {
|
|
1671
2023
|
this.observedOpenCodeMessages.delete(messageId);
|