Merge ba88359371972d590e6d8ec7fe9503a18b294026 into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Andy Tien 2026-03-21 10:05:00 +08:00 committed by GitHub
commit 159ce970ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 129 additions and 3 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

@ -9,6 +9,7 @@
.chat-main {
min-width: 400px;
min-height: 0; /* Prevent flex overflow that can hide input area */
display: flex;
flex-direction: column;
overflow: hidden;

View File

@ -0,0 +1,105 @@
/**
* Tests for #45707 - WebChat empty state overlay blocking input
*
* This test ensures that sessions with tool-call messages (heartbeat/cron),
* streaming content, or live streams are not treated as empty, preventing
* the welcome overlay from blocking the input box.
*/
import { describe, it, expect } from "vitest";
describe("chat empty state logic (#45707)", () => {
it("should not treat session with tool messages as empty", () => {
const messages: unknown[] = [];
const toolMessages: unknown[] = [{ role: "tool", content: "Heartbeat", timestamp: Date.now() }];
const streamSegments: Array<{ text: string; ts: number }> = [];
const stream: string | null = null;
const hasSessionActivity =
messages.length > 0 ||
toolMessages.length > 0 ||
streamSegments.some((segment) => segment.text.trim()) ||
stream !== null;
expect(hasSessionActivity).toBe(true);
});
it("should treat truly empty session as empty", () => {
const messages: unknown[] = [];
const toolMessages: unknown[] = [];
const streamSegments: Array<{ text: string; ts: number }> = [];
const stream: string | null = null;
const hasSessionActivity =
messages.length > 0 ||
toolMessages.length > 0 ||
streamSegments.some((segment) => segment.text.trim()) ||
stream !== null;
expect(hasSessionActivity).toBe(false);
});
it("should not treat session with history messages as empty", () => {
const messages: unknown[] = [{ role: "user", content: "Hello", timestamp: Date.now() }];
const toolMessages: unknown[] = [];
const streamSegments: Array<{ text: string; ts: number }> = [];
const stream: string | null = null;
const hasSessionActivity =
messages.length > 0 ||
toolMessages.length > 0 ||
streamSegments.some((segment) => segment.text.trim()) ||
stream !== null;
expect(hasSessionActivity).toBe(true);
});
it("should not treat session with streaming content as empty", () => {
const messages: unknown[] = [];
const toolMessages: unknown[] = [];
const streamSegments: Array<{ text: string; ts: number }> = [
{ text: "Streaming response...", ts: Date.now() },
];
const stream: string | null = null;
const hasSessionActivity =
messages.length > 0 ||
toolMessages.length > 0 ||
streamSegments.some((segment) => segment.text.trim()) ||
stream !== null;
expect(hasSessionActivity).toBe(true);
});
it("should not treat session with live stream as empty", () => {
const messages: unknown[] = [];
const toolMessages: unknown[] = [];
const streamSegments: Array<{ text: string; ts: number }> = [];
const stream: string | null = "Live streaming...";
const hasSessionActivity =
messages.length > 0 ||
toolMessages.length > 0 ||
streamSegments.some((segment) => segment.text.trim()) ||
stream !== null;
expect(hasSessionActivity).toBe(true);
});
it("should handle mixed messages and streaming", () => {
const messages: unknown[] = [{ role: "user", content: "Check", timestamp: Date.now() }];
const toolMessages: unknown[] = [{ role: "tool", content: "Cron", timestamp: Date.now() }];
const streamSegments: Array<{ text: string; ts: number }> = [
{ text: "Thinking...", ts: Date.now() },
];
const stream: string | null = null;
const hasSessionActivity =
messages.length > 0 ||
toolMessages.length > 0 ||
streamSegments.some((segment) => segment.text.trim()) ||
stream !== null;
expect(hasSessionActivity).toBe(true);
});
});

View File

@ -850,7 +850,16 @@ export function renderChat(props: ChatProps) {
};
const chatItems = buildChatItems(props);
const isEmpty = chatItems.length === 0 && !props.loading;
// Fix #45707: Check for any session activity, not just renderable messages
// Includes tool-call messages (heartbeat/cron), streaming content, and live streams
const hasSessionActivity =
(Array.isArray(props.messages) && props.messages.length > 0) ||
(Array.isArray(props.toolMessages) && props.toolMessages.length > 0) ||
(Array.isArray(props.streamSegments) &&
props.streamSegments.some((segment) => segment.text.trim())) ||
props.stream !== null;
const showWelcomeState = !hasSessionActivity && !props.loading && !vs.searchOpen;
const showEmptySearch = chatItems.length === 0 && !props.loading && vs.searchOpen;
const thread = html`
<div
@ -893,9 +902,9 @@ export function renderChat(props: ChatProps) {
`
: nothing
}
${isEmpty && !vs.searchOpen ? renderWelcomeState(props) : nothing}
${showWelcomeState ? renderWelcomeState(props) : nothing}
${
isEmpty && vs.searchOpen
showEmptySearch
? html`
<div class="agent-chat__empty">No matching messages</div>
`