opencode-queue 0.12.2 → 0.13.1
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 +133 -59
- 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 globally. |
|
|
76
|
+
| `/queue always on` | Enable automatic queueing in every project. |
|
|
77
|
+
| `/queue always off` | Disable automatic queueing in every project. |
|
|
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 with their previous running or stopped state after OpenCode restarts or crashes. 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.
|
|
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 every OpenCode project. 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
|
@@ -2,15 +2,15 @@ import type { Plugin } from "@opencode-ai/plugin"
|
|
|
2
2
|
import type { AgentPartInput, FilePart, FilePartInput, SubtaskPartInput, TextPart, TextPartInput } from "@opencode-ai/sdk"
|
|
3
3
|
import { HttpServerResponse } from "effect/unstable/http"
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto"
|
|
5
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
5
|
+
import { link, 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 ControlOp =
|
|
37
|
+
| { kind: "list" }
|
|
38
|
+
| { kind: "clear"; indices: number[] }
|
|
39
|
+
| { kind: "flush" }
|
|
40
|
+
| { kind: "start" }
|
|
41
|
+
| { kind: "stop" }
|
|
42
|
+
| { kind: "always"; enabled?: boolean }
|
|
43
|
+
|
|
36
44
|
type Activity = { kind: "idle" } | { kind: "restored" } | { kind: "busy" } | { kind: "sending"; idle: boolean; items: Item[] }
|
|
37
45
|
type State = { items: Item[]; activity: Activity; stopped: boolean; failed: boolean; hidden: Set<string> }
|
|
38
|
-
type
|
|
46
|
+
type Draft = Pick<State, "items" | "stopped" | "hidden">
|
|
39
47
|
type Store = { version: 1; projectID: string; sessions: Record<string, { items: Item[]; stopped: boolean; hidden: string[] }> }
|
|
40
48
|
type Placeholder = { id: string; part: TextPart }
|
|
41
49
|
|
|
42
50
|
type Op =
|
|
43
|
-
|
|
|
44
|
-
| { kind: "clear"; indices: number[] }
|
|
45
|
-
| { kind: "flush" }
|
|
46
|
-
| { kind: "start" }
|
|
47
|
-
| { kind: "stop" }
|
|
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
|
|
@@ -192,14 +197,41 @@ const dataHome = () => {
|
|
|
192
197
|
return join(homedir(), ".local", "share")
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
const writeJson = async (path: string, value: unknown, commit = rename) => {
|
|
201
|
+
await mkdir(dirname(path), { recursive: true })
|
|
202
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`
|
|
203
|
+
try {
|
|
204
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
|
|
205
|
+
await commit(temporary, path)
|
|
206
|
+
} finally {
|
|
207
|
+
await rm(temporary, { force: true }).catch((error) => console.warn("QueuePlugin failed to remove temporary storage", error))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
195
211
|
export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
196
212
|
const sessions = new Map<string, State>()
|
|
197
213
|
const deleted = new Set<string>()
|
|
198
214
|
const enqueueTurns = new Map<string, Promise<unknown>>()
|
|
215
|
+
const internalCommands: { sid: string; command: string; args: string; used: boolean }[] = []
|
|
216
|
+
const origin = randomUUID()
|
|
199
217
|
const post = (client as unknown as { _client?: { post?: Post } })._client?.post
|
|
200
|
-
const
|
|
218
|
+
const root = join(dataHome(), "opencode", "opencode-queue")
|
|
219
|
+
const path = join(root, `${createHash("sha256").update(project.id).digest("hex")}.json`)
|
|
220
|
+
const settingsPath = join(root, "settings.json")
|
|
201
221
|
let writes = Promise.resolve()
|
|
202
222
|
|
|
223
|
+
const readAlways = async () => {
|
|
224
|
+
try {
|
|
225
|
+
const parsed: unknown = JSON.parse(await readFile(settingsPath, "utf8"))
|
|
226
|
+
if (record(parsed) && typeof parsed.always === "boolean") return parsed.always
|
|
227
|
+
console.warn("QueuePlugin ignored invalid global settings", settingsPath)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (!record(error) || error.code !== "ENOENT") console.error("QueuePlugin failed to load global settings", error)
|
|
230
|
+
}
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
let legacyAlways = false
|
|
234
|
+
|
|
203
235
|
try {
|
|
204
236
|
const parsed: unknown = JSON.parse(await readFile(path, "utf8"))
|
|
205
237
|
if (!record(parsed) || parsed.version !== 1 || parsed.projectID !== project.id || !record(parsed.sessions)) {
|
|
@@ -218,12 +250,19 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
218
250
|
const activity: Activity = { kind: items.length && !value.stopped ? "restored" : "idle" }
|
|
219
251
|
if (items.length || value.stopped || hidden.size) sessions.set(sid, { items, activity, stopped: value.stopped, failed: false, hidden })
|
|
220
252
|
}
|
|
253
|
+
legacyAlways = parsed.always === true
|
|
221
254
|
}
|
|
222
255
|
} catch (error) {
|
|
223
256
|
if (!record(error) || error.code !== "ENOENT") console.error("QueuePlugin failed to load queue storage", error)
|
|
224
257
|
}
|
|
225
|
-
|
|
226
|
-
|
|
258
|
+
if (legacyAlways) {
|
|
259
|
+
try {
|
|
260
|
+
await writeJson(settingsPath, { always: true }, link)
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (!record(error) || error.code !== "EEXIST") throw error
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const save = async (sid?: string, draft?: Draft) => {
|
|
227
266
|
const stored: Store = { version: 1, projectID: project.id, sessions: {} }
|
|
228
267
|
for (const [id, current] of sessions) {
|
|
229
268
|
if (deleted.has(id) || (id === sid && !draft)) continue
|
|
@@ -231,15 +270,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
231
270
|
const items = current.activity.kind === "sending" ? [...current.activity.items, ...durable.items] : durable.items
|
|
232
271
|
if (items.length || durable.stopped || durable.hidden.size) stored.sessions[id] = { items, stopped: durable.stopped, hidden: [...durable.hidden] }
|
|
233
272
|
}
|
|
234
|
-
|
|
235
|
-
await mkdir(dirname(path), { recursive: true })
|
|
236
|
-
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`
|
|
237
|
-
try {
|
|
238
|
-
await writeFile(temporary, contents, { mode: 0o600 })
|
|
239
|
-
await rename(temporary, path)
|
|
240
|
-
} finally {
|
|
241
|
-
await rm(temporary, { force: true }).catch((error) => console.warn("QueuePlugin failed to remove temporary queue storage", error))
|
|
242
|
-
}
|
|
273
|
+
await writeJson(path, stored)
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
const serialize = <T>(action: () => Promise<T>) => {
|
|
@@ -257,7 +288,9 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
257
288
|
return current
|
|
258
289
|
}
|
|
259
290
|
|
|
260
|
-
const
|
|
291
|
+
const automaticallyQueue = async (sid: string) => shouldQueue(sessions.get(sid)) && (await readAlways())
|
|
292
|
+
|
|
293
|
+
const store = async (sid: string, current: State, draft: Draft, placeholder?: Placeholder) => {
|
|
261
294
|
try {
|
|
262
295
|
await save(sid, draft)
|
|
263
296
|
} catch (error) {
|
|
@@ -270,11 +303,11 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
270
303
|
if (placeholder) Object.assign(placeholder.part, { text: "", synthetic: true, ignored: true })
|
|
271
304
|
}
|
|
272
305
|
|
|
273
|
-
const persist = <T>(sid: string, placeholder: Placeholder | undefined, mutate: (draft:
|
|
306
|
+
const persist = <T>(sid: string, placeholder: Placeholder | undefined, mutate: (draft: Draft) => T) =>
|
|
274
307
|
serialize(async () => {
|
|
275
308
|
if (deleted.has(sid)) throw new Error(`QueuePlugin cannot persist queue state for deleted session ${sid}`)
|
|
276
309
|
const current = state(sid)
|
|
277
|
-
const draft:
|
|
310
|
+
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden) }
|
|
278
311
|
if (placeholder) draft.hidden.add(placeholder.id)
|
|
279
312
|
const value = mutate(draft)
|
|
280
313
|
await store(sid, current, draft, placeholder)
|
|
@@ -364,6 +397,21 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
364
397
|
throwOnError: true,
|
|
365
398
|
})
|
|
366
399
|
|
|
400
|
+
const markInternal = (parts: { type: string; metadata?: Record<string, unknown> }[]) => {
|
|
401
|
+
const text = parts.find((part) => part.type === "text")
|
|
402
|
+
if (text) text.metadata = { ...text.metadata, [INTERNAL]: origin }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const callCommand = async <T>(sid: string, command: string, args: string, call: () => Promise<T>) => {
|
|
406
|
+
const pending = { sid, command, args, used: false }
|
|
407
|
+
internalCommands.push(pending)
|
|
408
|
+
try {
|
|
409
|
+
return await call()
|
|
410
|
+
} finally {
|
|
411
|
+
internalCommands.splice(internalCommands.indexOf(pending), 1)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
367
415
|
const replay = async (sid: string, item: Item) => {
|
|
368
416
|
switch (item.kind) {
|
|
369
417
|
case "shell":
|
|
@@ -371,19 +419,22 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
371
419
|
case "compact":
|
|
372
420
|
return compact(sid, item.info)
|
|
373
421
|
case "command":
|
|
374
|
-
return
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
422
|
+
return callCommand(sid, item.cmd, item.args, () =>
|
|
423
|
+
client.session.command({
|
|
424
|
+
path: { id: sid },
|
|
425
|
+
body: {
|
|
426
|
+
...opts(item.info),
|
|
427
|
+
model: `${item.info.model.providerID}/${item.info.model.modelID}`,
|
|
428
|
+
command: item.cmd,
|
|
429
|
+
arguments: item.args,
|
|
430
|
+
parts: item.files,
|
|
431
|
+
} as any,
|
|
432
|
+
throwOnError: true,
|
|
433
|
+
}),
|
|
434
|
+
)
|
|
385
435
|
case "prompt": {
|
|
386
436
|
const parts = item.parts.map((part) => ({ ...part, id: undefined }))
|
|
437
|
+
markInternal(parts)
|
|
387
438
|
return client.session.prompt({ path: { id: sid }, body: { ...opts(item.info), parts } as any, throwOnError: true })
|
|
388
439
|
}
|
|
389
440
|
}
|
|
@@ -420,7 +471,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
420
471
|
|
|
421
472
|
const items = current.items.slice(0, count)
|
|
422
473
|
if (placeholder) {
|
|
423
|
-
const draft:
|
|
474
|
+
const draft: Draft = { items: [...current.items], stopped: current.stopped, hidden: new Set(current.hidden).add(placeholder.id) }
|
|
424
475
|
await store(sid, current, draft, placeholder)
|
|
425
476
|
if (deleted.has(sid)) return undefined
|
|
426
477
|
}
|
|
@@ -488,6 +539,16 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
488
539
|
return result.failed ? `${message}; ${result.failed} failed` : message
|
|
489
540
|
}
|
|
490
541
|
|
|
542
|
+
if (op.kind === "always") {
|
|
543
|
+
const enabled = await serialize(async () => {
|
|
544
|
+
if (op.enabled === undefined) return readAlways()
|
|
545
|
+
await writeJson(settingsPath, { always: op.enabled })
|
|
546
|
+
return op.enabled
|
|
547
|
+
})
|
|
548
|
+
if (placeholder) await persist(sid, placeholder, () => undefined)
|
|
549
|
+
return `Always queue is ${enabled ? "on" : "off"} globally`
|
|
550
|
+
}
|
|
551
|
+
|
|
491
552
|
const message = await persist(sid, placeholder, (draft) => {
|
|
492
553
|
switch (op.kind) {
|
|
493
554
|
case "list": {
|
|
@@ -514,7 +575,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
514
575
|
const hooks: Awaited<ReturnType<Plugin>> = {
|
|
515
576
|
config: async (cfg) => {
|
|
516
577
|
cfg.command ??= {}
|
|
517
|
-
cfg.command.queue = { template: "", description: "Queue input until the session is idle" }
|
|
578
|
+
cfg.command.q = cfg.command.queue = { template: "", description: "Queue input until the session is idle" }
|
|
518
579
|
},
|
|
519
580
|
event: async ({ event }) => {
|
|
520
581
|
if (plan(event)) {
|
|
@@ -576,8 +637,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
576
637
|
const body = input.arguments ?? ""
|
|
577
638
|
const parts = files(output.parts)
|
|
578
639
|
|
|
579
|
-
|
|
580
|
-
|
|
640
|
+
const internal = internalCommands.find((pending) => !pending.used && pending.sid === sid && pending.command === input.command && pending.args === body)
|
|
641
|
+
if (internal) {
|
|
642
|
+
internal.used = true
|
|
643
|
+
markInternal(output.parts)
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!isQueue(input.command)) {
|
|
648
|
+
const queued = parseSuffix(body) ?? ((await automaticallyQueue(sid)) ? { body } : undefined)
|
|
581
649
|
if (!queued) return
|
|
582
650
|
|
|
583
651
|
if (!shouldQueue(sessions.get(sid))) {
|
|
@@ -585,16 +653,17 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
585
653
|
return
|
|
586
654
|
}
|
|
587
655
|
|
|
588
|
-
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)
|
|
656
|
+
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)
|
|
589
657
|
return
|
|
590
658
|
}
|
|
591
659
|
|
|
592
|
-
const
|
|
660
|
+
const request = parsePrefix(body)
|
|
661
|
+
const op = parse(request, parts.length)
|
|
593
662
|
|
|
594
663
|
if (control(op)) return stop(await afterEnqueue(sid, () => manage(sid, op)))
|
|
595
664
|
if (op.kind === "invalid") return stop(op.message, "error")
|
|
596
665
|
|
|
597
|
-
if (!shouldQueue(sessions.get(sid))) {
|
|
666
|
+
if (!shouldQueue(sessions.get(sid)) || (request.modifier === "now" && op.kind !== "prompt" && op.kind !== "shell")) {
|
|
598
667
|
if (op.kind === "shell") {
|
|
599
668
|
await shell(sid, op.shell, await run(sid))
|
|
600
669
|
return handled()
|
|
@@ -606,7 +675,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
606
675
|
}
|
|
607
676
|
|
|
608
677
|
if (op.kind === "command") {
|
|
609
|
-
await client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any })
|
|
678
|
+
await callCommand(sid, op.cmd, op.args, () => client.session.command({ path: { id: sid }, body: { command: op.cmd, arguments: op.args, parts } as any }))
|
|
610
679
|
return handled()
|
|
611
680
|
}
|
|
612
681
|
|
|
@@ -622,10 +691,15 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
622
691
|
console.warn("QueuePlugin ignored input for a deleted session", sid)
|
|
623
692
|
return
|
|
624
693
|
}
|
|
694
|
+
const internal = output.parts.find((part): part is TextPart => part.type === "text" && part.metadata?.[INTERNAL] === origin)
|
|
695
|
+
if (internal) {
|
|
696
|
+
delete internal.metadata![INTERNAL]
|
|
697
|
+
return
|
|
698
|
+
}
|
|
625
699
|
const text = output.parts.find((part): part is TextPart => part.type === "text" && !part.synthetic)
|
|
626
700
|
if (!text) return
|
|
627
701
|
|
|
628
|
-
const request = parseInput(text.text)
|
|
702
|
+
const request = parseInput(text.text) ?? ((await automaticallyQueue(sid)) ? { body: text.text } : undefined)
|
|
629
703
|
if (!request) return
|
|
630
704
|
|
|
631
705
|
const current = state(sid)
|
|
@@ -645,7 +719,7 @@ export const QueuePlugin: Plugin = async ({ client, project, directory }) => {
|
|
|
645
719
|
return
|
|
646
720
|
}
|
|
647
721
|
|
|
648
|
-
if (!shouldQueue(current)) {
|
|
722
|
+
if ((request.modifier === "now" && op.kind !== "shell") || !shouldQueue(current)) {
|
|
649
723
|
if (op.kind === "command") return
|
|
650
724
|
if (op.kind === "compact") {
|
|
651
725
|
await persist(sid, placeholder, () => undefined)
|
package/package.json
CHANGED