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.
- package/README.md +9 -3
- package/index.ts +82 -38
- 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
|
-
-
|
|
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
|
-
|
|
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;
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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