@unlinearity/cli-wechat-bridge 1.2.0 → 1.2.2
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 +25 -10
- package/bin/wechat-check-update.mjs +0 -0
- package/bin/wechat-claude.mjs +0 -0
- package/bin/wechat-codex.mjs +0 -0
- package/bin/wechat-daemon.mjs +0 -0
- package/bin/wechat-opencode.mjs +0 -0
- package/bin/wechat-pi.mjs +0 -0
- package/bin/wechat-setup.mjs +0 -0
- package/bin/wecom-check-update.mjs +0 -0
- package/bin/wecom-claude.mjs +0 -0
- package/bin/wecom-codex.mjs +0 -0
- package/bin/wecom-daemon.mjs +0 -0
- package/bin/wecom-opencode.mjs +0 -0
- package/bin/wecom-pi.mjs +0 -0
- package/bin/wecom-setup.mjs +0 -0
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +295 -90
- package/dist/bridge/bridge-adapters.core.js +84 -7
- package/dist/bridge/bridge-adapters.opencode.js +344 -74
- package/dist/bridge/bridge-adapters.pi.js +23 -18
- package/dist/bridge/bridge-adapters.shared.js +9 -9
- package/dist/bridge/bridge-session-resume.js +13 -6
- package/dist/bridge/bridge-utils.js +94 -144
- package/dist/bridge/claude-hooks.js +60 -3
- package/dist/bridge/claude-native-controls.js +32 -13
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/language-control.js +33 -0
- package/dist/bridge/native-terminal-control.js +38 -10
- package/dist/bridge/opencode-v2-compat.js +458 -0
- package/dist/bridge/wechat-bridge.js +57 -42
- package/dist/channels/channel-driver-factory.js +44 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- package/dist/companion/local-companion.js +27 -7
- package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
- package/dist/companion/pi-tui-bridge-extension.js +10 -4
- package/dist/core/bridge-control.js +3 -0
- package/dist/core/bridge-defer.js +3 -2
- package/dist/core/bridge-event-forwarder.js +3 -0
- package/dist/core/channel-types.js +5 -1
- package/dist/daemon/wechat-daemon.js +202 -124
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +244 -1
- package/dist/i18n/messages-zh.js +244 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- package/package.json +4 -2
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
5
|
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
6
|
// LICENSE.txt. Network services built on it must offer source to users.
|
|
7
|
+
import { t } from "../i18n/index.js";
|
|
7
8
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
8
9
|
import { randomBytes } from "node:crypto";
|
|
9
10
|
import fs from "node:fs";
|
|
@@ -12,6 +13,7 @@ import path from "node:path";
|
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, OPENCODE_HTTP_READY_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS, OPENCODE_HTTP_READY_PROBE_INTERVAL_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
|
|
14
15
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
16
|
+
import { createOpenCodeV2CompatClient } from "./opencode-v2-compat.js";
|
|
15
17
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
16
18
|
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
17
19
|
const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
|
|
@@ -24,7 +26,11 @@ const TUI_SESSION_SELECT_SUPPRESSION_TTL_MS = 5_000;
|
|
|
24
26
|
const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
|
|
25
27
|
const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
26
28
|
const OPENCODE_MINIMUM_SUPPORTED_VERSION = "1.18.0";
|
|
27
|
-
const OPENCODE_MAXIMUM_SUPPORTED_MAJOR =
|
|
29
|
+
const OPENCODE_MAXIMUM_SUPPORTED_MAJOR = 3;
|
|
30
|
+
const OPENCODE_VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
31
|
+
export function parseOpenCodeCliVersion(output) {
|
|
32
|
+
return /\bv?(\d+\.\d+\.\d+)\b/.exec(output)?.[1] ?? null;
|
|
33
|
+
}
|
|
28
34
|
const OPENCODE_TUI_ROUTE_READY_TIMEOUT_MS = 10_000;
|
|
29
35
|
const OPENCODE_TUI_ROUTE_MAX_BUFFER_SIZE = 1024 * 1024;
|
|
30
36
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
@@ -83,6 +89,8 @@ export class OpenCodeServerAdapter {
|
|
|
83
89
|
tuiRoutePort = 0;
|
|
84
90
|
tuiRouteConfigDir = null;
|
|
85
91
|
tuiRouteConfigPath = null;
|
|
92
|
+
tuiRoutePluginPath = null;
|
|
93
|
+
openCodeMajor = 1;
|
|
86
94
|
visibleTuiSessionId = null;
|
|
87
95
|
tuiRouteStateReceived = false;
|
|
88
96
|
expectedVisibleTuiSessionId = null;
|
|
@@ -90,8 +98,14 @@ export class OpenCodeServerAdapter {
|
|
|
90
98
|
lastMirroredLocalPrompt = null;
|
|
91
99
|
pendingLocalSessionCreateFollowUntilMs = 0;
|
|
92
100
|
pendingPermission = null;
|
|
101
|
+
pendingPermissions = new Map();
|
|
102
|
+
approvalOwnershipGeneration = 0;
|
|
103
|
+
settledPermissionIds = new Set();
|
|
104
|
+
resolvingPermissions = new Set();
|
|
105
|
+
childSessionParents = new Map();
|
|
93
106
|
pendingQuestion = null;
|
|
94
107
|
previousAgentBySession = new Map();
|
|
108
|
+
childSessionIds = new Set();
|
|
95
109
|
async listModels() {
|
|
96
110
|
this.assertRemoteControlReady();
|
|
97
111
|
const session = await this.ensureControlSession();
|
|
@@ -109,7 +123,7 @@ export class OpenCodeServerAdapter {
|
|
|
109
123
|
const models = await this.listControlModels();
|
|
110
124
|
const target = models.find((model) => model.id === modelId);
|
|
111
125
|
if (!target) {
|
|
112
|
-
throw new Error("
|
|
126
|
+
throw new Error(t("adapter.opencode.modelUnavailable"));
|
|
113
127
|
}
|
|
114
128
|
const currentVariant = session.model?.variant;
|
|
115
129
|
const variant = currentVariant && currentVariant !== "default" && target.variants.includes(currentVariant)
|
|
@@ -129,7 +143,7 @@ export class OpenCodeServerAdapter {
|
|
|
129
143
|
updated.model?.providerID !== target.providerID ||
|
|
130
144
|
updated.model.id !== target.modelID ||
|
|
131
145
|
(updated.model.variant ?? "default") !== variant) {
|
|
132
|
-
throw new Error("
|
|
146
|
+
throw new Error(t("adapter.opencode.modelUnconfirmed"));
|
|
133
147
|
}
|
|
134
148
|
return { id: target.id, displayName: target.displayName, isCurrent: true };
|
|
135
149
|
}
|
|
@@ -139,11 +153,11 @@ export class OpenCodeServerAdapter {
|
|
|
139
153
|
const agents = await this.listPrimaryAgents();
|
|
140
154
|
const planAgent = agents.find((agent) => agent.name === "plan");
|
|
141
155
|
if (!planAgent) {
|
|
142
|
-
throw new Error("
|
|
156
|
+
throw new Error(t("adapter.opencode.planUnavailable"));
|
|
143
157
|
}
|
|
144
158
|
const currentAgent = session.agent ?? agents[0]?.name;
|
|
145
159
|
if (!currentAgent) {
|
|
146
|
-
throw new Error("
|
|
160
|
+
throw new Error(t("adapter.opencode.agentsMissing"));
|
|
147
161
|
}
|
|
148
162
|
if (enabled && currentAgent === "plan") {
|
|
149
163
|
return true;
|
|
@@ -160,7 +174,7 @@ export class OpenCodeServerAdapter {
|
|
|
160
174
|
: agents.find((agent) => agent.name === "build")?.name ??
|
|
161
175
|
agents.find((agent) => agent.name !== "plan")?.name;
|
|
162
176
|
if (!target) {
|
|
163
|
-
throw new Error("
|
|
177
|
+
throw new Error(t("adapter.opencode.planReplacementMissing"));
|
|
164
178
|
}
|
|
165
179
|
const result = await this.client.v2.session.switchAgent({
|
|
166
180
|
sessionID: session.id,
|
|
@@ -169,7 +183,7 @@ export class OpenCodeServerAdapter {
|
|
|
169
183
|
this.unwrapOrThrow(result);
|
|
170
184
|
const updated = await this.getSessionForCurrentDirectory(session.id);
|
|
171
185
|
if (!updated || updated.agent !== target) {
|
|
172
|
-
throw new Error("
|
|
186
|
+
throw new Error(t("adapter.opencode.agentUnconfirmed"));
|
|
173
187
|
}
|
|
174
188
|
if (enabled) {
|
|
175
189
|
this.previousAgentBySession.set(session.id, currentAgent);
|
|
@@ -181,16 +195,16 @@ export class OpenCodeServerAdapter {
|
|
|
181
195
|
}
|
|
182
196
|
assertRemoteControlReady() {
|
|
183
197
|
if (!this.client) {
|
|
184
|
-
throw new Error("
|
|
198
|
+
throw new Error(t("adapter.opencode.notRunning"));
|
|
185
199
|
}
|
|
186
200
|
if (this.state.status === "busy") {
|
|
187
|
-
throw new Error("
|
|
201
|
+
throw new Error(t("adapter.opencode.busy"));
|
|
188
202
|
}
|
|
189
203
|
if (this.pendingPermission) {
|
|
190
|
-
throw new Error("
|
|
204
|
+
throw new Error(t("adapter.opencode.approvalPending"));
|
|
191
205
|
}
|
|
192
206
|
if (this.pendingQuestion) {
|
|
193
|
-
throw new Error("
|
|
207
|
+
throw new Error(t("adapter.opencode.inputPending"));
|
|
194
208
|
}
|
|
195
209
|
}
|
|
196
210
|
async ensureControlSession() {
|
|
@@ -207,7 +221,7 @@ export class OpenCodeServerAdapter {
|
|
|
207
221
|
}
|
|
208
222
|
async listPrimaryAgents() {
|
|
209
223
|
if (!this.client)
|
|
210
|
-
throw new Error("
|
|
224
|
+
throw new Error(t("adapter.opencode.notRunning"));
|
|
211
225
|
const agents = this.unwrapOrThrow(await this.client.app.agents({
|
|
212
226
|
directory: this.options.cwd,
|
|
213
227
|
workspace: this.activeWorkspaceId ?? undefined,
|
|
@@ -216,7 +230,7 @@ export class OpenCodeServerAdapter {
|
|
|
216
230
|
}
|
|
217
231
|
async listControlModels() {
|
|
218
232
|
if (!this.client)
|
|
219
|
-
throw new Error("
|
|
233
|
+
throw new Error(t("adapter.opencode.notRunning"));
|
|
220
234
|
const providers = this.unwrapOrThrow(await this.client.provider.list({ directory: this.options.cwd, workspace: this.activeWorkspaceId ?? undefined }, { signal: AbortSignal.timeout(10_000) }));
|
|
221
235
|
return providers.all.filter((provider) => providers.connected.includes(provider.id)).flatMap((provider) => Object.entries(provider.models).filter(([id, model]) => model.status !== "deprecated" && !(provider.id === "opencode" && id.includes("-nano"))).map(([id, model]) => ({
|
|
222
236
|
id: `${provider.id}/${id}`, displayName: `${model.name || id} (${provider.name})`,
|
|
@@ -229,6 +243,8 @@ export class OpenCodeServerAdapter {
|
|
|
229
243
|
this.state = {
|
|
230
244
|
kind: options.kind,
|
|
231
245
|
status: "stopped",
|
|
246
|
+
supportsApprovalRequestIds: true,
|
|
247
|
+
pendingApprovals: [],
|
|
232
248
|
cwd: options.cwd,
|
|
233
249
|
command: options.command,
|
|
234
250
|
profile: options.profile,
|
|
@@ -250,6 +266,7 @@ export class OpenCodeServerAdapter {
|
|
|
250
266
|
this.shuttingDown = false;
|
|
251
267
|
this.setStatus("starting", "Starting OpenCode companion...");
|
|
252
268
|
try {
|
|
269
|
+
await this.probeOpenCodeVersion();
|
|
253
270
|
this.serverPort = await reserveLocalPort();
|
|
254
271
|
const serverProcess = await this.startServerProcess();
|
|
255
272
|
await waitForTcpPort(OPENCODE_SERVER_HOST, this.serverPort, OPENCODE_SERVER_READY_TIMEOUT_MS);
|
|
@@ -282,16 +299,16 @@ export class OpenCodeServerAdapter {
|
|
|
282
299
|
}
|
|
283
300
|
async sendInput(text) {
|
|
284
301
|
if (!this.client) {
|
|
285
|
-
throw new Error("
|
|
302
|
+
throw new Error(t("adapter.opencode.adapterNotRunning"));
|
|
286
303
|
}
|
|
287
304
|
if (this.state.status === "busy") {
|
|
288
|
-
throw new Error("
|
|
305
|
+
throw new Error(t("adapter.opencode.busy"));
|
|
289
306
|
}
|
|
290
307
|
if (this.pendingPermission) {
|
|
291
|
-
throw new Error("
|
|
308
|
+
throw new Error(t("adapter.opencode.approvalPending"));
|
|
292
309
|
}
|
|
293
310
|
if (this.pendingQuestion) {
|
|
294
|
-
throw new Error("
|
|
311
|
+
throw new Error(t("adapter.opencode.inputPending"));
|
|
295
312
|
}
|
|
296
313
|
const normalized = normalizeOutput(text).trim();
|
|
297
314
|
if (!normalized) {
|
|
@@ -341,7 +358,7 @@ export class OpenCodeServerAdapter {
|
|
|
341
358
|
}
|
|
342
359
|
async listResumeSessions(limit = 10) {
|
|
343
360
|
if (!this.client) {
|
|
344
|
-
throw new Error("
|
|
361
|
+
throw new Error(t("adapter.opencode.notReady"));
|
|
345
362
|
}
|
|
346
363
|
try {
|
|
347
364
|
const sessions = this.unwrapOrThrow(await this.client.session.list({
|
|
@@ -377,16 +394,16 @@ export class OpenCodeServerAdapter {
|
|
|
377
394
|
}
|
|
378
395
|
async resumeSession(sessionId) {
|
|
379
396
|
if (!this.client) {
|
|
380
|
-
throw new Error("
|
|
397
|
+
throw new Error(t("adapter.opencode.notReady"));
|
|
381
398
|
}
|
|
382
399
|
if (this.pendingPermission) {
|
|
383
|
-
throw new Error("
|
|
400
|
+
throw new Error(t("adapter.opencode.approvalPending"));
|
|
384
401
|
}
|
|
385
402
|
if (this.pendingQuestion) {
|
|
386
|
-
throw new Error("
|
|
403
|
+
throw new Error(t("adapter.opencode.inputPending"));
|
|
387
404
|
}
|
|
388
405
|
if (this.state.status === "busy") {
|
|
389
|
-
throw new Error("
|
|
406
|
+
throw new Error(t("adapter.opencode.busy"));
|
|
390
407
|
}
|
|
391
408
|
if (this.state.status !== "idle") {
|
|
392
409
|
throw new Error(`OpenCode cannot switch sessions while its status is ${this.state.status}.`);
|
|
@@ -414,16 +431,16 @@ export class OpenCodeServerAdapter {
|
|
|
414
431
|
}
|
|
415
432
|
async createSession() {
|
|
416
433
|
if (!this.client) {
|
|
417
|
-
throw new Error("
|
|
434
|
+
throw new Error(t("adapter.opencode.adapterNotRunning"));
|
|
418
435
|
}
|
|
419
436
|
if (this.state.status === "busy") {
|
|
420
|
-
throw new Error("
|
|
437
|
+
throw new Error(t("adapter.opencode.busy"));
|
|
421
438
|
}
|
|
422
439
|
if (this.pendingPermission) {
|
|
423
|
-
throw new Error("
|
|
440
|
+
throw new Error(t("adapter.opencode.approvalPending"));
|
|
424
441
|
}
|
|
425
442
|
if (this.pendingQuestion) {
|
|
426
|
-
throw new Error("
|
|
443
|
+
throw new Error(t("adapter.opencode.inputPending"));
|
|
427
444
|
}
|
|
428
445
|
this.outputBatcher.clear();
|
|
429
446
|
this.clearStreamedPartState();
|
|
@@ -490,22 +507,30 @@ export class OpenCodeServerAdapter {
|
|
|
490
507
|
await this.dispose();
|
|
491
508
|
await this.start();
|
|
492
509
|
}
|
|
493
|
-
async resolveApproval(action) {
|
|
494
|
-
|
|
510
|
+
async resolveApproval(action, requestId) {
|
|
511
|
+
const pending = requestId === undefined
|
|
512
|
+
? this.pendingPermission
|
|
513
|
+
: this.pendingPermissions.get(requestId);
|
|
514
|
+
if (!pending || !this.client || this.resolvingPermissions.has(pending.permissionId)) {
|
|
495
515
|
return false;
|
|
496
516
|
}
|
|
497
|
-
|
|
498
|
-
const response = action === "confirm" ? "once" : "reject";
|
|
517
|
+
this.resolvingPermissions.add(pending.permissionId);
|
|
499
518
|
try {
|
|
500
519
|
const result = await this.client.permission.reply({
|
|
501
|
-
requestID: permissionId,
|
|
520
|
+
requestID: pending.permissionId,
|
|
521
|
+
sessionID: pending.sessionId,
|
|
502
522
|
directory: this.options.cwd,
|
|
503
523
|
workspace: this.activeWorkspaceId ?? undefined,
|
|
504
|
-
reply:
|
|
524
|
+
reply: action === "confirm" ? "once" : "reject",
|
|
505
525
|
});
|
|
506
526
|
if (result.error !== undefined) {
|
|
507
527
|
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
508
528
|
}
|
|
529
|
+
this.rememberSettledPermission(pending.permissionId);
|
|
530
|
+
this.pendingPermissions.delete(pending.permissionId);
|
|
531
|
+
this.clearWechatWorkingNotice();
|
|
532
|
+
this.syncPendingPermissions();
|
|
533
|
+
return true;
|
|
509
534
|
}
|
|
510
535
|
catch (err) {
|
|
511
536
|
this.emit({
|
|
@@ -515,14 +540,17 @@ export class OpenCodeServerAdapter {
|
|
|
515
540
|
});
|
|
516
541
|
return false;
|
|
517
542
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return true;
|
|
543
|
+
finally {
|
|
544
|
+
this.resolvingPermissions.delete(pending.permissionId);
|
|
545
|
+
}
|
|
522
546
|
}
|
|
523
547
|
async resolveAllApprovals(action) {
|
|
524
|
-
|
|
525
|
-
|
|
548
|
+
let count = 0;
|
|
549
|
+
for (const id of Array.from(this.pendingPermissions.keys())) {
|
|
550
|
+
if (await this.resolveApproval(action, id))
|
|
551
|
+
count++;
|
|
552
|
+
}
|
|
553
|
+
return count;
|
|
526
554
|
}
|
|
527
555
|
async submitUserInput(answers) {
|
|
528
556
|
if (!this.pendingQuestion || !this.client) {
|
|
@@ -550,11 +578,14 @@ export class OpenCodeServerAdapter {
|
|
|
550
578
|
return false;
|
|
551
579
|
}
|
|
552
580
|
this.clearPendingQuestionState();
|
|
553
|
-
this.setStatus("busy", "OpenCode user input submitted.");
|
|
581
|
+
this.setStatus(this.pendingPermissions.size ? "awaiting_approval" : "busy", "OpenCode user input submitted.");
|
|
554
582
|
return true;
|
|
555
583
|
}
|
|
556
584
|
async dispose() {
|
|
557
585
|
this.shuttingDown = true;
|
|
586
|
+
this.childSessionIds.clear();
|
|
587
|
+
this.childSessionParents.clear();
|
|
588
|
+
this.settledPermissionIds.clear();
|
|
558
589
|
this.clearWechatWorkingNotice(true);
|
|
559
590
|
this.pendingLocalPrompt = "";
|
|
560
591
|
this.localPromptNoticeSent = false;
|
|
@@ -632,13 +663,28 @@ export class OpenCodeServerAdapter {
|
|
|
632
663
|
resolve(address.port);
|
|
633
664
|
});
|
|
634
665
|
});
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const pluginPath = path.join(MODULE_DIR, "..", "companion",
|
|
666
|
+
const pluginName = this.openCodeMajor >= 2
|
|
667
|
+
? "opencode-v2-tui-bridge-plugin"
|
|
668
|
+
: "opencode-tui-bridge-plugin";
|
|
669
|
+
const pluginPath = path.join(MODULE_DIR, "..", "companion", `${pluginName}${RUNTIME_ENTRY_EXTENSION}`);
|
|
639
670
|
if (!fs.existsSync(pluginPath)) {
|
|
640
671
|
throw new Error(`OpenCode TUI bridge plugin was not found: ${pluginPath}`);
|
|
641
672
|
}
|
|
673
|
+
const workspaceDir = ensureWorkspaceChannelDir(this.options.cwd).workspaceDir;
|
|
674
|
+
const configDir = fs.mkdtempSync(path.join(workspaceDir, "opencode-tui-route-"));
|
|
675
|
+
this.tuiRouteConfigDir = configDir;
|
|
676
|
+
if (this.openCodeMajor >= 2) {
|
|
677
|
+
fs.copyFileSync(pluginPath, path.join(configDir, `tui${RUNTIME_ENTRY_EXTENSION}`));
|
|
678
|
+
fs.writeFileSync(path.join(configDir, "package.json"), `${JSON.stringify({
|
|
679
|
+
name: "cli-wechat-bridge-opencode-tui-route",
|
|
680
|
+
private: true,
|
|
681
|
+
type: "module",
|
|
682
|
+
}, null, 2)}\n`, "utf8");
|
|
683
|
+
this.tuiRoutePluginPath = configDir;
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
this.tuiRoutePluginPath = pluginPath;
|
|
687
|
+
const configPath = path.join(configDir, "tui.json");
|
|
642
688
|
fs.writeFileSync(configPath, `${JSON.stringify({
|
|
643
689
|
plugin: [
|
|
644
690
|
[
|
|
@@ -650,7 +696,6 @@ export class OpenCodeServerAdapter {
|
|
|
650
696
|
],
|
|
651
697
|
],
|
|
652
698
|
}, null, 2)}\n`, "utf8");
|
|
653
|
-
this.tuiRouteConfigDir = configDir;
|
|
654
699
|
this.tuiRouteConfigPath = configPath;
|
|
655
700
|
}
|
|
656
701
|
acceptTuiRouteSocket(socket) {
|
|
@@ -765,6 +810,7 @@ export class OpenCodeServerAdapter {
|
|
|
765
810
|
const configDir = this.tuiRouteConfigDir;
|
|
766
811
|
this.tuiRouteConfigDir = null;
|
|
767
812
|
this.tuiRouteConfigPath = null;
|
|
813
|
+
this.tuiRoutePluginPath = null;
|
|
768
814
|
this.tuiRoutePort = 0;
|
|
769
815
|
this.tuiRouteToken = "";
|
|
770
816
|
this.tuiRouteStateReceived = false;
|
|
@@ -781,6 +827,50 @@ export class OpenCodeServerAdapter {
|
|
|
781
827
|
}
|
|
782
828
|
}
|
|
783
829
|
}
|
|
830
|
+
async probeOpenCodeVersion() {
|
|
831
|
+
const env = buildCliEnvironment(this.options.kind);
|
|
832
|
+
const target = resolveSpawnTarget(this.options.command, this.options.kind, { env });
|
|
833
|
+
const version = await new Promise((resolve, reject) => {
|
|
834
|
+
const child = spawnChildProcess(target.file, [...target.args, "--version"], {
|
|
835
|
+
cwd: this.options.cwd,
|
|
836
|
+
env,
|
|
837
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
838
|
+
windowsHide: true,
|
|
839
|
+
});
|
|
840
|
+
let output = "";
|
|
841
|
+
let settled = false;
|
|
842
|
+
const finish = (error) => {
|
|
843
|
+
if (settled)
|
|
844
|
+
return;
|
|
845
|
+
settled = true;
|
|
846
|
+
clearTimeout(timer);
|
|
847
|
+
if (error)
|
|
848
|
+
reject(error);
|
|
849
|
+
else
|
|
850
|
+
resolve(output);
|
|
851
|
+
};
|
|
852
|
+
const timer = setTimeout(() => {
|
|
853
|
+
if (child.pid != null) {
|
|
854
|
+
killProcessTreeSync(child.pid);
|
|
855
|
+
}
|
|
856
|
+
finish(new Error("Timed out checking the OpenCode CLI version."));
|
|
857
|
+
}, OPENCODE_VERSION_PROBE_TIMEOUT_MS);
|
|
858
|
+
child.stdout?.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
859
|
+
child.stderr?.on("data", (chunk) => { output += chunk.toString("utf8"); });
|
|
860
|
+
child.once("error", (error) => finish(error));
|
|
861
|
+
child.once("exit", (code) => {
|
|
862
|
+
if (code === 0)
|
|
863
|
+
finish();
|
|
864
|
+
else
|
|
865
|
+
finish(new Error(`OpenCode --version exited with code ${code ?? "unknown"}.`));
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
const parsed = parseOpenCodeCliVersion(version);
|
|
869
|
+
if (!parsed || !this.isSupportedOpenCodeVersion(parsed)) {
|
|
870
|
+
throw new Error(`Unsupported OpenCode version ${parsed ?? (version.trim() || "unknown")}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
|
|
871
|
+
}
|
|
872
|
+
this.openCodeMajor = Number(parsed.split(".")[0]);
|
|
873
|
+
}
|
|
784
874
|
async startServerProcess() {
|
|
785
875
|
const env = buildCliEnvironment(this.options.kind);
|
|
786
876
|
this.serverPassword = randomBytes(24).toString("hex");
|
|
@@ -886,8 +976,9 @@ export class OpenCodeServerAdapter {
|
|
|
886
976
|
if ((this.options.extraCliArgs ?? []).includes("--pure")) {
|
|
887
977
|
throw new Error("OpenCode --pure cannot be used with the WeChat bridge because local session following requires the managed TUI plugin.");
|
|
888
978
|
}
|
|
889
|
-
const args =
|
|
890
|
-
|
|
979
|
+
const args = this.openCodeMajor >= 2
|
|
980
|
+
? ["--server", this.getServerUrl()]
|
|
981
|
+
: ["attach", this.getServerUrl(), "--dir", this.options.cwd];
|
|
891
982
|
const sessionId = this.activeSessionId;
|
|
892
983
|
if (sessionId && (await this.hasSession(sessionId))) {
|
|
893
984
|
args.push("--session", sessionId);
|
|
@@ -897,7 +988,37 @@ export class OpenCodeServerAdapter {
|
|
|
897
988
|
}
|
|
898
989
|
buildNativeClientEnv() {
|
|
899
990
|
const env = buildCliEnvironment(this.options.kind);
|
|
900
|
-
if (this.
|
|
991
|
+
if (this.openCodeMajor >= 2 && this.tuiRoutePluginPath) {
|
|
992
|
+
let existingConfig = {};
|
|
993
|
+
if (env.OPENCODE_CLI_CONFIG_CONTENT) {
|
|
994
|
+
try {
|
|
995
|
+
const parsed = JSON.parse(env.OPENCODE_CLI_CONFIG_CONTENT);
|
|
996
|
+
if (isRecord(parsed))
|
|
997
|
+
existingConfig = parsed;
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
throw new Error(`OpenCode cannot merge the managed TUI plugin into invalid OPENCODE_CLI_CONFIG_CONTENT: ${describeUnknownError(error)}`, { cause: error });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const existingPlugins = Array.isArray(existingConfig.plugins)
|
|
1004
|
+
? existingConfig.plugins
|
|
1005
|
+
: [];
|
|
1006
|
+
env.OPENCODE_CLI_CONFIG_CONTENT = JSON.stringify({
|
|
1007
|
+
...existingConfig,
|
|
1008
|
+
plugins: [
|
|
1009
|
+
...existingPlugins,
|
|
1010
|
+
{
|
|
1011
|
+
package: this.tuiRoutePluginPath,
|
|
1012
|
+
options: {
|
|
1013
|
+
port: this.tuiRoutePort,
|
|
1014
|
+
token: this.tuiRouteToken,
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
],
|
|
1018
|
+
});
|
|
1019
|
+
env.OPENCODE_PASSWORD = this.serverPassword ?? "";
|
|
1020
|
+
}
|
|
1021
|
+
else if (this.tuiRouteConfigDir && this.tuiRouteConfigPath) {
|
|
901
1022
|
if (env.OPENCODE_CONFIG_DIR && env.OPENCODE_TUI_CONFIG) {
|
|
902
1023
|
throw new Error("OpenCode cannot inject the managed TUI session plugin while both OPENCODE_CONFIG_DIR and OPENCODE_TUI_CONFIG are set.");
|
|
903
1024
|
}
|
|
@@ -911,7 +1032,7 @@ export class OpenCodeServerAdapter {
|
|
|
911
1032
|
if (this.serverPassword) {
|
|
912
1033
|
env.OPENCODE_SERVER_PASSWORD = this.serverPassword;
|
|
913
1034
|
}
|
|
914
|
-
if (this.activeSessionId) {
|
|
1035
|
+
if (this.activeSessionId && this.openCodeMajor < 2) {
|
|
915
1036
|
env.OPENCODE_ROUTE = JSON.stringify({
|
|
916
1037
|
type: "session",
|
|
917
1038
|
sessionID: this.activeSessionId,
|
|
@@ -928,6 +1049,14 @@ export class OpenCodeServerAdapter {
|
|
|
928
1049
|
}
|
|
929
1050
|
async createSdkClient() {
|
|
930
1051
|
try {
|
|
1052
|
+
if (this.openCodeMajor >= 2) {
|
|
1053
|
+
this.client = await createOpenCodeV2CompatClient({
|
|
1054
|
+
baseUrl: `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`,
|
|
1055
|
+
directory: this.options.cwd,
|
|
1056
|
+
headers: this.buildServerAuthHeaders(),
|
|
1057
|
+
});
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
931
1060
|
const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
|
|
932
1061
|
this.client = createOpencodeClient({
|
|
933
1062
|
baseUrl: `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`,
|
|
@@ -937,7 +1066,8 @@ export class OpenCodeServerAdapter {
|
|
|
937
1066
|
});
|
|
938
1067
|
}
|
|
939
1068
|
catch (err) {
|
|
940
|
-
|
|
1069
|
+
const packageName = this.openCodeMajor >= 2 ? "@opencode/client" : "@opencode-ai/sdk";
|
|
1070
|
+
throw new Error(`Failed to load ${packageName}. Make sure it is installed: ${describeUnknownError(err)}`, { cause: err });
|
|
941
1071
|
}
|
|
942
1072
|
}
|
|
943
1073
|
async checkHealth() {
|
|
@@ -952,17 +1082,24 @@ export class OpenCodeServerAdapter {
|
|
|
952
1082
|
let lastError;
|
|
953
1083
|
while (Date.now() < deadline) {
|
|
954
1084
|
try {
|
|
955
|
-
const
|
|
1085
|
+
const healthPath = this.openCodeMajor >= 2 ? "/api/info" : "/global/health";
|
|
1086
|
+
const response = await fetch(`${baseUrl}${healthPath}`, {
|
|
956
1087
|
headers: this.buildServerAuthHeaders(),
|
|
957
1088
|
signal: AbortSignal.timeout(OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS),
|
|
958
1089
|
});
|
|
959
1090
|
if (response.ok) {
|
|
960
1091
|
const health = await response.json();
|
|
961
|
-
|
|
1092
|
+
const version = isRecord(health) && typeof health.version === "string"
|
|
1093
|
+
? health.version
|
|
1094
|
+
: undefined;
|
|
1095
|
+
const healthy = this.openCodeMajor >= 2
|
|
1096
|
+
? Boolean(version)
|
|
1097
|
+
: isRecord(health) && health.healthy === true;
|
|
1098
|
+
if (!healthy || !version) {
|
|
962
1099
|
lastError = new Error("OpenCode health check returned an invalid response.");
|
|
963
1100
|
}
|
|
964
|
-
else if (!this.isSupportedOpenCodeVersion(
|
|
965
|
-
throw new Error(`Unsupported OpenCode version ${
|
|
1101
|
+
else if (!this.isSupportedOpenCodeVersion(version)) {
|
|
1102
|
+
throw new Error(`Unsupported OpenCode version ${version}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
|
|
966
1103
|
}
|
|
967
1104
|
else {
|
|
968
1105
|
return;
|
|
@@ -1099,10 +1236,9 @@ export class OpenCodeServerAdapter {
|
|
|
1099
1236
|
return;
|
|
1100
1237
|
}
|
|
1101
1238
|
this.sseAbortController = new AbortController();
|
|
1102
|
-
const sseLoops =
|
|
1103
|
-
this.runSseLoop("event")
|
|
1104
|
-
this.runSseLoop("global-event")
|
|
1105
|
-
];
|
|
1239
|
+
const sseLoops = this.openCodeMajor >= 2
|
|
1240
|
+
? [this.runSseLoop("event")]
|
|
1241
|
+
: [this.runSseLoop("event"), this.runSseLoop("global-event")];
|
|
1106
1242
|
this.sseLoopPromise = Promise.all(sseLoops).then(() => undefined);
|
|
1107
1243
|
}
|
|
1108
1244
|
async runSseLoop(streamName) {
|
|
@@ -1242,6 +1378,10 @@ export class OpenCodeServerAdapter {
|
|
|
1242
1378
|
}
|
|
1243
1379
|
// Note: only permission.asked exists in the OpenCode wire protocol;
|
|
1244
1380
|
// permission.updated was never emitted by the server.
|
|
1381
|
+
case "permission.replied": {
|
|
1382
|
+
this.handlePermissionSettled(payload);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1245
1385
|
case "permission.asked": {
|
|
1246
1386
|
this.handlePermissionRequest(payload);
|
|
1247
1387
|
return;
|
|
@@ -1303,7 +1443,6 @@ export class OpenCodeServerAdapter {
|
|
|
1303
1443
|
case "session.diff":
|
|
1304
1444
|
case "session.diff.delta":
|
|
1305
1445
|
case "message.removed":
|
|
1306
|
-
case "permission.replied":
|
|
1307
1446
|
case "tui.toast.show":
|
|
1308
1447
|
return;
|
|
1309
1448
|
default:
|
|
@@ -1410,7 +1549,17 @@ export class OpenCodeServerAdapter {
|
|
|
1410
1549
|
return;
|
|
1411
1550
|
}
|
|
1412
1551
|
const sessionId = this.extractSessionId(properties);
|
|
1413
|
-
if (
|
|
1552
|
+
if (!sessionId)
|
|
1553
|
+
return;
|
|
1554
|
+
if (!this.isApprovalSessionOwned(sessionId)) {
|
|
1555
|
+
const ownerSessionId = this.activeSessionId;
|
|
1556
|
+
const generation = this.approvalOwnershipGeneration;
|
|
1557
|
+
void this.discoverApprovalSessionOwnership(sessionId).then((owned) => {
|
|
1558
|
+
if (owned && ownerSessionId === this.activeSessionId && generation === this.approvalOwnershipGeneration)
|
|
1559
|
+
this.handlePermissionRequest(properties);
|
|
1560
|
+
}).catch((error) => {
|
|
1561
|
+
this.logDebug(`[opencode-adapter:approval-owner] ${describeUnknownError(error)}`);
|
|
1562
|
+
});
|
|
1414
1563
|
return;
|
|
1415
1564
|
}
|
|
1416
1565
|
const pendingPermission = this.buildPendingPermission(properties);
|
|
@@ -1423,21 +1572,86 @@ export class OpenCodeServerAdapter {
|
|
|
1423
1572
|
this.rejectPermission(pendingPermission, denyMessage);
|
|
1424
1573
|
return;
|
|
1425
1574
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
this.
|
|
1429
|
-
this.
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1575
|
+
if (this.pendingPermissions.has(pendingPermission.permissionId) || this.settledPermissionIds.has(pendingPermission.permissionId))
|
|
1576
|
+
return;
|
|
1577
|
+
this.pendingPermissions.set(pendingPermission.permissionId, pendingPermission);
|
|
1578
|
+
this.syncPendingPermissions();
|
|
1579
|
+
}
|
|
1580
|
+
isApprovalSessionOwned(sessionId) {
|
|
1581
|
+
const seen = new Set();
|
|
1582
|
+
let current = sessionId;
|
|
1583
|
+
while (current && !seen.has(current)) {
|
|
1584
|
+
if (current === this.activeSessionId)
|
|
1585
|
+
return true;
|
|
1586
|
+
seen.add(current);
|
|
1587
|
+
current = this.childSessionParents.get(current);
|
|
1588
|
+
}
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
async discoverApprovalSessionOwnership(sessionId, seen = new Set()) {
|
|
1592
|
+
if (this.isApprovalSessionOwned(sessionId))
|
|
1593
|
+
return true;
|
|
1594
|
+
if (seen.has(sessionId) || seen.size >= 32 || !this.client?.session?.get)
|
|
1595
|
+
return false;
|
|
1596
|
+
seen.add(sessionId);
|
|
1597
|
+
const result = await this.client.session.get({ sessionID: sessionId, directory: this.options.cwd });
|
|
1598
|
+
const session = result.data;
|
|
1599
|
+
if (!session || !this.isCurrentDirectorySession(session) || !session.parentID)
|
|
1600
|
+
return false;
|
|
1601
|
+
this.childSessionIds.add(sessionId);
|
|
1602
|
+
if (!await this.discoverApprovalSessionOwnership(session.parentID, seen))
|
|
1603
|
+
return false;
|
|
1604
|
+
this.childSessionParents.set(sessionId, session.parentID);
|
|
1605
|
+
return this.isApprovalSessionOwned(sessionId);
|
|
1606
|
+
}
|
|
1607
|
+
rememberSettledPermission(id) {
|
|
1608
|
+
this.settledPermissionIds.add(id);
|
|
1609
|
+
if (this.settledPermissionIds.size > 512) {
|
|
1610
|
+
this.settledPermissionIds.delete(this.settledPermissionIds.values().next().value);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
handlePermissionSettled(properties) {
|
|
1614
|
+
if (!isRecord(properties))
|
|
1615
|
+
return;
|
|
1616
|
+
const id = typeof properties.requestID === "string" ? properties.requestID : properties.id;
|
|
1617
|
+
if (typeof id !== "string")
|
|
1618
|
+
return;
|
|
1619
|
+
const pending = this.pendingPermissions.get(id);
|
|
1620
|
+
if (!pending) {
|
|
1621
|
+
this.rememberSettledPermission(id);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
if (typeof properties.sessionID === "string" && properties.sessionID !== pending.sessionId)
|
|
1625
|
+
return;
|
|
1626
|
+
this.rememberSettledPermission(id);
|
|
1627
|
+
this.pendingPermissions.delete(id);
|
|
1628
|
+
this.syncPendingPermissions();
|
|
1629
|
+
}
|
|
1630
|
+
syncPendingPermissions() {
|
|
1631
|
+
const previousId = this.state.pendingApproval?.requestId;
|
|
1632
|
+
this.pendingPermission = this.pendingPermissions.values().next().value ?? null;
|
|
1633
|
+
const requests = Array.from(this.pendingPermissions.values(), (pending) => this.toPendingApproval(pending));
|
|
1634
|
+
this.state.pendingApprovals = requests;
|
|
1635
|
+
this.state.pendingApproval = requests[0] ?? null;
|
|
1636
|
+
this.state.pendingApprovalOrigin = this.pendingPermission?.origin;
|
|
1637
|
+
this.setStatus(requests.length ? "awaiting_approval" : this.state.pendingUserInput ? "awaiting_input" : "busy");
|
|
1638
|
+
this.emit({ type: "approval_state_changed", requests, timestamp: nowIso() });
|
|
1639
|
+
if (requests[0] && requests[0].requestId !== previousId) {
|
|
1640
|
+
this.emit({ type: "approval_required", request: requests[0], timestamp: nowIso() });
|
|
1641
|
+
}
|
|
1436
1642
|
}
|
|
1437
1643
|
clearPendingPermissionState() {
|
|
1644
|
+
this.approvalOwnershipGeneration++;
|
|
1645
|
+
const hadPending = this.pendingPermissions.size > 0;
|
|
1646
|
+
for (const id of this.pendingPermissions.keys())
|
|
1647
|
+
this.rememberSettledPermission(id);
|
|
1648
|
+
this.pendingPermissions.clear();
|
|
1438
1649
|
this.pendingPermission = null;
|
|
1439
1650
|
this.state.pendingApproval = null;
|
|
1651
|
+
this.state.pendingApprovals = [];
|
|
1440
1652
|
this.state.pendingApprovalOrigin = undefined;
|
|
1653
|
+
if (hadPending)
|
|
1654
|
+
this.emit({ type: "approval_state_changed", requests: [], timestamp: nowIso() });
|
|
1441
1655
|
}
|
|
1442
1656
|
handleQuestionAsked(properties) {
|
|
1443
1657
|
if (!isRecord(properties) || !this.client) {
|
|
@@ -1486,7 +1700,7 @@ export class OpenCodeServerAdapter {
|
|
|
1486
1700
|
return;
|
|
1487
1701
|
}
|
|
1488
1702
|
const request = {
|
|
1489
|
-
summary:
|
|
1703
|
+
summary: t("input.request.opencode", { count: questions.length, questions: t(questions.length === 1 ? "input.request.singular" : "input.request.plural") }),
|
|
1490
1704
|
questions,
|
|
1491
1705
|
};
|
|
1492
1706
|
this.pendingQuestion = {
|
|
@@ -1497,7 +1711,7 @@ export class OpenCodeServerAdapter {
|
|
|
1497
1711
|
this.state.pendingUserInput = request;
|
|
1498
1712
|
this.state.pendingUserInputOrigin = this.state.activeTurnOrigin;
|
|
1499
1713
|
this.clearWechatWorkingNotice();
|
|
1500
|
-
this.setStatus("awaiting_input", "OpenCode is waiting for user input.");
|
|
1714
|
+
this.setStatus(this.pendingPermissions.size ? "awaiting_approval" : "awaiting_input", "OpenCode is waiting for user input.");
|
|
1501
1715
|
this.emit({
|
|
1502
1716
|
type: "user_input_required",
|
|
1503
1717
|
request,
|
|
@@ -1580,6 +1794,7 @@ export class OpenCodeServerAdapter {
|
|
|
1580
1794
|
try {
|
|
1581
1795
|
const result = await client.permission.reply({
|
|
1582
1796
|
requestID: pendingPermission.permissionId,
|
|
1797
|
+
sessionID: pendingPermission.sessionId,
|
|
1583
1798
|
directory: this.options.cwd,
|
|
1584
1799
|
workspace: this.activeWorkspaceId ?? undefined,
|
|
1585
1800
|
reply: "reject",
|
|
@@ -1623,12 +1838,14 @@ export class OpenCodeServerAdapter {
|
|
|
1623
1838
|
const metadata = isRecord(properties.metadata) ? properties.metadata : {};
|
|
1624
1839
|
const command = this.extractPermissionCommand(properties, metadata);
|
|
1625
1840
|
return {
|
|
1841
|
+
origin: this.state.activeTurnOrigin,
|
|
1626
1842
|
sessionId,
|
|
1627
1843
|
permissionId,
|
|
1628
1844
|
code: buildOneTimeCode(),
|
|
1629
1845
|
createdAt: nowIso(),
|
|
1630
1846
|
request: {
|
|
1631
1847
|
source: "cli",
|
|
1848
|
+
requestId: permissionId,
|
|
1632
1849
|
summary: title ?? `OpenCode needs approval${toolName ? ` for tool: ${toolName}` : ""}.`,
|
|
1633
1850
|
commandPreview: truncatePreview(command ?? title ?? "Permission request", 180),
|
|
1634
1851
|
toolName,
|
|
@@ -1664,6 +1881,17 @@ export class OpenCodeServerAdapter {
|
|
|
1664
1881
|
return;
|
|
1665
1882
|
}
|
|
1666
1883
|
const session = this.extractSessionReference(properties);
|
|
1884
|
+
if (session?.parentID) {
|
|
1885
|
+
// Subagent creation is not a visible TUI selection. Remember the child
|
|
1886
|
+
// so its later status/output events cannot implicitly follow it either.
|
|
1887
|
+
this.childSessionIds.add(session.id);
|
|
1888
|
+
const directory = this.extractSessionDirectory(properties);
|
|
1889
|
+
if ((!directory || this.normalizeDirectory(directory) === this.normalizeDirectory(this.options.cwd))
|
|
1890
|
+
&& (!session.workspaceID || !this.activeWorkspaceId || session.workspaceID === this.activeWorkspaceId)) {
|
|
1891
|
+
this.childSessionParents.set(session.id, session.parentID);
|
|
1892
|
+
}
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1667
1895
|
if (this.syncTrackedSessionFromEvent(session)) {
|
|
1668
1896
|
return;
|
|
1669
1897
|
}
|
|
@@ -1689,6 +1917,31 @@ export class OpenCodeServerAdapter {
|
|
|
1689
1917
|
if (!sessionId) {
|
|
1690
1918
|
return;
|
|
1691
1919
|
}
|
|
1920
|
+
const removedSessions = new Set([sessionId]);
|
|
1921
|
+
let expanded = true;
|
|
1922
|
+
while (expanded) {
|
|
1923
|
+
expanded = false;
|
|
1924
|
+
for (const [child, parent] of this.childSessionParents) {
|
|
1925
|
+
if (removedSessions.has(parent) && !removedSessions.has(child)) {
|
|
1926
|
+
removedSessions.add(child);
|
|
1927
|
+
expanded = true;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
for (const id of removedSessions) {
|
|
1932
|
+
this.childSessionIds.delete(id);
|
|
1933
|
+
this.childSessionParents.delete(id);
|
|
1934
|
+
}
|
|
1935
|
+
let removedApproval = false;
|
|
1936
|
+
for (const [id, pending] of this.pendingPermissions) {
|
|
1937
|
+
if (removedSessions.has(pending.sessionId)) {
|
|
1938
|
+
this.rememberSettledPermission(id);
|
|
1939
|
+
this.pendingPermissions.delete(id);
|
|
1940
|
+
removedApproval = true;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (removedApproval)
|
|
1944
|
+
this.syncPendingPermissions();
|
|
1692
1945
|
this.previousAgentBySession.delete(sessionId);
|
|
1693
1946
|
if (sessionId !== this.activeSessionId) {
|
|
1694
1947
|
return;
|
|
@@ -2479,6 +2732,9 @@ export class OpenCodeServerAdapter {
|
|
|
2479
2732
|
this.assignActiveSession(session);
|
|
2480
2733
|
return true;
|
|
2481
2734
|
}
|
|
2735
|
+
if (this.childSessionIds.has(sessionId)) {
|
|
2736
|
+
return false;
|
|
2737
|
+
}
|
|
2482
2738
|
if (options.allowLocalTurnFollow !== false && this.shouldFollowLocalTurnSession(sessionId)) {
|
|
2483
2739
|
this.switchSharedSession(session ?? sessionId, {
|
|
2484
2740
|
source: "local",
|
|
@@ -2491,6 +2747,9 @@ export class OpenCodeServerAdapter {
|
|
|
2491
2747
|
return false;
|
|
2492
2748
|
}
|
|
2493
2749
|
extractSessionReference(properties) {
|
|
2750
|
+
const session = isRecord(properties.session) ? properties.session : undefined;
|
|
2751
|
+
const info = isRecord(properties.info) ? properties.info : undefined;
|
|
2752
|
+
const parentID = [properties.parentID, session?.parentID, info?.parentID].find((value) => typeof value === "string" && value.length > 0);
|
|
2494
2753
|
if (typeof properties.sessionID === "string" || typeof properties.sessionId === "string") {
|
|
2495
2754
|
const id = typeof properties.sessionID === "string"
|
|
2496
2755
|
? properties.sessionID
|
|
@@ -2498,20 +2757,21 @@ export class OpenCodeServerAdapter {
|
|
|
2498
2757
|
return {
|
|
2499
2758
|
id,
|
|
2500
2759
|
workspaceID: this.extractWorkspaceId(properties) ?? undefined,
|
|
2760
|
+
parentID,
|
|
2501
2761
|
};
|
|
2502
2762
|
}
|
|
2503
|
-
const session = isRecord(properties.session) ? properties.session : undefined;
|
|
2504
2763
|
if (typeof session?.id === "string") {
|
|
2505
2764
|
return {
|
|
2506
2765
|
id: session.id,
|
|
2507
2766
|
workspaceID: this.extractWorkspaceId(session) ?? this.extractWorkspaceId(properties) ?? undefined,
|
|
2767
|
+
parentID,
|
|
2508
2768
|
};
|
|
2509
2769
|
}
|
|
2510
|
-
const info = isRecord(properties.info) ? properties.info : undefined;
|
|
2511
2770
|
if (typeof info?.id === "string") {
|
|
2512
2771
|
return {
|
|
2513
2772
|
id: info.id,
|
|
2514
2773
|
workspaceID: this.extractWorkspaceId(info) ?? this.extractWorkspaceId(properties) ?? undefined,
|
|
2774
|
+
parentID,
|
|
2515
2775
|
};
|
|
2516
2776
|
}
|
|
2517
2777
|
return null;
|
|
@@ -2557,7 +2817,7 @@ export class OpenCodeServerAdapter {
|
|
|
2557
2817
|
return false;
|
|
2558
2818
|
}
|
|
2559
2819
|
const session = this.extractSessionReference(payload);
|
|
2560
|
-
if (!session?.id || session.id === this.activeSessionId) {
|
|
2820
|
+
if (!session?.id || session.parentID || session.id === this.activeSessionId) {
|
|
2561
2821
|
return false;
|
|
2562
2822
|
}
|
|
2563
2823
|
if (typeof event.directory === "string" ||
|
|
@@ -2745,6 +3005,16 @@ export class OpenCodeServerAdapter {
|
|
|
2745
3005
|
}
|
|
2746
3006
|
}
|
|
2747
3007
|
async sendVisibleSessionSelection(session) {
|
|
3008
|
+
if (this.openCodeMajor >= 2) {
|
|
3009
|
+
if (!this.tuiRouteSocket || this.tuiRouteSocket.destroyed) {
|
|
3010
|
+
throw new Error("The OpenCode 2 TUI route bridge is not connected.");
|
|
3011
|
+
}
|
|
3012
|
+
this.tuiRouteSocket.write(`${JSON.stringify({
|
|
3013
|
+
type: "select_session",
|
|
3014
|
+
sessionId: session.id,
|
|
3015
|
+
})}\n`);
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
2748
3018
|
if (!this.client?.tui) {
|
|
2749
3019
|
return;
|
|
2750
3020
|
}
|