Skip to content

feat(web-ui): AgentChatPanel component (#505)#515

Merged
frankbria merged 2 commits into
mainfrom
feature/issue-505-agent-chat-panel
Apr 1, 2026
Merged

feat(web-ui): AgentChatPanel component (#505)#515
frankbria merged 2 commits into
mainfrom
feature/issue-505-agent-chat-panel

Conversation

@frankbria

@frankbria frankbria commented Mar 31, 2026

Copy link
Copy Markdown
Owner

Summary

Changes

New files:

  • web-ui/src/components/sessions/AgentChatPanel.tsx — single-file component with co-located sub-renderers
  • web-ui/src/components/sessions/index.ts — barrel export
  • web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx — 30 unit tests (TDD)

Modified files:

  • web-ui/__mocks__/@hugeicons/react.js — added ArrowRight01Icon, Alert01Icon mocks
  • web-ui/jest.setup.js — added scrollIntoView polyfill for jsdom

Acceptance Criteria

  • Renders all 7 message roles with correct visual treatment
  • Tool use cards are collapsible (collapsed by default)
  • Tool result cards truncate and have expand button
  • Streaming cursor indicator visible while status === "streaming"
  • Input bar disabled while agent is thinking/streaming
  • Interrupt button appears during thinking/streaming, hidden otherwise
  • Auto-scrolls to bottom on new messages, stops on manual scroll-up
  • Empty state displayed with no messages
  • Header shows live cost and connection status
  • Accessible: keyboard navigable, proper ARIA labels on buttons

Test plan

  • 30 unit tests — all passing
  • No regressions in existing test suites (pre-existing failures in QuickActions and e2e are unrelated)

Closes #505

Summary by CodeRabbit

  • New Features

    • Added AgentChatPanel for interactive agent chat sessions with message composer, streaming cursor, tool call/result views, auto-scroll, and interrupt controls.
  • Tests

    • Added comprehensive tests covering message rendering, streaming behavior, input/send flows, interrupt actions, expand/truncate toggles, auto-scroll, and accessibility.
  • Chores

    • Improved test setup and icon mocks for reliable UI tests (scrollIntoView and additional icon mocks).

…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
@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds 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

Cohort / File(s) Summary
Test Infrastructure
web-ui/__mocks__/@hugeicons/react.js, web-ui/jest.setup.js
Added ArrowRight01Icon and Alert01Icon to the Hugeicons Jest mock; mocked window.HTMLElement.prototype.scrollIntoView with jest.fn().
AgentChatPanel Component
web-ui/src/components/sessions/AgentChatPanel.tsx
New exported AgentChatPanel component implementing multi-role message rendering, streaming cursor, collapsible tool cards, truncated tool results, auto-scroll behavior, input composer with Enter/Shift+Enter semantics, interrupt control, and header status/cost display.
Tests
web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx
New comprehensive RTL/Jest test suite covering empty state, all message roles, tool card expand/collapse, streaming behavior and cursor, auto-scroll, input send/Shift+Enter, interrupt button, status/cost, and accessibility attributes.
Module Exports
web-ui/src/components/sessions/index.ts
Added export { AgentChatPanel } from './AgentChatPanel'; to the sessions barrel file.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant AgentChatPanel as Panel
participant useAgentChat as Hook
participant DOM
User->>Panel: Type message + Enter
Panel->>Hook: sendMessage(text)
Hook->>Panel: status="streaming", messages stream delta
Panel->>DOM: render messages, show streaming cursor, scroll to bottom
User->>Panel: Click "Interrupt"
Panel->>Hook: interrupt()
Hook->>Panel: status="idle"

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 In a burrow of code I hop and play,
New panels and tests brighten the day,
Messages stream, tool cards unfold,
I nibble bugs, keep behavior bold—
Hopping off to review, hooray! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing the AgentChatPanel component, which is the primary deliverable of this PR.
Linked Issues check ✅ Passed The PR implements all key requirements from #505: renders all 7 message roles with visual treatments, collapsible tool_use cards, expandable tool_result cards, streaming cursor indicator, disabled input during thinking/streaming with interrupt button, auto-scroll behavior, empty state, and header with cost/status.
Out of Scope Changes check ✅ Passed All changes are in scope: AgentChatPanel component and test suite directly implement #505 requirements; mock updates and jest.setup additions are supporting changes necessary for the component to function and be tested.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-505-agent-chat-panel

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1cdf4d3 and 6bbd85a.

📒 Files selected for processing (5)
  • web-ui/__mocks__/@hugeicons/react.js
  • web-ui/jest.setup.js
  • web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx
  • web-ui/src/components/sessions/AgentChatPanel.tsx
  • web-ui/src/components/sessions/index.ts

Comment thread web-ui/src/components/sessions/AgentChatPanel.tsx Outdated
@claude

claude Bot commented Mar 31, 2026

Copy link
Copy Markdown

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.
@claude

claude Bot commented Mar 31, 2026

Copy link
Copy Markdown

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
web-ui/src/components/sessions/AgentChatPanel.tsx (1)

19-22: Move AgentChatPanelProps into 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.ts and API client functionality in web-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

📥 Commits

Reviewing files that changed from the base of the PR and between 6bbd85a and 4e0447d.

📒 Files selected for processing (2)
  • web-ui/src/__tests__/components/sessions/AgentChatPanel.test.tsx
  • web-ui/src/components/sessions/AgentChatPanel.tsx

Comment on lines +176 to +188
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" />);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +44 to +45
<div className="max-w-[80%] rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground leading-relaxed">
{message.content}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +84 to +94
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
<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.

Comment on lines +225 to +233
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@frankbria frankbria merged commit 199f5fb into main Apr 1, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frontend: AgentChatPanel component — messages, tool calls, thinking blocks, input bar

1 participant