fix(web): queue structured prompts
ci / web (pull_request) Successful in 47s
ci / flake (pull_request) Successful in 1m17s
ci / rust (pull_request) Successful in 9m35s

This commit is contained in:
Miguel Palhas
2026-09-07 10:42:49 +01:00
parent 0dc10ffe76
commit 43bfda5a60
4 changed files with 107 additions and 23 deletions
+43 -8
View File
@@ -108,6 +108,7 @@ export function createStructuredConnection(
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7 5 5 5-5 5M12 10H3"/></svg>
</button>
<button type="button" class="btn structured-cancel" hidden>Cancel turn</button>
<span class="structured-queue" role="status" aria-live="polite" hidden></span>
</form>
</div>
</div>
@@ -127,7 +128,9 @@ export function createStructuredConnection(
const commandMenu = el.querySelector(".structured-command-menu") as HTMLDivElement;
const input = el.querySelector(".structured-input") as HTMLTextAreaElement;
const sendBtn = el.querySelector(".structured-send") as HTMLButtonElement;
const sendLabel = sendBtn.querySelector("span") as HTMLSpanElement;
const cancelBtn = el.querySelector(".structured-cancel") as HTMLButtonElement;
const queueEl = el.querySelector(".structured-queue") as HTMLSpanElement;
disconnectBtn.addEventListener("click", () => opts.onRequestClose?.(sessionId));
// Mobile-only (CSS hides it on desktop, where the sidebar is always in
@@ -165,6 +168,8 @@ export function createStructuredConnection(
let disposed = false;
let optimisticTurnStartedAt: number | null = null;
let activityTimer: number | null = null;
const promptQueue: string[] = [];
let promptRequestPending = false;
// Within 64px, wheel and touch imprecision should still count as following.
const FOLLOW_BOTTOM_THRESHOLD_PX = 64;
@@ -529,11 +534,15 @@ export function createStructuredConnection(
function renderComposerState(): void {
const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null;
const projectedActive = state.turn.responding || state.turn.thinking;
input.disabled = active;
sendBtn.disabled = active;
sendBtn.hidden = projectedActive;
input.disabled = false;
sendBtn.disabled = false;
sendBtn.hidden = false;
sendLabel.textContent = active ? "Queue" : "Send";
cancelBtn.hidden = !projectedActive;
if (active) closeCommandMenu();
queueEl.hidden = promptQueue.length === 0;
queueEl.textContent = promptQueue.length === 1
? "1 message queued"
: `${promptQueue.length} messages queued`;
renderActivity();
}
@@ -595,6 +604,7 @@ export function createStructuredConnection(
}
renderConfig();
renderComposerState();
dispatchQueuedPrompt();
}
function closeConfigMenu(): void {
@@ -794,21 +804,20 @@ export function createStructuredConnection(
if (!commandMenu.contains(document.activeElement)) closeCommandMenu();
}, 0);
});
composerForm.addEventListener("submit", (e) => {
e.preventDefault();
const text = input.value;
if (!text.trim() || input.disabled) return;
function dispatchPrompt(text: string): void {
const generation = ++submissionGeneration;
const submissionSocketGeneration = socketGeneration;
awaitingBeginGeneration = generation;
optimisticTurnStartedAt = Date.now();
acceptedSubmission = null;
promptRequestPending = true;
input.value = "";
autoResize();
clearError();
renderComposerState();
sendStructuredPrompt(sessionId, text).then(
() => {
promptRequestPending = false;
if (
disposed
|| generation !== submissionGeneration
@@ -819,8 +828,11 @@ export function createStructuredConnection(
// A replacement socket may have captured idle before the POST claimed
// the prompt. Reconnect again so its snapshot cannot predate the 204.
if (acceptanceSocketGeneration > submissionSocketGeneration) reconnectForReconciliation();
renderComposerState();
dispatchQueuedPrompt();
},
(e: unknown) => {
promptRequestPending = false;
if (disposed || generation !== submissionGeneration) return;
if (awaitingBeginGeneration === generation) awaitingBeginGeneration = null;
if (input.value === "") {
@@ -831,6 +843,29 @@ export function createStructuredConnection(
renderComposerState();
},
);
}
function dispatchQueuedPrompt(): void {
const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null;
if (disposed || active || promptRequestPending || promptQueue.length === 0) return;
dispatchPrompt(promptQueue.shift()!);
}
composerForm.addEventListener("submit", (e) => {
e.preventDefault();
const text = input.value;
if (!text.trim()) return;
const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null;
if (active || promptQueue.length > 0) {
promptQueue.push(text);
input.value = "";
autoResize();
clearError();
renderComposerState();
dispatchQueuedPrompt();
return;
}
dispatchPrompt(text);
});
cancelBtn.addEventListener("click", () => {
clearError();