opencode-queue 0.6.3 → 0.6.5
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 +1 -0
- package/index.ts +56 -27
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -82,3 +82,4 @@ The queue is in-memory and scoped to the current session.
|
|
|
82
82
|
- This plugin registers `/queue` as a real OpenCode slash command.
|
|
83
83
|
- It does not add a keyboard shortcut. OpenCode plugins cannot currently register custom TUI keybindings.
|
|
84
84
|
- Queued placeholders are hidden instead of deleted, then filtered out before messages are sent to the model.
|
|
85
|
+
- If plan mode asks to switch to the build agent while more queued work is waiting, the plugin answers `No` so the queue can continue.
|
package/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ type Meta = { variant?: string; controls?: string[]; fast?: boolean }
|
|
|
12
12
|
type Run = { agent: string; model?: Model }
|
|
13
13
|
type Info = { agent: string; model: Model } & Meta
|
|
14
14
|
type Msg = { info: { role: string; agent?: string; mode?: string; model?: Model; providerID?: string; modelID?: string } & Meta }
|
|
15
|
+
type Ask = { type: string; properties: { id: string; sessionID: string; questions: { question: string; header: string }[] } }
|
|
16
|
+
type Post = (input: { url: string; path?: Record<string, string>; body?: unknown; headers?: Record<string, string> }) => Promise<{ response?: Response; error?: unknown } | undefined>
|
|
15
17
|
|
|
16
18
|
type Item =
|
|
17
19
|
| { kind: "prompt"; info: Info; text: string; parts: InputPart[] }
|
|
@@ -53,12 +55,18 @@ const parse = (body: string, files = 0): Op => {
|
|
|
53
55
|
const trailing = (text: string) => (text.trim() === "/queue" ? "" : text.match(SUFFIX)?.[1])
|
|
54
56
|
const strip = (text: string) => trailing(text) ?? text
|
|
55
57
|
const queued = (text: string) => text.match(QUEUE)?.[1] ?? trailing(text)
|
|
58
|
+
const plan = (event: unknown): event is Ask => {
|
|
59
|
+
if (typeof event !== "object" || !event || !("type" in event) || event.type !== "question.asked") return false
|
|
60
|
+
const question = (event as Ask).properties?.questions?.[0]
|
|
61
|
+
return question?.header === "Build Agent" && question.question.includes("switch to the build agent")
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
export const QueuePlugin: Plugin = async ({ client }) => {
|
|
58
65
|
const queue = new Map<string, Item[]>()
|
|
59
66
|
const hidden = new Set<string>()
|
|
60
67
|
const busy = new Set<string>()
|
|
61
|
-
const
|
|
68
|
+
const active = new Set<string>()
|
|
69
|
+
const post = (client as unknown as { _client?: { post?: Post } })._client?.post
|
|
62
70
|
|
|
63
71
|
const toast = (message: string, variant: "info" | "error", duration = 2500) =>
|
|
64
72
|
client.tui.showToast({ body: { message, variant, duration } }).catch(() => undefined)
|
|
@@ -68,6 +76,19 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
68
76
|
throw new Error(HANDLED)
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
const no = async (id: string) => {
|
|
80
|
+
if (!post) {
|
|
81
|
+
console.warn("QueuePlugin cannot answer plan prompt because the SDK client has no internal request method")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await post({ url: "/question/{requestID}/reply", path: { requestID: id }, body: { answers: [["No"]] } }).catch((error) => {
|
|
86
|
+
console.warn("QueuePlugin failed to answer plan prompt", error)
|
|
87
|
+
return undefined
|
|
88
|
+
})
|
|
89
|
+
if (!result?.response?.ok) console.warn("QueuePlugin failed to answer plan prompt", result?.error ?? result?.response?.status)
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
const hide = (id: string, part: TextPart) => {
|
|
72
93
|
hidden.add(id)
|
|
73
94
|
Object.assign(part, { text: "", synthetic: true, ignored: true })
|
|
@@ -89,14 +110,13 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
89
110
|
return []
|
|
90
111
|
})
|
|
91
112
|
|
|
92
|
-
|
|
93
|
-
if (msg.info.role === "user" && msg.info.agent && msg.info.model) return { agent: msg.info.agent, model: msg.info.model, variant: msg.info.variant, controls: msg.info.controls, fast: msg.info.fast }
|
|
113
|
+
return ([...(Array.isArray(result) ? result : (result.data ?? []))].reverse() as Msg[]).flatMap((msg): Info[] => {
|
|
114
|
+
if (msg.info.role === "user" && msg.info.agent && msg.info.model) return [{ agent: msg.info.agent, model: msg.info.model, variant: msg.info.variant, controls: msg.info.controls, fast: msg.info.fast }]
|
|
94
115
|
if (msg.info.role === "assistant" && (msg.info.agent || msg.info.mode) && msg.info.providerID && msg.info.modelID) {
|
|
95
|
-
return { agent: msg.info.agent ?? msg.info.mode!, model: { providerID: msg.info.providerID, modelID: msg.info.modelID }, variant: msg.info.variant, controls: msg.info.controls, fast: msg.info.fast }
|
|
116
|
+
return [{ agent: msg.info.agent ?? msg.info.mode!, model: { providerID: msg.info.providerID, modelID: msg.info.modelID }, variant: msg.info.variant, controls: msg.info.controls, fast: msg.info.fast }]
|
|
96
117
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return undefined
|
|
118
|
+
return []
|
|
119
|
+
})[0]
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
const run = async (sid: string): Promise<Run> => {
|
|
@@ -137,19 +157,18 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
137
157
|
|
|
138
158
|
const flush = (sid: string) => {
|
|
139
159
|
const list = queue.get(sid)
|
|
140
|
-
if (
|
|
160
|
+
if (!list?.length) return
|
|
141
161
|
const item = list.shift()
|
|
142
162
|
if (!item) return
|
|
163
|
+
if (!list.length) queue.delete(sid)
|
|
143
164
|
|
|
144
|
-
|
|
145
|
-
else queue.delete(sid)
|
|
146
|
-
|
|
147
|
-
flushing.add(sid)
|
|
165
|
+
active.add(sid)
|
|
148
166
|
void replay(sid, item).catch(async (error) => {
|
|
149
167
|
console.error("QueuePlugin failed to flush queued input", error)
|
|
150
168
|
await toast(`Queue failed: ${error instanceof Error ? error.message : String(error)}`, "error")
|
|
169
|
+
}).finally(() => {
|
|
170
|
+
active.delete(sid)
|
|
151
171
|
})
|
|
152
|
-
flushing.delete(sid)
|
|
153
172
|
}
|
|
154
173
|
|
|
155
174
|
return {
|
|
@@ -158,6 +177,14 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
158
177
|
cfg.command.queue = { template: "", description: "Queue input until the session is idle" }
|
|
159
178
|
},
|
|
160
179
|
event: async ({ event }) => {
|
|
180
|
+
if (plan(event)) {
|
|
181
|
+
const sid = event.properties.sessionID
|
|
182
|
+
if (!active.has(sid) && !queue.get(sid)?.length) return
|
|
183
|
+
await no(event.properties.id)
|
|
184
|
+
await toast("Declined plan approval to continue queued work", "info")
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
161
188
|
if (event.type !== "session.status") return
|
|
162
189
|
|
|
163
190
|
const sid = event.properties.sessionID
|
|
@@ -183,8 +210,7 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
183
210
|
return
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
output.parts.length
|
|
187
|
-
output.parts.push({ type: "text", text: `/queue /${input.command}${args.trim() ? ` ${args.trim()}` : ""}` } as any, ...parts)
|
|
213
|
+
output.parts.splice(0, output.parts.length, { type: "text", text: `/queue /${input.command}${args.trim() ? ` ${args.trim()}` : ""}` } as any, ...parts)
|
|
188
214
|
return
|
|
189
215
|
}
|
|
190
216
|
|
|
@@ -205,13 +231,11 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
205
231
|
throw new Error(HANDLED)
|
|
206
232
|
}
|
|
207
233
|
|
|
208
|
-
output.parts.length
|
|
209
|
-
output.parts.push({ type: "text", text: op.body } as any, ...parts)
|
|
234
|
+
output.parts.splice(0, output.parts.length, { type: "text", text: op.body } as any, ...parts)
|
|
210
235
|
return
|
|
211
236
|
}
|
|
212
237
|
|
|
213
|
-
output.parts.length
|
|
214
|
-
output.parts.push({ type: "text", text: `/queue ${body}` } as any, ...parts)
|
|
238
|
+
output.parts.splice(0, output.parts.length, { type: "text", text: `/queue ${body}` } as any, ...parts)
|
|
215
239
|
},
|
|
216
240
|
"chat.message": async (input, output) => {
|
|
217
241
|
const sid = input.sessionID
|
|
@@ -252,14 +276,19 @@ export const QueuePlugin: Plugin = async ({ client }) => {
|
|
|
252
276
|
const prior = await latest(sid)
|
|
253
277
|
if (prior) Object.assign(output.message, opts(prior))
|
|
254
278
|
else console.warn("QueuePlugin could not neutralize queued placeholder metadata because the session has no previous message context")
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
279
|
+
const item: Item =
|
|
280
|
+
op.kind === "shell" ? { ...op, info } :
|
|
281
|
+
op.kind === "command" ? { ...op, info, files: parts } :
|
|
282
|
+
{
|
|
283
|
+
...op,
|
|
284
|
+
info,
|
|
285
|
+
parts: output.parts.flatMap((part): InputPart[] => {
|
|
286
|
+
if (part.type === "text") return part.id === text.id ? (body ? [{ ...part, text: body }] : []) : [{ ...part }]
|
|
287
|
+
if (part.type === "file" || part.type === "agent" || part.type === "subtask") return [{ ...part }]
|
|
288
|
+
console.warn("QueuePlugin skipped unexpected part", part.type)
|
|
289
|
+
return []
|
|
290
|
+
}),
|
|
291
|
+
}
|
|
263
292
|
|
|
264
293
|
queue.set(sid, [...(queue.get(sid) ?? []), item])
|
|
265
294
|
hide(output.message.id, text)
|
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.6.
|
|
4
|
+
"version": "0.6.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Queue OpenCode prompts and slash commands until the agent is idle",
|
|
7
7
|
"main": "./index.ts",
|
|
@@ -38,5 +38,8 @@
|
|
|
38
38
|
"@opencode-ai/plugin": "latest",
|
|
39
39
|
"@opencode-ai/sdk": "latest",
|
|
40
40
|
"typescript": "^5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"overrides": {
|
|
43
|
+
"uuid": "^14.0.0"
|
|
41
44
|
}
|
|
42
45
|
}
|