@khalilgharbaoui/opencode-claude-code-plugin 0.58.0 → 0.59.0

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/dist/index.d.ts CHANGED
@@ -128,13 +128,26 @@ interface V2CommandDefinition {
128
128
  readonly description?: string;
129
129
  readonly execute: (input: V2CommandInvocation) => Promise<void>;
130
130
  }
131
- /** `session.deleted` is the one event this plugin acts on. */
131
+ /**
132
+ * A bus event. Acted on: `session.deleted`, `session.execution.started` /
133
+ * `succeeded` / `failed` / `interrupted` (with `reason`, measured on 2.0.31,
134
+ * h #g253), and the never-emitted-there `session.status` / `session.idle`.
135
+ */
132
136
  interface V2Event {
133
137
  readonly type?: string;
134
138
  readonly data?: {
135
139
  readonly sessionID?: string;
136
140
  } & Record<string, unknown>;
137
141
  }
142
+ /**
143
+ * `ctx.rpc.register`'s result (`@opencode/plugin` 2.0.31 `RpcRegistration`):
144
+ * a registration whose events travel on the bus as `rpc.<id>.<event>`.
145
+ */
146
+ interface V2RpcRegistration extends V2Registration {
147
+ readonly events: {
148
+ emit(name: string, data: Record<string, unknown>): Promise<void>;
149
+ };
150
+ }
138
151
  interface V2Context {
139
152
  readonly app: {
140
153
  readonly version: string;
@@ -167,6 +180,14 @@ interface V2Context {
167
180
  signal?: AbortSignal;
168
181
  }): AsyncIterable<V2Event>;
169
182
  };
