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 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 is queued until the session becomes idle; it never writes to
169
- the user's input field.
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.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 = { idle: false, flushing: false, queue: [], queued: /* @__PURE__ */ new Set() };
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" || event.type === "question.asked" || event.type === "question.v2.asked" || event.type === "todo.updated") {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backeer-opencode",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Backeer lifecycle notifications and remote permission decisions for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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 = { idle: false, flushing: false, queue: [], queued: new Set() }
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 (disposed || state.flushing || !state.idle || state.queue.length === 0) return
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 (!disposed && state.idle && state.queue.length > 0) {
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
- event.type === "permission.asked" ||
107
- event.type === "question.asked" ||
108
- event.type === "question.v2.asked" ||
109
- event.type === "todo.updated"
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
@@ -1,4 +1,4 @@
1
- export const PLUGIN_VERSION = "0.2.2"
1
+ export const PLUGIN_VERSION = "0.2.3"
2
2
 
3
3
  const PACKAGE_NAME = "backeer-opencode"
4
4
  const NPM_REGISTRY_URL = "https://registry.npmjs.org"