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.
This commit is contained in:
Val Alexander
2026-04-30 01:02:14 -05:00
committed by GitHub
parent e9d4cb2bb6
commit c34ed90822
2 changed files with 34 additions and 1 deletions

View File

@@ -15,6 +15,9 @@ function createState(overrides: Partial<AppViewState> = {}) {
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<AppViewState> = {}) {
} as unknown as AppViewState;
}
function renderRefreshButton(overrides: Partial<AppViewState> = {}) {
const container = document.createElement("div");
render(renderChatControls(createState(overrides)), container);
const button = container.querySelector<HTMLButtonElement>(
`.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: {

View File

@@ -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`
<svg
width="18"
@@ -275,7 +281,7 @@ export function renderChatControls(state: AppViewState) {
<div class="chat-controls">
<button
class="btn btn--sm btn--icon"
?disabled=${state.chatLoading || !state.connected}
?disabled=${refreshDisabled}
@click=${async () => {
const app = state as unknown as ChatRefreshHost;
app.chatManualRefreshInFlight = true;