@unlinearity/cli-wechat-bridge 1.0.3 → 1.0.5

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.
@@ -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 = this.serverProcess?.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: "companion_disconnected",
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 summary = this.outputBatcher.getRecentSummary(500);
742
+ .then(async () => {
743
+ const finalReplyText = await this.resolveFinalReplyText(sessionId, turnStartedAtMs);
737
744
  this.setStatus("idle");
738
- if (summary && summary !== "(no output)") {
745
+ if (finalReplyText) {
739
746
  this.emit({
740
747
  type: "final_reply",
741
- text: summary,
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
- const normalizedText = normalizeOutput(text).trim();
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 wrappedDirectory = typeof event.directory === "string"
1657
- ? event.directory
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);
@@ -8,6 +8,8 @@ import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLoc
8
8
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
9
  import { buildClaudeFailureMessage, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeResumeConversationId, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
10
10
  import { detectCliApproval, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
11
+ import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
12
+ export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
11
13
  export const DEFAULT_COLS = 120;
12
14
  export const DEFAULT_ROWS = 30;
13
15
  export const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -39,101 +41,6 @@ export const OPENCODE_SERVER_READY_TIMEOUT_MS = 10_000;
39
41
  export const OPENCODE_SSE_RECONNECT_DELAY_MS = 2_000;
40
42
  export const OPENCODE_SESSION_IDLE_SETTLE_MS = 1_500;
41
43
  export const OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS = 12_000;
42
- export function isRecord(value) {
43
- return typeof value === "object" && value !== null && !Array.isArray(value);
44
- }
45
- export function getCodexRpcRequestId(value) {
46
- return typeof value === "string" || typeof value === "number" ? value : null;
47
- }
48
- export function getNotificationThreadId(params) {
49
- if (!isRecord(params)) {
50
- return null;
51
- }
52
- if (typeof params.threadId === "string") {
53
- return params.threadId;
54
- }
55
- if (isRecord(params.thread) && typeof params.thread.id === "string") {
56
- return params.thread.id;
57
- }
58
- return null;
59
- }
60
- export function getNotificationTurnId(params) {
61
- if (!isRecord(params)) {
62
- return null;
63
- }
64
- if (typeof params.turnId === "string") {
65
- return params.turnId;
66
- }
67
- if (isRecord(params.turn) && typeof params.turn.id === "string") {
68
- return params.turn.id;
69
- }
70
- return null;
71
- }
72
- export function describeUnknownError(error) {
73
- if (error instanceof Error && error.message) {
74
- return error.message;
75
- }
76
- return String(error);
77
- }
78
- export function normalizeCodexRpcError(error) {
79
- if (isRecord(error)) {
80
- const message = typeof error.message === "string"
81
- ? error.message
82
- : typeof error.code === "number"
83
- ? `RPC error ${error.code}`
84
- : "";
85
- const data = typeof error.data === "string"
86
- ? error.data
87
- : typeof error.details === "string"
88
- ? error.details
89
- : "";
90
- const combined = [message, data].filter(Boolean).join(": ");
91
- if (combined) {
92
- return combined;
93
- }
94
- }
95
- return describeUnknownError(error);
96
- }
97
- export function getLocalCompanionCommandName(kind) {
98
- switch (kind) {
99
- case "codex":
100
- return "wechat-codex";
101
- case "claude":
102
- return "wechat-claude";
103
- case "opencode":
104
- return "wechat-opencode";
105
- default:
106
- return "local companion";
107
- }
108
- }
109
- export function getSharedSessionIdFromAdapterState(state) {
110
- return state.sharedSessionId ?? state.sharedThreadId;
111
- }
112
- export function quoteWindowsCommandArg(value) {
113
- return `"${value.replace(/"/g, '""')}"`;
114
- }
115
- export function quotePosixCommandArg(value) {
116
- return `'${value.replace(/'/g, `'\\''`)}'`;
117
- }
118
- export function isRecentIsoTimestamp(timestamp, maxAgeMs) {
119
- const parsedMs = Date.parse(timestamp);
120
- if (!Number.isFinite(parsedMs)) {
121
- return false;
122
- }
123
- return parsedMs >= Date.now() - maxAgeMs;
124
- }
125
- export function coerceWebSocketMessageData(data) {
126
- if (typeof data === "string") {
127
- return data;
128
- }
129
- if (data instanceof ArrayBuffer) {
130
- return Buffer.from(data).toString("utf8");
131
- }
132
- if (ArrayBuffer.isView(data)) {
133
- return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
134
- }
135
- return null;
136
- }
137
44
  export function buildCodexCliArgs(remoteUrl, options = {}) {
138
45
  assertNoReservedExtraCliArgs(options.extraCliArgs ?? [], ["--remote", "--remote-auth-token-env"], "Codex remote connection");
139
46
  const args = [];
@@ -196,7 +103,7 @@ export function shouldIncludeClaudeNoAltScreen(command) {
196
103
  if (cached !== undefined) {
197
104
  return cached;
198
105
  }
199
- let supported = false;
106
+ let supported;
200
107
  try {
201
108
  const probe = spawnSync(spawnTarget.file, [...spawnTarget.args, "--help"], {
202
109
  cwd: process.cwd(),
@@ -660,7 +567,7 @@ export function escapePowerShellString(text) {
660
567
  return text.replace(/`/g, "``").replace(/"/g, '`"');
661
568
  }
662
569
  export function escapePosixShellString(text) {
663
- return `'${text.replace(/'/g, `'\"'\"'`)}'`;
570
+ return `'${text.replace(/'/g, `'"'"'`)}'`;
664
571
  }
665
572
  export function buildShellProfileCommand(profilePath, family) {
666
573
  const resolved = path.resolve(profilePath);
@@ -17,7 +17,9 @@ export class BridgeController {
17
17
  this.clearLocalClientEndpoint();
18
18
  return;
19
19
  }
20
- const existing = readLocalCompanionEndpoint(this.cwd);
20
+ const existing = readLocalCompanionEndpoint(this.cwd, {
21
+ adapter: endpoint.kind,
22
+ });
21
23
  const adapterState = this.adapter.getState();
22
24
  const nextEndpoint = existing?.instanceId === endpoint.instanceId
23
25
  ? {
@@ -38,7 +40,9 @@ export class BridgeController {
38
40
  writeLocalCompanionEndpoint(nextEndpoint);
39
41
  }
40
42
  clearLocalClientEndpoint() {
41
- clearLocalCompanionEndpoint(this.cwd, this.endpointInstanceId ?? undefined);
43
+ clearLocalCompanionEndpoint(this.cwd, this.endpointInstanceId ?? undefined, {
44
+ adapter: this.adapter.getState().kind,
45
+ });
42
46
  this.endpointInstanceId = null;
43
47
  }
44
48
  }
@@ -1,10 +1,25 @@
1
1
  import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, } from "./bridge-utils.js";
2
+ export const OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE = "OpenCode 没有产生可发送到微信的可见回复。请查看本地终端输出,或重试这条消息。";
2
3
  export async function forwardWechatFinalReply(params) {
3
- const { adapter, rawText, sender } = params;
4
+ const { adapter, rawText, sender, onEmptyVisibleReply } = params;
4
5
  const parsed = parseWechatFinalReply(rawText);
5
- const visibleText = formatFinalReplyMessage(adapter, sanitizeWechatFinalReplyText(adapter, parsed.visibleText)).trim();
6
+ const sanitizedText = sanitizeWechatFinalReplyText(adapter, parsed.visibleText);
7
+ const visibleText = formatFinalReplyMessage(adapter, sanitizedText).trim();
6
8
  if (visibleText) {
7
- await sender.sendText(visibleText);
9
+ const sent = await sender.sendText(visibleText);
10
+ if (sent === false) {
11
+ return;
12
+ }
13
+ }
14
+ else if (adapter === "opencode" && parsed.visibleText.trim()) {
15
+ onEmptyVisibleReply?.({
16
+ adapter,
17
+ rawVisibleText: parsed.visibleText,
18
+ });
19
+ const sent = await sender.sendText(OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE);
20
+ if (sent === false) {
21
+ return;
22
+ }
8
23
  }
9
24
  for (const attachment of parsed.attachments) {
10
25
  try {
@@ -3,6 +3,14 @@ import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, ensureWorkspaceChannelDir, ensureCha
3
3
  import { buildInstanceId } from "./bridge-utils.js";
4
4
  const ORPHAN_LOCK_RECLAIM_TIMEOUT_MS = 2_000;
5
5
  const ORPHAN_LOCK_RECLAIM_POLL_MS = 100;
6
+ export function resolveRestorableSharedSessionId(persisted, options) {
7
+ if (!persisted ||
8
+ persisted.cwd !== options.cwd ||
9
+ persisted.adapter !== options.adapter) {
10
+ return undefined;
11
+ }
12
+ return persisted.sharedSessionId ?? persisted.sharedThreadId;
13
+ }
6
14
  function cloneState(state) {
7
15
  return JSON.parse(JSON.stringify(state));
8
16
  }
@@ -162,9 +170,7 @@ export class BridgeStateStore {
162
170
  };
163
171
  this.acquireLock();
164
172
  const persisted = this.readStateFile();
165
- const persistedSharedSessionId = persisted?.cwd === options.cwd
166
- ? persisted.sharedSessionId ?? persisted.sharedThreadId
167
- : undefined;
173
+ const persistedSharedSessionId = resolveRestorableSharedSessionId(persisted, options);
168
174
  const persistedResumeConversationId = options.adapter === "claude" &&
169
175
  persisted?.cwd === options.cwd &&
170
176
  typeof persisted.resumeConversationId === "string"