Skip to content

Chat UI: composer polish, reducer dedup, tool-card layout fixes#484

Merged
shanselman merged 31 commits into
openclaw:masterfrom
kenehong:feat/chatui-nested-tool
May 21, 2026
Merged

Chat UI: composer polish, reducer dedup, tool-card layout fixes#484
shanselman merged 31 commits into
openclaw:masterfrom
kenehong:feat/chatui-nested-tool

Conversation

@kenehong
Copy link
Copy Markdown
Contributor

@kenehong kenehong commented May 21, 2026

Changes

Composer

  • Image paste support
  • Tightened the gap between composer and action row
  • Improved overall spacing, radius alignment
  • Agent avatar only

Tool card

  • Single / Auto / CompactSummary tool bursts now nest inside the assistant bubble
  • Done pill clipping fix:
    • AnchorLeft Grid switched to a single Star column so the card's measured width is bounded by the chat viewport
    • headerRow / summaryHeader restructured to outer [Star, Auto] + inner 4-col grid (Done pill in the outer Auto)
    • Summary Caption: TextWrapping=Wrap + MaxLines=1 + CharacterEllipsis for safe single-line ellipsis
    • Nested card now insets by bubbleRadius (16px) on the right so the bubble's CornerRadius arc no longer clips the pill

User bubble tone

  • New ChatUserBubbleTone enum (Accent / Secondary), exposed in the Explorations panel
  • Default: Secondary — uses AccentFillColorSecondaryBrush (softer accent variant) so the bubble still reads as the brand color but doesn't compete with the accent-colored avatar / send button
  • Both tones pair with TextOnAccentFillColorPrimaryBrush — Fluent guarantees WCAG AA contrast in light, dark, and HighContrast themes

Reducer

  • Scan-back fix for the text→tool→tool→text sequence so the assistant message no longer duplicates across tool boundaries

Out of scope

Validation

  • ./build.ps1
  • Tray 1122 ✓ / Shared 1803 ✓
  • XAML-skill design review ✓ (HC + WCAG verified for the new brush pairings)

kenehong and others added 29 commits May 20, 2026 20:39
…width

Visual cleanup pass on the chat surface (no functional change):

- OpenClawComposer: right-edge padding 8 -> 14 in both ThreeRow and
  InlinePill branches so the action icons (attach / mic / settings / send)
  no longer jam against the window edge.
- OpenClawComposer: dropdowns row ColumnGap 4 -> 6 for clearer separation
  between Channel / Model / Reasoning pickers.
- OpenClawChatTimeline: cap tool-burst cards (CardOf + TaskList listCard)
  at MaxWidth=720 with HAlign.Left so a single 'exec' row no longer
  stretches across the full viewport with the Done pill floating at the
  far right edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Symmetrize user/assistant/tool burst outer margins (16px both sides)

- Use bubbleRadius for tool CardOf/listCard and conditional header buttons

- Make tool card background Transparent so outline distinguishes it from filled assistant bubble

- Drop Plain tool burst footer; assistant follow-up bubble already carries the time

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…thing room

- Drop the 36x36 spacer that mid-run assistant bubbles inherited; continuation bubbles now sit at the same left inset as tool burst cards above them, so the agent column reads as a single straight edge.

- Add 20px top padding above the first message in the scroll content so the conversation does not crowd the window edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ign composer inset

- When no assistant avatar shown, drop the leftSlot's right margin so assistant bubbles share the same left edge (16px) as tool burst cards.

- Tool burst row header now uses bubblePadding instead of (12,8,12,8) and a 32px MinHeight, so tool rows match chat bubble heights.

- Composer outer padding 14->16 to align dropdowns/input flush with chat bubble left edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Assistant footer leftInset now respects per-entry avatar visibility (not the global flag), so continuation entries' timestamps align with the bubble's left edge instead of being indented 44px.

- Tool burst button hover/press alphas 0x22/0x33 -> 0x10/0x1C for a subtler reveal that doesn't darken the card on every pointer pass.

- Tool card background back to a faint LayerOnAcrylicFillColorDefault tint so the card has gentle presence instead of looking like a pure outline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
User/assistant footer insets now include bubblePadding so timestamps
sit flush with the bubble's text content edge instead of the outer
bubble border.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the user avatar is hidden, the rightSlot column still added an
8px left margin even though the column body was empty, leaving an
8px gap on the right of the bubble. Gate the margin on showUserAvatar
so the bubble actually reaches the container's right edge when the
avatar is off — this makes rightInset (= bubblePadding.Right) place
the timestamp flush with the bubble's inner text right edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The TaskHeader burst style reserved an avatar slot (36+8px) so the
list card lined up with the assistant bubble's text edge, but the
Plain and FooterReframe styles started flush at the gutter. When the
assistant entry above the burst showed an avatar, the tool cards
appeared 44px further left than the bubble.

