Remove scan queue and enable parallel plugin scans
Remove the scan queue mechanism that serialized plugin scans. Each scan now runs in its own temporary chat context immediately upon request, allowing multiple scans to execute in parallel. Update UI to reflect that scans are no longer queued and remove the "queued" state tracking from store and API.
frdel committed
Mar 28, 2026 at 18:52 UTC
1eb78607c9adaa36db929dda11e5f1359d02714e
4 files changed
+16
-54
plugins/_plugin_scan/README.md
+6
-4
@@ -11,11 +11,13 @@ This plugin builds a structured scanning prompt from a selectable checklist, run
11
- **Prompt-driven scan**
12
- Loads scan checks and a markdown prompt template from the plugin's `webui/` assets.
13
- **Temporary scan context**
14
- - Creates a temporary chat context, sends the generated prompt as a user message, waits for the model result, and then removes the chat.
14
+ - Creates a temporary chat context, logs the generated prompt into it, starts the agent immediately, and waits for the model result.
15
+- **Parallel-friendly execution**
16
+ - Each scan runs in its own chat context; the plugin does not serialize scans behind a "wait for another scan" queue.
17
- **Selectable checks**
18
- Supports scanning all checks by default or only the subset selected by the caller.
19
- **UI integration**
18
- - Includes API endpoints and web UI files for queueing, starting, and running scans.
20
+ - Includes API endpoints and web UI files for logging the prompt, starting the scan, and running scans synchronously.
21
22
## Key Files
23
@@ -24,8 +26,8 @@ This plugin builds a structured scanning prompt from a selectable checklist, run
26
- **Prompt builder**
27
- `helpers/prompt.py` loads check definitions and renders the final scan prompt.
28
- **Additional APIs**
27
- - `api/plugin_scan_queue.py`
28
- - `api/plugin_scan_start.py`
29
+ - `api/plugin_scan_queue.py` logs the prompt into the temporary chat.
30
+ - `api/plugin_scan_start.py` starts the agent in that chat.
31
32
## Configuration Scope
33
plugins/_plugin_scan/api/plugin_scan_queue.py
+1
-5
@@ -4,12 +4,11 @@ from helpers import message_queue as mq
4
5
6
class PluginScanQueue(ApiHandler):
7
- """Log the scan prompt into a chat. Optionally set progress to 'Queued'."""
7
+ """Log the scan prompt into a chat before the scan starts."""
8
9
async def process(self, input: Input, request: Request) -> Output:
10
ctxid: str = input.get("context", "")
11
text: str = input.get("text", "")
12
- queued: bool = input.get("queued", False)
12
13
if not ctxid or not text:
14
return Response("Missing 'context' or 'text'.", 400)
@@ -20,7 +19,4 @@ class PluginScanQueue(ApiHandler):
19
20
mq.log_user_message(context, text, [])
21
23
- if queued:
24
- context.log.set_progress("icon://hourglass_empty Queued - waiting for another scan to finish", 0, True)
25
-
22
return {"ok": True, "context": ctxid}
plugins/_plugin_scan/webui/plugin-scan-store.js
+6
-40
@@ -67,10 +67,6 @@ function formatRatingIcons(ratings) {
67
return Object.values(ratings).map((r) => r.icon).join("/");
68
}
69
let _pollGen = 0;
70
-/** @type {{ gen: number, ctxId: string, prompt: string }[]} */
71
-let _queue = [];
72
-/** @type {{ gen: number, ctxId: string } | null} */
73
-let _running = null;
70
const POLL_INTERVAL = 2000;
71
const MAX_POLL_MS = 10 * 60 * 1000;
72
const SCAN_TITLE = "Plugin Scanner";
@@ -86,7 +82,6 @@ export const store = createStore("pluginScan", {
82
prompt: "",
83
output: "",
84
scanning: false,
89
- queued: false,
85
scanCtxId: "",
86
87
get renderedOutput() {
@@ -105,7 +100,6 @@ export const store = createStore("pluginScan", {
100
async onOpen(url) {
101
this.output = "";
102
this.scanning = false;
108
- this.queued = false;
103
if (url) this.gitUrl = url;
104
const cfg = await loadConfig();
105
if (cfg && Object.keys(this.checks).length === 0) {
@@ -171,11 +165,7 @@ export const store = createStore("pluginScan", {
165
}
166
},
167
174
- /**
175
- * Create a context immediately and either execute or queue the scan.
176
- * Queued scans have their prompt logged to the chat + progress bar set to "Queued",
177
- * but the agent is NOT started until it's their turn.
178
- */
168
+ /** Create a fresh context, log the prompt into it, and start the scan immediately. */
169
async runScan() {
170
if (!this.gitUrl.trim()) {
171
void toastFrontendError("Please enter a Git URL", SCAN_TITLE);
@@ -198,26 +188,15 @@ export const store = createStore("pluginScan", {
188
}
189
this.scanCtxId = ctxId;
190
201
- if (_running) {
202
- try {
203
- await api.callJsonApi("/plugins/_plugin_scan/plugin_scan_queue", { context: ctxId, text: capturedPrompt, queued: true });
204
- } catch { /* best-effort */ }
205
- _queue.push({ gen, ctxId, prompt: capturedPrompt });
206
- this.queued = true;
207
- this.scanning = false;
208
- } else {
209
- try {
210
- await api.callJsonApi("/plugins/_plugin_scan/plugin_scan_queue", { context: ctxId, text: capturedPrompt });
211
- } catch { /* best-effort */ }
212
- this.queued = false;
213
- this.scanning = true;
214
- this._runNext(gen, ctxId, capturedPrompt);
215
- }
191
+ try {
192
+ await api.callJsonApi("/plugins/_plugin_scan/plugin_scan_queue", { context: ctxId, text: capturedPrompt });
193
+ } catch { /* best-effort */ }
194
+ this.scanning = true;
195
+ this._runNext(gen, ctxId, capturedPrompt);
196
},
197
198
/** @param {number} gen @param {string} ctxId @param {string} prompt */
199
async _runNext(gen, ctxId, prompt) {
220
- _running = { gen, ctxId };
200
try {
201
await api.callJsonApi("/plugins/_plugin_scan/plugin_scan_start", { text: prompt, context: ctxId });
202
await this._pollLoop(gen, ctxId);
@@ -225,19 +204,6 @@ export const store = createStore("pluginScan", {
204
if (gen === _pollGen) {
205
void toastFrontendError(`Scan failed: ${formatErrorMessage(e)}`, SCAN_TITLE);
206
this.scanning = false;
228
- this.queued = false;
229
- }
230
- } finally {
231
- _running = null;
232
- while (_queue.length) {
233
- const next = /** @type {{ gen: number, ctxId: string, prompt: string }} */ (_queue.shift());
234
- if (!next || next.gen !== _pollGen) {
235
- continue;
236
- }
237
- this.queued = false;
238
- this.scanning = true;
239
- this._runNext(next.gen, next.ctxId, next.prompt);
240
- break;
207
}
208
}
209
},
plugins/_plugin_scan/webui/plugin-scan.html
+3
-5
@@ -40,11 +40,9 @@
40
<!-- Actions -->
41
<div class="scan-actions">
42
<button class="button" @click="$store.pluginScan.copyPrompt()">Copy Prompt</button>
43
- <button class="button confirm" @click="$store.pluginScan.runScan()"
44
- :disabled="$store.pluginScan.scanning || $store.pluginScan.queued">
45
- <span x-show="$store.pluginScan.queued"><span class="scan-spinner"></span>Queued…</span>
46
- <span x-show="$store.pluginScan.scanning && !$store.pluginScan.queued"><span class="scan-spinner"></span>Scanning…</span>
47
- <span x-show="!$store.pluginScan.scanning && !$store.pluginScan.queued">Run Scan</span>
43
+ <button class="button confirm" @click="$store.pluginScan.runScan()">
44
+ <span x-show="$store.pluginScan.scanning"><span class="scan-spinner"></span>Run Another Scan</span>
45
+ <span x-show="!$store.pluginScan.scanning">Run Scan</span>
46
</button>
47
<button class="button" @click="$store.pluginScan.openChatInNewWindow()"
48
x-show="$store.pluginScan.scanCtxId"