183
+ /**
184
+ * Absent before 2.0.31's measured shape: the definition is plain JSON
185
+ * Schema, which `Rpc.PortableDefinition` accepts, and the handlers object is
186
+ * empty because the definition declares no methods.
187
+ */
188
+ readonly rpc?: {
189
+ register(definition: unknown, handlers: Record<string, never>): Promise<V2RpcRegistration>;
190
+ };
170
191
  readonly skill?: {
171
192
  transform(callback: (editor: {
172
193
  get(id: string): unknown;
@@ -1549,8 +1570,6 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
1549
1570
  * 3. `"default"`.
1550
1571
  */
1551
1572
  private sessionAffinity;
1552
- private controlRequestBehaviorForTool;
1553
- private writeControlResponse;
1554
1573
  /**
1555
1574
  * Handle Claude stream-json control requests (`can_use_tool`, etc.) and
1556
1575
  * respond via stdin with a matching `control_response`.
@@ -1567,7 +1586,6 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
1567
1586
  */
1568
1587
  private resolveCompactionModel;
1569
1588
  private thinkingCliOptions;
1570
- private latestUserText;
1571
1589
  private synthesizeTitle;
1572
1590
  /**
1573
1591
  * Aggregates the one turn implementation into a `doGenerate` result.
package/dist/index.js CHANGED
@@ -272,6 +272,8 @@ var MAX_TOASTED_MESSAGES = 200;
272
272
  var toastBurstLimit = 3;
273
273
  var toastBurstWindowMs = 2e3;
274
274
  var activeSink = null;
275
+ var activeNotifier = null;
276
+ var bufferedNotices = [];
275
277
  var bufferedEntries = [];
276
278
  var droppedBeforeSink = 0;
277
279
  var toastedMessages = /* @__PURE__ */ new Set();
@@ -296,6 +298,13 @@ function setLogSink(sink) {
296
298
  });
297
299
  }
298
300
  }
301
+ function setToastNotifier(notifier) {
302
+ activeNotifier = notifier;
303
+ if (!notifier) return;
304
+ const pending2 = bufferedNotices;
305
+ bufferedNotices = [];
306
+ for (const entry of pending2) maybeToast(entry, notifier);
307
+ }
299
308
  function routeToSink(entry) {
300
309
  if (!activeSink) {
301
310
  if (bufferedEntries.length >= MAX_BUFFERED_SINK_ENTRIES) {
@@ -314,18 +323,34 @@ function deliverToSink(entry) {
314
323
  sink.log(entry);
315
324
  } catch {
316
325
  }
326
+ maybeToast(entry, (toast) => sink.toast(toast));
327
+ }
328
+ function maybeToast(entry, send) {
317
329
  if (entry.level !== "warn" && entry.level !== "error") return;
318
330
  if (toastedMessages.has(entry.message)) return;
319
331
  if (toastedMessages.size < MAX_TOASTED_MESSAGES) toastedMessages.add(entry.message);
320
332
  if (!allowToastNow()) return;
321
- sendToast(sink, entry);
333
+ sendToast(send, entry);
322
334
  }
323
- function sendToast(sink, entry) {
335
+ function sendToast(send, entry) {
324
336
  try {
325
- sink.toast(entry);
337
+ send(entry);
326
338
  } catch {
327
339
  }
328
340
  }
341
+ function toastTarget() {
342
+ const sink = activeSink;
343
+ if (sink) return (entry) => sink.toast(entry);
344
+ return activeNotifier;
345
+ }
346
+ function routeToNotifier(entry) {
347
+ const notifier = activeNotifier;
348
+ if (notifier) {
349
+ maybeToast(entry, notifier);
350
+ return;
351
+ }
352
+ if (bufferedNotices.length < MAX_BUFFERED_SINK_ENTRIES) bufferedNotices.push(entry);
353
+ }
329
354
  function allowToastNow() {
330
355
  const now = Date.now();
331
356
  if (now - burstWindowStart >= toastBurstWindowMs) {
@@ -347,10 +372,11 @@ function armBurstSummary() {
347
372
  const suppressed = burstSuppressed;
348
373
  burstSuppressed = 0;
349
374
  if (suppressed <= 0) return;
350
- const sink = activeSink;
351
- if (!sink) return;
352
- const message = `${suppressed} more claude-code warning(s) were not shown; the full text is in opencode's log`;
353
- sendToast(sink, { level: "warn", message, line: fmt("WARN", message) });
375
+ const target = toastTarget();
376
+ if (!target) return;
377
+ const where = activeSink ? "opencode's log" : "the plugin log file";
378
+ const message = `${suppressed} more claude-code warning(s) were not shown; the full text is in ${where}`;
379
+ sendToast(target, { level: "warn", message, line: fmt("WARN", message) });
354
380
  }, toastBurstWindowMs);
355
381
  burstSummaryTimer.unref?.();
356
382
  }
@@ -366,6 +392,7 @@ function emit(level, msg, data) {
366
392
  routeToSink({ level, message: msg, data, line });
367
393
  } else {
368
394
  console.error(line);
395
+ if (level === "warn" || level === "error") routeToNotifier({ level, message: msg, data, line });
369
396
  }
370
397
  }
371
398
  writeToFile(line);
@@ -6374,10 +6401,71 @@ function watchChildSession(sessionID) {
6374
6401
  });
6375
6402
  return () => answer;
6376
6403
  }
6404
+ var eventRunStates = /* @__PURE__ */ new Map();
6405
+ var MAX_EVENT_RUN_STATES = 512;
6406
+ var openEventFeeds = 0;
6407
+ function openSessionRunEventFeed() {
6408
+ openEventFeeds += 1;
6409
+ let closed = false;
6410
+ return () => {
6411
+ if (closed) return;
6412
+ closed = true;
6413
+ openEventFeeds = Math.max(0, openEventFeeds - 1);
6414
+ eventRunStates.clear();
6415
+ };
6416
+ }
6417
+ function noteSessionRunEvent(event) {
6418
+ if (openEventFeeds === 0 || !event || typeof event !== "object") return;
6419
+ const { type, data } = event;
6420
+ if (typeof type !== "string" || !data || typeof data !== "object") return;
6421
+ const sessionID = data.sessionID;
6422
+ if (typeof sessionID !== "string" || !sessionID || sessionID === "default") return;
6423
+ if (type === "session.deleted") {
6424
+ eventRunStates.delete(sessionID);
6425
+ return;
6426
+ }
6427
+ let next;
6428
+ switch (type) {
6429
+ case "session.execution.started":
6430
+ next = "busy";
6431
+ break;
6432
+ case "session.execution.succeeded":
6433
+ case "session.execution.failed":
6434
+ case "session.execution.interrupted":
6435
+ case "session.idle":
6436
+ next = "idle";
6437
+ break;
6438
+ case "session.status": {
6439
+ const status = data.status?.type;
6440
+ if (typeof status === "string") next = status === "idle" ? "idle" : "busy";
6441
+ break;
6442
+ }
6443
+ }
6444
+ if (!next) return;
6445
+ eventRunStates.delete(sessionID);
6446
+ eventRunStates.set(sessionID, next);
6447
+ while (eventRunStates.size > MAX_EVENT_RUN_STATES) {
6448
+ const oldest = eventRunStates.keys().next().value;
6449
+ if (oldest === void 0) break;
6450
+ eventRunStates.delete(oldest);
6451
+ }
6452
+ }
6453
+ function sessionRunStateFromEvents(sessionID) {
6454
+ if (openEventFeeds === 0) return "unknown";
6455
+ return eventRunStates.get(sessionID) ?? "unknown";
6456
+ }
6457
+ function busySessionsFromEvents() {
6458
+ if (openEventFeeds === 0) return void 0;
6459
+ const busy = {};
6460
+ for (const [sessionID, state] of eventRunStates) {
6461
+ if (state === "busy") busy[sessionID] = { type: "busy" };
6462
+ }
6463
+ return busy;
6464
+ }
6377
6465
  async function fetchSessionRunState(sessionID, directory) {
6378
6466
  if (!sessionID || sessionID === "default") return "unknown";
6379
6467
  const status = opencodeClient?.session?.status;
6380
- if (!status) return "unknown";
6468
+ if (!status) return sessionRunStateFromEvents(sessionID);
6381
6469
  try {
6382
6470
  const dir = directory ?? await fetchSessionDirectory(sessionID);
6383
6471
  const res = await status.call(
@@ -7734,6 +7822,27 @@ function deleteActiveProcessesForSession(sessionID) {
7734
7822
  forgetDispatchChoicesForSession(sessionID);
7735
7823
  return released;
7736
7824
  }
7825
+ var STOPPED_TURN_MESSAGE = "opencode stopped the turn while it was running its proxy tool calls";
7826
+ function releaseParkedProxyCallsForSession(sessionID, reason = STOPPED_TURN_MESSAGE) {
7827
+ if (!sessionID || sessionID === "default") return [];
7828
+ const released = [];
7829
+ for (const [key, ap] of [...activeProcesses]) {
7830
+ const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
7831
+ if (!owned) continue;
7832
+ if (ap.lineEmitter.listenerCount("line") > 0) continue;
7833
+ if (getPendingProxyCalls(key).length === 0) continue;
7834
+ log.info("opencode stopped the turn at a proxy tool boundary; releasing pending calls", {
7835
+ sessionKey: key,
7836
+ sessionID
7837
+ });
7838
+ void interruptTurn(ap).then((idle) => {
7839
+ log.info("interrupt sent for aborted turn", { sk: key, idle });
7840
+ });
7841
+ rejectAllPendingProxyCallsForSession(key, new Error(reason));
7842
+ released.push(key);
7843
+ }
7844
+ return released;
7845
+ }
7737
7846
  function killAllActiveProcesses() {
7738
7847
  const keys = [...activeProcesses.keys()];
7739
7848
  for (const key of keys) deleteActiveProcess(key);
@@ -8373,17 +8482,14 @@ async function fetchAsideHistory(client, sessionID, question) {
8373
8482
  return [];
8374
8483
  }
8375
8484
  }
8376
- async function handleBtwCommand(client, input, options = {}) {
8485
+ async function runBtwCommand(client, input, options = {}) {
8377
8486
  const question = input.arguments.trim();
8378
- if (!question) {
8379
- showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
8380
- throw new BtwHandledError("/btw needs a question.");
8381
- }
8487
+ if (!question) return "bare";
8382
8488
  const active = await waitForAsideProcess(client, input.sessionID, options);
8383
8489
  const transport = active?.asideTransport;
8384
8490
  if (!active || !transport) {
8385
8491
  if (active) log.info("btw: process has no aside transport, leaving it to the turn", { sessionID: input.sessionID });
8386
- return;
8492
+ return "release";
8387
8493
  }
8388
8494
  let busy = false;
8389
8495
  let inlineDone = false;
@@ -8444,7 +8550,7 @@ async function handleBtwCommand(client, input, options = {}) {
8444
8550
  }
8445
8551
  );
8446
8552
  }
8447
- if (!busy) return;
8553
+ if (!busy) return "release";
8448
8554
  const started2 = Date.now();
8449
8555
  const outcome = await Promise.race([
8450
8556
  inlineDelivered,
@@ -8457,18 +8563,46 @@ async function handleBtwCommand(client, input, options = {}) {
8457
8563
  sessionID: input.sessionID,
8458
8564
  waitedMs: Date.now() - started2
8459
8565
  });
8460
- throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
8566
+ return "inline";
8461
8567
  }
8462
8568
  log.info("btw: turn over, releasing the /btw message", {
8463
8569
  sessionID: input.sessionID,
8464
8570
  idle: outcome === "idle",
8465
8571
  waitedMs: Date.now() - started2
8466
8572
  });
8573
+ return outcome === "timeout" ? "timeout" : "release";
8574
+ }
8575
+ async function handleBtwCommand(client, input, options = {}) {
8576
+ const outcome = await runBtwCommand(client, input, options);
8577
+ if (outcome === "bare") {
8578
+ showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
8579
+ throw new BtwHandledError("/btw needs a question.");
8580
+ }
8581
+ if (outcome === "inline") throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
8467
8582
  if (outcome === "timeout") {
8468
8583
  showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
8469
8584
  throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
8470
8585
  }
8471
8586
  }
8587
+ async function askBtwThenRelease(client, input, release2, options = {}) {
8588
+ let outcome;
8589
+ try {
8590
+ outcome = await runBtwCommand(client, input, options);
8591
+ } catch (error) {
8592
+ log.warn("btw: the early aside failed; sending the /btw message as it is", {
8593
+ sessionID: input.sessionID,
8594
+ error: errorText(error)
8595
+ });
8596
+ outcome = "failed";
8597
+ }
8598
+ if (outcome === "inline") return outcome;
8599
+ try {
8600
+ await release2();
8601
+ } catch (error) {
8602
+ log.warn("btw: could not send the /btw message", { sessionID: input.sessionID, error: errorText(error) });
8603
+ }
8604
+ return outcome;
8605
+ }
8472
8606
 
8473
8607
  // src/budget-ledger.ts
8474
8608
  var BUDGET_MARKER = "\u258C **budget:**";
@@ -8638,7 +8772,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
8638
8772
  "automatic account switch skipped an account whose runtime could not be prepared",
8639
8773
  "background dispatch refused by maxBackgroundSubagents",
8640
8774
  "btw: a turn is running but no claude process appeared for it",
8775
+ "btw: could not send the /btw message",
8641
8776
  "btw: early aside failed; the message will ask again",
8777
+ "btw: the early aside failed; sending the /btw message as it is",
8642
8778
  "budget option refused: not a positive number of dollars",
8643
8779
  "budgetWarnAt refused: expected percentages between 0 and 100",
8644
8780
  "cannot interrupt this transport; waiting for the turn to end",
@@ -8727,7 +8863,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
8727
8863
  "no configured account could take this turn; it ends here",
8728
8864
  "no proxyTools entry was recognised; nothing will be proxied this turn",
8729
8865
  "no stdout after envelope write; respawning claude process to resume conversation",
8730
- "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
8866
+ "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction, and stopped turns keep their proxy calls until the next message",
8731
8867
  "opencode's stored OAuth token for this MCP server has expired, so the bridged server carries no credential; re-authenticate it in opencode",
8732
8868
  "pending proxy call arrived after stream close; rejecting",
8733
8869
  'permissionMode "plan" is enforced: claude cannot edit files or run commands, and --dangerously-skip-permissions is deliberately not passed so it stays that way. Headless Claude Code is not offered an ExitPlanMode tool, so nothing releases plan mode mid-session; approving a plan in chat does not unlock writes. Leaving plan mode means changing the config and restarting opencode.',
@@ -17064,12 +17200,6 @@ var ClaudeCodeLanguageModel = class {
17064
17200
  this.config.provider
17065
17201
  );
17066
17202
  }
17067
- controlRequestBehaviorForTool(toolName) {
17068
- return controlRequestBehaviorForTool(this.config, toolName);
17069
- }
17070
- writeControlResponse(proc, requestId, response) {
17071
- writeControlResponse(proc, requestId, response);
17072
- }
17073
17203
  /**
17074
17204
  * Handle Claude stream-json control requests (`can_use_tool`, etc.) and
17075
17205
  * respond via stdin with a matching `control_response`.
@@ -17118,9 +17248,6 @@ var ClaudeCodeLanguageModel = class {
17118
17248
  thinkingDisplay: process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0 ? "summarized" : void 0
17119
17249
  };
17120
17250
  }
17121
- latestUserText(prompt) {
17122
- return latestUserText(prompt);
17123
- }
17124
17251
  synthesizeTitle(prompt) {
17125
17252
  return synthesizeTitle(prompt);
17126
17253
  }
@@ -19295,38 +19422,40 @@ var ClaudeCodeLanguageModel = class {
19295
19422
  dropped: unattended.dropped
19296
19423
  });
19297
19424
  let partialText = false;
19298
- {
19425
+ const narration = [];
19426
+ for (const line of unattended.lines) {
19427
+ try {
19428
+ const outer = JSON.parse(line);
19429
+ const msg = outer.type === "stream_event" && outer.event ? outer.event : outer;
19430
+ let text = "";
19431
+ if (msg.type === "content_block_delta" && msg.delta?.type === "text_delta") {
19432
+ text = msg.delta.text ?? "";
19433
+ partialText = true;
19434
+ } else if (msg.type === "assistant") {
19435
+ if (!partialText) text = (msg.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
19436
+ partialText = false;
19437
+ } else if (msg.type === "result") {
19438
+ state.unattendedTurnEnded = true;
19439
+ for (const entry of state.activeProcess.pendingProxyCompletions?.values() ?? []) {
19440
+ if (isPendingProxyCallChannelClosed(entry.call)) entry.recoveryRequired = true;
19441
+ }
19442
+ if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
19443
+ if (msg.is_error && msg.result) text = msg.result;
19444
+ }
19445
+ if (text) narration.push(text);
19446
+ } catch {
19447
+ }
19448
+ }
19449
+ if (narration.length > 0 || unattended.dropped > 0) {
19299
19450
  const replayId = state.startTextBlock();
19300
19451
  controller.enqueue({
19301
19452
  type: "text-delta",
19302
19453
  id: replayId,
19303
19454
  delta: formatUnattendedReplayNote(unattended.dropped)
19304
19455
  });
19305
- for (const line of unattended.lines) {
19306
- try {
19307
- const outer = JSON.parse(line);
19308
- const msg = outer.type === "stream_event" && outer.event ? outer.event : outer;
19309
- let text = "";
19310
- if (msg.type === "content_block_delta" && msg.delta?.type === "text_delta") {
19311
- text = msg.delta.text ?? "";
19312
- partialText = true;
19313
- } else if (msg.type === "assistant") {
19314
- if (!partialText) text = (msg.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
19315
- partialText = false;
19316
- } else if (msg.type === "result") {
19317
- state.unattendedTurnEnded = true;
19318
- for (const entry of state.activeProcess.pendingProxyCompletions?.values() ?? []) {
19319
- if (isPendingProxyCallChannelClosed(entry.call)) entry.recoveryRequired = true;
19320
- }
19321
- if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
19322
- if (msg.is_error && msg.result) text = msg.result;
19323
- }
19324
- if (text) controller.enqueue({ type: "text-delta", id: state.currentTextId ?? state.startTextBlock(), delta: text });
19325
- } catch {
19326
- }
19327
- }
19456
+ for (const text of narration) controller.enqueue({ type: "text-delta", id: replayId, delta: text });
19457
+ state.endTextBlock();
19328
19458
  }
19329
- state.endTextBlock();
19330
19459
  clearFallbackTimer(state);
19331
19460
  state.hasReceivedContent = false;
19332
19461
  }
@@ -19754,11 +19883,42 @@ function cleanupOne(cacheRoot, ourDir) {
19754
19883
  }
19755
19884
  }
19756
19885
 
19757
- // src/tui-log-sink.ts
19758
- var LOG_SERVICE_NAME = "opencode-claude-code";
19886
+ // src/plugin-notice.ts
19887
+ var NOTICE_RPC_ID = "opencode-claude-code-plugin";
19888
+ var NOTICE_EVENT = "notice";
19889
+ var NOTICE_EVENT_TYPE = `rpc.${NOTICE_RPC_ID}.${NOTICE_EVENT}`;
19890
+ var NOTICE_RPC_DEFINITION = {
19891
+ id: NOTICE_RPC_ID,
19892
+ methods: {},
19893
+ events: {
19894
+ [NOTICE_EVENT]: {
19895
+ schema: {
19896
+ type: "object",
19897
+ properties: {
19898
+ id: { type: "string" },
19899
+ level: { type: "string", enum: ["warn", "error"] },
19900
+ message: { type: "string" }
19901
+ },
19902
+ required: ["id", "level", "message"],
19903
+ additionalProperties: false
19904
+ }
19905
+ }
19906
+ }
19907
+ };
19759
19908
  var TOAST_TITLE = "claude-code";
19760
19909
  var TOAST_DURATION_MS = 1e4;
19761
19910
  var MAX_TOAST_CHARS = 240;
19911
+ function toastMessage(message) {
19912
+ const flat = message.replace(/\s+/g, " ").trim();
19913
+ if (flat.length <= MAX_TOAST_CHARS) return flat;
19914
+ return `${flat.slice(0, MAX_TOAST_CHARS - 1)}\u2026`;
19915
+ }
19916
+ function pluginNotice(id, level, message) {
19917
+ return { id, level, message: toastMessage(message) };
19918
+ }
19919
+
19920
+ // src/tui-log-sink.ts
19921
+ var LOG_SERVICE_NAME = "opencode-claude-code";
19762
19922
  var APP_LOG_LEVEL = {
19763
19923
  debug: "debug",
19764
19924
  info: "info",
@@ -19766,11 +19926,6 @@ var APP_LOG_LEVEL = {
19766
19926
  warn: "warn",
19767
19927
  error: "error"
19768
19928
  };
19769
- function toastMessage(message) {
19770
- const flat = message.replace(/\s+/g, " ").trim();
19771
- if (flat.length <= MAX_TOAST_CHARS) return flat;
19772
- return `${flat.slice(0, MAX_TOAST_CHARS - 1)}\u2026`;
19773
- }
19774
19929
  function appLogBody(entry) {
19775
19930
  return {
19776
19931
  service: LOG_SERVICE_NAME,
@@ -19820,6 +19975,9 @@ function registerOpencodeLogSink(client) {
19820
19975
  return true;
19821
19976
  }
19822
19977
 
19978
+ // src/v2.ts
19979
+ import { randomUUID as randomUUID8 } from "crypto";
19980
+
19823
19981
  // src/v2-client.ts
19824
19982
  function formatAgentTypeList(agents) {
19825
19983
  const lines = agents.flatMap((agent) => {
@@ -20109,24 +20267,89 @@ function deletedSessionIdV2(event) {
20109
20267
  const id = event.data?.sessionID;
20110
20268
  return typeof id === "string" && id.length > 0 ? id : void 0;
20111
20269
  }
20112
- async function releaseDeletedSessions(subscribe, signal) {
20270
+ function stoppedSessionIdV2(event) {
20271
+ if (!event || event.type !== "session.execution.interrupted") return void 0;
20272
+ if (event.data?.reason !== "user") return void 0;
20273
+ const id = event.data?.sessionID;
20274
+ return typeof id === "string" && id.length > 0 ? id : void 0;
20275
+ }
20276
+ async function followSessionEvents(subscribe, signal) {
20277
+ const closeFeed = openSessionRunEventFeed();
20113
20278
  try {
20114
20279
  for await (const event of subscribe({ signal })) {
20115
20280
  if (signal.aborted) break;
20116
- const sessionID = deletedSessionIdV2(event);
20117
- if (!sessionID) continue;
20118
- const released = deleteActiveProcessesForSession(sessionID);
20119
- if (released.length > 0) {
20120
- log.info("released claude state for deleted session", { sessionID, released });
20281
+ noteSessionRunEvent(event);
20282
+ const deleted = deletedSessionIdV2(event);
20283
+ if (deleted) {
20284
+ const released = deleteActiveProcessesForSession(deleted);
20285
+ if (released.length > 0) {
20286
+ log.info("released claude state for deleted session", { sessionID: deleted, released });
20287
+ }
20288
+ continue;
20121
20289
  }
20290
+ const stopped = stoppedSessionIdV2(event);
20291
+ if (stopped) releaseParkedProxyCallsForSession(stopped);
20122
20292
  }
20123
20293
  } catch (err) {
20124
20294
  if (!signal.aborted) {
20125
- log.warn("opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction", {
20126
- error: err instanceof Error ? err.message : String(err)
20127
- });
20295
+ log.warn(
20296
+ "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction, and stopped turns keep their proxy calls until the next message",
20297
+ { error: err instanceof Error ? err.message : String(err) }
20298
+ );
20128
20299
  }
20300
+ } finally {
20301
+ closeFeed();
20302
+ }
20303
+ }
20304
+ var noticeEmitters = /* @__PURE__ */ new Set();
20305
+ var NOTICE_PROCESS_TAG = randomUUID8().slice(0, 8);
20306
+ var noticeCounter = 0;
20307
+ function emitNotice(entry) {
20308
+ if (entry.level !== "warn" && entry.level !== "error") return;
20309
+ noticeCounter += 1;
20310
+ const notice = pluginNotice(`${NOTICE_PROCESS_TAG}-${noticeCounter}`, entry.level, entry.message);
20311
+ for (const emit2 of noticeEmitters) emit2(notice);
20312
+ }
20313
+ async function registerNoticeChannel(ctx) {
20314
+ const rpc = ctx.rpc;
20315
+ if (typeof rpc?.register !== "function") return void 0;
20316
+ let registration;
20317
+ try {
20318
+ registration = await rpc.register(NOTICE_RPC_DEFINITION, {});
20319
+ } catch (err) {
20320
+ log.debug("opencode 2 refused the notice channel; warnings stay in the plugin log", {
20321
+ error: err instanceof Error ? err.message : String(err)
20322
+ });
20323
+ return void 0;
20129
20324
  }
20325
+ const emitter2 = (notice) => {
20326
+ try {
20327
+ void Promise.resolve(registration.events.emit(NOTICE_EVENT, { ...notice })).catch(() => void 0);
20328
+ } catch {
20329
+ }
20330
+ };
20331
+ noticeEmitters.add(emitter2);
20332
+ setToastNotifier(emitNotice);
20333
+ return {
20334
+ dispose: async () => {
20335
+ noticeEmitters.delete(emitter2);
20336
+ if (noticeEmitters.size === 0) setToastNotifier(null);
20337
+ await registration.dispose();
20338
+ }
20339
+ };
20340
+ }
20341
+ function v2BtwClient(shim) {
20342
+ const shimSession = shim.session;
20343
+ const messages = shimSession?.messages;
20344
+ const status = async () => ({ data: busySessionsFromEvents() ?? {} });
20345
+ return {
20346
+ session: {
20347
+ ...typeof messages === "function" ? { messages: (options) => messages.call(shimSession, options) } : {},
20348
+ get status() {
20349
+ return busySessionsFromEvents() === void 0 ? void 0 : status;
20350
+ }
20351
+ }
20352
+ };
20130
20353
  }
20131
20354
  function createV2Setup(deps) {
20132
20355
  return async (ctx) => {
@@ -20137,9 +20360,13 @@ function createV2Setup(deps) {
20137
20360
  return () => void 0;
20138
20361
  }
20139
20362
  ensureProcessExitCleanup();
20140
- setOpencodeClient(createV1ClientShim(ctx));
20363
+ const shim = createV1ClientShim(ctx);
20364
+ setOpencodeClient(shim);
20141
20365
  const directory = ctx.location?.directory;
20142
20366
  setOpencodeProjectDirectory(isUsableDirectory(directory) ? directory : void 0);
20367
+ const registrations = [];
20368
+ const noticeChannel = await registerNoticeChannel(ctx);
20369
+ if (noticeChannel) registrations.push(noticeChannel);
20143
20370
  if (deps.buildAgentRegistry) {
20144
20371
  try {
20145
20372
  const config = deps.loadConfig(directory ?? process.cwd());
@@ -20159,7 +20386,6 @@ function createV2Setup(deps) {
20159
20386
  }
20160
20387
  }
20161
20388
  const planned = /* @__PURE__ */ new Map();
20162
- const registrations = [];
20163
20389
  registrations.push(
20164
20390
  await ctx.provider.transform((editor) => {
20165
20391
  const seed = editor.get(BASE_PROVIDER_ID);
@@ -20215,6 +20441,7 @@ function createV2Setup(deps) {
20215
20441
  })
20216
20442
  );
20217
20443
  const prompt = ctx.session.prompt;
20444
+ const btwClient = v2BtwClient(shim);
20218
20445
  if (ctx.command?.transform && typeof prompt === "function") {
20219
20446
  const send = (input, command, delivery) => prompt.call(ctx.session, {
20220
20447
  ...input.prompt,
@@ -20232,7 +20459,15 @@ function createV2Setup(deps) {
20232
20459
  editor.add({
20233
20460
  name: "btw",
20234
20461
  description: BTW_COMMAND_DESCRIPTION,
20235
- execute: (input) => send(input, "btw", "queue")
20462
+ execute: (input) => {
20463
+ void askBtwThenRelease(
20464
+ btwClient,
20465
+ { command: "btw", sessionID: input.sessionID, arguments: input.prompt.text ?? "" },
20466
+ () => send(input, "btw", "queue"),
20467
+ deps.btwWaitOptions
20468
+ );
20469
+ return Promise.resolve();
20470
+ }
20236
20471
  });
20237
20472
  })
20238
20473
  );
@@ -20253,7 +20488,7 @@ function createV2Setup(deps) {
20253
20488
  }
20254
20489
  const events = new AbortController();
20255
20490
  if (typeof ctx.event?.subscribe === "function") {
20256
- void releaseDeletedSessions(ctx.event.subscribe.bind(ctx.event), events.signal);
20491
+ void followSessionEvents(ctx.event.subscribe.bind(ctx.event), events.signal);
20257
20492
  }
20258
20493
  log.info("claude-code plugin set up for opencode 2", {
20259
20494
  opencode: ctx.app?.version,