Extracted the avatar-slot wrap into a helper and applied it to all
three burst styles so user/assistant/tool share the same left edge
regardless of burst style.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Default tool burst rendering switches from Plain (verbose per-row stack) to TaskList:
 - While any step is InProgress: auto-expanded, shows 'Working on X...'
 - When the burst completes: auto-collapses to a single one-line summary card
   ('Ran N steps' or last result snippet) with a chevron to expand
 - Click chevron to override; per-step rows still individually expandable for
   full args/output (3-tier disclosure)

Addresses Scott's feedback: 'I'd like to be able to have tool calls summarized,
or made smaller, or collapsible, so there would be some way to be clear that
work is happening, and if I want to see the verbose logs, I could.'

No data-flow changes — purely the default value of the existing ToolBurstStyle
enum. The dev exploration panel still exposes Plain/TaskHeader/CompactSummary/
FooterReframe/TaskList for tuning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Tool card aligns under bubble (toolLeftMargin = gutter + avatarSlot + 16) with right edge matched (MaxWidth -= indent). Plain/FooterReframe/CompactSummary/TaskHeader unified.
- Footer priority: hide sender/model by default, surface input/output tokens + context % pills.
- Preset record defaults updated to match (new presets inherit token/ctx ON, sender/model OFF).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reorder timeline display within each turn so ToolCall bursts render AFTER the assistant reply (or the thinking indicator if none yet). Gateway still emits tool_start before assistant_delta; only the visual order changes.
- Inline 'agent is thinking…' indicator right after the most recent User entry instead of pinning to bottom of timeline, so tool cards visually hang below it.
- Tool burst card HAlign Left→Stretch (Plain/FooterReframe/CompactSummary/TaskHeader) so the right edge fills to the bubble's max right boundary instead of shrinking to content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assistant bubble previously used HAlign=Left with no MaxWidth, so its right edge tracked content width. Tool burst card used HAlign=Stretch with MaxWidth=704, filling further right than the bubble.

Give the assistant card MaxWidth=720 and HAlign=Stretch so it always pins to the same max right boundary as the tool card (60 + 720 = 76 + 704 = 780). Tool card now sits indented 16px inside the bubble's left edge with an identical right stroke, reading as a true child of the bubble above.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…p anchored)

WinUI's HAlign=Stretch + finite MaxWidth centers the element inside its slot (rather than pinning it left), which detached the bubble from the avatar + timestamp column. Revert to HAlign=Left so the bubble grows from the avatar's edge; MaxWidth=720 still caps the right edge so long messages line up with the tool burst card's right stroke. Short messages keep the previous content-width behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-on-wide-screen)

Same WinUI quirk as the assistant bubble: HAlign=Stretch with finite MaxWidth centers the element inside an oversized slot rather than pinning it left. On wide screens the tool burst card drifted away from the bubble's left edge.

Revert all four tool burst styles (Plain, FooterReframe, CompactSummary, TaskHeader) to HAlign=Left. Both bubble and tool card now anchor on the left next to the avatar/timestamp column; right edges line up when both fill their MaxWidth (720 / 720-indent).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Star Grid

Tool card needs to fill its 704-wide slot so the right stroke aligns with the assistant bubble's right edge, but HAlign=Stretch alone centers the card on wide screens (WinUI's Stretch + finite MaxWidth quirk).

Wrap the card in an Auto/Star Grid: the Auto column sizes to the card's MaxWidth (704), keeping the card pinned to toolLeftMargin and filling the slot. The Star column absorbs the rest. Applied to Plain, FooterReframe, CompactSummary, TaskHeader.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assistant bubble is content-sized (HAlign=Left, MaxWidth=720), so we can't predict its rendered width at layout time. Result: the tool card's right edge rarely matched the bubble's right edge, especially with short replies.

Solution: thread a per-turn Border[1] slot from RenderAssistantEntry into RenderToolBurst. The assistant bubble's Border drops itself into slot[0] on materialize; the tool card subscribes to bubble.SizeChanged and sets its own Width = bubble.ActualWidth - toolIndent. The two cards' left indent and right edges now stay exactly parallel as the bubble grows, regardless of content length.

Reset slot at each User entry boundary so tool cards never bind to a prior turn's bubble. Falls back to MaxWidth/AnchorLeft when no bubble exists (tool-only turn).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scott feedback: when an agent's reply bubble lands and the tool burst beneath it is fully terminal, fold the N step rows into a single collapsed summary (clickable chevron to expand). While tools are still running, keep showing them as per-step rows so each Running/Done pill stays visible.

