opencode-queue 0.12.1 → 0.13.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 +12 -17
  2. package/index.ts +98 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Queue OpenCode input until the current session is idle.
8
8
 
9
- `opencode-queue` adds a real `/queue` slash command. It lets you type the next prompt, slash command, or shell command while an agent is still working, without interrupting the current run.
9
+ `opencode-queue` adds `/queue` and its shorter `/q` alias. It lets you type the next prompt, slash command, or shell command while an agent is still working, without interrupting the current run.
10
10
 
11
11
  ## Install
12
12
 
@@ -23,10 +23,11 @@ Restart OpenCode after installing. OpenCode installs npm plugins automatically a
23
23
  ## Quick Examples
24
24
 
25
25
  ```text
26
- /queue continue after this task
26
+ /q continue after this task
27
27
  continue after this task /queue
28
28
  /queue front do this next
29
29
  do this next /queue front
30
+ /queue now send this immediately
30
31
 
31
32
  /queue /review
32
33
  /review /queue
@@ -50,12 +51,15 @@ do this next /queue front
50
51
 
51
52
  ## Syntax
52
53
 
54
+ `/q` is accepted anywhere `/queue` is shown.
55
+
53
56
  | Input | What it does |
54
57
  | --- | --- |
55
58
  | `/queue message` | Queue a normal prompt. |
56
59
  | `message /queue` | Queue a normal prompt using trailing syntax. |
57
60
  | `/queue front message` | Queue a normal prompt before existing queued entries. |
58
61
  | `message /queue front` | Queue a normal prompt before existing queued entries using trailing syntax. |
62
+ | `/queue now input` | Send a prompt or slash command immediately. Shell commands still wait until the session is idle. |
59
63
  | `/queue /review` | Queue a slash command. |
60
64
  | `/review /queue` | Queue a slash command using trailing syntax. |
61
65
  | `/queue front /review` | Queue a slash command before existing queued entries. |
@@ -68,6 +72,9 @@ do this next /queue front
68
72
  | `/queue list` | Show the current queue. |
69
73
  | `/queue stop` | Pause automatic sending of queued entries. |
70
74
  | `/queue start` | Resume automatic sending of queued entries. |
75
+ | `/queue always` | Show whether automatic queueing is enabled for this project. |
76
+ | `/queue always on` | Automatically queue plain input while the session is busy. |
77
+ | `/queue always off` | Require `/queue` again. |
71
78
  | `/queue flush` | Send all queued entries immediately. |
72
79
  | `/queue clear` | Clear the current queue. |
73
80
  | `/queue clear 1` | Clear item 1 from the current queue. |
@@ -82,9 +89,11 @@ When the session is busy:
82
89
  - Each queued entry replays with the agent, model, and thinking variant selected when it was queued.
83
90
  - Queued entries replay in order after the session completes normally and becomes idle.
84
91
  - `/queue front ...` puts an entry before the existing queued entries.
92
+ - `/queue now ...` sends prompts and slash commands immediately regardless of queue state or mode. Shell commands remain queued until the session is idle.
85
93
  - Only one queued entry is sent per idle transition, so queued work runs one item at a time.
86
94
  - Queued entries are kept in place after an error, abort, crash, or restart.
87
95
  - `/queue stop` pauses automatic replay without clearing queued entries, and `/queue start` resumes it.
96
+ - `/queue always on` also queues plain prompts and custom slash commands while the session is busy, paused, or already has queued work. OpenCode does not expose native shell or `/compact` submissions to these plugin hooks.
88
97
  - `/queue flush` sends all queued entries immediately in one batch, even before the session is idle.
89
98
 
90
99
  When the session is idle:
@@ -100,24 +109,10 @@ When the session is idle:
100
109
  - `/queue flush` sends all queued entries immediately in one batch.
101
110
  - `/queue clear` clears the current queue, and `/queue clear 1` clears a specific queued item.
102
111
 
103
- ## Queue Management
104
-
105
- ```text
106
- /queue
107
- /queue list
108
- /queue stop
109
- /queue start
110
- /queue flush
111
- /queue clear
112
- /queue clear 1
113
- /queue clear 2 3
114
- ```
115
-
116
- Queues are scoped to the current project and session. They are stored in OpenCode's user data directory and restored after OpenCode restarts or crashes. A send interrupted by a crash remains queued because the plugin cannot know whether OpenCode accepted it before exiting, so restored queues stay paused until explicitly started.
112
+ Queues are scoped to the current project and session. They are stored in OpenCode's user data directory and restored with their previous running or stopped state after OpenCode restarts or crashes. The `always` setting applies to the whole project and is stored with its queues. Restored queues do not replay just because the session starts idle; a running queue resumes after the session becomes busy and then finishes successfully. A send interrupted by a crash remains queued because the plugin cannot know whether OpenCode accepted it before exiting.
117
113
 
118
114
  ## Notes
119
115
 
120
- - This plugin registers `/queue` as a real OpenCode slash command.
121
116
  - It does not add a keyboard shortcut. OpenCode plugins cannot currently register custom TUI keybindings.
122
117
  - Queued placeholders are hidden instead of deleted, then filtered out before messages are sent to the model.
123
118
  - If plan mode asks to switch to the build agent while more queued work is waiting, the plugin answers `No` so the queue can continue.
package/index.ts CHANGED
@@ -6,11 +6,11 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
6
6
  import { homedir } from "node:os"
7
7
  import { dirname, join } from "node:path"
8
8
 
9
- const QUEUE = /^\/queue(?:\s+([\s\S]*))?$/
10
- const SUFFIX = /^([\s\S]*?)\s+\/queue(?:\s+(front))?\s*$/
9
+ const SUFFIX = /^(?:([\s\S]*?)\s+)?\/(\S+)(?:\s+(front))?\s*$/
11
10
  const CMD = /^\/(\S+)(?:\s+([\s\S]*))?$/
12
11
  const ITEM_NUMBER = /^[1-9]\d*$/
13
12
  const TUI_COMPACT = "session_compact"
13
+ const INTERNAL = "opencodeQueueInternal"
14
14
 
15
15
  type InputPart = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
16
16
  type Model = { providerID: string; modelID: string }
@@ -19,7 +19,7 @@ type Info = { agent: string; model: Model; variant?: string }
19
19
  type Msg = { info: { role: string; agent?: string; mode?: string; model?: Model; providerID?: string; modelID?: string; variant?: string } }
20
20
  type Ask = { type: string; properties: { id: string; sessionID: string; questions: { question: string; header: string }[] } }
21
21
  type Post = (input: { url: string; path?: Record<string, string>; body?: unknown; headers?: Record<string, string> }) => Promise<{ response?: Response; error?: unknown } | undefined>
22
- type QueueInput = { body: string; front: boolean }
22
+ type QueueInput = { body: string; modifier?: "front" | "now" }
23
23
 
24
24
  type Item =
25
25
  | { kind: "prompt"; info: Info; body: string; parts: InputPart[] }
@@ -33,32 +33,36 @@ type EntryOp =
33
33
  | { kind: "compact"; source: string }
34
34
  | { kind: "shell"; source: string; shell: string }
35
35
 
36
- type Activity = { kind: "idle" } | { kind: "busy" } | { kind: "sending"; idle: boolean; items: Item[] }
37
- type State = { items: Item[]; activity: Activity; stopped: boolean; failed: boolean; hidden: Set<string> }
38
- type Durable = Pick<State, "items" | "stopped" | "hidden">
39
- type Store = { version: 1; projectID: string; sessions: Record<string, { items: Item[]; stopped: boolean; hidden: string[] }> }
40
- type Placeholder = { id: string; part: TextPart }
41
-
42
- type Op =
36
+ type ControlOp =
43
37
  | { kind: "list" }
44
38
  | { kind: "clear"; indices: number[] }
45
39
  | { kind: "flush" }
46
40
  | { kind: "start" }
47
41
  | { kind: "stop" }
42
+ | { kind: "always"; enabled?: boolean }
43
+
44
+ type Activity = { kind: "idle" } | { kind: "restored" } | { kind: "busy" } | { kind: "sending"; idle: boolean; items: Item[] }
45
+ type State = { items: Item[]; activity: Activity; stopped: boolean; failed: boolean; hidden: Set<string> }
46
+ type Draft = Pick<State, "items" | "stopped" | "hidden"> & { always: boolean }
47
+ type Store = { version: 1; projectID: string; always: boolean; sessions: Record<string, { items: Item[]; stopped: boolean; hidden: string[] }> }
48
+ type Placeholder = { id: string; part: TextPart }
49
+
50
+ type Op =
51
+ | ControlOp
48
52
  | { kind: "invalid"; message: string }
49
53
  | (EntryOp & { front: boolean })
50
54
 
51
- type ControlOp = Extract<Op, { kind: "list" | "clear" | "flush" | "start" | "stop" }>
55
+ const isQueue = (command: string) => command === "q" || command === "queue"
52
56
 
53
57
  const parsePrefix = (body: string): QueueInput => {
54
- const match = body.trim().match(/^front(?:\s+([\s\S]*))?$/)
55
- return match ? { body: match[1] ?? "", front: true } : { body, front: false }
58
+ const match = body.trim().match(/^(front|now)(?:\s+([\s\S]*))?$/)
59
+ return match ? { body: match[2] ?? "", modifier: match[1] === "front" ? "front" : "now" } : { body }
56
60
  }
57
61
 
58
62
  const parse = (input: QueueInput, files = 0): Op => {
59
63
  const text = input.body.trim()
60
- const front = input.front
61
- if (!front && !files) {
64
+ const front = input.modifier === "front"
65
+ if (!input.modifier && !files) {
62
66
  switch (text) {
63
67
  case "":
64
68
  case "list":
@@ -71,6 +75,10 @@ const parse = (input: QueueInput, files = 0): Op => {
71
75
  return { kind: "stop" }
72
76
  }
73
77
 
78
+ if (text === "always") return { kind: "always" }
79
+ if (text === "always on" || text === "always off") return { kind: "always", enabled: text === "always on" }
80
+ if (/^always(?:\s|$)/.test(text)) return { kind: "invalid", message: "Queue always expects on or off" }
81
+
74
82
  const clear = text.match(/^clear(?:\s+([\s\S]+))?$/)
75
83
  if (clear) {
76
84
  const values = clear[1]?.trim().split(/\s+/) ?? []
@@ -79,7 +87,7 @@ const parse = (input: QueueInput, files = 0): Op => {
79
87
  return { kind: "clear", indices }
80
88
  }
81
89
  }
82
- if (front && !text && !files) return { kind: "invalid", message: "Queue front input is empty" }
90
+ if (input.modifier && !text && !files) return { kind: "invalid", message: `Queue ${input.modifier} input is empty` }
83
91
 
84
92
  if (text.startsWith("!")) {
85
93
  const shell = text.slice(1).trim()
@@ -103,17 +111,13 @@ const parse = (input: QueueInput, files = 0): Op => {
103
111
  }
104
112
 
105
113
  const parseSuffix = (text: string): QueueInput | undefined => {
106
- const trimmed = text.trim()
107
- if (trimmed === "/queue") return { body: "", front: false }
108
- if (trimmed === "/queue front") return { body: "", front: true }
109
-
110
114
  const match = text.match(SUFFIX)
111
- return match ? { body: match[1], front: match[2] === "front" } : undefined
115
+ return match && isQueue(match[2]) ? { body: match[1] ?? "", modifier: match[3] ? "front" : undefined } : undefined
112
116
  }
113
117
  const stripSuffix = (text: string) => parseSuffix(text)?.body ?? text
114
118
  const parseInput = (text: string): QueueInput | undefined => {
115
- const prefix = text.match(QUEUE)
116
- return prefix ? parsePrefix(prefix[1] ?? "") : parseSuffix(text)
119
+ const prefix = text.match(CMD)
120
+ return prefix && isQueue(prefix[1]) ? parsePrefix(prefix[2] ?? "") : parseSuffix(text)
117
121
  }
118
122
  const control = (op: Op): op is ControlOp => {
119
123
  switch (op.kind) {
@@ -122,6 +126,7 @@ const control = (op: Op): op is ControlOp => {
122
126
  case "flush":
123
127
  case "start":
124
128
  case "stop":
129
+ case "always":
125
130
  return true
126
131
  default:
127
132
  return false
@@ -196,8 +201,11 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
196
201
  const sessions = new Map<string, State>()
197
202
  const deleted = new Set<string>()
198
203
  const enqueueTurns = new Map<string, Promise<unknown>>()
204
+ const internalCommands: { sid: string; command: string; args: string; used: boolean }[] = []
205
+ const origin = randomUUID()
199
206
  const post = (client as unknown as { _client?: { post?: Post } })._client?.post
200
207
  const path = join(dataHome(), "opencode", "opencode-queue", `${createHash("sha256").update(project.id).digest("hex")}.json`)
208
+ let alwaysQueue = false
201
209
  let writes = Promise.resolve()
202
210
 
203
211
  try {
@@ -205,6 +213,8 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
205
213
  if (!record(parsed) || parsed.version !== 1 || parsed.projectID !== project.id || !record(parsed.sessions)) {
206
214
  console.warn("QueuePlugin ignored invalid queue storage", path)
207
215
  } else {
216
+ if (typeof parsed.always === "boolean") alwaysQueue = parsed.always
217
+ else if (parsed.always !== undefined) console.warn("QueuePlugin ignored invalid always queue setting", path)
208
218
  for (const [sid, value] of Object.entries(parsed.sessions)) {
209
219
  if (!record(value) || typeof value.stopped !== "boolean" || !Array.isArray(value.items)) {
210
220
  console.warn("QueuePlugin skipped invalid stored session", sid)
@@ -215,17 +225,16 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
215
225
  const validHidden = Array.isArray(value.hidden) && value.hidden.every((id) => typeof id === "string")
216
226
  if (!validHidden) console.warn("QueuePlugin skipped invalid stored hidden messages", sid)
217
227
  const hidden = new Set(validHidden ? (value.hidden as string[]) : [])
218
- const stopped = value.stopped || items.length > 0
219
- if (items.length && !value.stopped) console.warn("QueuePlugin paused restored queued input to prevent duplicate replay after a restart", sid)
220
- if (items.length || stopped || hidden.size) sessions.set(sid, { items, activity: { kind: "idle" }, stopped, failed: false, hidden })
228
+ const activity: Activity = { kind: items.length && !value.stopped ? "restored" : "idle" }
229
+ if (items.length || value.stopped || hidden.size) sessions.set(sid, { items, activity, stopped: value.stopped, failed: false, hidden })
221
230
  }
222
231
  }
223
232
  } catch (error) {
224
233
  if (!record(error) || error.code !== "ENOENT") console.error("QueuePlugin failed to load queue storage", error)
225
234
  }
226
235
 
227
- const save = async (sid?: string, draft?: Durable) => {
228
- const stored: Store = { version: 1, projectID: project.id, sessions: {} }
236
+ const save = async (sid?: string, draft?: Draft) => {
237
+ const stored: Store = { version: 1, projectID: project.id, always: draft?.always ?? alwaysQueue, sessions: {} }
229
238
  for (const [id, current] of sessions) {
230
239
  if (deleted.has(id) || (id === sid && !draft)) continue
231
240
  const durable = id === sid ? draft! : current
@@ -258,7 +267,9 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
258
267
  return current
259
268
  }
260
269
 
261
- const store = async (sid: string, current: State, draft: Durable, placeholder?: Placeholder) => {
270
+ const automaticallyQueue = (sid: string) => alwaysQueue && shouldQueue(sessions.get(sid))
271
+
272
+ const store = async (sid: string, current: State, draft: Draft, placeholder?: Placeholder) => {
262
273
  try {
263
274
  await save(sid, draft)
264
275
  } catch (error) {
@@ -268,14 +279,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
268
279
  current.items.splice(0, current.items.length, ...draft.items)
269
280
  current.stopped = draft.stopped
270
281
  current.hidden = draft.hidden
282
+ alwaysQueue = draft.always
271
283
  if (placeholder) Object.assign(placeholder.part, { text: "", synthetic: true, ignored: true })
272
284
  }
273
285
 
274
- const persist = <T>(sid: string, placeholder: Placeholder | undefined, mutate: (draft: Durable) => T) =>
286
+ const persist = <T>(sid: string, placeholder: Placeholder | undefined, mutate: (draft: Draft) => T) =>
275
287
  serialize(async () => {
276
288
  if (deleted.has(sid)) throw new Error(`QueuePlugin cannot persist queue state for deleted session ${sid}`)
277
289
  const current = state(sid)
278
- const draft: Durable = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden) }
290
+ const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden), always: alwaysQueue }
279
291
  if (placeholder) draft.hidden.add(placeholder.id)
280
292
  const value = mutate(draft)
281
293
  await store(sid, current, draft, placeholder)
@@ -365,6 +377,21 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
365
377
  throwOnError: true,
366
378
  })
367
379
 
380
+ const markInternal = (parts: { type: string; metadata?: Record<string, unknown> }[]) => {
381
+ const text = parts.find((part) => part.type === "text")
382
+ if (text) text.metadata = { ...text.metadata, [INTERNAL]: origin }
383
+ }
384
+
385
+ const callCommand = async <T>(sid: string, command: string, args: string, call: () => Promise<T>) => {
386
+ const pending = { sid, command, args, used: false }
387
+ internalCommands.push(pending)
388
+ try {
389
+ return await call()
390
+ } finally {
391
+ internalCommands.splice(internalCommands.indexOf(pending), 1)
392
+ }
393
+ }
394
+
368
395
  const replay = async (sid: string, item: Item) => {
369
396
  switch (item.kind) {
370
397
  case "shell":
@@ -372,19 +399,22 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
372
399
  case "compact":
373
400
  return compact(sid, item.info)
374
401
  case "command":
375
- return client.session.command({
376
- path: { id: sid },
377
- body: {
378
- ...opts(item.info),
379
- model: `${item.info.model.providerID}/${item.info.model.modelID}`,
380
- command: item.cmd,
381
- arguments: item.args,
382
- parts: item.files,
383
- } as any,
384
- throwOnError: true,
385
- })
402
+ return callCommand(sid, item.cmd, item.args, () =>
403
+ client.session.command({
404
+ path: { id: sid },
405
+ body: {
406
+ ...opts(item.info),
407
+ model: `${item.info.model.providerID}/${item.info.model.modelID}`,
408
+ command: item.cmd,
409
+ arguments: item.args,
410
+ parts: item.files,
411
+ } as any,
412
+ throwOnError: true,
413
+ }),
414
+ )
386
415
  case "prompt": {
387
416
  const parts = item.parts.map((part) => ({ ...part, id: undefined }))
417
+ markInternal(parts)
388
418
  return client.session.prompt({ path: { id: sid }, body: { ...opts(item.info), parts } as any, throwOnError: true })
389
419
  }
390
420
  }
@@ -421,7 +451,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
421
451
 
422
452
  const items = current.items.slice(0, count)
423
453
  if (placeholder) {
424
- const draft: Durable = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden).add(placeholder.id) }
454
+ const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden).add(placeholder.id), always: alwaysQueue }
425
455
  await store(sid, current, draft, placeholder)
426
456
  if (deleted.has(sid)) return undefined
427
457
  }
@@ -503,6 +533,9 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
503
533
  case "start":
504
534
  draft.stopped = false
505
535
  return "Queue started"
536
+ case "always":
537
+ if (op.enabled !== undefined) draft.always = op.enabled
538
+ return `Always queue is ${draft.always ? "on" : "off"} for this project`
506
539
  }
507
540
  })
508
541
  if (op.kind === "start") {
@@ -515,7 +548,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
515
548
  const hooks: Awaited<ReturnType<Plugin>> = {
516
549
  config: async (cfg) => {
517
550
  cfg.command ??= {}
518
- cfg.command.queue = { template: "", description: "Queue input until the session is idle" }
551
+ cfg.command.q = cfg.command.queue = { template: "", description: "Queue input until the session is idle" }
519
552
  },
520
553
  event: async ({ event }) => {
521
554
  if (plan(event)) {
@@ -577,8 +610,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
577
610
  const body = input.arguments ?? ""
578
611
  const parts = files(output.parts)
579
612
 
580
- if (input.command !== "queue") {
581
- const queued = parseSuffix(body)
613
+ const internal = internalCommands.find((pending) => !pending.used && pending.sid === sid && pending.command === input.command && pending.args === body)
614
+ if (internal) {
615
+ internal.used = true
616
+ markInternal(output.parts)
617
+ return
618
+ }
619
+
620
+ if (!isQueue(input.command)) {
621
+ const queued = parseSuffix(body) ?? (automaticallyQueue(sid) ? { body } : undefined)
582
622
  if (!queued) return
583
623
 
584
624
  if (!shouldQueue(sessions.get(sid))) {
@@ -586,16 +626,17 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
586
626
  return
587
627
  }
588
628
 
589
- output.parts.splice(0, output.parts.length, { type: "text", text: `/queue${queued.front ? " front" : ""} /${input.command}${queued.body.trim() ? ` ${queued.body.trim()}` : ""}` } as any, ...parts)
629
+ output.parts.splice(0, output.parts.length, { type: "text", text: `/queue${queued.modifier === "front" ? " front" : ""} /${input.command}${queued.body.trim() ? ` ${queued.body.trim()}` : ""}` } as any, ...parts)
590
630
  return
591
631
  }
592
632
 
593
- const op = parse(parsePrefix(body), parts.length)
633
+ const request = parsePrefix(body)
634
+ const op = parse(request, parts.length)
594
635
 
595
636
  if (control(op)) return stop(await afterEnqueue(sid, () => manage(sid, op)))
596
637
  if (op.kind === "invalid") return stop(op.message, "error")
597
638
 
598
- if (!shouldQueue(sessions.get(sid))) {
639
+ if (!shouldQueue(sessions.get(sid)) || (request.modifier === "now" && op.kind !== "prompt" && op.kind !== "shell")) {
599
640
  if (op.kind === "shell") {
600
641
  await shell(sid, op.shell, await run(sid))
601
642
  return handled()
@@ -607,7 +648,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
607
648
  }
608
649
 
609
650
  if (op.kind === "command") {
610
- await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
651
+ await callCommand(sid, op.cmd, op.args, () => client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any }))
611
652
  return handled()
612
653
  }
613
654
 
@@ -623,10 +664,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
623
664
  console.warn("QueuePlugin ignored input for a deleted session", sid)
624
665
  return
625
666
  }
667
+ const internal = output.parts.find((part): part is TextPart => part.type === "text" && part.metadata?.[INTERNAL] === origin)
668
+ if (internal) {
669
+ delete internal.metadata![INTERNAL]
670
+ return
671
+ }
626
672
  const text = output.parts.find((part): part is TextPart => part.type === "text" && !part.synthetic)
627
673
  if (!text) return
628
674
 
629
- const request = parseInput(text.text)
675
+ const request = parseInput(text.text) ?? (automaticallyQueue(sid) ? { body: text.text } : undefined)
630
676
  if (!request) return
631
677
 
632
678
  const current = state(sid)
@@ -646,7 +692,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
646
692
  return
647
693
  }
648
694
 
649
- if (!shouldQueue(current)) {
695
+ if ((request.modifier === "now" && op.kind !== "shell") || !shouldQueue(current)) {
650
696
  if (op.kind === "command") return
651
697
  if (op.kind === "compact") {
652
698
  await persist(sid, placeholder, () => undefined)
@@ -708,7 +754,6 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
708
754
  },
709
755
  }
710
756
 
711
- for (const [sid, current] of sessions) if (current.items.length && !current.stopped) setTimeout(() => advance(sid), 0)
712
757
  return hooks
713
758
  }
714
759
 
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.12.1",
4
+ "version": "0.13.0",
5
5
  "type": "module",
6
6
  "description": "Queue OpenCode prompts and slash commands until the agent is idle",
7
7
  "main": "./index.ts",