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"