Added ToolBurstStyle.Auto and made it the process-wide default. Auto resolves per burst at render time:
  - count == 1                        -> Plain (one inline row)
  - count >= 2 && all terminal        -> CompactSummary (1-line + chevron)
  - count >= 2 && any InProgress      -> Plain (live status visible)

CompactSummary's existing expand machinery (expandedToolChips HashSet) is reused, so the collapse/expand state persists for the session.

Exposed Auto in the exploration panel dropdown for testing the other styles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two follow-ups from Scott's screenshot:

1. The collapsed CompactSummary header used FlexRow(ColumnGap=6)+padding(12,8,12,8)+MinHeight=22 while BuildRow used Grid[Auto,Auto,Auto,Star,Auto]+margin(6,0,0,0)+bubblePadding+MinHeight=32. Rebuilt the summary header with the exact same Grid template, margins, padding, and MinHeight as the step rows so the chevron / lightning / Task label / Done pill axes line up vertically when the burst is expanded.

2. Removed the trailing FooterCaption(timeStr/TaskFooter()) from CompactSummary, TaskHeader, and FooterReframe returns. The assistant bubble above the tool burst already shows its own timestamp/model/tokens footer, so the time under the tool card was redundant noise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When an assistant message is followed by a tool burst in the same turn,
defer the timestamp/model/tokens footer so it renders BELOW the tool
card(s) instead of between the bubble and the tools. Order becomes:

  bubble -> tool card(s) -> timestamp/model/tokens

Implementation mirrors the existing bubbleSlot pattern: RenderAssistantEntry
accepts an Element[1] footerSlot; when supplied, the built footer is
handed back to the caller and the inline footer slot in the VStack
collapses to Empty(). The outer loop precomputes turn boundaries, hands
out a footerSlot whenever a tool entry follows in the same turn, and
splices the captured footer Element into timelineRows just after the
last entry of the turn (alongside the thinking indicator splice).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the assistant has produced a reply AND the tool burst would render
as a single visible row (one chip OR a collapsed multi-step summary),
embed the tool card inside the assistant bubble's content area instead
of rendering it as a sibling card below.

  bubble {
    text...
    [tool card]   <- nested
  }

In-flight multi-step bursts (Plain expanded) and bursts arriving before
the assistant reply still render externally so live progress stays
visible. Plain / TaskHeader / TaskList / FooterReframe styles never
nest — only Auto and CompactSummary opt in.

Implementation:
- RenderAssistantEntry gains an Element? nestedTool param. When set,
  the bubble wraps its markdown text in a VStack(8, text, nestedTool)
  so the tool card sits flush below the message with an 8px top gap,
  inside the bubble's existing padding/border/radius.
- RenderToolBurst gains a bool nested flag. In nested mode CardOf
  drops MaxWidth/HAlign.Left and the bubbleSlot Width binding (the
  parent bubble already constrains us); a new Wrap helper bypasses
  AnchorLeft and the toolLeftMargin/gutter outer margin so the card
  stretches inside the bubble.
- Outer loop precomputes turn boundaries, looks ahead from the last
  assistant entry of the turn for a contiguous tool burst, and asks
  BurstIsNestable to gate the decision (count==1 OR all-terminal under
  Auto/CompactSummary). Consumed orderedIdx positions are tracked in a
  HashSet so the external render branch emits Empty() for them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Markdown text has tight line-height with no trailing descender, so an
8px top gap reads as visibly tighter than the 8px bottom padding below
the nested tool card. Set the top gap to bubblePadding.Bottom + 4 so
the optical spacing matches the gap from the card to the bubble's
bottom edge across all PaddingDensity presets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The +16 indent on external tool cards exists so the card visually nests
inside the assistant bubble's text column. When the agent is still
'thinking…' and no reply bubble has streamed, that indent makes the
tool card hang further right than the thinking indicator above it.

When bubbleSlot is null (no assistant entry seen in this turn) drop the
indent so the tool card aligns flush under the thinking indicator
instead of jutting right.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
High priority fixes:
- AutomationName: only set bubble-level name when no nested tool card
  is present so Narrator can traverse into the nested card.
- Status colors: replace hardcoded Done-green / Running-orange / pill
  white with SystemFillColorSuccessBrush / CautionBrush and
  TextOnAccentFillColorPrimaryBrush so they adapt to dark/HC themes.
- Hover/pressed: 3 button surfaces now use SubtleFillColorTertiary
  (hover) and SubtleFillColorSecondary (pressed) themed brushes. The
  tool card uses the lighter Tertiary on hover for a more subtle look
  (Scott feedback) and to stay visible in dark/HC.
