opencode-queue 0.9.0 → 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 +101 -50
- 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,14 +21,17 @@ 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
|
-
type
|
|
33
|
+
type Activity = { kind: "idle" } | { kind: "busy" } | { kind: "sending"; idle: boolean }
|
|
34
|
+
type State = { items: Item[]; activity: Activity; stopped: boolean; failed: boolean }
|
|
31
35
|
|
|
32
36
|
type Op =
|
|
33
37
|
| { kind: "list" }
|
|
@@ -84,7 +88,16 @@ const parse = (input: QueueInput, files = 0): Op => {
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
const match = text.match(CMD)
|
|
87
|
-
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
|
+
}
|
|
88
101
|
return { kind: "prompt", label: brief(input.body, files), body: input.body, front }
|
|
89
102
|
}
|
|
90
103
|
|
|
@@ -113,6 +126,9 @@ const control = (op: Op): op is ControlOp => {
|
|
|
113
126
|
return false
|
|
114
127
|
}
|
|
115
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)
|
|
116
132
|
const plan = (event: unknown): event is Ask => {
|
|
117
133
|
if (typeof event !== "object" || !event || !("type" in event) || event.type !== "question.asked") return false
|
|
118
134
|
const question = (event as Ask).properties?.questions?.[0]
|
|
@@ -127,7 +143,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
127
143
|
const state = (sid: string) => {
|
|
128
144
|
let current = sessions.get(sid)
|
|
129
145
|
if (!current) {
|
|
130
|
-
current = { items: [],
|
|
146
|
+
current = { items: [], activity: { kind: "idle" }, stopped: false, failed: false }
|
|
131
147
|
sessions.set(sid, current)
|
|
132
148
|
}
|
|
133
149
|
return current
|
|
@@ -203,30 +219,68 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
203
219
|
|
|
204
220
|
const opts = (info: Info) => ({ agent: info.agent, model: info.model, variant: info.variant, controls: info.controls, fast: info.fast })
|
|
205
221
|
|
|
206
|
-
const prompt = (sid: string, info: Info, parts: InputPart[], noReply?: boolean) => client.session.prompt({ path: { id: sid }, body: { ...opts(info), noReply, parts } as any })
|
|
207
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
|
+
})
|
|
208
230
|
|
|
209
231
|
const replay = async (sid: string, item: Item) => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
253
|
+
|
|
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
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const advance = (sid: string) => {
|
|
261
|
+
const current = state(sid)
|
|
262
|
+
if (current.activity.kind !== "idle" || current.stopped || !current.items.length) return
|
|
263
|
+
void flush(sid, 1)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const settle = (sid: string, resume: boolean) => {
|
|
267
|
+
const current = state(sid)
|
|
268
|
+
current.activity = { kind: "idle" }
|
|
269
|
+
if (current.failed) {
|
|
270
|
+
current.failed = false
|
|
223
271
|
return
|
|
224
272
|
}
|
|
225
273
|
|
|
226
|
-
if (
|
|
227
|
-
|
|
274
|
+
if (resume) advance(sid)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const idle = (sid: string) => {
|
|
278
|
+
const current = state(sid)
|
|
279
|
+
if (current.activity.kind === "sending") {
|
|
280
|
+
current.activity.idle = true
|
|
281
|
+
return
|
|
228
282
|
}
|
|
229
|
-
|
|
283
|
+
if (current.activity.kind === "busy") settle(sid, true)
|
|
230
284
|
}
|
|
231
285
|
|
|
232
286
|
const flush = async (sid: string, count = Infinity) => {
|
|
@@ -234,11 +288,11 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
234
288
|
const items = current.items.splice(0, count)
|
|
235
289
|
if (!items.length) return { sent: 0, failed: 0 }
|
|
236
290
|
|
|
237
|
-
current.flushing = true
|
|
238
291
|
let failed = 0
|
|
239
292
|
const retry: Item[] = []
|
|
240
293
|
try {
|
|
241
294
|
for (const item of items) {
|
|
295
|
+
current.activity = { kind: "sending", idle: false }
|
|
242
296
|
try {
|
|
243
297
|
await replay(sid, item)
|
|
244
298
|
} catch (error) {
|
|
@@ -250,26 +304,19 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
250
304
|
}
|
|
251
305
|
} finally {
|
|
252
306
|
if (retry.length) current.items.unshift(...retry)
|
|
253
|
-
current.
|
|
307
|
+
const replayCompleted = current.activity.kind === "sending" && current.activity.idle
|
|
308
|
+
if (replayCompleted) settle(sid, count === 1 && failed === 0)
|
|
309
|
+
else current.activity = failed ? { kind: "idle" } : { kind: "busy" }
|
|
254
310
|
}
|
|
255
311
|
return { sent: items.length - failed, failed }
|
|
256
312
|
}
|
|
257
313
|
|
|
258
|
-
const advance = (sid: string) => {
|
|
259
|
-
const current = state(sid)
|
|
260
|
-
if (!current.items.length || current.busy || current.stopped || current.flushing) return
|
|
261
|
-
void flush(sid, 1)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
314
|
const manage = async (sid: string, op: ControlOp) => {
|
|
265
315
|
const current = state(sid)
|
|
266
316
|
|
|
267
317
|
switch (op.kind) {
|
|
268
318
|
case "list": {
|
|
269
|
-
const list =
|
|
270
|
-
current.items
|
|
271
|
-
.map((item, i) => `${i + 1}. ${item.kind === "prompt" ? (item.body.trim() ? item.body : item.label) : item.source}`)
|
|
272
|
-
.join("\n") || "Queue is empty"
|
|
319
|
+
const list = current.items.map((item, i) => `${i + 1}. ${itemText(item)}`).join("\n") || "Queue is empty"
|
|
273
320
|
return current.stopped ? `${list}\nQueue is stopped` : list
|
|
274
321
|
}
|
|
275
322
|
case "clear":
|
|
@@ -300,8 +347,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
300
347
|
event: async ({ event }) => {
|
|
301
348
|
if (plan(event)) {
|
|
302
349
|
const sid = event.properties.sessionID
|
|
303
|
-
|
|
304
|
-
if (!current || (!current.flushing && (current.stopped || !current.items.length))) return
|
|
350
|
+
if (!shouldDeclinePlan(sessions.get(sid))) return
|
|
305
351
|
await no(event.properties.id)
|
|
306
352
|
await toast("Declined plan approval to continue queued work", "info")
|
|
307
353
|
return
|
|
@@ -318,23 +364,21 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
318
364
|
}
|
|
319
365
|
|
|
320
366
|
if (event.type === "session.idle") {
|
|
321
|
-
|
|
322
|
-
current.busy = false
|
|
323
|
-
if (!current.failed) advance(event.properties.sessionID)
|
|
324
|
-
current.failed = false
|
|
367
|
+
idle(event.properties.sessionID)
|
|
325
368
|
return
|
|
326
369
|
}
|
|
327
370
|
|
|
328
371
|
if (event.type !== "session.status") return
|
|
329
372
|
|
|
330
|
-
const
|
|
373
|
+
const sid = event.properties.sessionID
|
|
374
|
+
const current = state(sid)
|
|
331
375
|
if (event.properties.status.type !== "idle") {
|
|
332
|
-
current.
|
|
376
|
+
if (current.activity.kind !== "sending") current.activity = { kind: "busy" }
|
|
333
377
|
current.failed = false
|
|
334
378
|
return
|
|
335
379
|
}
|
|
336
380
|
|
|
337
|
-
|
|
381
|
+
idle(sid)
|
|
338
382
|
},
|
|
339
383
|
"command.execute.before": async (input, output) => {
|
|
340
384
|
const sid = input.sessionID
|
|
@@ -345,9 +389,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
345
389
|
const queued = parseSuffix(body)
|
|
346
390
|
if (!queued) return
|
|
347
391
|
|
|
348
|
-
|
|
349
|
-
const shouldQueue = Boolean(current?.busy || current?.stopped)
|
|
350
|
-
if (!shouldQueue) {
|
|
392
|
+
if (!shouldQueue(sessions.get(sid))) {
|
|
351
393
|
for (const part of output.parts) if (part.type === "text") part.text = stripSuffix(part.text)
|
|
352
394
|
return
|
|
353
395
|
}
|
|
@@ -356,19 +398,22 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
356
398
|
return
|
|
357
399
|
}
|
|
358
400
|
|
|
359
|
-
const current = sessions.get(sid)
|
|
360
|
-
const shouldQueue = Boolean(current?.busy || current?.stopped)
|
|
361
401
|
const op = parse(parsePrefix(body), parts.length)
|
|
362
402
|
|
|
363
403
|
if (control(op)) return stop(await manage(sid, op))
|
|
364
404
|
if (op.kind === "invalid") return stop(op.message, "error")
|
|
365
405
|
|
|
366
|
-
if (!shouldQueue) {
|
|
406
|
+
if (!shouldQueue(sessions.get(sid))) {
|
|
367
407
|
if (op.kind === "shell") {
|
|
368
408
|
await shell(sid, op.shell, await run(sid))
|
|
369
409
|
throw new Error(HANDLED)
|
|
370
410
|
}
|
|
371
411
|
|
|
412
|
+
if (op.kind === "compact") {
|
|
413
|
+
await client.tui.executeCommand({ body: { command: TUI_COMPACT }, throwOnError: true })
|
|
414
|
+
throw new Error(HANDLED)
|
|
415
|
+
}
|
|
416
|
+
|
|
372
417
|
if (op.kind === "command") {
|
|
373
418
|
await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
|
|
374
419
|
throw new Error(HANDLED)
|
|
@@ -391,6 +436,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
391
436
|
const current = state(sid)
|
|
392
437
|
const parts = files(output.parts)
|
|
393
438
|
const op = parse(request, parts.length)
|
|
439
|
+
const meta = input as Meta
|
|
394
440
|
|
|
395
441
|
if (control(op)) {
|
|
396
442
|
hide(output.message.id, text)
|
|
@@ -404,8 +450,13 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
404
450
|
return
|
|
405
451
|
}
|
|
406
452
|
|
|
407
|
-
if (
|
|
453
|
+
if (current.activity.kind === "idle" && !current.stopped) {
|
|
408
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
|
+
}
|
|
409
460
|
if (op.kind === "shell") {
|
|
410
461
|
hide(output.message.id, text)
|
|
411
462
|
await shell(sid, op.shell, { agent: output.message.agent, model: output.message.model })
|
|
@@ -415,13 +466,13 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
415
466
|
return
|
|
416
467
|
}
|
|
417
468
|
|
|
418
|
-
const meta = input as Meta
|
|
419
469
|
const info = { agent: output.message.agent, model: { ...output.message.model }, variant: meta.variant, controls: meta.controls, fast: meta.fast }
|
|
420
470
|
const prior = await latest(sid)
|
|
421
471
|
if (prior) Object.assign(output.message, opts(prior))
|
|
422
472
|
else console.warn("QueuePlugin could not neutralize queued placeholder metadata because the session has no previous message context")
|
|
423
473
|
let item: Item
|
|
424
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 }
|
|
425
476
|
else if (op.kind === "command") item = { kind: "command", info, source: op.source, cmd: op.cmd, args: op.args, files: parts }
|
|
426
477
|
else {
|
|
427
478
|
item = {
|
|
@@ -441,7 +492,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
441
492
|
if (op.front) current.items.unshift(item)
|
|
442
493
|
else current.items.push(item)
|
|
443
494
|
hide(output.message.id, text)
|
|
444
|
-
await toast(`${op.front ? "Queued first" : "Queued"}: ${item
|
|
495
|
+
await toast(`${op.front ? "Queued first" : "Queued"}: ${itemText(item)}`, "info")
|
|
445
496
|
},
|
|
446
497
|
"experimental.chat.messages.transform": async (_, output) => {
|
|
447
498
|
output.messages = output.messages.filter((msg) => !hidden.has(msg.info.id))
|
package/package.json
CHANGED