opencode-queue 0.9.1 → 0.10.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 +6 -0
- package/index.ts +64 -34
- package/package.json +1 -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
|
@@ -6,6 +6,7 @@ const SUFFIX = /^([\s\S]*?)\s+\/queue(?:\s+(front))?\s*$/
|
|
|
6
6
|
const CMD = /^\/(\S+)(?:\s+([\s\S]*))?$/
|
|
7
7
|
const ITEM_NUMBER = /^[1-9]\d*$/
|
|
8
8
|
const HANDLED = "__QUEUE_HANDLED__"
|
|
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,9 @@ 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)
|
|
117
132
|
const plan = (event: unknown): event is Ask => {
|
|
118
133
|
if (typeof event !== "object" || !event || !("type" in event) || event.type !== "question.asked") return false
|
|
119
134
|
const question = (event as Ask).properties?.questions?.[0]
|
|
@@ -204,30 +219,42 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
204
219
|
|
|
205
220
|
const opts = (info: Info) => ({ agent: info.agent, model: info.model, variant: info.variant, controls: info.controls, fast: info.fast })
|
|
206
221
|
|
|
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
222
|
const shell = (sid: string, command: string, info: Run) => client.session.shell({ path: { id: sid }, body: { agent: info.agent, model: info.model, command } })
|
|
223
|
+
// TUI command events target the focused session; queued replay must target the original session.
|
|
224
|
+
const compact = (sid: string, info: Info) =>
|
|
225
|
+
client.session.summarize({
|
|
226
|
+
path: { id: sid },
|
|
227
|
+
body: { providerID: info.model.providerID, modelID: info.model.modelID },
|
|
228
|
+
throwOnError: true,
|
|
229
|
+
})
|
|
209
230
|
|
|
210
231
|
const replay = async (sid: string, item: Item) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
switch (item.kind) {
|
|
233
|
+
case "shell":
|
|
234
|
+
return shell(sid, item.shell, item.info)
|
|
235
|
+
case "compact":
|
|
236
|
+
return compact(sid, item.info)
|
|
237
|
+
case "command":
|
|
238
|
+
return client.session.command({
|
|
239
|
+
path: { id: sid },
|
|
240
|
+
body: {
|
|
241
|
+
...opts(item.info),
|
|
242
|
+
model: `${item.info.model.providerID}/${item.info.model.modelID}`,
|
|
243
|
+
command: item.cmd,
|
|
244
|
+
arguments: item.args,
|
|
245
|
+
parts: item.files,
|
|
246
|
+
} as any,
|
|
247
|
+
})
|
|
248
|
+
case "prompt": {
|
|
249
|
+
if (!item.parts.length) {
|
|
250
|
+
console.warn("QueuePlugin skipped queued item without replayable content")
|
|
251
|
+
return
|
|
252
|
+
}
|
|
226
253
|
|
|
227
|
-
|
|
228
|
-
|
|
254
|
+
const parts = item.parts.map((part) => ({ ...part, id: undefined }))
|
|
255
|
+
return client.session.prompt({ path: { id: sid }, body: { ...opts(item.info), parts } as any })
|
|
256
|
+
}
|
|
229
257
|
}
|
|
230
|
-
console.warn("QueuePlugin skipped queued item without replayable content")
|
|
231
258
|
}
|
|
232
259
|
|
|
233
260
|
const advance = (sid: string) => {
|
|
@@ -289,10 +316,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
289
316
|
|
|
290
317
|
switch (op.kind) {
|
|
291
318
|
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"
|
|
319
|
+
const list = current.items.map((item, i) => `${i + 1}. ${itemText(item)}`).join("\n") || "Queue is empty"
|
|
296
320
|
return current.stopped ? `${list}\nQueue is stopped` : list
|
|
297
321
|
}
|
|
298
322
|
case "clear":
|
|
@@ -323,8 +347,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
323
347
|
event: async ({ event }) => {
|
|
324
348
|
if (plan(event)) {
|
|
325
349
|
const sid = event.properties.sessionID
|
|
326
|
-
|
|
327
|
-
if (!current || (current.activity.kind !== "sending" && (current.stopped || !current.items.length))) return
|
|
350
|
+
if (!shouldDeclinePlan(sessions.get(sid))) return
|
|
328
351
|
await no(event.properties.id)
|
|
329
352
|
await toast("Declined plan approval to continue queued work", "info")
|
|
330
353
|
return
|
|
@@ -366,9 +389,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
366
389
|
const queued = parseSuffix(body)
|
|
367
390
|
if (!queued) return
|
|
368
391
|
|
|
369
|
-
|
|
370
|
-
const shouldQueue = Boolean(current && (current.activity.kind !== "idle" || current.stopped))
|
|
371
|
-
if (!shouldQueue) {
|
|
392
|
+
if (!shouldQueue(sessions.get(sid))) {
|
|
372
393
|
for (const part of output.parts) if (part.type === "text") part.text = stripSuffix(part.text)
|
|
373
394
|
return
|
|
374
395
|
}
|
|
@@ -377,19 +398,22 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
377
398
|
return
|
|
378
399
|
}
|
|
379
400
|
|
|
380
|
-
const current = sessions.get(sid)
|
|
381
|
-
const shouldQueue = Boolean(current && (current.activity.kind !== "idle" || current.stopped))
|
|
382
401
|
const op = parse(parsePrefix(body), parts.length)
|
|
383
402
|
|
|
384
403
|
if (control(op)) return stop(await manage(sid, op))
|
|
385
404
|
if (op.kind === "invalid") return stop(op.message, "error")
|
|
386
405
|
|
|
387
|
-
if (!shouldQueue) {
|
|
406
|
+
if (!shouldQueue(sessions.get(sid))) {
|
|
388
407
|
if (op.kind === "shell") {
|
|
389
408
|
await shell(sid, op.shell, await run(sid))
|
|
390
409
|
throw new Error(HANDLED)
|
|
391
410
|
}
|
|
392
411
|
|
|
412
|
+
if (op.kind === "compact") {
|
|
413
|
+
await client.tui.executeCommand({ body: { command: TUI_COMPACT }, throwOnError: true })
|
|
414
|
+
throw new Error(HANDLED)
|
|
415
|
+
}
|
|
416
|
+
|
|
393
417
|
if (op.kind === "command") {
|
|
394
418
|
await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
|
|
395
419
|
throw new Error(HANDLED)
|
|
@@ -412,6 +436,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
412
436
|
const current = state(sid)
|
|
413
437
|
const parts = files(output.parts)
|
|
414
438
|
const op = parse(request, parts.length)
|
|
439
|
+
const meta = input as Meta
|
|
415
440
|
|
|
416
441
|
if (control(op)) {
|
|
417
442
|
hide(output.message.id, text)
|
|
@@ -427,6 +452,11 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
427
452
|
|
|
428
453
|
if (current.activity.kind === "idle" && !current.stopped) {
|
|
429
454
|
if (op.kind === "command") return
|
|
455
|
+
if (op.kind === "compact") {
|
|
456
|
+
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 })
|
|
458
|
+
return
|
|
459
|
+
}
|
|
430
460
|
if (op.kind === "shell") {
|
|
431
461
|
hide(output.message.id, text)
|
|
432
462
|
await shell(sid, op.shell, { agent: output.message.agent, model: output.message.model })
|
|
@@ -436,13 +466,13 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
436
466
|
return
|
|
437
467
|
}
|
|
438
468
|
|
|
439
|
-
const meta = input as Meta
|
|
440
469
|
const info = { agent: output.message.agent, model: { ...output.message.model }, variant: meta.variant, controls: meta.controls, fast: meta.fast }
|
|
441
470
|
const prior = await latest(sid)
|
|
442
471
|
if (prior) Object.assign(output.message, opts(prior))
|
|
443
472
|
else console.warn("QueuePlugin could not neutralize queued placeholder metadata because the session has no previous message context")
|
|
444
473
|
let item: Item
|
|
445
474
|
if (op.kind === "shell") item = { kind: "shell", info, source: op.source, shell: op.shell }
|
|
475
|
+
else if (op.kind === "compact") item = { kind: "compact", info, source: op.source }
|
|
446
476
|
else if (op.kind === "command") item = { kind: "command", info, source: op.source, cmd: op.cmd, args: op.args, files: parts }
|
|
447
477
|
else {
|
|
448
478
|
item = {
|
|
@@ -462,7 +492,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
462
492
|
if (op.front) current.items.unshift(item)
|
|
463
493
|
else current.items.push(item)
|
|
464
494
|
hide(output.message.id, text)
|
|
465
|
-
await toast(`${op.front ? "Queued first" : "Queued"}: ${item
|
|
495
|
+
await toast(`${op.front ? "Queued first" : "Queued"}: ${itemText(item)}`, "info")
|
|
466
496
|
},
|
|
467
497
|
"experimental.chat.messages.transform": async (_, output) => {
|
|
468
498
|
output.messages = output.messages.filter((msg) => !hidden.has(msg.info.id))
|
package/package.json
CHANGED