From 4554e041fd770b74e93f14a1d029bb05a88ad859 Mon Sep 17 00:00:00 2001 From: seekskyworld Date: Fri, 4 Sep 2026 11:12:47 +0800 Subject: [PATCH 1/3] fix(web): preserve running state for queued prompts Signed-off-by: seekskyworld --- web/ui/app.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/web/ui/app.js b/web/ui/app.js index bcff449d..1896bdf2 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -214,6 +214,12 @@ function compactSummary(value, limit = 96) { return text.length > limit ? `${text.slice(0, limit - 1)}…` : text; } +function applyPromptAcceptedState(alreadySettled) { + state.liveRunning = !alreadySettled; + if (alreadySettled) state.livePhase = "idle"; + else if (state.livePhase !== "running") state.livePhase = "preparing"; +} + function relativeTime(value) { const elapsed = Date.now() - new Date(value).getTime(); if (elapsed < 60_000) return "now"; @@ -747,8 +753,7 @@ async function sendPrompt() { }); if (epoch !== state.sessionEpoch || state.promptAdmissionToken !== admissionToken) return; const alreadySettled = state.terminalPromptIds.has(receipt.id); - state.liveRunning = !alreadySettled; - state.livePhase = alreadySettled ? "idle" : "preparing"; + applyPromptAcceptedState(alreadySettled); $("prompt-input").value = ""; resizePrompt(); $("composer-hint").textContent = t("acceptedHint"); @@ -1066,8 +1071,7 @@ function applyRuntimeEvent(event) { scheduleSnapshotRefresh(); } else if (event.type === "prompt_accepted") { const alreadySettled = state.terminalPromptIds.has(event.detail?.commandId); - state.liveRunning = !alreadySettled; - state.livePhase = alreadySettled ? "idle" : "preparing"; + applyPromptAcceptedState(alreadySettled); state.liveRetry = null; renderConversation(); } else if (event.type === "agent_start") { From fec8baa1714052ca035e902f6a850713198a7d3a Mon Sep 17 00:00:00 2001 From: tt-a1i Date: Sun, 6 Sep 2026 00:16:09 +0800 Subject: [PATCH 2/3] fix(web): keep active agents running on handled settlement --- tests/web/app-render.test.ts | 17 +++++++++++++++++ web/ui/app.js | 11 +++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/web/app-render.test.ts b/tests/web/app-render.test.ts index c35a3f88..e285b54c 100644 --- a/tests/web/app-render.test.ts +++ b/tests/web/app-render.test.ts @@ -867,6 +867,23 @@ test("app.js settles an admitted prompt that Pi handles without an agent turn", assert.equal((app.state.terminalPromptIds as Set).size, 32); }); +test("app.js keeps an active agent running when a handled prompt settles", async () => { + const app = await renderApp(); + vm.runInContext( + 'applyRuntimeEvent({sequence: 2, type: "agent_start", detail: {sessionId: "s1"}}); applyRuntimeEvent({sequence: 3, type: "prompt_settled", detail: {sessionId: "s1", commandId: "handled"}})', + app.context as vm.Context, + ); + assert.equal(app.state.liveRunning, true); + assert.equal(app.state.livePhase, "running"); + + vm.runInContext( + 'applyRuntimeEvent({sequence: 4, type: "agent_settled", detail: {sessionId: "s1"}})', + app.context as vm.Context, + ); + assert.equal(app.state.liveRunning, false); + assert.equal(app.state.livePhase, "idle"); +}); + test("app.js scopes model selection to its session epoch", async () => { const app = await renderApp(); const model = deferred>(); diff --git a/web/ui/app.js b/web/ui/app.js index 1896bdf2..b693d841 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -1079,12 +1079,19 @@ function applyRuntimeEvent(event) { state.livePhase = "running"; state.liveRetry = null; renderConversation(); - } else if (event.type === "agent_settled" || event.type === "prompt_settled") { - if (event.type === "prompt_settled") rememberTerminalPrompt(event.detail?.commandId); + } else if (event.type === "agent_settled") { state.liveRunning = false; state.livePhase = "idle"; state.liveRetry = null; renderConversation(); + } else if (event.type === "prompt_settled") { + rememberTerminalPrompt(event.detail?.commandId); + if (state.livePhase !== "running") { + state.liveRunning = false; + state.livePhase = "idle"; + state.liveRetry = null; + } + renderConversation(); } else if (event.detail?.message) { if (event.detail.message.role === "user") { state.liveMessages = state.liveMessages.filter( From a94abed05082ad1b8946819500e0bb983a25d09e Mon Sep 17 00:00:00 2001 From: tt-a1i Date: Sun, 6 Sep 2026 00:19:42 +0800 Subject: [PATCH 3/3] fix(web): preserve active agents across late prompt receipts --- tests/web/app-render.test.ts | 39 ++++++++++++++++++++++++++++++++++-- web/ui/app.js | 12 ++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/tests/web/app-render.test.ts b/tests/web/app-render.test.ts index e285b54c..46848da0 100644 --- a/tests/web/app-render.test.ts +++ b/tests/web/app-render.test.ts @@ -870,20 +870,55 @@ test("app.js settles an admitted prompt that Pi handles without an agent turn", test("app.js keeps an active agent running when a handled prompt settles", async () => { const app = await renderApp(); vm.runInContext( - 'applyRuntimeEvent({sequence: 2, type: "agent_start", detail: {sessionId: "s1"}}); applyRuntimeEvent({sequence: 3, type: "prompt_settled", detail: {sessionId: "s1", commandId: "handled"}})', + 'applyRuntimeEvent({sequence: 2, type: "agent_start", detail: {sessionId: "s1"}}); applyRuntimeEvent({sequence: 3, type: "prompt_settled", detail: {sessionId: "s1", commandId: "handled"}}); applyRuntimeEvent({sequence: 4, type: "prompt_accepted", detail: {sessionId: "s1", commandId: "handled"}})', app.context as vm.Context, ); assert.equal(app.state.liveRunning, true); assert.equal(app.state.livePhase, "running"); vm.runInContext( - 'applyRuntimeEvent({sequence: 4, type: "agent_settled", detail: {sessionId: "s1"}})', + 'applyRuntimeEvent({sequence: 5, type: "agent_settled", detail: {sessionId: "s1"}})', app.context as vm.Context, ); assert.equal(app.state.liveRunning, false); assert.equal(app.state.livePhase, "idle"); }); +test("app.js keeps an active agent running when a queued prompt is accepted", async () => { + const app = await renderApp(); + vm.runInContext( + 'applyRuntimeEvent({sequence: 2, type: "agent_start", detail: {sessionId: "s1"}}); applyRuntimeEvent({sequence: 3, type: "prompt_accepted", detail: {sessionId: "s1", commandId: "queued"}})', + app.context as vm.Context, + ); + assert.equal(app.state.liveRunning, true); + assert.equal(app.state.livePhase, "running"); +}); + +test("app.js keeps an active agent running when its prompt receipt settles late", async () => { + const app = await renderApp(); + const prompt = deferred>(); + app.context.fetch = async (url: unknown) => { + if (String(url) === "/api/prompt") return prompt.promise; + if (String(url).startsWith("/api/snapshot")) return response(SNAPSHOT); + throw new Error(`unexpected request: ${String(url)}`); + }; + const input = app.elements.get("prompt-input"); + assert.ok(input); + input.value = "late receipt"; + const sending = app.sendPrompt(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + vm.runInContext( + 'applyRuntimeEvent({sequence: 2, type: "agent_start", detail: {sessionId: "s1"}}); applyRuntimeEvent({sequence: 3, type: "prompt_settled", detail: {sessionId: "s1", commandId: "late"}})', + app.context as vm.Context, + ); + prompt.resolve(response({ id: "late", accepted: true })); + await sending; + + assert.equal(app.state.liveRunning, true); + assert.equal(app.state.livePhase, "running"); +}); + test("app.js scopes model selection to its session epoch", async () => { const app = await renderApp(); const model = deferred>(); diff --git a/web/ui/app.js b/web/ui/app.js index b693d841..53f020ea 100644 --- a/web/ui/app.js +++ b/web/ui/app.js @@ -215,9 +215,15 @@ function compactSummary(value, limit = 96) { } function applyPromptAcceptedState(alreadySettled) { - state.liveRunning = !alreadySettled; - if (alreadySettled) state.livePhase = "idle"; - else if (state.livePhase !== "running") state.livePhase = "preparing"; + if (alreadySettled) { + if (state.livePhase !== "running") { + state.liveRunning = false; + state.livePhase = "idle"; + } + return; + } + state.liveRunning = true; + if (state.livePhase !== "running") state.livePhase = "preparing"; } function relativeTime(value) {