fix(ui): prevent empty state overlay from blocking chat input (#45707)

Problem: WebChat empty state covers input box for tool-only sessions
(heartbeat/cron), causing complete session lockout.

Three-layer defense:
1. Fix emptiness logic (check history + tools, not just renderable)
2. Force input z-index: 100 above overlay
3. Disable overlay pointer-events for click-through

Added: chat.empty-state.test.ts (4 test cases)
Fixes #45707
This commit is contained in:
hope 2026-03-14 14:38:20 +08:00
parent 2c5fd8e0c1
commit a9b89e41ab
3 changed files with 60 additions and 1 deletions

View File

@ -363,8 +363,10 @@
box-sizing: border-box;
}
/* Fix #45707: Ensure input always accessible, even when empty state renders */
.agent-chat__input {
position: relative;
z-index: 100; /* Force above empty state overlay */
display: flex;
flex-direction: column;
margin: 0 18px 14px;
@ -903,6 +905,7 @@
}
/* Welcome state (new session) */
/* Fix #45707: Constrain to message area, never cover input */
.agent-chat__welcome {
display: flex;
flex-direction: column;
@ -913,6 +916,14 @@
padding: 48px 24px;
flex: 1;
min-height: 0;
/* Ensure welcome state stays below input box in stacking context */
z-index: 1;
pointer-events: none; /* Allow clicks to pass through to input area */
}
/* Re-enable pointer events on interactive elements within welcome state */
.agent-chat__welcome > * {
pointer-events: auto;
}
.agent-chat__welcome-glow {

View File

@ -0,0 +1,43 @@
/**
* Tests for #45707 - WebChat empty state overlay blocking input
*/
import { describe, it, expect } from "vitest";
describe("chat empty state logic (#45707)", () => {
it("should not treat session with tool messages as empty", () => {
const history: unknown[] = [];
const tools: unknown[] = [{ role: "tool", content: "Heartbeat", timestamp: Date.now() }];
const hasAnyMessages = history.length > 0 || tools.length > 0;
const isEmpty = !hasAnyMessages;
expect(hasAnyMessages).toBe(true);
expect(isEmpty).toBe(false);
});
it("should treat truly empty session as empty", () => {
const history: unknown[] = [];
const tools: unknown[] = [];
const hasAnyMessages = history.length > 0 || tools.length > 0;
const isEmpty = !hasAnyMessages;
expect(hasAnyMessages).toBe(false);
expect(isEmpty).toBe(true);
});
it("should not treat session with history messages as empty", () => {
const history: unknown[] = [{ role: "user", content: "Hello", timestamp: Date.now() }];
const tools: unknown[] = [];
const hasAnyMessages = history.length > 0 || tools.length > 0;
const isEmpty = !hasAnyMessages;
expect(hasAnyMessages).toBe(true);
expect(isEmpty).toBe(false);
});
it("should handle mixed history and tool messages", () => {
const history: unknown[] = [{ role: "user", content: "Check", timestamp: Date.now() }];
const tools: unknown[] = [{ role: "tool", content: "Cron", timestamp: Date.now() }];
const hasAnyMessages = history.length > 0 || tools.length > 0;
const isEmpty = !hasAnyMessages;
expect(hasAnyMessages).toBe(true);
expect(isEmpty).toBe(false);
});
});

View File

@ -850,7 +850,12 @@ export function renderChat(props: ChatProps) {
};
const chatItems = buildChatItems(props);
const isEmpty = chatItems.length === 0 && !props.loading;
// Fix #45707: Check total message count, not just renderable items
// Sessions with tool-call messages (heartbeat/cron) should not be treated as empty
const history = Array.isArray(props.messages) ? props.messages : [];
const tools = Array.isArray(props.toolMessages) ? props.toolMessages : [];
const hasAnyMessages = history.length > 0 || tools.length > 0;
const isEmpty = !hasAnyMessages && !props.loading;
const thread = html`
<div