opencode-queue 0.12.1 → 0.13.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 +12 -17
- package/index.ts +98 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Queue OpenCode input until the current session is idle.
|
|
8
8
|
|
|
9
|
-
`opencode-queue` adds
|
|
9
|
+
`opencode-queue` adds `/queue` and its shorter `/q` alias. It lets you type the next prompt, slash command, or shell command while an agent is still working, without interrupting the current run.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -23,10 +23,11 @@ Restart OpenCode after installing. OpenCode installs npm plugins automatically a
|
|
|
23
23
|
## Quick Examples
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
|
-
/
|
|
26
|
+
/q continue after this task
|
|
27
27
|
continue after this task /queue
|
|
28
28
|
/queue front do this next
|
|
29
29
|
do this next /queue front
|
|
30
|
+
/queue now send this immediately
|
|
30
31
|
|
|
31
32
|
/queue /review
|
|
32
33
|
/review /queue
|
|
@@ -50,12 +51,15 @@ do this next /queue front
|
|
|
50
51
|
|
|
51
52
|
## Syntax
|
|
52
53
|
|
|
54
|
+
`/q` is accepted anywhere `/queue` is shown.
|
|
55
|
+
|
|
53
56
|
| Input | What it does |
|
|
54
57
|
| --- | --- |
|
|
55
58
|
| `/queue message` | Queue a normal prompt. |
|
|
56
59
|
| `message /queue` | Queue a normal prompt using trailing syntax. |
|
|
57
60
|
| `/queue front message` | Queue a normal prompt before existing queued entries. |
|
|
58
61
|
| `message /queue front` | Queue a normal prompt before existing queued entries using trailing syntax. |
|
|
62
|
+
| `/queue now input` | Send a prompt or slash command immediately. Shell commands still wait until the session is idle. |
|
|
59
63
|
| `/queue /review` | Queue a slash command. |
|
|
60
64
|
| `/review /queue` | Queue a slash command using trailing syntax. |
|
|
61
65
|
| `/queue front /review` | Queue a slash command before existing queued entries. |
|
|
@@ -68,6 +72,9 @@ do this next /queue front
|
|
|
68
72
|
| `/queue list` | Show the current queue. |
|
|
69
73
|
| `/queue stop` | Pause automatic sending of queued entries. |
|
|
70
74
|
| `/queue start` | Resume automatic sending of queued entries. |
|
|
75
|
+
| `/queue always` | Show whether automatic queueing is enabled for this project. |
|
|
76
|
+
| `/queue always on` | Automatically queue plain input while the session is busy. |
|
|
77
|
+
| `/queue always off` | Require `/queue` again. |
|
|
71
78
|
| `/queue flush` | Send all queued entries immediately. |
|
|
72
79
|
| `/queue clear` | Clear the current queue. |
|
|
73
80
|
| `/queue clear 1` | Clear item 1 from the current queue. |
|
|
@@ -82,9 +89,11 @@ When the session is busy:
|
|
|
82
89
|
- Each queued entry replays with the agent, model, and thinking variant selected when it was queued.
|
|
83
90
|
- Queued entries replay in order after the session completes normally and becomes idle.
|
|
84
91
|
- `/queue front ...` puts an entry before the existing queued entries.
|
|
92
|
+
- `/queue now ...` sends prompts and slash commands immediately regardless of queue state or mode. Shell commands remain queued until the session is idle.
|
|
85
93
|
- Only one queued entry is sent per idle transition, so queued work runs one item at a time.
|
|
86
94
|
- Queued entries are kept in place after an error, abort, crash, or restart.
|
|
87
95
|
- `/queue stop` pauses automatic replay without clearing queued entries, and `/queue start` resumes it.
|
|
96
|
+
- `/queue always on` also queues plain prompts and custom slash commands while the session is busy, paused, or already has queued work. OpenCode does not expose native shell or `/compact` submissions to these plugin hooks.
|
|
88
97
|
- `/queue flush` sends all queued entries immediately in one batch, even before the session is idle.
|
|
89
98
|
|
|
90
99
|
When the session is idle:
|
|
@@ -100,24 +109,10 @@ When the session is idle:
|
|
|
100
109
|
- `/queue flush` sends all queued entries immediately in one batch.
|
|
101
110
|
- `/queue clear` clears the current queue, and `/queue clear 1` clears a specific queued item.
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```text
|
|
106
|
-
/queue
|
|
107
|
-
/queue list
|
|
108
|
-
/queue stop
|
|
109
|
-
/queue start
|
|
110
|
-
/queue flush
|
|
111
|
-
/queue clear
|
|
112
|
-
/queue clear 1
|
|
113
|
-
/queue clear 2 3
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Queues are scoped to the current project and session. They are stored in OpenCode's user data directory and restored after OpenCode restarts or crashes. A send interrupted by a crash remains queued because the plugin cannot know whether OpenCode accepted it before exiting, so restored queues stay paused until explicitly started.
|
|
112
|
+
Queues are scoped to the current project and session. They are stored in OpenCode's user data directory and restored with their previous running or stopped state after OpenCode restarts or crashes. The `always` setting applies to the whole project and is stored with its queues. Restored queues do not replay just because the session starts idle; a running queue resumes after the session becomes busy and then finishes successfully. A send interrupted by a crash remains queued because the plugin cannot know whether OpenCode accepted it before exiting.
|
|
117
113
|
|
|
118
114
|
## Notes
|
|
119
115
|
|
|
120
|
-
- This plugin registers `/queue` as a real OpenCode slash command.
|
|
121
116
|
- It does not add a keyboard shortcut. OpenCode plugins cannot currently register custom TUI keybindings.
|
|
122
117
|
- Queued placeholders are hidden instead of deleted, then filtered out before messages are sent to the model.
|
|
123
118
|
- 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
|
@@ -6,11 +6,11 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
|
6
6
|
import { homedir } from "node:os"
|
|
7
7
|
import { dirname, join } from "node:path"
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const SUFFIX = /^([\s\S]*?)\s+\/queue(?:\s+(front))?\s*$/
|
|
9
|
+
const SUFFIX = /^(?:([\s\S]*?)\s+)?\/(\S+)(?:\s+(front))?\s*$/
|
|
11
10
|
const CMD = /^\/(\S+)(?:\s+([\s\S]*))?$/
|
|
12
11
|
const ITEM_NUMBER = /^[1-9]\d*$/
|
|
13
12
|
const TUI_COMPACT = "session_compact"
|
|
13
|
+
const INTERNAL = "opencodeQueueInternal"
|
|
14
14
|
|
|
15
15
|
type InputPart = TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput
|
|
16
16
|
type Model = { providerID: string; modelID: string }
|
|
@@ -19,7 +19,7 @@ type Info = { agent: string; model: Model; variant?: string }
|
|
|
19
19
|
type Msg = { info: { role: string; agent?: string; mode?: string; model?: Model; providerID?: string; modelID?: string; variant?: string } }
|
|
20
20
|
type Ask = { type: string; properties: { id: string; sessionID: string; questions: { question: string; header: string }[] } }
|
|
21
21
|
type Post = (input: { url: string; path?: Record<string, string>; body?: unknown; headers?: Record<string, string> }) => Promise<{ response?: Response; error?: unknown } | undefined>
|
|
22
|
-
type QueueInput = { body: string; front
|
|
22
|
+
type QueueInput = { body: string; modifier?: "front" | "now" }
|
|
23
23
|
|
|
24
24
|
type Item =
|
|
25
25
|
| { kind: "prompt"; info: Info; body: string; parts: InputPart[] }
|
|
@@ -33,32 +33,36 @@ type EntryOp =
|
|
|
33
33
|
| { kind: "compact"; source: string }
|
|
34
34
|
| { kind: "shell"; source: string; shell: string }
|
|
35
35
|
|
|
36
|
-
type
|
|
37
|
-
type State = { items: Item[]; activity: Activity; stopped: boolean; failed: boolean; hidden: Set<string> }
|
|
38
|
-
type Durable = Pick<State, "items" | "stopped" | "hidden">
|
|
39
|
-
type Store = { version: 1; projectID: string; sessions: Record<string, { items: Item[]; stopped: boolean; hidden: string[] }> }
|
|
40
|
-
type Placeholder = { id: string; part: TextPart }
|
|
41
|
-
|
|
42
|
-
type Op =
|
|
36
|
+
type ControlOp =
|
|
43
37
|
| { kind: "list" }
|
|
44
38
|
| { kind: "clear"; indices: number[] }
|
|
45
39
|
| { kind: "flush" }
|
|
46
40
|
| { kind: "start" }
|
|
47
41
|
| { kind: "stop" }
|
|
42
|
+
| { kind: "always"; enabled?: boolean }
|
|
43
|
+
|
|
44
|
+
type Activity = { kind: "idle" } | { kind: "restored" } | { kind: "busy" } | { kind: "sending"; idle: boolean; items: Item[] }
|
|
45
|
+
type State = { items: Item[]; activity: Activity; stopped: boolean; failed: boolean; hidden: Set<string> }
|
|
46
|
+
type Draft = Pick<State, "items" | "stopped" | "hidden"> & { always: boolean }
|
|
47
|
+
type Store = { version: 1; projectID: string; always: boolean; sessions: Record<string, { items: Item[]; stopped: boolean; hidden: string[] }> }
|
|
48
|
+
type Placeholder = { id: string; part: TextPart }
|
|
49
|
+
|
|
50
|
+
type Op =
|
|
51
|
+
| ControlOp
|
|
48
52
|
| { kind: "invalid"; message: string }
|
|
49
53
|
| (EntryOp & { front: boolean })
|
|
50
54
|
|
|
51
|
-
|
|
55
|
+
const isQueue = (command: string) => command === "q" || command === "queue"
|
|
52
56
|
|
|
53
57
|
const parsePrefix = (body: string): QueueInput => {
|
|
54
|
-
const match = body.trim().match(/^front(?:\s+([\s\S]*))?$/)
|
|
55
|
-
return match ? { body: match[
|
|
58
|
+
const match = body.trim().match(/^(front|now)(?:\s+([\s\S]*))?$/)
|
|
59
|
+
return match ? { body: match[2] ?? "", modifier: match[1] === "front" ? "front" : "now" } : { body }
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
const parse = (input: QueueInput, files = 0): Op => {
|
|
59
63
|
const text = input.body.trim()
|
|
60
|
-
const front = input.front
|
|
61
|
-
if (!
|
|
64
|
+
const front = input.modifier === "front"
|
|
65
|
+
if (!input.modifier && !files) {
|
|
62
66
|
switch (text) {
|
|
63
67
|
case "":
|
|
64
68
|
case "list":
|
|
@@ -71,6 +75,10 @@ const parse = (input: QueueInput, files = 0): Op => {
|
|
|
71
75
|
return { kind: "stop" }
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
if (text === "always") return { kind: "always" }
|
|
79
|
+
if (text === "always on" || text === "always off") return { kind: "always", enabled: text === "always on" }
|
|
80
|
+
if (/^always(?:\s|$)/.test(text)) return { kind: "invalid", message: "Queue always expects on or off" }
|
|
81
|
+
|
|
74
82
|
const clear = text.match(/^clear(?:\s+([\s\S]+))?$/)
|
|
75
83
|
if (clear) {
|
|
76
84
|
const values = clear[1]?.trim().split(/\s+/) ?? []
|
|
@@ -79,7 +87,7 @@ const parse = (input: QueueInput, files = 0): Op => {
|
|
|
79
87
|
return { kind: "clear", indices }
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
|
-
if (
|
|
90
|
+
if (input.modifier && !text && !files) return { kind: "invalid", message: `Queue ${input.modifier} input is empty` }
|
|
83
91
|
|
|
84
92
|
if (text.startsWith("!")) {
|
|
85
93
|
const shell = text.slice(1).trim()
|
|
@@ -103,17 +111,13 @@ const parse = (input: QueueInput, files = 0): Op => {
|
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
const parseSuffix = (text: string): QueueInput | undefined => {
|
|
106
|
-
const trimmed = text.trim()
|
|
107
|
-
if (trimmed === "/queue") return { body: "", front: false }
|
|
108
|
-
if (trimmed === "/queue front") return { body: "", front: true }
|
|
109
|
-
|
|
110
114
|
const match = text.match(SUFFIX)
|
|
111
|
-
return match ? { body: match[1],
|
|
115
|
+
return match && isQueue(match[2]) ? { body: match[1] ?? "", modifier: match[3] ? "front" : undefined } : undefined
|
|
112
116
|
}
|
|
113
117
|
const stripSuffix = (text: string) => parseSuffix(text)?.body ?? text
|
|
114
118
|
const parseInput = (text: string): QueueInput | undefined => {
|
|
115
|
-
const prefix = text.match(
|
|
116
|
-
return prefix ? parsePrefix(prefix[
|
|
119
|
+
const prefix = text.match(CMD)
|
|
120
|
+
return prefix && isQueue(prefix[1]) ? parsePrefix(prefix[2] ?? "") : parseSuffix(text)
|
|
117
121
|
}
|
|
118
122
|
const control = (op: Op): op is ControlOp => {
|
|
119
123
|
switch (op.kind) {
|
|
@@ -122,6 +126,7 @@ const control = (op: Op): op is ControlOp => {
|
|
|
122
126
|
case "flush":
|
|
123
127
|
case "start":
|
|
124
128
|
case "stop":
|
|
129
|
+
case "always":
|
|
125
130
|
return true
|
|
126
131
|
default:
|
|
127
132
|
return false
|
|
@@ -196,8 +201,11 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
196
201
|
const sessions = new Map<string, State>()
|
|
197
202
|
const deleted = new Set<string>()
|
|
198
203
|
const enqueueTurns = new Map<string, Promise<unknown>>()
|
|
204
|
+
const internalCommands: { sid: string; command: string; args: string; used: boolean }[] = []
|
|
205
|
+
const origin = randomUUID()
|
|
199
206
|
const post = (client as unknown as { _client?: { post?: Post } })._client?.post
|
|
200
207
|
const path = join(dataHome(), "opencode", "opencode-queue", `${createHash("sha256").update(project.id).digest("hex")}.json`)
|
|
208
|
+
let alwaysQueue = false
|
|
201
209
|
let writes = Promise.resolve()
|
|
202
210
|
|
|
203
211
|
try {
|
|
@@ -205,6 +213,8 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
205
213
|
if (!record(parsed) || parsed.version !== 1 || parsed.projectID !== project.id || !record(parsed.sessions)) {
|
|
206
214
|
console.warn("QueuePlugin ignored invalid queue storage", path)
|
|
207
215
|
} else {
|
|
216
|
+
if (typeof parsed.always === "boolean") alwaysQueue = parsed.always
|
|
217
|
+
else if (parsed.always !== undefined) console.warn("QueuePlugin ignored invalid always queue setting", path)
|
|
208
218
|
for (const [sid, value] of Object.entries(parsed.sessions)) {
|
|
209
219
|
if (!record(value) || typeof value.stopped !== "boolean" || !Array.isArray(value.items)) {
|
|
210
220
|
console.warn("QueuePlugin skipped invalid stored session", sid)
|
|
@@ -215,17 +225,16 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
215
225
|
const validHidden = Array.isArray(value.hidden) && value.hidden.every((id) => typeof id === "string")
|
|
216
226
|
if (!validHidden) console.warn("QueuePlugin skipped invalid stored hidden messages", sid)
|
|
217
227
|
const hidden = new Set(validHidden ? (value.hidden as string[]) : [])
|
|
218
|
-
const
|
|
219
|
-
if (items.length
|
|
220
|
-
if (items.length || stopped || hidden.size) sessions.set(sid, { items, activity: { kind: "idle" }, stopped, failed: false, hidden })
|
|
228
|
+
const activity: Activity = { kind: items.length && !value.stopped ? "restored" : "idle" }
|
|
229
|
+
if (items.length || value.stopped || hidden.size) sessions.set(sid, { items, activity, stopped: value.stopped, failed: false, hidden })
|
|
221
230
|
}
|
|
222
231
|
}
|
|
223
232
|
} catch (error) {
|
|
224
233
|
if (!record(error) || error.code !== "ENOENT") console.error("QueuePlugin failed to load queue storage", error)
|
|
225
234
|
}
|
|
226
235
|
|
|
227
|
-
const save = async (sid?: string, draft?:
|
|
228
|
-
const stored: Store = { version: 1, projectID: project.id, sessions: {} }
|
|
236
|
+
const save = async (sid?: string, draft?: Draft) => {
|
|
237
|
+
const stored: Store = { version: 1, projectID: project.id, always: draft?.always ?? alwaysQueue, sessions: {} }
|
|
229
238
|
for (const [id, current] of sessions) {
|
|
230
239
|
if (deleted.has(id) || (id === sid && !draft)) continue
|
|
231
240
|
const durable = id === sid ? draft! : current
|
|
@@ -258,7 +267,9 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
258
267
|
return current
|
|
259
268
|
}
|
|
260
269
|
|
|
261
|
-
const
|
|
270
|
+
const automaticallyQueue = (sid: string) => alwaysQueue && shouldQueue(sessions.get(sid))
|
|
271
|
+
|
|
272
|
+
const store = async (sid: string, current: State, draft: Draft, placeholder?: Placeholder) => {
|
|
262
273
|
try {
|
|
263
274
|
await save(sid, draft)
|
|
264
275
|
} catch (error) {
|
|
@@ -268,14 +279,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
268
279
|
current.items.splice(0, current.items.length, ...draft.items)
|
|
269
280
|
current.stopped = draft.stopped
|
|
270
281
|
current.hidden = draft.hidden
|
|
282
|
+
alwaysQueue = draft.always
|
|
271
283
|
if (placeholder) Object.assign(placeholder.part, { text: "", synthetic: true, ignored: true })
|
|
272
284
|
}
|
|
273
285
|
|
|
274
|
-
const persist = <T>(sid: string, placeholder: Placeholder | undefined, mutate: (draft:
|
|
286
|
+
const persist = <T>(sid: string, placeholder: Placeholder | undefined, mutate: (draft: Draft) => T) =>
|
|
275
287
|
serialize(async () => {
|
|
276
288
|
if (deleted.has(sid)) throw new Error(`QueuePlugin cannot persist queue state for deleted session ${sid}`)
|
|
277
289
|
const current = state(sid)
|
|
278
|
-
const draft:
|
|
290
|
+
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden), always: alwaysQueue }
|
|
279
291
|
if (placeholder) draft.hidden.add(placeholder.id)
|
|
280
292
|
const value = mutate(draft)
|
|
281
293
|
await store(sid, current, draft, placeholder)
|
|
@@ -365,6 +377,21 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
365
377
|
throwOnError: true,
|
|
366
378
|
})
|
|
367
379
|
|
|
380
|
+
const markInternal = (parts: { type: string; metadata?: Record<string, unknown> }[]) => {
|
|
381
|
+
const text = parts.find((part) => part.type === "text")
|
|
382
|
+
if (text) text.metadata = { ...text.metadata, [INTERNAL]: origin }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const callCommand = async <T>(sid: string, command: string, args: string, call: () => Promise<T>) => {
|
|
386
|
+
const pending = { sid, command, args, used: false }
|
|
387
|
+
internalCommands.push(pending)
|
|
388
|
+
try {
|
|
389
|
+
return await call()
|
|
390
|
+
} finally {
|
|
391
|
+
internalCommands.splice(internalCommands.indexOf(pending), 1)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
368
395
|
const replay = async (sid: string, item: Item) => {
|
|
369
396
|
switch (item.kind) {
|
|
370
397
|
case "shell":
|
|
@@ -372,19 +399,22 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
372
399
|
case "compact":
|
|
373
400
|
return compact(sid, item.info)
|
|
374
401
|
case "command":
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
402
|
+
return callCommand(sid, item.cmd, item.args, () =>
|
|
403
|
+
client.session.command({
|
|
404
|
+
path: { id: sid },
|
|
405
|
+
body: {
|
|
406
|
+
...opts(item.info),
|
|
407
|
+
model: `${item.info.model.providerID}/${item.info.model.modelID}`,
|
|
408
|
+
command: item.cmd,
|
|
409
|
+
arguments: item.args,
|
|
410
|
+
parts: item.files,
|
|
411
|
+
} as any,
|
|
412
|
+
throwOnError: true,
|
|
413
|
+
}),
|
|
414
|
+
)
|
|
386
415
|
case "prompt": {
|
|
387
416
|
const parts = item.parts.map((part) => ({ ...part, id: undefined }))
|
|
417
|
+
markInternal(parts)
|
|
388
418
|
return client.session.prompt({ path: { id: sid }, body: { ...opts(item.info), parts } as any, throwOnError: true })
|
|
389
419
|
}
|
|
390
420
|
}
|
|
@@ -421,7 +451,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
421
451
|
|
|
422
452
|
const items = current.items.slice(0, count)
|
|
423
453
|
if (placeholder) {
|
|
424
|
-
const draft:
|
|
454
|
+
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden).add(placeholder.id), always: alwaysQueue }
|
|
425
455
|
await store(sid, current, draft, placeholder)
|
|
426
456
|
if (deleted.has(sid)) return undefined
|
|
427
457
|
}
|
|
@@ -503,6 +533,9 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
503
533
|
case "start":
|
|
504
534
|
draft.stopped = false
|
|
505
535
|
return "Queue started"
|
|
536
|
+
case "always":
|
|
537
|
+
if (op.enabled !== undefined) draft.always = op.enabled
|
|
538
|
+
return `Always queue is ${draft.always ? "on" : "off"} for this project`
|
|
506
539
|
}
|
|
507
540
|
})
|
|
508
541
|
if (op.kind === "start") {
|
|
@@ -515,7 +548,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
515
548
|
const hooks: Awaited<ReturnType<Plugin>> = {
|
|
516
549
|
config: async (cfg) => {
|
|
517
550
|
cfg.command ??= {}
|
|
518
|
-
cfg.command.queue = { template: "", description: "Queue input until the session is idle" }
|
|
551
|
+
cfg.command.q = cfg.command.queue = { template: "", description: "Queue input until the session is idle" }
|
|
519
552
|
},
|
|
520
553
|
event: async ({ event }) => {
|
|
521
554
|
if (plan(event)) {
|
|
@@ -577,8 +610,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
577
610
|
const body = input.arguments ?? ""
|
|
578
611
|
const parts = files(output.parts)
|
|
579
612
|
|
|
580
|
-
|
|
581
|
-
|
|
613
|
+
const internal = internalCommands.find((pending) => !pending.used && pending.sid === sid && pending.command === input.command && pending.args === body)
|
|
614
|
+
if (internal) {
|
|
615
|
+
internal.used = true
|
|
616
|
+
markInternal(output.parts)
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!isQueue(input.command)) {
|
|
621
|
+
const queued = parseSuffix(body) ?? (automaticallyQueue(sid) ? { body } : undefined)
|
|
582
622
|
if (!queued) return
|
|
583
623
|
|
|
584
624
|
if (!shouldQueue(sessions.get(sid))) {
|
|
@@ -586,16 +626,17 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
586
626
|
return
|
|
587
627
|
}
|
|
588
628
|
|
|
589
|
-
output.parts.splice(0, output.parts.length, { type: "text", text: `/queue${queued.front ? " front" : ""} /${input.command}${queued.body.trim() ? ` ${queued.body.trim()}` : ""}` } as any, ...parts)
|
|
629
|
+
output.parts.splice(0, output.parts.length, { type: "text", text: `/queue${queued.modifier === "front" ? " front" : ""} /${input.command}${queued.body.trim() ? ` ${queued.body.trim()}` : ""}` } as any, ...parts)
|
|
590
630
|
return
|
|
591
631
|
}
|
|
592
632
|
|
|
593
|
-
const
|
|
633
|
+
const request = parsePrefix(body)
|
|
634
|
+
const op = parse(request, parts.length)
|
|
594
635
|
|
|
595
636
|
if (control(op)) return stop(await afterEnqueue(sid, () => manage(sid, op)))
|
|
596
637
|
if (op.kind === "invalid") return stop(op.message, "error")
|
|
597
638
|
|
|
598
|
-
if (!shouldQueue(sessions.get(sid))) {
|
|
639
|
+
if (!shouldQueue(sessions.get(sid)) || (request.modifier === "now" && op.kind !== "prompt" && op.kind !== "shell")) {
|
|
599
640
|
if (op.kind === "shell") {
|
|
600
641
|
await shell(sid, op.shell, await run(sid))
|
|
601
642
|
return handled()
|
|
@@ -607,7 +648,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
607
648
|
}
|
|
608
649
|
|
|
609
650
|
if (op.kind === "command") {
|
|
610
|
-
await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
|
|
651
|
+
await callCommand(sid, op.cmd, op.args, () => client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any }))
|
|
611
652
|
return handled()
|
|
612
653
|
}
|
|
613
654
|
|
|
@@ -623,10 +664,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
623
664
|
console.warn("QueuePlugin ignored input for a deleted session", sid)
|
|
624
665
|
return
|
|
625
666
|
}
|
|
667
|
+
const internal = output.parts.find((part): part is TextPart => part.type === "text" && part.metadata?.[INTERNAL] === origin)
|
|
668
|
+
if (internal) {
|
|
669
|
+
delete internal.metadata![INTERNAL]
|
|
670
|
+
return
|
|
671
|
+
}
|
|
626
672
|
const text = output.parts.find((part): part is TextPart => part.type === "text" && !part.synthetic)
|
|
627
673
|
if (!text) return
|
|
628
674
|
|
|
629
|
-
const request = parseInput(text.text)
|
|
675
|
+
const request = parseInput(text.text) ?? (automaticallyQueue(sid) ? { body: text.text } : undefined)
|
|
630
676
|
if (!request) return
|
|
631
677
|
|
|
632
678
|
const current = state(sid)
|
|
@@ -646,7 +692,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
646
692
|
return
|
|
647
693
|
}
|
|
648
694
|
|
|
649
|
-
if (!shouldQueue(current)) {
|
|
695
|
+
if ((request.modifier === "now" && op.kind !== "shell") || !shouldQueue(current)) {
|
|
650
696
|
if (op.kind === "command") return
|
|
651
697
|
if (op.kind === "compact") {
|
|
652
698
|
await persist(sid, placeholder, () => undefined)
|
|
@@ -708,7 +754,6 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
708
754
|
},
|
|
709
755
|
}
|
|
710
756
|
|
|
711
|
-
for (const [sid, current] of sessions) if (current.items.length && !current.stopped) setTimeout(() => advance(sid), 0)
|
|
712
757
|
return hooks
|
|
713
758
|
}
|
|
714
759
|
|
package/package.json
CHANGED