opencode-queue 0.6.5 → 0.7.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/README.md +12 -1
- package/index.ts +78 -24
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -32,7 +32,10 @@ continue after this task /queue
|
|
|
32
32
|
/queue !ls
|
|
33
33
|
|
|
34
34
|
/queue list
|
|
35
|
+
/queue flush
|
|
35
36
|
/queue clear
|
|
37
|
+
/queue clear 1
|
|
38
|
+
/queue clear 2 3
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
## Syntax
|
|
@@ -46,7 +49,10 @@ continue after this task /queue
|
|
|
46
49
|
| `/queue !ls` | Queue an OpenCode shell block. |
|
|
47
50
|
| `/queue` | Show the current queue. |
|
|
48
51
|
| `/queue list` | Show the current queue. |
|
|
52
|
+
| `/queue flush` | Send all queued entries immediately. |
|
|
49
53
|
| `/queue clear` | Clear the current queue. |
|
|
54
|
+
| `/queue clear 1` | Clear item 1 from the current queue. |
|
|
55
|
+
| `/queue clear 2 3` | Clear items 2 and 3 from the current queue. |
|
|
50
56
|
|
|
51
57
|
## Behavior
|
|
52
58
|
|
|
@@ -56,6 +62,7 @@ When the session is busy:
|
|
|
56
62
|
- The current agent run keeps using its original agent, model, and thinking variant.
|
|
57
63
|
- Queued entries replay in order after the session becomes idle.
|
|
58
64
|
- Only one queued entry is sent per idle transition, so queued work runs one item at a time.
|
|
65
|
+
- `/queue flush` sends all queued entries immediately, even before the session is idle.
|
|
59
66
|
|
|
60
67
|
When the session is idle:
|
|
61
68
|
|
|
@@ -65,14 +72,18 @@ When the session is idle:
|
|
|
65
72
|
- `/review /queue` runs `/review` immediately.
|
|
66
73
|
- `/queue !ls` runs `ls` immediately as an OpenCode shell block.
|
|
67
74
|
- `/queue` and `/queue list` show the current queue.
|
|
68
|
-
- `/queue
|
|
75
|
+
- `/queue flush` sends all queued entries immediately.
|
|
76
|
+
- `/queue clear` clears the current queue, and `/queue clear 1` clears a specific queued item.
|
|
69
77
|
|
|
70
78
|
## Queue Management
|
|
71
79
|
|
|
72
80
|
```text
|
|
73
81
|
/queue
|
|
74
82
|
/queue list
|
|
83
|
+
/queue flush
|
|
75
84
|
/queue clear
|
|
85
|
+
/queue clear 1
|
|
86
|
+
/queue clear 2 3
|
|
76
87
|
```
|
|
77
88
|
|
|
78
89
|
The queue is in-memory and scoped to the current session.
|
package/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AgentPartInput, FilePart, FilePartInput, SubtaskPartInput, TextPar
|
|
|
4
4
|
const QUEUE = /^\/queue(?:\s+([\s\S]*))?$/
|
|
5
5
|
const SUFFIX = /^([\s\S]*?)\s+\/queue\s*$/
|
|
6
6
|
const CMD = /^\/(\S+)(?:\s+([\s\S]*))?$/
|
|
7
|
+
const ITEM_NUMBER = /^[1-9]\d*$/
|
|
7
8
|
const HANDLED = "__QUEUE_HANDLED__"
|
|
8
9
|
|
|
9
10
|
type InputPart = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
|
|
@@ -22,12 +23,15 @@ type Item =
|
|
|
22
23
|
|
|
23
24
|
type Op =
|
|
24
25
|
| { kind: "list" }
|
|
25
|
-
| { kind: "clear" }
|
|
26
|
+
| { kind: "clear"; indices: number[] }
|
|
27
|
+
| { kind: "flush" }
|
|
26
28
|
| { kind: "invalid"; text: string }
|
|
27
29
|
| { kind: "prompt"; text: string; body: string }
|
|
28
30
|
| { kind: "command"; text: string; cmd: string; args: string }
|
|
29
31
|
| { kind: "shell"; text: string; shell: string }
|
|
30
32
|
|
|
33
|
+
type ControlOp = Extract<Op, { kind: "list" | "clear" | "flush" }>
|
|
34
|
+
|
|
31
35
|
const label = (body: string, files: number) => {
|
|
32
36
|
const text = body.trim() || `${files} attachment${files === 1 ? "" : "s"}`
|
|
33
37
|
return text.length > 72 ? `${text.slice(0, 69)}...` : text
|
|
@@ -37,7 +41,14 @@ const parse = (body: string, files = 0): Op => {
|
|
|
37
41
|
const text = body.trim()
|
|
38
42
|
if (!files) {
|
|
39
43
|
if (!text || text === "list") return { kind: "list" }
|
|
40
|
-
if (text === "
|
|
44
|
+
if (text === "flush") return { kind: "flush" }
|
|
45
|
+
const clear = text.match(/^clear(?:\s+([\s\S]+))?$/)
|
|
46
|
+
if (clear) {
|
|
47
|
+
const values = clear[1]?.trim().split(/\s+/) ?? []
|
|
48
|
+
const indices = values.map(Number)
|
|
49
|
+
if (values.some((value) => !ITEM_NUMBER.test(value)) || indices.some((index) => !Number.isSafeInteger(index))) return { kind: "invalid", text: "Queue clear expects one or more positive item numbers" }
|
|
50
|
+
return { kind: "clear", indices }
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
if (text.startsWith("!")) {
|
|
@@ -55,6 +66,7 @@ const parse = (body: string, files = 0): Op => {
|
|
|
55
66
|
const trailing = (text: string) => (text.trim() === "/queue" ? "" : text.match(SUFFIX)?.[1])
|
|
56
67
|
const strip = (text: string) => trailing(text) ?? text
|
|
57
68
|
const queued = (text: string) => text.match(QUEUE)?.[1] ?? trailing(text)
|
|
69
|
+
const control = (op: Op): op is ControlOp => op.kind === "list" || op.kind === "clear" || op.kind === "flush"
|
|
58
70
|
const plan = (event: unknown): event is Ask => {
|
|
59
71
|
if (typeof event !== "object" || !event || !("type" in event) || event.type !== "question.asked") return false
|
|
60
72
|
const question = (event as Ask).properties?.questions?.[0]
|
|
@@ -96,12 +108,36 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
96
108
|
|
|
97
109
|
const files = (parts: { type: string }[]) => parts.filter((part): part is FilePart => part.type === "file").map((part) => ({ ...part }))
|
|
98
110
|
|
|
99
|
-
const
|
|
100
|
-
|
|
111
|
+
const take = (sid: string, count = Infinity) => {
|
|
112
|
+
const list = queue.get(sid)
|
|
113
|
+
if (!list?.length) return []
|
|
114
|
+
|
|
115
|
+
const items = list.splice(0, count)
|
|
116
|
+
if (!list.length) queue.delete(sid)
|
|
117
|
+
return items
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const requeue = (sid: string, items: Item[]) => {
|
|
121
|
+
if (items.length) queue.set(sid, [...items, ...(queue.get(sid) ?? [])])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const clear = (sid: string, indices: number[]) => {
|
|
125
|
+
const list = queue.get(sid)
|
|
126
|
+
if (!list?.length) return "Queue is empty"
|
|
127
|
+
|
|
128
|
+
if (!indices.length) {
|
|
129
|
+
const count = list.length
|
|
130
|
+
queue.delete(sid)
|
|
131
|
+
return `Cleared ${count} queued item${count === 1 ? "" : "s"}`
|
|
132
|
+
}
|
|
101
133
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
const targets = [...new Set(indices)].sort((a, b) => a - b)
|
|
135
|
+
const missing = targets.filter((index) => index > list.length)
|
|
136
|
+
if (missing.length) return `Queue item${missing.length === 1 ? "" : "s"} ${missing.join(", ")} ${missing.length === 1 ? "does" : "do"} not exist`
|
|
137
|
+
|
|
138
|
+
for (const index of targets.toReversed()) list.splice(index - 1, 1)
|
|
139
|
+
if (!list.length) queue.delete(sid)
|
|
140
|
+
return `Cleared queued item${targets.length === 1 ? "" : "s"} ${targets.join(", ")}`
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
const latest = async (sid: string): Promise<Info | undefined> => {
|
|
@@ -135,7 +171,6 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
135
171
|
if (item.kind === "shell") return shell(sid, item.shell, item.info)
|
|
136
172
|
|
|
137
173
|
if (item.kind === "command") {
|
|
138
|
-
await prompt(sid, item.info, [{ type: "text", text: item.text }, ...item.files], true)
|
|
139
174
|
await client.session.command({
|
|
140
175
|
path: { id: sid },
|
|
141
176
|
body: {
|
|
@@ -155,20 +190,40 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
155
190
|
console.warn("QueuePlugin skipped queued item without replayable content")
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
const flush = (sid: string) => {
|
|
159
|
-
const
|
|
160
|
-
if (!
|
|
161
|
-
const item = list.shift()
|
|
162
|
-
if (!item) return
|
|
163
|
-
if (!list.length) queue.delete(sid)
|
|
193
|
+
const flush = async (sid: string, count = Infinity) => {
|
|
194
|
+
const items = take(sid, count)
|
|
195
|
+
if (!items.length) return { sent: 0, failed: 0 }
|
|
164
196
|
|
|
165
197
|
active.add(sid)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
let failed = 0
|
|
199
|
+
const retry: Item[] = []
|
|
200
|
+
try {
|
|
201
|
+
for (const item of items) {
|
|
202
|
+
try {
|
|
203
|
+
await replay(sid, item)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
failed++
|
|
206
|
+
retry.push(item)
|
|
207
|
+
console.error("QueuePlugin failed to flush queued input", error)
|
|
208
|
+
await toast(`Queue failed: ${error instanceof Error ? error.message : String(error)}`, "error")
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
requeue(sid, retry)
|
|
170
213
|
active.delete(sid)
|
|
171
|
-
}
|
|
214
|
+
}
|
|
215
|
+
return { sent: items.length - failed, failed }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const manage = async (sid: string, op: ControlOp) => {
|
|
219
|
+
if (op.kind === "list") return (queue.get(sid) ?? []).map((item, i) => `${i + 1}. ${item.text}`).join("\n") || "Queue is empty"
|
|
220
|
+
if (op.kind === "clear") return clear(sid, op.indices)
|
|
221
|
+
|
|
222
|
+
const result = await flush(sid)
|
|
223
|
+
if (!result.sent && !result.failed) return "Queue is empty"
|
|
224
|
+
|
|
225
|
+
const message = `Flushed ${result.sent} queued item${result.sent === 1 ? "" : "s"}`
|
|
226
|
+
return result.failed ? `${message}; ${result.failed} failed` : message
|
|
172
227
|
}
|
|
173
228
|
|
|
174
229
|
return {
|
|
@@ -194,7 +249,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
194
249
|
}
|
|
195
250
|
|
|
196
251
|
busy.delete(sid)
|
|
197
|
-
flush(sid)
|
|
252
|
+
void flush(sid, 1)
|
|
198
253
|
},
|
|
199
254
|
"command.execute.before": async (input, output) => {
|
|
200
255
|
const sid = input.sessionID
|
|
@@ -216,7 +271,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
216
271
|
|
|
217
272
|
const op = parse(body, parts.length)
|
|
218
273
|
|
|
219
|
-
if (op
|
|
274
|
+
if (control(op)) return stop(await manage(sid, op))
|
|
220
275
|
if (op.kind === "invalid") return stop(op.text, "error")
|
|
221
276
|
|
|
222
277
|
if (!busy.has(sid)) {
|
|
@@ -226,7 +281,6 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
226
281
|
}
|
|
227
282
|
|
|
228
283
|
if (op.kind === "command") {
|
|
229
|
-
await client.session.prompt({ path: { id: sid }, body: { noReply: true, parts: [{ type: "text", text: op.text }, ...parts] } })
|
|
230
284
|
await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
|
|
231
285
|
throw new Error(HANDLED)
|
|
232
286
|
}
|
|
@@ -248,9 +302,9 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
248
302
|
const parts = files(output.parts)
|
|
249
303
|
const op = parse(body, parts.length)
|
|
250
304
|
|
|
251
|
-
if (op
|
|
305
|
+
if (control(op)) {
|
|
252
306
|
hide(output.message.id, text)
|
|
253
|
-
await toast(manage(sid, op), "info", 5000)
|
|
307
|
+
await toast(await manage(sid, op), "info", 5000)
|
|
254
308
|
return
|
|
255
309
|
}
|
|
256
310
|
|
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.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Queue OpenCode prompts and slash commands until the agent is idle",
|
|
7
7
|
"main": "./index.ts",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"exports": {
|
|
29
29
|
".": "./index.ts"
|
|
30
30
|
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
31
34
|
"publishConfig": {
|
|
32
35
|
"access": "public"
|
|
33
36
|
},
|
|
@@ -35,9 +38,9 @@
|
|
|
35
38
|
"typecheck": "tsc --noEmit"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@opencode-ai/plugin": "
|
|
39
|
-
"@opencode-ai/sdk": "
|
|
40
|
-
"typescript": "^
|
|
41
|
+
"@opencode-ai/plugin": "^1.14.37",
|
|
42
|
+
"@opencode-ai/sdk": "^1.14.37",
|
|
43
|
+
"typescript": "^6.0.3"
|
|
41
44
|
},
|
|
42
45
|
"overrides": {
|
|
43
46
|
"uuid": "^14.0.0"
|