opencode-queue 0.1.0 → 0.3.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 +9 -3
  2. package/index.ts +82 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,9 +8,11 @@ This plugin adds a `/queue ...` prefix that keeps the current run focused instea
8
8
 
9
9
  - Queues normal prompts entered while a session is busy
10
10
  - Queues slash commands like `/queue /review` and `/queue /commit`
11
- - Keeps queued placeholder messages visible in the UI
12
- - Filters queued placeholders out of model input so they do not interrupt the current run
11
+ - Hides queued placeholders from both the UI transcript and the running agent
13
12
  - Replays queued input in order once the session becomes idle
13
+ - Replays queued commands as a visible `/command` message before executing them
14
+ - Shows the current queue with `/queue list`
15
+ - Clears the current queue with `/queue clear`
14
16
 
15
17
  ## Install
16
18
 
@@ -34,15 +36,19 @@ While the agent is busy:
34
36
  /queue continue after the current task finishes
35
37
  /queue /review
36
38
  /queue /commit
39
+ /queue list
40
+ /queue clear
37
41
  ```
38
42
 
39
- The queued message is shown as `[queued] ...` and is sent automatically when the current run becomes idle.
43
+ Queued items stay hidden while the current run is still working, then replay automatically when the session becomes idle.
40
44
 
41
45
  ## Notes
42
46
 
43
47
  - This is a `/queue` plugin, not a keyboard shortcut plugin. OpenCode plugins cannot currently register custom TUI keybindings.
44
48
  - Idle `/queue some text` is treated like a normal prompt with the `/queue` prefix removed.
45
49
  - Idle `/queue /command` is left alone and is not intercepted.
50
+ - `/queue list` shows the in-memory queue for the current session.
51
+ - `/queue clear` drops all currently queued items for the current session.
46
52
 
47
53
  ## License
48
54
 
package/index.ts CHANGED
@@ -13,7 +13,7 @@ const COMMAND = /^\/(\S+)(?:\s+([\s\S]*))?$/
13
13
 
14
14
  type InputPart = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
15
15
  type Info = { agent: string; model: { providerID: string; modelID: string } }
16
- type Item = { info: Info; command: string; arguments: string } | { info: Info; parts: InputPart[] }
16
+ type Item = { info: Info; text: string; parts?: InputPart[]; command?: string; arguments?: string }
17
17
 
18
18
  const label = (body: string, files: number) => {
19
19
  const text = body.trim() || `${files} attachment${files === 1 ? "" : "s"}`
@@ -25,14 +25,78 @@ const parseCommand = (body: string) => {
25
25
  return match ? { command: match[1], arguments: match[2] ?? "" } : undefined
26
26
  }
27
27
 
28
- const QueuePlugin: Plugin = async ({ client }) => {
28
+ export const QueuePlugin: Plugin = async ({ client }) => {
29
29
  const queue = new Map<string, Item[]>()
30
30
  const hidden = new Set<string>()
31
31
  const busy = new Set<string>()
32
32
  const flushing = new Set<string>()
33
33
 
34
- const toast = (message: string, variant: "info" | "error") =>
35
- client.tui.showToast({ body: { message, variant, duration: 2500 } }).catch(() => undefined)
34
+ const toast = (message: string, variant: "info" | "error", duration = 2500) =>
35
+ client.tui.showToast({ body: { message, variant, duration } }).catch(() => undefined)
36
+
37
+ const hide = (id: string, text: TextPart) => {
38
+ hidden.add(id)
39
+ text.text = ""
40
+ text.synthetic = true
41
+ text.ignored = true
42
+ }
43
+
44
+ const enqueue = (sid: string, item: Item) => {
45
+ const items = queue.get(sid)
46
+ if (items) items.push(item)
47
+ else queue.set(sid, [item])
48
+ }
49
+
50
+ const summary = (sid: string) => {
51
+ const items = queue.get(sid) ?? []
52
+ if (!items.length) return "Queue is empty"
53
+ return items.map((item, i) => `${i + 1}. ${item.text}`).join("\n")
54
+ }
55
+
56
+ const clear = (sid: string) => {
57
+ const count = queue.get(sid)?.length ?? 0
58
+ queue.delete(sid)
59
+ return count ? `Cleared ${count} queued item${count === 1 ? "" : "s"}` : "Queue is empty"
60
+ }
61
+
62
+ const replay = async (sid: string, item: Item) => {
63
+ if (!item.command || item.arguments === undefined) {
64
+ if (!item.parts?.length) {
65
+ console.warn("QueuePlugin skipped queued item without replayable content")
66
+ return
67
+ }
68
+
69
+ await client.session.prompt({
70
+ path: { id: sid },
71
+ body: {
72
+ agent: item.info.agent,
73
+ model: item.info.model,
74
+ parts: item.parts.map((part) => ({ ...part, id: undefined })),
75
+ },
76
+ })
77
+ return
78
+ }
79
+
80
+ await client.session.prompt({
81
+ path: { id: sid },
82
+ body: {
83
+ agent: item.info.agent,
84
+ model: item.info.model,
85
+ noReply: true,
86
+ parts: [{ type: "text", text: item.text }],
87
+ },
88
+ })
89
+
90
+ await client.session.command({
91
+ path: { id: sid },
92
+ body: {
93
+ agent: item.info.agent,
94
+ model: `${item.info.model.providerID}/${item.info.model.modelID}`,
95
+ command: item.command,
96
+ arguments: item.arguments,
97
+ },
98
+ })
99
+ }
36
100
 
37
101
  const flush = async (sid: string) => {
38
102
  if (flushing.has(sid)) return
@@ -46,28 +110,7 @@ const QueuePlugin: Plugin = async ({ client }) => {
46
110
  while (list.length) {
47
111
  const item = list.shift()
48
112
  if (!item) break
49
-
50
- if ("command" in item) {
51
- await client.session.command({
52
- path: { id: sid },
53
- body: {
54
- agent: item.info.agent,
55
- model: `${item.info.model.providerID}/${item.info.model.modelID}`,
56
- command: item.command,
57
- arguments: item.arguments,
58
- },
59
- })
60
- continue
61
- }
62
-
63
- await client.session.prompt({
64
- path: { id: sid },
65
- body: {
66
- agent: item.info.agent,
67
- model: item.info.model,
68
- parts: item.parts.map((part) => ({ ...part, id: undefined })),
69
- },
70
- })
113
+ await replay(sid, item)
71
114
  }
72
115
  } catch (error) {
73
116
  const message = error instanceof Error ? error.message : String(error)
@@ -101,10 +144,16 @@ const QueuePlugin: Plugin = async ({ client }) => {
101
144
  if (body === undefined) return
102
145
 
103
146
  const files = output.parts.filter((part): part is FilePart => part.type === "file")
104
- if (!body.trim() && !files.length) return
147
+ const trimmed = body.trim()
148
+
149
+ if ((!trimmed || trimmed === "list" || trimmed === "clear") && !files.length) {
150
+ hide(output.message.id, text)
151
+ await toast(trimmed === "clear" ? clear(sessionID) : summary(sessionID), "info", 5000)
152
+ return
153
+ }
105
154
 
106
155
  if (!busy.has(sessionID)) {
107
- if (body.trimStart().startsWith("/")) return
156
+ if (trimmed.startsWith("/")) return
108
157
  text.text = body
109
158
  return
110
159
  }
@@ -126,15 +175,11 @@ const QueuePlugin: Plugin = async ({ client }) => {
126
175
 
127
176
  const info = { agent: output.message.agent, model: { ...output.message.model } }
128
177
  const command = parseCommand(body)
129
- const item = command ? { info, ...command } : { info, parts }
130
- const list = queue.get(sessionID)
131
- if (list) list.push(item)
132
- else queue.set(sessionID, [item])
133
-
134
- const note = label(body, files.length)
135
- hidden.add(output.message.id)
136
- text.text = `[queued] ${note}`
137
- await toast(`Queued: ${note}`, "info")
178
+ const item = command ? { info, text: trimmed, ...command } : { info, text: label(body, files.length), parts }
179
+
180
+ enqueue(sessionID, item)
181
+ hide(output.message.id, text)
182
+ await toast(`Queued: ${item.text}`, "info")
138
183
  },
139
184
  "experimental.chat.messages.transform": async (_, output) => {
140
185
  output.messages = output.messages.filter((msg) => !hidden.has(msg.info.id))
@@ -142,5 +187,4 @@ const QueuePlugin: Plugin = async ({ client }) => {
142
187
  }
143
188
  }
144
189
 
145
- export { QueuePlugin }
146
190
  export default QueuePlugin
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.1.0",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "description": "Queue OpenCode prompts and slash commands until the agent is idle",
7
7
  "main": "./index.ts",