From c34ed90822a9c3f145db68a5072218668b039eaa Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Thu, 30 Apr 2026 01:02:14 -0500 Subject: [PATCH] fix(control-ui): disable refresh during active runs Disable the Control UI refresh button while chat is disconnected, loading, sending, running, or streaming. This prevents manual chat-history refresh from racing active run/stream state and adds browser render coverage for the disabled-state matrix. Closes #65522. Validation: - Exact PR head `1511a086614a727fc4200730e7ad9622134bb7d3` reached `CLEAN` merge state. - GitHub CI for the exact head completed with no failed or pending checks. --- ui/src/ui/app-render.helpers.browser.test.ts | 27 ++++++++++++++++++++ ui/src/ui/app-render.helpers.ts | 8 +++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ui/src/ui/app-render.helpers.browser.test.ts b/ui/src/ui/app-render.helpers.browser.test.ts index bbb2aec7062..fdb83310b53 100644 --- a/ui/src/ui/app-render.helpers.browser.test.ts +++ b/ui/src/ui/app-render.helpers.browser.test.ts @@ -15,6 +15,9 @@ function createState(overrides: Partial = {}) { return { connected: true, chatLoading: false, + chatRunId: null, + chatSending: false, + chatStream: null, onboarding: false, sessionKey: "main", sessionsHideCron: true, @@ -49,6 +52,17 @@ function createState(overrides: Partial = {}) { } as unknown as AppViewState; } +function renderRefreshButton(overrides: Partial = {}) { + const container = document.createElement("div"); + render(renderChatControls(createState(overrides)), container); + + const button = container.querySelector( + `.chat-controls .btn--icon[data-tooltip="${t("chat.refreshTitle")}"]`, + ); + expect(button).not.toBeNull(); + return button!; +} + describe("chat header controls (browser)", () => { it("renders explicit hover tooltip metadata for the top-right action buttons", async () => { const container = document.createElement("div"); @@ -76,6 +90,19 @@ describe("chat header controls (browser)", () => { } }); + it.each([ + ["connected and idle", {}, false], + ["chat history loading", { chatLoading: true }, true], + ["chat send in flight", { chatSending: true }, true], + ["active run", { chatRunId: "run-123" }, true], + ["active stream", { chatStream: "streaming" }, true], + ["disconnected", { connected: false }, true], + ] as const)("sets refresh disabled state while %s", (_name, overrides, disabled) => { + const button = renderRefreshButton(overrides); + + expect(button.disabled).toBe(disabled); + }); + it("renders the cron session filter in the mobile dropdown controls", async () => { const state = createState({ sessionsResult: { diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 4f7c6b9ea47..8faa31b053b 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -222,6 +222,12 @@ export function renderChatControls(state: AppViewState) { ? t("chat.showCronSessionsHidden", { count: String(hiddenCronCount) }) : t("chat.showCronSessions") : t("chat.hideCronSessions"); + const refreshDisabled = + !state.connected || + state.chatLoading || + state.chatSending || + Boolean(state.chatRunId) || + state.chatStream !== null; const toolCallsIcon = html`