opencode-queue 0.6.0 → 0.6.1

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 +5 -1
  2. package/index.ts +39 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,6 +9,7 @@ This plugin adds a real `/queue` slash command that keeps the current run focuse
9
9
  - Queues normal prompts entered while a session is busy
10
10
  - Queues prompts with either `/queue prompt` or `prompt /queue`
11
11
  - Queues slash commands with either `/queue /review` or `/review /queue`
12
+ - Queues shell commands with `/queue !ls`
12
13
  - Hides queued placeholders from both the UI transcript and the running agent
13
14
  - Preserves the selected agent, model, and thinking variant for queued input
14
15
  - Replays queued input in order once the session becomes idle
@@ -39,6 +40,7 @@ While the agent is busy:
39
40
  /queue continue after the current task finishes
40
41
  /queue /review
41
42
  /queue /commit
43
+ /queue !ls
42
44
  continue after the current task finishes /queue
43
45
  /review /queue
44
46
  /queue list
@@ -50,6 +52,7 @@ When the session is idle:
50
52
  ```text
51
53
  /queue hello
52
54
  /queue /review
55
+ /queue !date
53
56
  hello /queue
54
57
  /review /queue
55
58
  /queue
@@ -63,9 +66,10 @@ Queued items stay hidden while the current run is still working, then replay aut
63
66
  - Idle `/queue some text` is treated like a normal prompt with the `/queue` prefix removed.
64
67
  - Idle `some text /queue` and `/command /queue` run immediately with the trailing `/queue` removed.
65
68
  - Idle `/queue /command` immediately runs the nested command.
69
+ - Idle `/queue !command` immediately runs the shell command as an OpenCode shell block.
66
70
  - `/queue` and `/queue list` show the in-memory queue for the current session.
67
71
  - `/queue clear` drops all currently queued items for the current session.
68
- - OpenCode shell mode is not supported because it uses a separate shell execution path outside normal prompt and slash-command handling.
72
+ - Native shell-mode suffixes like `!command /queue` are not supported because OpenCode handles leading `!` before plugin command hooks run.
69
73
 
70
74
  ## License
71
75
 
package/index.ts CHANGED
@@ -9,18 +9,22 @@ const HANDLED = "__QUEUE_HANDLED__"
9
9
  type InputPart = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
10
10
  type Model = { providerID: string; modelID: string }
11
11
  type Meta = { variant?: string; controls?: string[]; fast?: boolean }
12
+ type Run = { agent: string; model?: Model }
12
13
  type Info = { agent: string; model: Model } & Meta
13
14
  type Msg = { info: { role: string; agent?: string; mode?: string; model?: Model; providerID?: string; modelID?: string } & Meta }
14
15
 
15
16
  type Item =
16
17
  | { kind: "prompt"; info: Info; text: string; parts: InputPart[] }
17
18
  | { kind: "command"; info: Info; text: string; cmd: string; args: string; files: FilePartInput[] }
19
+ | { kind: "shell"; info: Info; text: string; shell: string }
18
20
 
19
21
  type Op =
20
22
  | { kind: "list" }
21
23
  | { kind: "clear" }
24
+ | { kind: "invalid"; text: string }
22
25
  | { kind: "prompt"; text: string; body: string }
23
26
  | { kind: "command"; text: string; cmd: string; args: string }
27
+ | { kind: "shell"; text: string; shell: string }
24
28
 
25
29
  const label = (body: string, files: number) => {
26
30
  const text = body.trim() || `${files} attachment${files === 1 ? "" : "s"}`
@@ -34,6 +38,13 @@ const parse = (body: string, files = 0): Op => {
34
38
  if (text === "clear") return { kind: "clear" }
35
39
  }
36
40
 
41
+ if (text.startsWith("!")) {
42
+ const shell = text.slice(1).trim()
43
+ if (!shell) return { kind: "invalid", text: "Queue shell command is empty" }
44
+ if (files) return { kind: "invalid", text: "Queued shell commands do not support attachments" }
45
+ return { kind: "shell", text, shell }
46
+ }
47
+
37
48
  const match = text.match(CMD)
38
49
  if (match) return { kind: "command", text, cmd: match[1], args: match[2] ?? "" }
39
50
  return { kind: "prompt", text: label(body, files), body }
@@ -88,11 +99,21 @@ export const QueuePlugin: Plugin = async ({ client }) => {
88
99
  return undefined
89
100
  }
90
101
 
102
+ const run = async (sid: string): Promise<Run> => {
103
+ const info = await latest(sid)
104
+ if (info) return info
105
+ console.warn("QueuePlugin shell replay fell back to the build agent because the session has no message context")
106
+ return { agent: "build" }
107
+ }
108
+
91
109
  const opts = (info: Info) => ({ agent: info.agent, model: info.model, variant: info.variant, controls: info.controls, fast: info.fast })
92
110
 
93
111
  const prompt = (sid: string, info: Info, parts: InputPart[], noReply?: boolean) => client.session.prompt({ path: { id: sid }, body: { ...opts(info), noReply, parts } as any })
112
+ const shell = (sid: string, command: string, info: Run) => client.session.shell({ path: { id: sid }, body: { agent: info.agent, model: info.model, command } })
94
113
 
95
114
  const replay = async (sid: string, item: Item) => {
115
+ if (item.kind === "shell") return shell(sid, item.shell, item.info)
116
+
96
117
  if (item.kind === "command") {
97
118
  await prompt(sid, item.info, [{ type: "text", text: item.text }, ...item.files], true)
98
119
  await client.session.command({
@@ -170,8 +191,14 @@ export const QueuePlugin: Plugin = async ({ client }) => {
170
191
  const op = parse(body, parts.length)
171
192
 
172
193
  if (op.kind === "list" || op.kind === "clear") return stop(manage(sid, op))
194
+ if (op.kind === "invalid") return stop(op.text, "error")
173
195
 
174
196
  if (!busy.has(sid)) {
197
+ if (op.kind === "shell") {
198
+ await shell(sid, op.shell, await run(sid))
199
+ throw new Error(HANDLED)
200
+ }
201
+
175
202
  if (op.kind === "command") {
176
203
  await client.session.prompt({ path: { id: sid }, body: { noReply: true, parts: [{ type: "text", text: op.text }, ...parts] } })
177
204
  await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
@@ -203,8 +230,19 @@ export const QueuePlugin: Plugin = async ({ client }) => {
203
230
  return
204
231
  }
205
232
 
233
+ if (op.kind === "invalid") {
234
+ hide(output.message.id, text)
235
+ await toast(op.text, "error", 5000)
236
+ return
237
+ }
238
+
206
239
  if (!busy.has(sid)) {
207
240
  if (op.kind === "command") return
241
+ if (op.kind === "shell") {
242
+ hide(output.message.id, text)
243
+ await shell(sid, op.shell, { agent: output.message.agent, model: output.message.model })
244
+ return
245
+ }
208
246
  text.text = body
209
247
  return
210
248
  }
@@ -221,7 +259,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
221
259
  console.warn("QueuePlugin skipped unexpected part", part.type)
222
260
  return []
223
261
  })
224
- const item: Item = op.kind === "command" ? { ...op, info, files: parts } : { ...op, info, parts: inputParts() }
262
+ const item: Item = op.kind === "shell" ? { ...op, info } : op.kind === "command" ? { ...op, info, files: parts } : { ...op, info, parts: inputParts() }
225
263
 
226
264
  queue.set(sid, [...(queue.get(sid) ?? []), item])
227
265
  hide(output.message.id, text)
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.6.0",
4
+ "version": "0.6.1",
5
5
  "type": "module",
6
6
  "description": "Queue OpenCode prompts and slash commands until the agent is idle",
7
7
  "main": "./index.ts",