backeer-opencode 0.2.2 → 0.2.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/CHANGELOG.md +8 -0
- package/README.md +3 -2
- package/dist/backeer-permission.js +49 -6
- package/package.json +1 -1
- package/src/session-messages.js +61 -10
- package/src/updates.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `backeer-opencode` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.3 - 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Keep conversation feedback queued while OpenCode permission or question UI is
|
|
8
|
+
active, even if OpenCode emits an intermediate `session.idle` event.
|
|
9
|
+
- Wait for the interaction reply and the following real idle transition before
|
|
10
|
+
injecting an ignored Backeer status message into the conversation.
|
|
11
|
+
- Add regression coverage for permission and question UI lifecycles.
|
|
12
|
+
|
|
5
13
|
## 0.2.2 - 2026-09-01
|
|
6
14
|
|
|
7
15
|
- Show user-facing Backeer delivery and decision results in the OpenCode
|
package/README.md
CHANGED
|
@@ -165,8 +165,9 @@ Backeer ✓ Decision applied: Always allow
|
|
|
165
165
|
|
|
166
166
|
These entries are marked `noReply` and `ignored`, so they are visible without
|
|
167
167
|
starting another agent turn or entering model context. Feedback generated while
|
|
168
|
-
an agent is active
|
|
169
|
-
the
|
|
168
|
+
an agent is active, a permission prompt is open, or a question is awaiting an
|
|
169
|
+
answer remains queued until the interaction closes and the session becomes idle.
|
|
170
|
+
It never writes to the user's input field or replaces interactive OpenCode UI.
|
|
170
171
|
|
|
171
172
|
Technical diagnostics use OpenCode's structured application log API instead of
|
|
172
173
|
stdout. Normal `info` logging omits HTTP details; set
|
|
@@ -404,7 +404,7 @@ import { randomUUID } from "node:crypto";
|
|
|
404
404
|
import { statSync } from "node:fs";
|
|
405
405
|
|
|
406
406
|
// src/updates.js
|
|
407
|
-
var PLUGIN_VERSION = "0.2.
|
|
407
|
+
var PLUGIN_VERSION = "0.2.3";
|
|
408
408
|
var PACKAGE_NAME = "backeer-opencode";
|
|
409
409
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
410
410
|
var CHECK_TIMEOUT_MS = 3e3;
|
|
@@ -650,6 +650,13 @@ var FEEDBACK_SYMBOLS = {
|
|
|
650
650
|
warning: "!",
|
|
651
651
|
error: "\xD7"
|
|
652
652
|
};
|
|
653
|
+
var QUESTION_ASKED_EVENTS = /* @__PURE__ */ new Set(["question.asked", "question.v2.asked"]);
|
|
654
|
+
var QUESTION_REPLIED_EVENTS = /* @__PURE__ */ new Set([
|
|
655
|
+
"question.replied",
|
|
656
|
+
"question.v2.replied",
|
|
657
|
+
"question.answered",
|
|
658
|
+
"question.v2.answered"
|
|
659
|
+
]);
|
|
653
660
|
async function sendSessionMessage(client, sessionID, text) {
|
|
654
661
|
if (typeof client?.session?.prompt !== "function") {
|
|
655
662
|
throw new Error("OpenCode session messaging is unavailable");
|
|
@@ -672,16 +679,22 @@ function createConversationFeedback({ client, reporter }) {
|
|
|
672
679
|
function sessionState(sessionID) {
|
|
673
680
|
let state = sessions.get(sessionID);
|
|
674
681
|
if (!state) {
|
|
675
|
-
state = {
|
|
682
|
+
state = {
|
|
683
|
+
idle: false,
|
|
684
|
+
flushing: false,
|
|
685
|
+
blockers: /* @__PURE__ */ new Set(),
|
|
686
|
+
queue: [],
|
|
687
|
+
queued: /* @__PURE__ */ new Set()
|
|
688
|
+
};
|
|
676
689
|
rememberBounded(sessions, sessionID, state);
|
|
677
690
|
}
|
|
678
691
|
return state;
|
|
679
692
|
}
|
|
680
693
|
async function flush(sessionID, state) {
|
|
681
|
-
if (disposed || state.flushing || !state.idle || state.queue.length === 0) return;
|
|
694
|
+
if (disposed || state.flushing || !state.idle || state.blockers.size > 0 || state.queue.length === 0) return;
|
|
682
695
|
state.flushing = true;
|
|
683
696
|
try {
|
|
684
|
-
while (!disposed && state.idle && state.queue.length > 0) {
|
|
697
|
+
while (!disposed && state.idle && state.blockers.size === 0 && state.queue.length > 0) {
|
|
685
698
|
const item = state.queue[0];
|
|
686
699
|
try {
|
|
687
700
|
await sendSessionMessage(
|
|
@@ -711,7 +724,7 @@ function createConversationFeedback({ client, reporter }) {
|
|
|
711
724
|
const removed = state.queue.shift();
|
|
712
725
|
state.queued.delete(removed.key);
|
|
713
726
|
}
|
|
714
|
-
if (state.idle) void flush(sessionID, state);
|
|
727
|
+
if (state.idle && state.blockers.size === 0) void flush(sessionID, state);
|
|
715
728
|
}
|
|
716
729
|
function setIdle(sessionID, idle) {
|
|
717
730
|
if (typeof sessionID !== "string" || !sessionID) return;
|
|
@@ -719,6 +732,24 @@ function createConversationFeedback({ client, reporter }) {
|
|
|
719
732
|
state.idle = idle;
|
|
720
733
|
if (idle) void flush(sessionID, state);
|
|
721
734
|
}
|
|
735
|
+
function interactionKey(kind, event) {
|
|
736
|
+
const value = event.properties?.requestID ?? event.properties?.id;
|
|
737
|
+
return `${kind}:${typeof value === "string" && value ? value : "pending"}`;
|
|
738
|
+
}
|
|
739
|
+
function beginInteraction(sessionID, kind, event) {
|
|
740
|
+
const state = sessionState(sessionID);
|
|
741
|
+
state.idle = false;
|
|
742
|
+
state.blockers.add(interactionKey(kind, event));
|
|
743
|
+
}
|
|
744
|
+
function endInteraction(sessionID, kind, event) {
|
|
745
|
+
const state = sessionState(sessionID);
|
|
746
|
+
const key = interactionKey(kind, event);
|
|
747
|
+
if (!state.blockers.delete(key)) {
|
|
748
|
+
const fallback = [...state.blockers].find((blocker) => blocker.startsWith(`${kind}:`));
|
|
749
|
+
if (fallback) state.blockers.delete(fallback);
|
|
750
|
+
}
|
|
751
|
+
state.idle = false;
|
|
752
|
+
}
|
|
722
753
|
return {
|
|
723
754
|
info: (sessionID, message) => enqueue("info", sessionID, message),
|
|
724
755
|
success: (sessionID, message) => enqueue("success", sessionID, message),
|
|
@@ -734,8 +765,20 @@ function createConversationFeedback({ client, reporter }) {
|
|
|
734
765
|
else if (status === "busy" || status === "retry") setIdle(sessionID, false);
|
|
735
766
|
} else if (event.type === "session.idle") {
|
|
736
767
|
setIdle(sessionID, true);
|
|
737
|
-
} else if (event.type === "permission.asked"
|
|
768
|
+
} else if (event.type === "permission.asked") {
|
|
769
|
+
beginInteraction(sessionID, "permission", event);
|
|
770
|
+
} else if (QUESTION_ASKED_EVENTS.has(event.type)) {
|
|
771
|
+
beginInteraction(sessionID, "question", event);
|
|
772
|
+
} else if (event.type === "permission.replied") {
|
|
773
|
+
endInteraction(sessionID, "permission", event);
|
|
774
|
+
} else if (QUESTION_REPLIED_EVENTS.has(event.type)) {
|
|
775
|
+
endInteraction(sessionID, "question", event);
|
|
776
|
+
} else if (event.type === "todo.updated") {
|
|
738
777
|
setIdle(sessionID, false);
|
|
778
|
+
} else if (event.type === "session.error") {
|
|
779
|
+
const state = sessionState(sessionID);
|
|
780
|
+
state.blockers.clear();
|
|
781
|
+
state.idle = false;
|
|
739
782
|
} else if (event.type === "session.deleted") {
|
|
740
783
|
sessions.delete(sessionID);
|
|
741
784
|
}
|
package/package.json
CHANGED
package/src/session-messages.js
CHANGED
|
@@ -8,6 +8,14 @@ const FEEDBACK_SYMBOLS = {
|
|
|
8
8
|
error: "×",
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const QUESTION_ASKED_EVENTS = new Set(["question.asked", "question.v2.asked"])
|
|
12
|
+
const QUESTION_REPLIED_EVENTS = new Set([
|
|
13
|
+
"question.replied",
|
|
14
|
+
"question.v2.replied",
|
|
15
|
+
"question.answered",
|
|
16
|
+
"question.v2.answered",
|
|
17
|
+
])
|
|
18
|
+
|
|
11
19
|
export async function sendSessionMessage(client, sessionID, text) {
|
|
12
20
|
if (typeof client?.session?.prompt !== "function") {
|
|
13
21
|
throw new Error("OpenCode session messaging is unavailable")
|
|
@@ -33,17 +41,29 @@ export function createConversationFeedback({ client, reporter }) {
|
|
|
33
41
|
function sessionState(sessionID) {
|
|
34
42
|
let state = sessions.get(sessionID)
|
|
35
43
|
if (!state) {
|
|
36
|
-
state = {
|
|
44
|
+
state = {
|
|
45
|
+
idle: false,
|
|
46
|
+
flushing: false,
|
|
47
|
+
blockers: new Set(),
|
|
48
|
+
queue: [],
|
|
49
|
+
queued: new Set(),
|
|
50
|
+
}
|
|
37
51
|
rememberBounded(sessions, sessionID, state)
|
|
38
52
|
}
|
|
39
53
|
return state
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
async function flush(sessionID, state) {
|
|
43
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
disposed || state.flushing || !state.idle ||
|
|
59
|
+
state.blockers.size > 0 || state.queue.length === 0
|
|
60
|
+
) return
|
|
44
61
|
state.flushing = true
|
|
45
62
|
try {
|
|
46
|
-
while (
|
|
63
|
+
while (
|
|
64
|
+
!disposed && state.idle && state.blockers.size === 0 &&
|
|
65
|
+
state.queue.length > 0
|
|
66
|
+
) {
|
|
47
67
|
const item = state.queue[0]
|
|
48
68
|
try {
|
|
49
69
|
await sendSessionMessage(
|
|
@@ -75,7 +95,7 @@ export function createConversationFeedback({ client, reporter }) {
|
|
|
75
95
|
const removed = state.queue.shift()
|
|
76
96
|
state.queued.delete(removed.key)
|
|
77
97
|
}
|
|
78
|
-
if (state.idle) void flush(sessionID, state)
|
|
98
|
+
if (state.idle && state.blockers.size === 0) void flush(sessionID, state)
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
function setIdle(sessionID, idle) {
|
|
@@ -85,6 +105,30 @@ export function createConversationFeedback({ client, reporter }) {
|
|
|
85
105
|
if (idle) void flush(sessionID, state)
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
function interactionKey(kind, event) {
|
|
109
|
+
const value = event.properties?.requestID ?? event.properties?.id
|
|
110
|
+
return `${kind}:${typeof value === "string" && value ? value : "pending"}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function beginInteraction(sessionID, kind, event) {
|
|
114
|
+
const state = sessionState(sessionID)
|
|
115
|
+
state.idle = false
|
|
116
|
+
state.blockers.add(interactionKey(kind, event))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function endInteraction(sessionID, kind, event) {
|
|
120
|
+
const state = sessionState(sessionID)
|
|
121
|
+
const key = interactionKey(kind, event)
|
|
122
|
+
if (!state.blockers.delete(key)) {
|
|
123
|
+
const fallback = [...state.blockers]
|
|
124
|
+
.find((blocker) => blocker.startsWith(`${kind}:`))
|
|
125
|
+
if (fallback) state.blockers.delete(fallback)
|
|
126
|
+
}
|
|
127
|
+
// The reply event can arrive before OpenCode has removed its permission UI
|
|
128
|
+
// or resumed the agent. Wait for the next idle event before injecting text.
|
|
129
|
+
state.idle = false
|
|
130
|
+
}
|
|
131
|
+
|
|
88
132
|
return {
|
|
89
133
|
info: (sessionID, message) => enqueue("info", sessionID, message),
|
|
90
134
|
success: (sessionID, message) => enqueue("success", sessionID, message),
|
|
@@ -102,13 +146,20 @@ export function createConversationFeedback({ client, reporter }) {
|
|
|
102
146
|
else if (status === "busy" || status === "retry") setIdle(sessionID, false)
|
|
103
147
|
} else if (event.type === "session.idle") {
|
|
104
148
|
setIdle(sessionID, true)
|
|
105
|
-
} else if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
149
|
+
} else if (event.type === "permission.asked") {
|
|
150
|
+
beginInteraction(sessionID, "permission", event)
|
|
151
|
+
} else if (QUESTION_ASKED_EVENTS.has(event.type)) {
|
|
152
|
+
beginInteraction(sessionID, "question", event)
|
|
153
|
+
} else if (event.type === "permission.replied") {
|
|
154
|
+
endInteraction(sessionID, "permission", event)
|
|
155
|
+
} else if (QUESTION_REPLIED_EVENTS.has(event.type)) {
|
|
156
|
+
endInteraction(sessionID, "question", event)
|
|
157
|
+
} else if (event.type === "todo.updated") {
|
|
111
158
|
setIdle(sessionID, false)
|
|
159
|
+
} else if (event.type === "session.error") {
|
|
160
|
+
const state = sessionState(sessionID)
|
|
161
|
+
state.blockers.clear()
|
|
162
|
+
state.idle = false
|
|
112
163
|
} else if (event.type === "session.deleted") {
|
|
113
164
|
sessions.delete(sessionID)
|
|
114
165
|
}
|
package/src/updates.js
CHANGED