opencode-queue 0.10.0 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/index.ts +18 -15
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -79,6 +79,7 @@ When the session is busy:
79
79
 
80
80
  - Queued entries are hidden from the transcript and from the running agent.
81
81
  - The current agent run keeps using its original agent, model, and thinking variant.
82
+ - Each queued entry replays with the agent, model, and thinking variant selected when it was queued.
82
83
  - Queued entries replay in order after the session completes normally and becomes idle.
83
84
  - `/queue front ...` puts an entry before the existing queued entries.
84
85
  - Only one queued entry is sent per idle transition, so queued work runs one item at a time.
package/index.ts CHANGED
@@ -1,19 +1,18 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
2
  import type { AgentPartInput, FilePart, FilePartInput, SubtaskPartInput, TextPart, TextPartInput } from "@opencode-ai/sdk"
3
+ import { HttpServerResponse } from "effect/unstable/http"
3
4
 
4
5
  const QUEUE = /^\/queue(?:\s+([\s\S]*))?$/
5
6
  const SUFFIX = /^([\s\S]*?)\s+\/queue(?:\s+(front))?\s*$/
6
7
  const CMD = /^\/(\S+)(?:\s+([\s\S]*))?$/
7
8
  const ITEM_NUMBER = /^[1-9]\d*$/
8
- const HANDLED = "__QUEUE_HANDLED__"
9
9
  const TUI_COMPACT = "session_compact"
10
10
 
11
11
  type InputPart = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
12
12
  type Model = { providerID: string; modelID: string }
13
- type Meta = { variant?: string; controls?: string[]; fast?: boolean }
14
13
  type Run = { agent: string; model?: Model }
15
- type Info = { agent: string; model: Model } & Meta
16
- type Msg = { info: { role: string; agent?: string; mode?: string; model?: Model; providerID?: string; modelID?: string } & Meta }
14
+ type Info = { agent: string; model: Model; variant?: string }
15
+ type Msg = { info: { role: string; agent?: string; mode?: string; model?: Model; providerID?: string; modelID?: string; variant?: string } }
17
16
  type Ask = { type: string; properties: { id: string; sessionID: string; questions: { question: string; header: string }[] } }
18
17
  type Post = (input: { url: string; path?: Record<string, string>; body?: unknown; headers?: Record<string, string> }) => Promise<{ response?: Response; error?: unknown } | undefined>
19
18
  type QueueInput = { body: string; front: boolean }
