opencode-queue 0.9.1 → 0.10.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.
- package/README.md +6 -0
- package/index.ts +73 -38
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -33,6 +33,9 @@ do this next /queue front
|
|
|
33
33
|
/queue front /review
|
|
34
34
|
/review /queue front
|
|
35
35
|
|
|
36
|
+
/queue /compact
|
|
37
|
+
/queue front /compact
|
|
38
|
+
|
|
36
39
|
/queue !ls
|
|
37
40
|
/queue front !pwd
|
|
38
41
|
|
|
@@ -57,6 +60,8 @@ do this next /queue front
|
|
|
57
60
|
| `/review /queue` | Queue a slash command using trailing syntax. |
|
|
58
61
|
| `/queue front /review` | Queue a slash command before existing queued entries. |
|
|
59
62
|
| `/review /queue front` | Queue a slash command before existing queued entries using trailing syntax. |
|
|
63
|
+
| `/queue /compact` | Queue OpenCode's built-in TUI `/compact` command. |
|
|
64
|
+
| `/queue front /compact` | Queue OpenCode's built-in TUI `/compact` command before existing queued entries. |
|
|
60
65
|
| `/queue !ls` | Queue an OpenCode shell block. |
|
|
61
66
|
| `/queue front !ls` | Queue an OpenCode shell block before existing queued entries. |
|
|
62
67
|
| `/queue` | Show the current queue. |
|
|
@@ -87,6 +92,7 @@ When the session is idle:
|
|
|
87
92
|
- `message /queue` sends `message` immediately.
|
|
88
93
|
- `/queue /review` runs `/review` immediately.
|
|
89
94
|
- `/review /queue` runs `/review` immediately.
|
|
95
|
+
- `/queue /compact` runs OpenCode's built-in TUI `/compact` command immediately.
|
|
90
96
|
- `/queue !ls` runs `ls` immediately as an OpenCode shell block.
|
|
91
97
|
- `/queue` and `/queue list` show the current queue.
|
|
92
98
|
- `/queue stop` pauses automatic replay, and `/queue start` resumes it.
|
package/index.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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
|
|
9
|
+
const TUI_COMPACT = "session_compact"
|
|
9
10
|
|
|
10
11
|
type InputPart = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
|
|
11
12
|
type Model = { providerID: string; modelID: string }
|
|
@@ -20,11 +21,13 @@ type QueueInput = { body: string; front: boolean }
|
|
|
20
21
|
type Item =
|
|
21
22
|
| { kind: "prompt"; info: Info; label: string; body: string; parts: InputPart[] }
|
|
22
23
|
| { kind: "command"; info: Info; source: string; cmd: string; args: string; files: FilePartInput[] }
|
|
24
|
+
| { kind: "compact"; info: Info; source: string }
|
|
23
25
|
| { kind: "shell"; info: Info; source: string; shell: string }
|
|
24
26
|
|
|
25
27
|
type EntryOp =
|
|
26
28
|
| { kind: "prompt"; label: string; body: string }
|
|
27
29
|
| { kind: "command"; source: string; cmd: string; args: string }
|
|
30
|
+
| { kind: "compact"; source: string }
|
|
28
31
|
| { kind: "shell"; source: string; shell: string }
|
|
29
32
|
|
|
30
33
|
type Activity = { kind: "idle" } | { kind: "busy" } | { kind: "sending"; idle: boolean }
|
|
@@ -85,7 +88,16 @@ const parse = (input: QueueInput, files = 0): Op => {
|
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
const match = text.match(CMD)
|
|
88
|
-
if (match)
|
|
91
|
+
if (match) {
|
|
92
|
+
const cmd = match[1]
|
|
93
|
+
const args = match[2] ?? ""
|
|
94
|
+
if (cmd === "compact") {
|
|
95
|
+
if (args.trim()) return { kind: "invalid", message: "Queue compact does not accept arguments" }
|
|
96
|
+
if (files) return { kind: "invalid", message: "Queue compact does not support attachments" }
|
|
97
|
+
return { kind: "compact", source: text, front }
|
|
98
|
+
}
|
|
99
|
+
return { kind: "command", source: text, cmd, args, front }
|
|
100
|
+
}
|
|
89
101
|
return { kind: "prompt", label: brief(input.body, files), body: input.body, front }
|
|
90
102
|
}
|
|
91
103
|
|
|
@@ -114,6 +126,14 @@ const control = (op: Op): op is ControlOp => {
|
|
|
114
126
|
return false
|
|
115
127
|
}
|
|
116
128
|
}
|
|
129
|
+
const shouldQueue = (state?: State) => Boolean(state && (state.activity.kind !== "idle" || state.stopped))
|
|
130
|
+
const shouldDeclinePlan = (state?: State) => Boolean(state && (state.activity.kind === "sending" || (!state.stopped && state.items.length)))
|
|
131
|
+
const itemText = (item: Item) => (item.kind === "prompt" ? item.body.trim() || item.label : item.source)
|
|
132
|
+
// OpenCode's command hook has no cancel/noReply output. Throwing a raw Effect
|
|
133
|
+
// response is handled by OpenCode's HTTP layer as an empty successful command.
|
|
134
|
+
const handled = (): never => {
|
|
135
|
+
throw HttpServerResponse.empty({ status: 204 })
|
|
136
|
+
}
|
|
117
137
|
const plan = (event: unknown): event is Ask => {
|
|
118
138
|
if (typeof event !== "object" || !event || !("type" in event) || event.type !== "question.asked") return false
|
|
119
139
|
const question = (event as Ask).properties?.questions?.[0]
|
|
@@ -139,7 +159,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
139
159
|
|
|
140
160
|
const stop = async (message: string, variant: "info" | "error" = "info", duration = 5000): Promise<never> => {
|
|
141
161
|
await toast(message, variant, duration)
|
|
142
|
-
|
|
162
|
+
return handled()
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
const no = async (id: string) => {
|
|
@@ -204,30 +224,42 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
204
224
|
|
|
205
225
|
const opts = (info: Info) => ({ agent: info.agent, model: info.model, variant: info.variant, controls: info.controls, fast: info.fast })
|
|
206
226
|
|
|
207
|
-
const prompt = (sid: string, info: Info, parts: InputPart[], noReply?: boolean) => client.session.prompt({ path: { id: sid }, body: { ...opts(info), noReply, parts } as any })
|
|
208
227
|
const shell = (sid: string, command: string, info: Run) => client.session.shell({ path: { id: sid }, body: { agent: info.agent, model: info.model, command } })
|
|
228
|
+
// TUI command events target the focused session; queued replay must target the original session.
|
|
229
|
+
const compact = (sid: string, info: Info) =>
|
|
230
|
+
client.session.summarize({
|
|
231
|
+
path: { id: sid },
|
|
232
|
+
body: { providerID: info.model.providerID, modelID: info.model.modelID },
|
|
233
|
+
throwOnError: true,
|
|
234
|
+
})
|
|
209
235
|
|
|
210
236
|
const replay = async (sid: string, item: Item) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
switch (item.kind) {
|
|
238
|
+
case "shell":
|
|
239
|
+
return shell(sid, item.shell, item.info)
|
|
240
|
+
case "compact":
|
|
241
|
+
return compact(sid, item.info)
|
|
242
|
+
case "command":
|
|
243
|
+
return client.session.command({
|
|
244
|
+
path: { id: sid },
|
|
245
|
+
body: {
|
|
246
|
+
...opts(item.info),
|
|
247
|
+
model: `${item.info.model.providerID}/${item.info.model.modelID}`,
|
|
248
|
+
command: item.cmd,
|
|
249
|
+
arguments: item.args,
|
|
250
|
+
parts: item.files,
|
|
251
|
+
} as any,
|
|
252
|
+
})
|
|
253
|
+
case "prompt": {
|
|
254
|
+
if (!item.parts.length) {
|
|
255
|
+
console.warn("QueuePlugin skipped queued item without replayable content")
|
|
256
|
+
return
|
|
257
|
+
}
|
|
226
258
|
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
const parts = item.parts.map((part) => ({ ...part, id: undefined }))
|
|
260
|
+
return client.session.prompt({ path: { id: sid }, body: { ...opts(item.info), parts } as any })
|
|
261
|
+
}
|
|
229
262
|
}
|
|
230
|
-
console.warn("QueuePlugin skipped queued item without replayable content")
|
|
231
263
|
}
|
|
232
264
|
|
|
233
265
|
const advance = (sid: string) => {
|
|
@@ -289,10 +321,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
289
321
|
|
|
290
322
|
switch (op.kind) {
|
|
291
323
|
case "list": {
|
|
292
|
-
const list =
|
|
293
|
-
current.items
|
|
294
|
-
.map((item, i) => `${i + 1}. ${item.kind === "prompt" ? (item.body.trim() ? item.body : item.label) : item.source}`)
|
|
295
|
-
.join("\n") || "Queue is empty"
|
|
324
|
+
const list = current.items.map((item, i) => `${i + 1}. ${itemText(item)}`).join("\n") || "Queue is empty"
|
|
296
325
|
return current.stopped ? `${list}\nQueue is stopped` : list
|
|
297
326
|
}
|
|
298
327
|
case "clear":
|
|
@@ -323,8 +352,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
323
352
|
event: async ({ event }) => {
|
|
324
353
|
if (plan(event)) {
|
|
325
354
|
const sid = event.properties.sessionID
|
|
326
|
-
|
|
327
|
-
if (!current || (current.activity.kind !== "sending" && (current.stopped || !current.items.length))) return
|
|
355
|
+
if (!shouldDeclinePlan(sessions.get(sid))) return
|
|
328
356
|
await no(event.properties.id)
|
|
329
357
|
await toast("Declined plan approval to continue queued work", "info")
|
|
330
358
|
return
|
|
@@ -366,9 +394,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
366
394
|
const queued = parseSuffix(body)
|
|
367
395
|
if (!queued) return
|
|
368
396
|
|
|
369
|
-
|
|
370
|
-
const shouldQueue = Boolean(current && (current.activity.kind !== "idle" || current.stopped))
|
|
371
|
-
if (!shouldQueue) {
|
|
397
|
+
if (!shouldQueue(sessions.get(sid))) {
|
|
372
398
|
for (const part of output.parts) if (part.type === "text") part.text = stripSuffix(part.text)
|
|
373
399
|
return
|
|
374
400
|
}
|
|
@@ -377,22 +403,25 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
377
403
|
return
|
|
378
404
|
}
|
|
379
405
|
|
|
380
|
-
const current = sessions.get(sid)
|
|
381
|
-
const shouldQueue = Boolean(current && (current.activity.kind !== "idle" || current.stopped))
|
|
382
406
|
const op = parse(parsePrefix(body), parts.length)
|
|
383
407
|
|
|
384
408
|
if (control(op)) return stop(await manage(sid, op))
|
|
385
409
|
if (op.kind === "invalid") return stop(op.message, "error")
|
|
386
410
|
|
|
387
|
-
if (!shouldQueue) {
|
|
411
|
+
if (!shouldQueue(sessions.get(sid))) {
|
|
388
412
|
if (op.kind === "shell") {
|
|
389
413
|
await shell(sid, op.shell, await run(sid))
|
|
390
|
-
|
|
414
|
+
return handled()
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (op.kind === "compact") {
|
|
418
|
+
await client.tui.executeCommand({ body: { command: TUI_COMPACT }, throwOnError: true })
|
|
419
|
+
return handled()
|
|
391
420
|
}
|
|
392
421
|
|
|
393
422
|
if (op.kind === "command") {
|
|
394
423
|
await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
|
|
395
|
-
|
|
424
|
+
return handled()
|
|
396
425
|
}
|
|
397
426
|
|
|
398
427
|
output.parts.splice(0, output.parts.length, { type: "text", text: op.body } as any, ...parts)
|
|
@@ -412,6 +441,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
412
441
|
const current = state(sid)
|
|
413
442
|
const parts = files(output.parts)
|
|
414
443
|
const op = parse(request, parts.length)
|
|
444
|
+
const meta = input as Meta
|
|
415
445
|
|
|
416
446
|
if (control(op)) {
|
|
417
447
|
hide(output.message.id, text)
|
|
@@ -427,6 +457,11 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
427
457
|
|
|
428
458
|
if (current.activity.kind === "idle" && !current.stopped) {
|
|
429
459
|
if (op.kind === "command") return
|
|
460
|
+
if (op.kind === "compact") {
|
|
461
|
+
hide(output.message.id, text)
|
|
462
|
+
await compact(sid, { agent: output.message.agent, model: output.message.model, variant: meta.variant, controls: meta.controls, fast: meta.fast })
|
|
463
|
+
return
|
|
464
|
+
}
|
|
430
465
|
if (op.kind === "shell") {
|
|
431
466
|
hide(output.message.id, text)
|
|
432
467
|
await shell(sid, op.shell, { agent: output.message.agent, model: output.message.model })
|
|
@@ -436,13 +471,13 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
436
471
|
return
|
|
437
472
|
}
|
|
438
473
|
|
|
439
|
-
const meta = input as Meta
|
|
440
474
|
const info = { agent: output.message.agent, model: { ...output.message.model }, variant: meta.variant, controls: meta.controls, fast: meta.fast }
|
|
441
475
|
const prior = await latest(sid)
|
|
442
476
|
if (prior) Object.assign(output.message, opts(prior))
|
|
443
477
|
else console.warn("QueuePlugin could not neutralize queued placeholder metadata because the session has no previous message context")
|
|
444
478
|
let item: Item
|
|
445
479
|
if (op.kind === "shell") item = { kind: "shell", info, source: op.source, shell: op.shell }
|
|
480
|
+
else if (op.kind === "compact") item = { kind: "compact", info, source: op.source }
|
|
446
481
|
else if (op.kind === "command") item = { kind: "command", info, source: op.source, cmd: op.cmd, args: op.args, files: parts }
|
|
447
482
|
else {
|
|
448
483
|
item = {
|
|
@@ -462,7 +497,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
462
497
|
if (op.front) current.items.unshift(item)
|
|
463
498
|
else current.items.push(item)
|
|
464
499
|
hide(output.message.id, text)
|
|
465
|
-
await toast(`${op.front ? "Queued first" : "Queued"}: ${item
|
|
500
|
+
await toast(`${op.front ? "Queued first" : "Queued"}: ${itemText(item)}`, "info")
|
|
466
501
|
},
|
|
467
502
|
"experimental.chat.messages.transform": async (_, output) => {
|
|
468
503
|
output.messages = output.messages.filter((msg) => !hidden.has(msg.info.id))
|
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.10.1",
|
|
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",
|