- SizeChanged handler leak on the bubble-Width binding fixed: subscribe
  on Loaded, detach on Unloaded with a stable handler reference so
  re-renders don't accumulate listeners.

Medium priority fixes:
- toolCardBgBrush: LayerOnAcrylicFillColorDefaultBrush
  → CardBackgroundFillColorDefaultBrush (semantic match — the bubble
  surface is opaque, not acrylic).
- Tool card MinWidth = 360 so the 5-column header grid doesn't clip
  when the assistant reply is short and the bubble shrinks below it.
- HC border thickness bumped to 2px via AccessibilitySettings probe
  (graceful 1px fallback if API throws in unpackaged hosts).
- BurstIsNestable now returns false when any tool errored — errors
  stay external so the failure is visually prominent.

Low priority fixes:
- (int)bubblePadding.Bottom + 4 → (int)Math.Round(...) for fractional
  density values.
- Remove redundant FontSize = 12 sets after Caption() (Caption already
  sets 12). Status pills bumped 10 → 11 for readability.
- Add uniform-CornerRadius assumption comments at two card sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The chat UI styling Kenny iterated on was being persisted to a local
preset (%APPDATA%\\OpenClawTray\\chat-exploration-presets.json with
IsDefault=true) that overrode the in-memory defaults on startup. New
installs and other users were therefore landing on the original code
defaults (Mica / Comfortable / Both avatars / 14px / 32px send button)
instead of the look reviewed by Scott.

Bake the preset values into the actual code defaults so a fresh install
matches the design without needing the JSON preset file:

  BackdropMode      Mica          -> Acrylic
  PaddingDensity    Comfortable   -> Cozy
  AvatarMode        Both          -> AgentOnly
  ComposerIconSize  14            -> 16
  SendButtonSize    32            -> 40

Updated in three places to keep them consistent:
- ChatExplorationState (the in-memory defaults applied at startup)
- ChatExplorationPreset record (defaults when a preset omits a field
  during deserialization)
- ChatVariationPresets.Calm (so users who hit Reset or pick the Calm
  variation land on the same baseline)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wrap BuildSection in a 2-col Grid with a transparent phantom chevron in col 0 (same glyph + 6px right margin) so the section's lightning glyph and the code block beneath it land at the same x as the header lightning above, regardless of density/font metrics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Composer (OpenClawComposer.cs):
- Paste image preview rendered above text in single user bubble
- X-button hover changes circular background opacity instead of
  fading the X glyph (uses opaque SolidBackgroundFillColor* brushes)
- Strip stray top/bottom line on TextBox via theme resource overrides
  (TextControlBorderThemeThickness / focused / pointerOver)
- Tighten composer<->actions-row gap (actionsRow margin -8/-4)

Reducer (ChatTimelineReducer.cs):
- UpsertAssistant reconcile path now scans backward for the most
  recent Assistant entry, stopping at User boundary, so streams of
  shape `text -> tool -> tool output -> final text` no longer
  duplicate the assistant text into a second bubble.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- AnchorLeft Grid: single Star col so card measure is bounded by chat viewport
- headerRow / summaryHeader: outer [Star, Auto] wrapping inner content + Done pill
- Summary Caption: TextWrapping=Wrap + MaxLines=1 + CharacterEllipsis for safe trim
- CardOf nested branch: MinWidth=360; external Sync clamps bubble width with Math.Max(360, w)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bubble CornerRadius (16) equals bubblePadding.Right (16) in Cozy preset,
so the bubble's corner arc reaches the inner content edge and clips the
Done pill at the card's right side. Move the nested card in by a full
bubbleRadius (and drop MinWidth=360 which forced the card wider than
the bubble in narrow viewports) so the pill is comfortably inside the
bubble's rounded shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kenehong kenehong force-pushed the feat/chatui-nested-tool branch from 048391c to 9f09e9b Compare May 21, 2026 03:39
kenehong and others added 2 commits May 20, 2026 21:56
Add ChatUserBubbleTone enum (default Secondary) and a panel control to
switch between the bold accent fill (AccentFillColorDefault) and a
softer accent variant (AccentFillColorSecondary). Both tones use
TextOnAccentFillColorPrimaryBrush for the text, which Fluent guarantees
meets WCAG AA contrast in light, dark, and HighContrast themes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolved conflicts:
- OpenClawComposer.cs: kept both new params (OnAttachmentPasted + IsCompact)
- OpenClawChatRoot.cs: pass both OnAttachmentPasted and IsCompact through
- OpenClawChatDataProvider.cs: accepted upstream cross-client user echo
  handling (openclaw#469) — supersedes our earlier rollback of that work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@shanselman shanselman merged commit 0f6e8fa into openclaw:master May 21, 2026
11 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.

2 participants