@@ -129,6 +128,11 @@ const control = (op: Op): op is ControlOp => {
129
128
  const shouldQueue = (state?: State) => Boolean(state && (state.activity.kind !== "idle" || state.stopped))
130
129
  const shouldDeclinePlan = (state?: State) => Boolean(state && (state.activity.kind === "sending" || (!state.stopped && state.items.length)))
131
130
  const itemText = (item: Item) => (item.kind === "prompt" ? item.body.trim() || item.label : item.source)
131
+ // OpenCode's command hook has no cancel/noReply output. Throwing a raw Effect
132
+ // response is handled by OpenCode's HTTP layer as an empty successful command.
133
+ const handled = (): never => {
134
+ throw HttpServerResponse.empty({ status: 204 })
135
+ }
132
136
  const plan = (event: unknown): event is Ask => {
133
137
  if (typeof event !== "object" || !event || !("type" in event) || event.type !== "question.asked") return false
134
138
  const question = (event as Ask).properties?.questions?.[0]
@@ -154,7 +158,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
154
158
 
155
159
  const stop = async (message: string, variant: "info" | "error" = "info", duration = 5000): Promise<never> => {
156
160
  await toast(message, variant, duration)
157
- throw new Error(HANDLED)
161
+ return handled()
158
162
  }
159
163
 
160
164
  const no = async (id: string) => {
@@ -202,9 +206,9 @@ export const QueuePlugin: Plugin = async ({ client }) => {
202
206
  })
203
207
 
204
208
  return ([...(Array.isArray(result) ? result : (result.data ?? []))].reverse() as Msg[]).flatMap((msg): Info[] => {
205
- if (msg.info.role === "user" && msg.info.agent && msg.info.model) return [{ agent: msg.info.agent, model: msg.info.model, variant: msg.info.variant, controls: msg.info.controls, fast: msg.info.fast }]
209
+ if (msg.info.role === "user" && msg.info.agent && msg.info.model) return [{ agent: msg.info.agent, model: msg.info.model, variant: msg.info.variant }]
206
210
  if (msg.info.role === "assistant" && (msg.info.agent || msg.info.mode) && msg.info.providerID && msg.info.modelID) {
207
- return [{ agent: msg.info.agent ?? msg.info.mode!, model: { providerID: msg.info.providerID, modelID: msg.info.modelID }, variant: msg.info.variant, controls: msg.info.controls, fast: msg.info.fast }]
211
+ return [{ agent: msg.info.agent ?? msg.info.mode!, model: { providerID: msg.info.providerID, modelID: msg.info.modelID }, variant: msg.info.variant }]
208
212
  }
209
213
  return []
210
214
  })[0]
@@ -217,7 +221,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
217
221
  return { agent: "build" }
218
222
  }
219
223
 
220
- const opts = (info: Info) => ({ agent: info.agent, model: info.model, variant: info.variant, controls: info.controls, fast: info.fast })
224
+ const opts = (info: Info) => ({ agent: info.agent, model: info.model, variant: info.variant })
221
225
 
222
226
  const shell = (sid: string, command: string, info: Run) => client.session.shell({ path: { id: sid }, body: { agent: info.agent, model: info.model, command } })
223
227
  // TUI command events target the focused session; queued replay must target the original session.
@@ -406,17 +410,17 @@ export const QueuePlugin: Plugin = async ({ client }) => {
406
410
  if (!shouldQueue(sessions.get(sid))) {
407
411
  if (op.kind === "shell") {
408
412
  await shell(sid, op.shell, await run(sid))
409
- throw new Error(HANDLED)
413
+ return handled()
410
414
  }
411
415
 
412
416
  if (op.kind === "compact") {
413
417
  await client.tui.executeCommand({ body: { command: TUI_COMPACT }, throwOnError: true })
414
- throw new Error(HANDLED)
418
+ return handled()
415
419
  }
416
420
 
417
421
  if (op.kind === "command") {
418
422
  await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
419
- throw new Error(HANDLED)
423
+ return handled()
420
424
  }
421
425
 
422
426
  output.parts.splice(0, output.parts.length, { type: "text", text: op.body } as any, ...parts)
@@ -436,7 +440,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
436
440
  const current = state(sid)
437
441
  const parts = files(output.parts)
438
442
  const op = parse(request, parts.length)
439
- const meta = input as Meta
443
+ const info = { agent: input.agent ?? output.message.agent, model: input.model ?? output.message.model, variant: input.variant }
440
444
 
441
445
  if (control(op)) {
442
446
  hide(output.message.id, text)
@@ -454,19 +458,18 @@ export const QueuePlugin: Plugin = async ({ client }) => {
454
458
  if (op.kind === "command") return
455
459
  if (op.kind === "compact") {
456
460
  hide(output.message.id, text)
457
- await compact(sid, { agent: output.message.agent, model: output.message.model, variant: meta.variant, controls: meta.controls, fast: meta.fast })
461
+ await compact(sid, info)
458
462
  return
459
463
  }
460
464
  if (op.kind === "shell") {
461
465
  hide(output.message.id, text)
462
- await shell(sid, op.shell, { agent: output.message.agent, model: output.message.model })
466
+ await shell(sid, op.shell, info)
463
467
  return
464
468
  }
465
469
  text.text = request.body
466
470
  return
467
471
  }
468
472
 
469
- const info = { agent: output.message.agent, model: { ...output.message.model }, variant: meta.variant, controls: meta.controls, fast: meta.fast }
470
473
  const prior = await latest(sid)
471
474
  if (prior) Object.assign(output.message, opts(prior))
472
475
  else console.warn("QueuePlugin could not neutralize queued placeholder metadata because the session has no previous message context")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-queue",
4
- "version": "0.10.0",
4
+ "version": "0.11.0",
5
5
  "type": "module",
6
6
  "description": "Queue OpenCode prompts and slash commands until the agent is idle",
7
7
  "main": "./index.ts",
@@ -37,6 +37,9 @@
37
37
  "scripts": {
38
38
  "typecheck": "tsc --noEmit"
39
39
  },
40
+ "dependencies": {
41
+ "effect": "4.0.0-beta.59"
42
+ },
40
43
  "devDependencies": {
41
44
  "@opencode-ai/plugin": "^1.14.37",
42
45
  "@opencode-ai/sdk": "^1.14.37",