@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.
@@ -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 = this.serverProcess?.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
- throw new Error(`Failed to send prompt: ${describeUnknownError(err)}`);
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: "companion_disconnected",
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 summary = this.outputBatcher.getRecentSummary(500);
742
+ .then(async () => {
743
+ const finalReplyText = await this.resolveFinalReplyText(sessionId, turnStartedAtMs);
701
744
  this.setStatus("idle");
702
- if (summary && summary !== "(no output)") {
745
+ if (finalReplyText) {
703
746
  this.emit({
704
747
  type: "final_reply",
705
- text: summary,
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 (!this.syncTrackedSessionFromEvent(session)) {
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 = typeof properties.sessionID === "string" ? properties.sessionID : undefined;
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
- const cutoff = Date.now() - OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS;
1163
- for (let index = this.pendingWechatPromptMirrorSuppressions.length - 1; index >= 0; index -= 1) {
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
- const cutoff = Date.now() - OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS;
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
- const normalizedText = normalizeOutput(text).trim();
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: properties.sessionID,
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 wrappedDirectory = typeof event.directory === "string"
1507
- ? event.directory
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);