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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/index.ts +64 -34
  3. 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) return { kind: "command", source: text, cmd: match[1], args: match[2] ?? "", front }
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
- if (item.kind === "shell") return shell(sid, item.shell, item.info)
212
-
213
- if (item.kind === "command") {
214
- await client.session.command({
215
- path: { id: sid },
216
- body: {
217
- ...opts(item.info),
218
- model: `${item.info.model.providerID}/${item.info.model.modelID}`,
219
- command: item.cmd,
220
- arguments: item.args,
221
- parts: item.files,
222
- } as any,
223
- })
224
- return
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
- if (item.parts.length) {
228
- return prompt(sid, item.info, item.parts.map((part) => ({ ...part, id: undefined })))
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
- const current = sessions.get(sid)
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
- const current = sessions.get(sid)
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.kind === "prompt" ? item.label : item.source}`, "info")
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "opencode-queue",
4
- "version": "0.9.1",
4
+ "version": "0.10.0",
5
5
  "type": "module",
6
6
  "description": "Queue OpenCode prompts and slash commands until the agent is idle",
7
7
  "main": "./index.ts",