cli-wechat-bridge 1.1.1 → 1.1.3
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 +60 -43
- package/bin/_run-entry.mjs +24 -0
- package/bin/wechat-bridge-pi.mjs +5 -0
- package/bin/wechat-pi-start.mjs +5 -0
- package/bin/wechat-pi.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +2 -0
- package/dist/bridge/bridge-adapters.claude.js +18 -0
- package/dist/bridge/bridge-adapters.codex.js +83 -2
- package/dist/bridge/bridge-adapters.core.js +36 -2
- package/dist/bridge/bridge-adapters.js +5 -0
- package/dist/bridge/bridge-adapters.opencode.js +278 -32
- package/dist/bridge/bridge-adapters.pi.js +697 -0
- package/dist/bridge/bridge-adapters.shared.js +44 -46
- package/dist/bridge/bridge-final-reply.js +8 -4
- package/dist/bridge/bridge-process-reaper.js +58 -3
- package/dist/bridge/bridge-state.js +15 -6
- package/dist/bridge/bridge-utils.js +129 -30
- package/dist/bridge/claude-hook.js +7 -0
- package/dist/bridge/claude-hooks.js +10 -2
- package/dist/bridge/wechat-bridge.js +20 -8
- package/dist/commands/check-update.js +15 -4
- package/dist/companion/local-companion-link.js +6 -1
- package/dist/companion/local-companion-start.js +29 -8
- package/dist/companion/local-companion.js +3 -3
- package/dist/companion/pi-tui-bridge-extension.js +256 -0
- package/dist/daemon/wechat-daemon.js +126 -25
- package/dist/i18n/messages-en.js +12 -2
- package/dist/i18n/messages-zh.js +12 -2
- package/dist/utils/doctor.js +85 -2
- package/dist/utils/version-checker.js +77 -44
- package/dist/wechat/channel-config.js +28 -0
- package/dist/wechat/setup.js +2 -0
- package/dist/wechat/wechat-transport.js +49 -13
- package/package.json +120 -100
- package/scripts/ensure-node-pty-permissions.mjs +73 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
|
-
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, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
|
|
2
|
+
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";
|
|
3
3
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
4
4
|
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
5
5
|
const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
|
|
@@ -8,6 +8,8 @@ const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
|
|
|
8
8
|
const OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS = 10_000;
|
|
9
9
|
const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
|
|
10
10
|
const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
|
|
11
|
+
const OPENCODE_MINIMUM_SUPPORTED_VERSION = "1.18.0";
|
|
12
|
+
const OPENCODE_MAXIMUM_SUPPORTED_MAJOR = 2;
|
|
11
13
|
/* ------------------------------------------------------------------ */
|
|
12
14
|
/* Adapter */
|
|
13
15
|
/* ------------------------------------------------------------------ */
|
|
@@ -31,6 +33,11 @@ export class OpenCodeServerAdapter {
|
|
|
31
33
|
workingNoticeTimer = null;
|
|
32
34
|
workingNoticeSent = false;
|
|
33
35
|
lastBusyAtMs = 0;
|
|
36
|
+
// Monotonic token bumped on every beginTrackedTurn. Lets a deferred turn
|
|
37
|
+
// completion (.then in handleSessionIdle) detect that a newer turn started
|
|
38
|
+
// while it was awaiting, so the stale completion becomes a no-op instead of
|
|
39
|
+
// clobbering the in-flight turn (stale final_reply / status flip / batcher clear).
|
|
40
|
+
currentTurnToken = 0;
|
|
34
41
|
pendingLocalPrompt = "";
|
|
35
42
|
localPromptNoticeSent = false;
|
|
36
43
|
loggedUnknownEventTypes = new Set();
|
|
@@ -48,6 +55,7 @@ export class OpenCodeServerAdapter {
|
|
|
48
55
|
lastMirroredLocalPrompt = null;
|
|
49
56
|
pendingLocalSessionCreateFollowUntilMs = 0;
|
|
50
57
|
pendingPermission = null;
|
|
58
|
+
pendingQuestion = null;
|
|
51
59
|
constructor(options) {
|
|
52
60
|
this.options = options;
|
|
53
61
|
this.state = {
|
|
@@ -112,6 +120,9 @@ export class OpenCodeServerAdapter {
|
|
|
112
120
|
if (this.pendingPermission) {
|
|
113
121
|
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
114
122
|
}
|
|
123
|
+
if (this.pendingQuestion) {
|
|
124
|
+
throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
|
|
125
|
+
}
|
|
115
126
|
const normalized = normalizeOutput(text).trim();
|
|
116
127
|
if (!normalized) {
|
|
117
128
|
return;
|
|
@@ -165,6 +176,9 @@ export class OpenCodeServerAdapter {
|
|
|
165
176
|
if (this.pendingPermission) {
|
|
166
177
|
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
167
178
|
}
|
|
179
|
+
if (this.pendingQuestion) {
|
|
180
|
+
throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
|
|
181
|
+
}
|
|
168
182
|
this.outputBatcher.clear();
|
|
169
183
|
this.clearStreamedPartState();
|
|
170
184
|
const session = this.unwrapOrThrow(await this.client.session.create({
|
|
@@ -185,10 +199,16 @@ export class OpenCodeServerAdapter {
|
|
|
185
199
|
if (!this.client || !this.activeSessionId) {
|
|
186
200
|
return false;
|
|
187
201
|
}
|
|
188
|
-
if (this.state.status !== "busy" &&
|
|
202
|
+
if (this.state.status !== "busy" &&
|
|
203
|
+
this.state.status !== "awaiting_approval" &&
|
|
204
|
+
this.state.status !== "awaiting_input") {
|
|
189
205
|
return false;
|
|
190
206
|
}
|
|
191
207
|
this.clearWechatWorkingNotice(true);
|
|
208
|
+
if (this.pendingQuestion) {
|
|
209
|
+
await this.rejectPendingQuestion(this.pendingQuestion);
|
|
210
|
+
this.clearPendingQuestionState();
|
|
211
|
+
}
|
|
192
212
|
try {
|
|
193
213
|
await this.client.session.abort({
|
|
194
214
|
sessionID: this.activeSessionId,
|
|
@@ -209,6 +229,7 @@ export class OpenCodeServerAdapter {
|
|
|
209
229
|
this.clearObservedMessageTracking();
|
|
210
230
|
this.recentSdkEventObservations.clear();
|
|
211
231
|
this.clearPendingPermissionState();
|
|
232
|
+
this.clearPendingQuestionState();
|
|
212
233
|
this.activeSessionId = null;
|
|
213
234
|
this.state.sharedSessionId = undefined;
|
|
214
235
|
this.state.sharedThreadId = undefined;
|
|
@@ -227,15 +248,14 @@ export class OpenCodeServerAdapter {
|
|
|
227
248
|
if (!this.pendingPermission || !this.client) {
|
|
228
249
|
return false;
|
|
229
250
|
}
|
|
230
|
-
const {
|
|
251
|
+
const { permissionId } = this.pendingPermission;
|
|
231
252
|
const response = action === "confirm" ? "once" : "reject";
|
|
232
253
|
try {
|
|
233
|
-
const result = await this.client.permission.
|
|
234
|
-
|
|
235
|
-
permissionID: permissionId,
|
|
254
|
+
const result = await this.client.permission.reply({
|
|
255
|
+
requestID: permissionId,
|
|
236
256
|
directory: this.options.cwd,
|
|
237
257
|
workspace: this.activeWorkspaceId ?? undefined,
|
|
238
|
-
response,
|
|
258
|
+
reply: response,
|
|
239
259
|
});
|
|
240
260
|
if (result.error !== undefined) {
|
|
241
261
|
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
@@ -258,8 +278,34 @@ export class OpenCodeServerAdapter {
|
|
|
258
278
|
const ok = await this.resolveApproval(action);
|
|
259
279
|
return ok ? 1 : 0;
|
|
260
280
|
}
|
|
261
|
-
async submitUserInput(
|
|
262
|
-
|
|
281
|
+
async submitUserInput(answers) {
|
|
282
|
+
if (!this.pendingQuestion || !this.client) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const pending = this.pendingQuestion;
|
|
286
|
+
const orderedAnswers = pending.request.questions.map((question) => answers[question.id] ?? []);
|
|
287
|
+
try {
|
|
288
|
+
const result = await this.client.question.reply({
|
|
289
|
+
requestID: pending.requestId,
|
|
290
|
+
directory: this.options.cwd,
|
|
291
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
292
|
+
answers: orderedAnswers,
|
|
293
|
+
});
|
|
294
|
+
if (result.error !== undefined) {
|
|
295
|
+
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
this.emit({
|
|
300
|
+
type: "stderr",
|
|
301
|
+
text: `Failed to submit OpenCode user input: ${describeUnknownError(err)}`,
|
|
302
|
+
timestamp: nowIso(),
|
|
303
|
+
});
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
this.clearPendingQuestionState();
|
|
307
|
+
this.setStatus("busy", "OpenCode user input submitted.");
|
|
308
|
+
return true;
|
|
263
309
|
}
|
|
264
310
|
async dispose() {
|
|
265
311
|
this.shuttingDown = true;
|
|
@@ -272,6 +318,7 @@ export class OpenCodeServerAdapter {
|
|
|
272
318
|
this.outputBatcher.clear();
|
|
273
319
|
this.clearStreamedPartState();
|
|
274
320
|
this.clearPendingPermissionState();
|
|
321
|
+
this.clearPendingQuestionState();
|
|
275
322
|
// Stop SSE listener
|
|
276
323
|
if (this.sseAbortController) {
|
|
277
324
|
this.sseAbortController.abort();
|
|
@@ -453,10 +500,54 @@ export class OpenCodeServerAdapter {
|
|
|
453
500
|
}
|
|
454
501
|
async checkHealth() {
|
|
455
502
|
const baseUrl = `http://${OPENCODE_SERVER_HOST}:${this.serverPort}`;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
503
|
+
// `opencode serve` binds its TCP port before the HTTP layer is ready,
|
|
504
|
+
// so a fetch fired the instant `waitForTcpPort` returns can be accepted
|
|
505
|
+
// into the listen backlog but never answered, hanging until undici's
|
|
506
|
+
// ~300s `headersTimeout` and surfacing as `fetch failed` after ~5min.
|
|
507
|
+
// Retry with a short per-attempt timeout so the startup race
|
|
508
|
+
// self-heals once the server finishes booting.
|
|
509
|
+
const deadline = Date.now() + OPENCODE_HTTP_READY_TIMEOUT_MS;
|
|
510
|
+
let lastError;
|
|
511
|
+
while (Date.now() < deadline) {
|
|
512
|
+
try {
|
|
513
|
+
const response = await fetch(`${baseUrl}/global/health`, {
|
|
514
|
+
signal: AbortSignal.timeout(OPENCODE_HTTP_READY_PROBE_TIMEOUT_MS),
|
|
515
|
+
});
|
|
516
|
+
if (response.ok) {
|
|
517
|
+
const health = await response.json();
|
|
518
|
+
if (!isRecord(health) || health.healthy !== true || typeof health.version !== "string") {
|
|
519
|
+
lastError = new Error("OpenCode health check returned an invalid response.");
|
|
520
|
+
}
|
|
521
|
+
else if (!this.isSupportedOpenCodeVersion(health.version)) {
|
|
522
|
+
throw new Error(`Unsupported OpenCode version ${health.version}. Install OpenCode >=${OPENCODE_MINIMUM_SUPPORTED_VERSION} <${OPENCODE_MAXIMUM_SUPPORTED_MAJOR}.0.0.`);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
lastError = new Error(`OpenCode health check failed (HTTP ${response.status}).`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
if (err instanceof Error &&
|
|
534
|
+
err.message.startsWith("Unsupported OpenCode version ")) {
|
|
535
|
+
throw err;
|
|
536
|
+
}
|
|
537
|
+
lastError = err;
|
|
538
|
+
}
|
|
539
|
+
await delay(OPENCODE_HTTP_READY_PROBE_INTERVAL_MS);
|
|
459
540
|
}
|
|
541
|
+
throw lastError ?? new Error("OpenCode health check timed out.");
|
|
542
|
+
}
|
|
543
|
+
isSupportedOpenCodeVersion(version) {
|
|
544
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
|
|
545
|
+
if (!match) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
const major = Number(match[1]);
|
|
549
|
+
const minor = Number(match[2]);
|
|
550
|
+
return major === 1 && minor >= 18;
|
|
460
551
|
}
|
|
461
552
|
async initializeSessions() {
|
|
462
553
|
if (!this.client) {
|
|
@@ -563,11 +654,11 @@ export class OpenCodeServerAdapter {
|
|
|
563
654
|
return;
|
|
564
655
|
}
|
|
565
656
|
this.sseAbortController = new AbortController();
|
|
566
|
-
|
|
657
|
+
const sseLoops = [
|
|
567
658
|
this.runSseLoop("event"),
|
|
568
659
|
this.runSseLoop("global-event"),
|
|
569
|
-
|
|
570
|
-
|
|
660
|
+
];
|
|
661
|
+
this.sseLoopPromise = Promise.all(sseLoops).then(() => undefined);
|
|
571
662
|
}
|
|
572
663
|
async runSseLoop(streamName) {
|
|
573
664
|
while (!this.shuttingDown) {
|
|
@@ -602,9 +693,6 @@ export class OpenCodeServerAdapter {
|
|
|
602
693
|
}
|
|
603
694
|
subscribeToSseStream(streamName) {
|
|
604
695
|
const signal = this.sseAbortController?.signal;
|
|
605
|
-
if (streamName === "global-sync") {
|
|
606
|
-
return this.client.global.syncEvent.subscribe({ signal });
|
|
607
|
-
}
|
|
608
696
|
if (streamName === "global-event") {
|
|
609
697
|
return this.client.global.event({ signal });
|
|
610
698
|
}
|
|
@@ -618,10 +706,16 @@ export class OpenCodeServerAdapter {
|
|
|
618
706
|
return null;
|
|
619
707
|
}
|
|
620
708
|
if (typeof rawEvent.type === "string") {
|
|
709
|
+
if (rawEvent.type === "sync") {
|
|
710
|
+
return this.normalizeGlobalSyncPayload(rawEvent, rawEvent);
|
|
711
|
+
}
|
|
621
712
|
return rawEvent;
|
|
622
713
|
}
|
|
623
714
|
const payload = rawEvent.payload;
|
|
624
715
|
if (isRecord(payload) && typeof payload.type === "string") {
|
|
716
|
+
if (payload.type === "sync") {
|
|
717
|
+
return this.normalizeGlobalSyncPayload(payload, rawEvent);
|
|
718
|
+
}
|
|
625
719
|
const normalized = { ...payload };
|
|
626
720
|
if (typeof rawEvent.directory === "string") {
|
|
627
721
|
normalized.directory = rawEvent.directory;
|
|
@@ -630,6 +724,17 @@ export class OpenCodeServerAdapter {
|
|
|
630
724
|
}
|
|
631
725
|
return null;
|
|
632
726
|
}
|
|
727
|
+
normalizeGlobalSyncPayload(payload, envelope) {
|
|
728
|
+
const syncEvent = isRecord(payload.syncEvent) ? payload.syncEvent : undefined;
|
|
729
|
+
if (!syncEvent || typeof syncEvent.type !== "string") {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const normalized = { ...syncEvent };
|
|
733
|
+
if (typeof envelope.directory === "string") {
|
|
734
|
+
normalized.directory = envelope.directory;
|
|
735
|
+
}
|
|
736
|
+
return normalized;
|
|
737
|
+
}
|
|
633
738
|
shouldHandleSseEvent(event, streamName) {
|
|
634
739
|
if (streamName === "event") {
|
|
635
740
|
return true;
|
|
@@ -642,7 +747,10 @@ export class OpenCodeServerAdapter {
|
|
|
642
747
|
type !== "command.executed" &&
|
|
643
748
|
type !== "session.created" &&
|
|
644
749
|
type !== "session.updated" &&
|
|
645
|
-
type !== "session.deleted"
|
|
750
|
+
type !== "session.deleted" &&
|
|
751
|
+
type !== "question.asked" &&
|
|
752
|
+
type !== "question.replied" &&
|
|
753
|
+
type !== "question.rejected") {
|
|
646
754
|
return false;
|
|
647
755
|
}
|
|
648
756
|
if (this.isImplicitLocalCompanionUiEvent(type)) {
|
|
@@ -651,13 +759,6 @@ export class OpenCodeServerAdapter {
|
|
|
651
759
|
return (this.matchesCurrentDirectoryEvent(event) ||
|
|
652
760
|
this.shouldAcceptUnscopedLocalSessionCreatedEvent(event));
|
|
653
761
|
}
|
|
654
|
-
if (type === "session.created" ||
|
|
655
|
-
type === "session.updated" ||
|
|
656
|
-
type === "session.deleted") {
|
|
657
|
-
return (this.matchesCurrentDirectoryEvent(event) ||
|
|
658
|
-
(type === "session.created" &&
|
|
659
|
-
this.shouldAcceptUnscopedLocalSessionCreatedEvent(event)));
|
|
660
|
-
}
|
|
661
762
|
return false;
|
|
662
763
|
}
|
|
663
764
|
isImplicitLocalCompanionUiEvent(type) {
|
|
@@ -696,6 +797,15 @@ export class OpenCodeServerAdapter {
|
|
|
696
797
|
this.handlePermissionRequest(payload);
|
|
697
798
|
return;
|
|
698
799
|
}
|
|
800
|
+
case "question.asked": {
|
|
801
|
+
this.handleQuestionAsked(payload);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
case "question.replied":
|
|
805
|
+
case "question.rejected": {
|
|
806
|
+
this.handleQuestionSettled(payload);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
699
809
|
case "session.created": {
|
|
700
810
|
this.handleSessionCreated(payload);
|
|
701
811
|
return;
|
|
@@ -757,26 +867,42 @@ export class OpenCodeServerAdapter {
|
|
|
757
867
|
if (!this.syncTrackedSessionFromEvent(sessionId, { allowLocalTurnFollow: false })) {
|
|
758
868
|
return;
|
|
759
869
|
}
|
|
760
|
-
if (this.state.status !== "busy" &&
|
|
870
|
+
if (this.state.status !== "busy" &&
|
|
871
|
+
this.state.status !== "awaiting_approval" &&
|
|
872
|
+
this.state.status !== "awaiting_input") {
|
|
761
873
|
return;
|
|
762
874
|
}
|
|
763
875
|
// Wait a short settle time before emitting task_complete,
|
|
764
876
|
// in case more events follow the idle signal.
|
|
765
877
|
setTimeout(() => {
|
|
766
|
-
if (this.state.status !== "busy" &&
|
|
878
|
+
if (this.state.status !== "busy" &&
|
|
879
|
+
this.state.status !== "awaiting_approval" &&
|
|
880
|
+
this.state.status !== "awaiting_input") {
|
|
767
881
|
return;
|
|
768
882
|
}
|
|
769
883
|
this.clearWechatWorkingNotice(true);
|
|
770
884
|
this.pendingLocalPrompt = "";
|
|
771
885
|
this.clearPendingPermissionState();
|
|
886
|
+
this.clearPendingQuestionState();
|
|
772
887
|
this.state.activeTurnOrigin = undefined;
|
|
773
888
|
this.hasAcceptedInput = false;
|
|
774
889
|
const completedPreview = this.currentPreview;
|
|
775
890
|
const turnStartedAtMs = this.lastBusyAtMs;
|
|
891
|
+
const completingTurnToken = this.currentTurnToken;
|
|
776
892
|
void this.outputBatcher.flushNow()
|
|
777
893
|
.catch(() => undefined)
|
|
778
894
|
.then(async () => {
|
|
895
|
+
// A newer turn (local or wechat) may have started while we awaited
|
|
896
|
+
// the flush. If so, this completion is stale: dropping it avoids
|
|
897
|
+
// flipping the active turn back to idle, emitting the old final_reply,
|
|
898
|
+
// and clearing the new turn's buffered output.
|
|
899
|
+
if (this.currentTurnToken !== completingTurnToken) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
779
902
|
const finalReplyText = await this.resolveFinalReplyText(sessionId, turnStartedAtMs);
|
|
903
|
+
if (this.currentTurnToken !== completingTurnToken) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
780
906
|
this.setStatus("idle");
|
|
781
907
|
if (finalReplyText) {
|
|
782
908
|
this.emit({
|
|
@@ -858,6 +984,114 @@ export class OpenCodeServerAdapter {
|
|
|
858
984
|
this.state.pendingApproval = null;
|
|
859
985
|
this.state.pendingApprovalOrigin = undefined;
|
|
860
986
|
}
|
|
987
|
+
handleQuestionAsked(properties) {
|
|
988
|
+
if (!isRecord(properties) || !this.client) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const sessionId = this.extractSessionId(properties);
|
|
992
|
+
if (!sessionId || !this.syncTrackedSessionFromEvent(sessionId)) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const requestId = typeof properties.id === "string" ? properties.id : null;
|
|
996
|
+
if (!requestId || !Array.isArray(properties.questions)) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const questions = properties.questions.flatMap((candidate, index) => {
|
|
1000
|
+
if (!isRecord(candidate)) {
|
|
1001
|
+
return [];
|
|
1002
|
+
}
|
|
1003
|
+
const header = typeof candidate.header === "string" ? candidate.header.trim() : "";
|
|
1004
|
+
const question = typeof candidate.question === "string" ? candidate.question.trim() : "";
|
|
1005
|
+
if (!header || !question) {
|
|
1006
|
+
return [];
|
|
1007
|
+
}
|
|
1008
|
+
const options = Array.isArray(candidate.options)
|
|
1009
|
+
? candidate.options.flatMap((option) => {
|
|
1010
|
+
if (!isRecord(option) || typeof option.label !== "string") {
|
|
1011
|
+
return [];
|
|
1012
|
+
}
|
|
1013
|
+
return [{
|
|
1014
|
+
label: option.label,
|
|
1015
|
+
description: typeof option.description === "string" ? option.description : "",
|
|
1016
|
+
}];
|
|
1017
|
+
})
|
|
1018
|
+
: [];
|
|
1019
|
+
return [{
|
|
1020
|
+
id: `question_${index + 1}`,
|
|
1021
|
+
header,
|
|
1022
|
+
question,
|
|
1023
|
+
isOther: candidate.custom === true,
|
|
1024
|
+
isSecret: false,
|
|
1025
|
+
multiple: candidate.multiple === true,
|
|
1026
|
+
customAnswerMode: "value",
|
|
1027
|
+
options,
|
|
1028
|
+
}];
|
|
1029
|
+
});
|
|
1030
|
+
if (questions.length !== properties.questions.length || questions.length === 0) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const request = {
|
|
1034
|
+
summary: `OpenCode requested answers to ${questions.length} question${questions.length === 1 ? "" : "s"}.`,
|
|
1035
|
+
questions,
|
|
1036
|
+
};
|
|
1037
|
+
this.pendingQuestion = {
|
|
1038
|
+
sessionId,
|
|
1039
|
+
requestId,
|
|
1040
|
+
request,
|
|
1041
|
+
};
|
|
1042
|
+
this.state.pendingUserInput = request;
|
|
1043
|
+
this.state.pendingUserInputOrigin = this.state.activeTurnOrigin;
|
|
1044
|
+
this.clearWechatWorkingNotice();
|
|
1045
|
+
this.setStatus("awaiting_input", "OpenCode is waiting for user input.");
|
|
1046
|
+
this.emit({
|
|
1047
|
+
type: "user_input_required",
|
|
1048
|
+
request,
|
|
1049
|
+
timestamp: nowIso(),
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
handleQuestionSettled(properties) {
|
|
1053
|
+
if (!isRecord(properties) || !this.pendingQuestion) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const sessionId = this.extractSessionId(properties);
|
|
1057
|
+
const requestId = typeof properties.requestID === "string" ? properties.requestID : null;
|
|
1058
|
+
if (sessionId !== this.pendingQuestion.sessionId ||
|
|
1059
|
+
requestId !== this.pendingQuestion.requestId) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
this.clearPendingQuestionState();
|
|
1063
|
+
if (this.state.status === "awaiting_input") {
|
|
1064
|
+
this.setStatus("busy", "OpenCode user input resolved.");
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
clearPendingQuestionState() {
|
|
1068
|
+
this.pendingQuestion = null;
|
|
1069
|
+
this.state.pendingUserInput = null;
|
|
1070
|
+
this.state.pendingUserInputOrigin = undefined;
|
|
1071
|
+
}
|
|
1072
|
+
async rejectPendingQuestion(pending) {
|
|
1073
|
+
const client = this.client;
|
|
1074
|
+
if (!client) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
const result = await client.question.reject({
|
|
1079
|
+
requestID: pending.requestId,
|
|
1080
|
+
directory: this.options.cwd,
|
|
1081
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
1082
|
+
});
|
|
1083
|
+
if (result.error !== undefined) {
|
|
1084
|
+
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
this.emit({
|
|
1089
|
+
type: "stderr",
|
|
1090
|
+
text: `Failed to reject OpenCode user input: ${describeUnknownError(err)}`,
|
|
1091
|
+
timestamp: nowIso(),
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
861
1095
|
getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission) {
|
|
862
1096
|
const metadata = isRecord(properties.metadata) ? properties.metadata : {};
|
|
863
1097
|
const command = this.extractPermissionCommand(properties, metadata);
|
|
@@ -889,12 +1123,11 @@ export class OpenCodeServerAdapter {
|
|
|
889
1123
|
}
|
|
890
1124
|
void (async () => {
|
|
891
1125
|
try {
|
|
892
|
-
const result = await client.permission.
|
|
893
|
-
|
|
894
|
-
permissionID: pendingPermission.permissionId,
|
|
1126
|
+
const result = await client.permission.reply({
|
|
1127
|
+
requestID: pendingPermission.permissionId,
|
|
895
1128
|
directory: this.options.cwd,
|
|
896
1129
|
workspace: this.activeWorkspaceId ?? undefined,
|
|
897
|
-
|
|
1130
|
+
reply: "reject",
|
|
898
1131
|
});
|
|
899
1132
|
if (result.error !== undefined) {
|
|
900
1133
|
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
@@ -1618,6 +1851,7 @@ export class OpenCodeServerAdapter {
|
|
|
1618
1851
|
return session;
|
|
1619
1852
|
}
|
|
1620
1853
|
beginTrackedTurn(text, origin, options = {}) {
|
|
1854
|
+
this.currentTurnToken += 1;
|
|
1621
1855
|
this.hasAcceptedInput = true;
|
|
1622
1856
|
this.currentPreview = truncatePreview(text);
|
|
1623
1857
|
this.state.lastInputAt = nowIso();
|
|
@@ -1644,8 +1878,10 @@ export class OpenCodeServerAdapter {
|
|
|
1644
1878
|
hasTrackedTurnState() {
|
|
1645
1879
|
return (this.state.status === "busy" ||
|
|
1646
1880
|
this.state.status === "awaiting_approval" ||
|
|
1881
|
+
this.state.status === "awaiting_input" ||
|
|
1647
1882
|
this.hasAcceptedInput ||
|
|
1648
1883
|
this.pendingPermission !== null ||
|
|
1884
|
+
this.pendingQuestion !== null ||
|
|
1649
1885
|
this.state.activeTurnOrigin !== undefined ||
|
|
1650
1886
|
this.currentPreview !== "(idle)");
|
|
1651
1887
|
}
|
|
@@ -1654,6 +1890,7 @@ export class OpenCodeServerAdapter {
|
|
|
1654
1890
|
this.pendingLocalPrompt = "";
|
|
1655
1891
|
this.localPromptNoticeSent = false;
|
|
1656
1892
|
this.clearPendingPermissionState();
|
|
1893
|
+
this.clearPendingQuestionState();
|
|
1657
1894
|
this.state.activeTurnOrigin = undefined;
|
|
1658
1895
|
this.hasAcceptedInput = false;
|
|
1659
1896
|
this.currentPreview = "(idle)";
|
|
@@ -2224,6 +2461,15 @@ export class OpenCodeServerAdapter {
|
|
|
2224
2461
|
const sessionId = this.extractSessionId(payload);
|
|
2225
2462
|
return sessionId ? `${type}:${sessionId}` : null;
|
|
2226
2463
|
}
|
|
2464
|
+
case "question.asked": {
|
|
2465
|
+
const requestId = typeof payload.id === "string" ? payload.id : undefined;
|
|
2466
|
+
return requestId ? `${type}:${requestId}` : null;
|
|
2467
|
+
}
|
|
2468
|
+
case "question.replied":
|
|
2469
|
+
case "question.rejected": {
|
|
2470
|
+
const requestId = typeof payload.requestID === "string" ? payload.requestID : undefined;
|
|
2471
|
+
return requestId ? `${type}:${requestId}` : null;
|
|
2472
|
+
}
|
|
2227
2473
|
default:
|
|
2228
2474
|
return null;
|
|
2229
2475
|
}
|