feat(web-ui): AgentChatPanel component (#505)#515
Conversation
…ng blocks, input bar (#505) Implements the main visual component for interactive agent chat sessions. - All 7 message roles rendered: user, assistant, tool_use, tool_result, thinking, system, error - Tool use cards collapsible (collapsed by default), tool result cards truncate + expand - Streaming cursor indicator on last assistant message while status === "streaming" - Input bar: Enter sends, Shift+Enter inserts newline, auto-grow textarea (max 9rem) - Interrupt button visible only while agent is thinking/streaming - Auto-scroll to bottom on new messages; stops on manual scroll-up - Header: live cost counter, model badge, status dot (green/yellow/red) - Empty state with ArtificialIntelligence01Icon + prompt text - Accessible: role=log aria-live=polite, aria-expanded on collapsibles, aria-labels on buttons - 30 unit tests covering all acceptance criteria - Mocks: ArrowRight01Icon, Alert01Icon added to Hugeicons mock; scrollIntoView polyfill in jest setup
WalkthroughAdds a new client-side AgentChatPanel component with full message rendering and composer behavior, accompanying RTL tests, a barrel export, and small Jest mock/setup updates for icons and scrollIntoView. Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx (1)
162-174: Add a regression test for streaming auto-scroll continuity.Current streaming assertions validate cursor rendering, but they don’t verify scroll-follow behavior when the same last message keeps growing. A rerender-based test here would protect against missed live-tail scrolling.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx` around lines 162 - 174, Add a regression test in AgentChatPanel.test.tsx that verifies auto-scroll continuity during streaming by simulating the same last assistant message growing across rerenders: use makeMessage/makeState to create an initial state with status 'streaming' and a single assistant message, call setupMock, render <AgentChatPanel />, capture the messages/scroll container (via testId used in the component), then call rerender with an updated state where the last message content is longer but still the same message id/status; assert that the scroll container's scrollTop (or that the last message element) is scrolled into view after the rerender. Ensure the test uses rerender from `@testing-library/react` and references AgentChatPanel, setupMock, makeState, and makeMessage so it will protect against breaking live-tail scrolling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web-ui/src/components/sessions/AgentChatPanel.tsx`:
- Around line 209-213: The current useEffect watches messages.length so in-place
updates to the last message (streaming changes to its content) won't trigger
auto-scroll; update the effect to depend on the last message identity/content
instead of just length — e.g., reference messages[messages.length - 1]?.id or
messages[messages.length - 1]?.content in the dependency array — and keep the
body calling bottomRef.current.scrollIntoView({ behavior: 'smooth' }) when
autoScroll is true so streaming updates still cause scrolling.
---
Nitpick comments:
In `@web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx`:
- Around line 162-174: Add a regression test in AgentChatPanel.test.tsx that
verifies auto-scroll continuity during streaming by simulating the same last
assistant message growing across rerenders: use makeMessage/makeState to create
an initial state with status 'streaming' and a single assistant message, call
setupMock, render <AgentChatPanel />, capture the messages/scroll container (via
testId used in the component), then call rerender with an updated state where
the last message content is longer but still the same message id/status; assert
that the scroll container's scrollTop (or that the last message element) is
scrolled into view after the rerender. Ensure the test uses rerender from
`@testing-library/react` and references AgentChatPanel, setupMock, makeState, and
makeMessage so it will protect against breaking live-tail scrolling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 681a6872-38a7-40f1-a44a-31fa32718d19
📒 Files selected for processing (5)
web-ui/__mocks__/@hugeicons/react.jsweb-ui/jest.setup.jsweb-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsxweb-ui/src/components/sessions/AgentChatPanel.tsxweb-ui/src/components/sessions/index.ts
|
Code Review -- AgentChatPanel (PR 505). Good, well-structured work overall. The TDD approach with 30 tests is exactly the right discipline for a UI component this interactive. Below are the issues found during review. BUGS AND CORRECTNESS: (1) Auto-scroll threshold too tight: The condition scrollHeight - scrollTop - clientHeight < 40 in AgentChatPanel.tsx ~line 248 means only 40px of tolerance before auto-scroll locks off permanently. A user scrolling slightly to re-read a tool result loses auto-scroll for the session. 100-150px is more forgiving and standard. (2) isLast is positional not role-aware: MessageRow passes isLast based on list position. If the last message is tool_result or tool_use, no AssistantBubble gets isLast=true, so the streaming cursor never shows even when status is streaming. Derive isLastAssistant separately from the last message with role=assistant. (3) break-all in ToolResultCard: The pre element uses whitespace-pre-wrap break-all which splits file paths and code tokens mid-character. Use break-words instead. DESIGN AND ARCHITECTURE: (4) Model name hard-coded in header: The Badge element hard-codes claude-sonnet-4-6. This should come from AgentChatState or a prop. Add a modelId field to the state. (5) handleInput not wrapped in useCallback: All other event handlers use useCallback but handleInput is a plain inline function recreated on every render. Wrap it for consistency. (6) clearMessages never used: The component destructures only state, sendMessage, and interrupt from useAgentChat. clearMessages is dead surface area -- either drop it from the hook return type or document why it is omitted. ACCESSIBILITY: (7) role=status is semantically wrong on the status dot: role=status is a live region. Screen readers will announce it on every status change (idle, thinking, streaming, connecting, disconnected, error) which will be noisy mid-task. Use role=img instead with the existing aria-label. The test passes only because jsdom does not enforce live-region semantics. TEST COVERAGE GAPS: (8) No test for auto-scroll/scroll-stop: scrollIntoView is polyfilled in jest.setup.js already. A test firing a scroll event then adding a message would guard threshold regressions. (9) No test for costUsd at zero: toFixed(4) produces 0.0000 for zero-cost sessions. Document intended behaviour with a test. (10) makeMessage uses Math.random() for IDs: Non-deterministic IDs will break snapshot tests if added later. Use a counter-based factory. NITS: ThinkingBlock renders all content untruncated -- consider the same expand/collapse pattern as ToolResultCard for long chains. The className prop adds a trailing space when omitted -- use cn() from @/lib/utils instead. UserBubble renders content as plain string with no markdown treatment, inconsistent with AssistantBubble. SUMMARY: Items 4 (hard-coded model name) and 7 (role=status) are most likely to cause real production problems. Happy to approve once those are addressed. |
…pdates Fixes CodeRabbit major finding: useEffect dependency was messages.length only, so token-by-token streaming updates (same array length) did not trigger scroll. Track lastMessage.id and lastMessage.content instead. Also adds regression test verifying scrollIntoView fires on content-only update.
|
Review: AgentChatPanel Good work overall — the component is clean, well-structured, and the 30-test suite gives solid coverage. coderabbitai's note about the streaming auto-scroll regression test is worth addressing. Here are a few additional items: Bug: Hardcoded model name in header In AgentChatPanel.tsx, the Badge hardcodes "claude-sonnet-4-6". This should come from state (e.g. state.model) or a prop. A hardcoded model name becomes stale and is misleading for sessions using a different model. Bug: statusLabel is misleading during thinking/streaming The statusLabel function returns "Status: connected" as the catch-all, so screen readers announce "Status: connected" when the agent is thinking or streaming. Add explicit branches for 'thinking' and 'streaming', or rename the catch-all to "Status: active". Minor: toolInput !== undefined does not guard against null If toolInput is null, JSON.stringify(null, null, 2) renders the string "null" inside the pre block. Use message.toolInput != null if null is a possible API value. Minor: handleInput is not memoized unlike other handlers handleSend, handleKeyDown, and handleScroll all use useCallback, but handleInput does not. Wrap it in useCallback for consistency since the textarea re-renders frequently during streaming. Observation: clearMessages is wired in tests but unused in the component The mock exposes clearMessages and the test helper sets it up, but the component only destructures { state, sendMessage, interrupt } from the hook. Either expose a Clear control in the UI or remove clearMessages from the test helper to avoid dead-code confusion. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
web-ui/src/components/sessions/AgentChatPanel.tsx (1)
19-22: MoveAgentChatPanelPropsinto the shared types module.This local interface breaks the repo rule that TypeScript types live in
web-ui/src/types/index.ts.As per coding guidelines "TypeScript types must be defined in
web-ui/src/types/index.tsand API client functionality inweb-ui/src/lib/api.ts".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web-ui/src/components/sessions/AgentChatPanel.tsx` around lines 19 - 22, Move the local interface AgentChatPanelProps out of AgentChatPanel.tsx into the central types module by defining it in web-ui/src/types/index.ts (exported) and importing it where used; update AgentChatPanel.tsx to remove the local interface declaration and add an import for AgentChatPanelProps, ensuring the exported name matches exactly and any optional fields (className?: string) and types remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx`:
- Around line 176-188: The test renders AgentChatPanel before initializing the
mocked hook, causing the initial render to use stale state; move the setupMock
call(s) so the mocked hook (via setupMock(makeState(...))) is invoked before the
first render call. Specifically, call setupMock(makeState({ messages: [msg],
status: 'streaming' })) before render(<AgentChatPanel sessionId="sess-1" />),
then perform the first rerender and later update the mock for the in-place
content change using setupMock(makeState({ messages: [updatedMsg], status:
'streaming' })) followed by rerender; keep references to makeMessage, makeState,
setupMock and AgentChatPanel to locate and update the test.
In `@web-ui/src/components/sessions/AgentChatPanel.tsx`:
- Around line 225-233: The handler handleSend clears the draft unconditionally
even when sendMessage aborts early (e.g., socket not open); update the contract
so sendMessage (from useAgentChat) returns a boolean or promise<boolean>
indicating success, and then change handleSend to only call setValue('') and
reset textareaRef.current.style.height when sendMessage reports success;
specifically modify sendMessage in useAgentChat (where it currently returns
early on socket closed) to return false on abort and true on success, and update
handleSend to await/check that return value before clearing the draft.
- Around line 44-45: The chat bubble divs in AgentChatPanel.tsx that render
message.content currently collapse newlines; add Tailwind's whitespace-pre-wrap
(e.g., change className on the two message bubble divs that start with
"max-w-[80%] rounded-lg bg-primary..." and the corresponding assistant/user
bubble at lines ~65-66) so the className includes "whitespace-pre-wrap" to
preserve line breaks and multi-paragraph formatting when rendering
message.content.
- Around line 84-94: The collapsed header in AgentChatPanel currently only shows
message.toolName and omits the short human-readable summary from
message.content; update the button rendering (the onClick/header block that
toggles setExpanded and contains ArrowRight01Icon) to also display a
truncated/inline summary of message.content (e.g., after the toolName span when
expanded === false) and include that summary in the aria-label so users and
screen readers see the tool-call summary; ensure you reference the existing
message variable and preserve the current toggle behavior and icon rotation in
the ArrowRight01Icon.
---
Nitpick comments:
In `@web-ui/src/components/sessions/AgentChatPanel.tsx`:
- Around line 19-22: Move the local interface AgentChatPanelProps out of
AgentChatPanel.tsx into the central types module by defining it in
web-ui/src/types/index.ts (exported) and importing it where used; update
AgentChatPanel.tsx to remove the local interface declaration and add an import
for AgentChatPanelProps, ensuring the exported name matches exactly and any
optional fields (className?: string) and types remain unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 15b56351-ecaf-4187-a726-eb71bbac42c4
📒 Files selected for processing (2)
web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsxweb-ui/src/components/sessions/AgentChatPanel.tsx
| it('scrollIntoView called when last message content updates (streaming in-place)', () => { | ||
| const msg = makeMessage({ role: 'assistant', content: 'Hello' }); | ||
| const { rerender } = render( | ||
| <AgentChatPanel sessionId="sess-1" /> | ||
| ); | ||
| // Initial render with one message | ||
| setupMock(makeState({ messages: [msg], status: 'streaming' })); | ||
| rerender(<AgentChatPanel sessionId="sess-1" />); | ||
|
|
||
| // Simulate in-place content update (same id, different content) | ||
| const updatedMsg = { ...msg, content: 'Hello world' }; | ||
| setupMock(makeState({ messages: [updatedMsg], status: 'streaming' })); | ||
| rerender(<AgentChatPanel sessionId="sess-1" />); |
There was a problem hiding this comment.
Initialize the mocked hook before the first render in this test.
This case renders AgentChatPanel before setupMock(...), so the initial pass depends on stale mock state instead of the fixture defined in the test. That makes the assertion order-dependent and can hide failures in the rerender path.
Proposed fix
it('scrollIntoView called when last message content updates (streaming in-place)', () => {
const msg = makeMessage({ role: 'assistant', content: 'Hello' });
+ setupMock(makeState({ messages: [msg], status: 'streaming' }));
const { rerender } = render(
<AgentChatPanel sessionId="sess-1" />
);
- // Initial render with one message
- setupMock(makeState({ messages: [msg], status: 'streaming' }));
- rerender(<AgentChatPanel sessionId="sess-1" />);
// Simulate in-place content update (same id, different content)
const updatedMsg = { ...msg, content: 'Hello world' };
setupMock(makeState({ messages: [updatedMsg], status: 'streaming' }));
rerender(<AgentChatPanel sessionId="sess-1" />);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('scrollIntoView called when last message content updates (streaming in-place)', () => { | |
| const msg = makeMessage({ role: 'assistant', content: 'Hello' }); | |
| const { rerender } = render( | |
| <AgentChatPanel sessionId="sess-1" /> | |
| ); | |
| // Initial render with one message | |
| setupMock(makeState({ messages: [msg], status: 'streaming' })); | |
| rerender(<AgentChatPanel sessionId="sess-1" />); | |
| // Simulate in-place content update (same id, different content) | |
| const updatedMsg = { ...msg, content: 'Hello world' }; | |
| setupMock(makeState({ messages: [updatedMsg], status: 'streaming' })); | |
| rerender(<AgentChatPanel sessionId="sess-1" />); | |
| it('scrollIntoView called when last message content updates (streaming in-place)', () => { | |
| const msg = makeMessage({ role: 'assistant', content: 'Hello' }); | |
| setupMock(makeState({ messages: [msg], status: 'streaming' })); | |
| const { rerender } = render( | |
| <AgentChatPanel sessionId="sess-1" /> | |
| ); | |
| // Simulate in-place content update (same id, different content) | |
| const updatedMsg = { ...msg, content: 'Hello world' }; | |
| setupMock(makeState({ messages: [updatedMsg], status: 'streaming' })); | |
| rerender(<AgentChatPanel sessionId="sess-1" />); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx` around
lines 176 - 188, The test renders AgentChatPanel before initializing the mocked
hook, causing the initial render to use stale state; move the setupMock call(s)
so the mocked hook (via setupMock(makeState(...))) is invoked before the first
render call. Specifically, call setupMock(makeState({ messages: [msg], status:
'streaming' })) before render(<AgentChatPanel sessionId="sess-1" />), then
perform the first rerender and later update the mock for the in-place content
change using setupMock(makeState({ messages: [updatedMsg], status: 'streaming'
})) followed by rerender; keep references to makeMessage, makeState, setupMock
and AgentChatPanel to locate and update the test.
| <div className="max-w-[80%] rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground leading-relaxed"> | ||
| {message.content} |
There was a problem hiding this comment.
Preserve multiline content in the chat bubbles.
These text containers collapse \n into spaces, so Shift+Enter messages and multi-paragraph assistant replies lose formatting when rendered.
Proposed fix
- <div className="max-w-[80%] rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground leading-relaxed">
+ <div className="max-w-[80%] whitespace-pre-wrap break-words rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground leading-relaxed">
{message.content}
</div>
@@
- <div className="max-w-[80%] rounded-lg bg-muted px-3 py-2 text-sm text-foreground leading-relaxed">
+ <div className="max-w-[80%] whitespace-pre-wrap break-words rounded-lg bg-muted px-3 py-2 text-sm text-foreground leading-relaxed">
{message.content}
{isLast && status === 'streaming' && (Also applies to: 65-66
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/components/sessions/AgentChatPanel.tsx` around lines 44 - 45, The
chat bubble divs in AgentChatPanel.tsx that render message.content currently
collapse newlines; add Tailwind's whitespace-pre-wrap (e.g., change className on
the two message bubble divs that start with "max-w-[80%] rounded-lg
bg-primary..." and the corresponding assistant/user bubble at lines ~65-66) so
the className includes "whitespace-pre-wrap" to preserve line breaks and
multi-paragraph formatting when rendering message.content.
| <button | ||
| className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40 rounded-lg" | ||
| onClick={() => setExpanded((v) => !v)} | ||
| aria-expanded={expanded} | ||
| aria-label={`${expanded ? 'Collapse' : 'Expand'} tool call: ${message.toolName ?? 'tool'}`} | ||
| > | ||
| <ArrowRight01Icon | ||
| className={`h-3 w-3 shrink-0 transition-transform ${expanded ? 'rotate-90' : ''}`} | ||
| /> | ||
| <span className="font-mono text-xs text-muted-foreground">{message.toolName ?? 'tool'}</span> | ||
| </button> |
There was a problem hiding this comment.
Render the tool-call summary in the collapsed header.
The collapsed tool_use row only shows toolName; message.content never appears anywhere in this renderer, so users lose the short human-readable summary the spec calls for.
Proposed fix
- <button
- className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40 rounded-lg"
+ <button
+ className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left hover:bg-muted/40 rounded-lg"
onClick={() => setExpanded((v) => !v)}
aria-expanded={expanded}
aria-label={`${expanded ? 'Collapse' : 'Expand'} tool call: ${message.toolName ?? 'tool'}`}
>
<ArrowRight01Icon
className={`h-3 w-3 shrink-0 transition-transform ${expanded ? 'rotate-90' : ''}`}
/>
<span className="font-mono text-xs text-muted-foreground">{message.toolName ?? 'tool'}</span>
+ {message.content && (
+ <span className="min-w-0 flex-1 truncate text-xs text-foreground/80">
+ {message.content}
+ </span>
+ )}
</button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40 rounded-lg" | |
| onClick={() => setExpanded((v) => !v)} | |
| aria-expanded={expanded} | |
| aria-label={`${expanded ? 'Collapse' : 'Expand'} tool call: ${message.toolName ?? 'tool'}`} | |
| > | |
| <ArrowRight01Icon | |
| className={`h-3 w-3 shrink-0 transition-transform ${expanded ? 'rotate-90' : ''}`} | |
| /> | |
| <span className="font-mono text-xs text-muted-foreground">{message.toolName ?? 'tool'}</span> | |
| </button> | |
| <button | |
| className="flex w-full min-w-0 items-center gap-2 px-3 py-2 text-left hover:bg-muted/40 rounded-lg" | |
| onClick={() => setExpanded((v) => !v)} | |
| aria-expanded={expanded} | |
| aria-label={`${expanded ? 'Collapse' : 'Expand'} tool call: ${message.toolName ?? 'tool'}`} | |
| > | |
| <ArrowRight01Icon | |
| className={`h-3 w-3 shrink-0 transition-transform ${expanded ? 'rotate-90' : ''}`} | |
| /> | |
| <span className="font-mono text-xs text-muted-foreground">{message.toolName ?? 'tool'}</span> | |
| {message.content && ( | |
| <span className="min-w-0 flex-1 truncate text-xs text-foreground/80"> | |
| {message.content} | |
| </span> | |
| )} | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/components/sessions/AgentChatPanel.tsx` around lines 84 - 94, The
collapsed header in AgentChatPanel currently only shows message.toolName and
omits the short human-readable summary from message.content; update the button
rendering (the onClick/header block that toggles setExpanded and contains
ArrowRight01Icon) to also display a truncated/inline summary of message.content
(e.g., after the toolName span when expanded === false) and include that summary
in the aria-label so users and screen readers see the tool-call summary; ensure
you reference the existing message variable and preserve the current toggle
behavior and icon rotation in the ArrowRight01Icon.
| const handleSend = useCallback(() => { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed || isBusy) return; | ||
| sendMessage(trimmed); | ||
| setValue(''); | ||
| if (textareaRef.current) { | ||
| textareaRef.current.style.height = 'auto'; | ||
| } | ||
| }, [value, isBusy, sendMessage]); |
There was a problem hiding this comment.
Don’t clear the draft after an unsendable submission.
web-ui/src/hooks/useAgentChat.ts:295-303 returns early from sendMessage when the socket is not open. This handler still clears value, so submitting while status is connecting, disconnected, or error drops the draft even though nothing was sent.
Proposed fix
export function AgentChatPanel({ sessionId, className }: AgentChatPanelProps) {
const { state, sendMessage, interrupt } = useAgentChat(sessionId);
- const { messages, status, costUsd } = state;
+ const { messages, status, costUsd, connected } = state;
@@
const isBusy = status === 'thinking' || status === 'streaming';
+ const canSend = connected && status === 'idle';
@@
const handleSend = useCallback(() => {
const trimmed = value.trim();
- if (!trimmed || isBusy) return;
+ if (!trimmed || !canSend) return;
sendMessage(trimmed);
setValue('');
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
}
- }, [value, isBusy, sendMessage]);
+ }, [value, canSend, sendMessage]);
@@
<Button
size="icon"
onClick={handleSend}
- disabled={isBusy || !value.trim()}
+ disabled={!canSend || !value.trim()}
aria-label="Send message"
className="shrink-0"
>Also applies to: 321-325
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web-ui/src/components/sessions/AgentChatPanel.tsx` around lines 225 - 233,
The handler handleSend clears the draft unconditionally even when sendMessage
aborts early (e.g., socket not open); update the contract so sendMessage (from
useAgentChat) returns a boolean or promise<boolean> indicating success, and then
change handleSend to only call setValue('') and reset
textareaRef.current.style.height when sendMessage reports success; specifically
modify sendMessage in useAgentChat (where it currently returns early on socket
closed) to return false on abort and true on success, and update handleSend to
await/check that return value before clearing the draft.
Summary
AgentChatPanel— the main chat UI component for interactive agent sessions (Frontend: AgentChatPanel component — messages, tool calls, thinking blocks, input bar #505)user,assistant,tool_use,tool_result,thinking,system,erroruseAgentChathook from Frontend: useAgentChat hook — WebSocket connection and message streaming #504 (already merged)Changes
New files:
web-ui/src/components/sessions/AgentChatPanel.tsx— single-file component with co-located sub-renderersweb-ui/src/components/sessions/index.ts— barrel exportweb-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx— 30 unit tests (TDD)Modified files:
web-ui/__mocks__/@hugeicons/react.js— addedArrowRight01Icon,Alert01Iconmocksweb-ui/jest.setup.js— addedscrollIntoViewpolyfill for jsdomAcceptance Criteria
status === "streaming"Test plan
Closes #505
Summary by CodeRabbit
New Features
Tests
Chores