From d25951aeb99ccb55466192bd4922d2e6c145c11b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 01:32:45 +0000 Subject: [PATCH 001/115] fix(chat): surface incompatible-gateway state when handshake lacks session key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the gateway handshake completes but does not advertise a mainSessionKey (or the legacy mainKey alias), the composer was showing "Connected" with a disabled-but-unlabelled send box. The user had no indication that the gateway needed updating. Changes: - OpenClawChatDataProvider: emit "Incompatible gateway" as ConnectionStatus when HasHandshakeSnapshot=true but MainSessionKey is null/empty and the socket is Connected, instead of plain "Connected". - OpenClawChatRoot: map the "Incompatible" prefix to the new "incompatible-gateway" connState token. - OpenClawComposer: handle "incompatible-gateway" → disable inputs + show Chat_Composer_Placeholder_IncompatibleGateway placeholder. - Resources.resw (all 5 locales): add Chat_Composer_Placeholder_IncompatibleGateway string. - OpenClawChatDataProviderTests: add two focused tests for the incompatible-handshake path (snapshot ConnectionStatus and ComposeTarget.IsReady=false). Closes #459 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Chat/OpenClawChatDataProvider.cs | 23 ++++++++---- .../Chat/OpenClawChatRoot.cs | 4 +- .../Chat/OpenClawComposer.cs | 1 + .../Strings/en-us/Resources.resw | 3 ++ .../Strings/fr-fr/Resources.resw | 3 ++ .../Strings/nl-nl/Resources.resw | 3 ++ .../Strings/zh-cn/Resources.resw | 3 ++ .../Strings/zh-tw/Resources.resw | 3 ++ .../OpenClawChatDataProviderTests.cs | 37 +++++++++++++++++++ 9 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs index 83dd83d7e..f0ebc5908 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs @@ -2037,14 +2037,21 @@ private ChatDataSnapshot BuildSnapshotLocked() var defaultThreadId = ResolveDefaultThreadIdLocked(); - var connectionLabel = _status switch - { - ConnectionStatus.Connected => "Connected", - ConnectionStatus.Connecting => "Connecting…", - ConnectionStatus.Disconnected => "Disconnected", - ConnectionStatus.Error => "Disconnected — error", - _ => _status.ToString() - }; + // When the gateway is connected and the handshake completed but no + // session key was advertised, distinguish this from a normal "Connected" + // state so the UI can surface a clear compatibility warning. + var connectionLabel = (_status == ConnectionStatus.Connected + && _bridge.HasHandshakeSnapshot + && string.IsNullOrWhiteSpace(composeKey)) + ? "Incompatible gateway" + : _status switch + { + ConnectionStatus.Connected => "Connected", + ConnectionStatus.Connecting => "Connecting…", + ConnectionStatus.Disconnected => "Disconnected", + ConnectionStatus.Error => "Disconnected — error", + _ => _status.ToString() + }; var composeTarget = composeReady ? new ChatComposeTarget(composeKey, true) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs index ae297b5d6..57c98a5e5 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatRoot.cs @@ -273,7 +273,9 @@ Element BuildLoadingElement() var connectedRaw = snapshot.ConnectionStatus; var hostConnected = connectedRaw is not null && connectedRaw.StartsWith("Connected", StringComparison.OrdinalIgnoreCase); - var connState = hostConnected ? "connected" + var connState = (connectedRaw is not null && connectedRaw.StartsWith("Incompatible", StringComparison.OrdinalIgnoreCase)) + ? "incompatible-gateway" + : hostConnected ? "connected" : (connectedRaw is not null && connectedRaw.StartsWith("Connecting", StringComparison.OrdinalIgnoreCase)) ? "connecting" : "disconnected"; diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs index 96c7fed00..3ca7afc69 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs @@ -183,6 +183,7 @@ public override Element Render() { "connected" => LocalizationHelper.GetString("Chat_Composer_Placeholder_Connected"), "connecting" => LocalizationHelper.GetString("Chat_Composer_Placeholder_Connecting"), + "incompatible-gateway" => LocalizationHelper.GetString("Chat_Composer_Placeholder_IncompatibleGateway"), _ => LocalizationHelper.GetString("Chat_Composer_Placeholder_NotConnected") }; diff --git a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw index 052ca9801..d82ac1204 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw @@ -2877,6 +2877,9 @@ On your gateway host (Mac/Linux), run: Not connected + + Gateway update required — incompatible version + Attach diff --git a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw index 69c912520..9fd29c2aa 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw @@ -2828,6 +2828,9 @@ Sur votre hôte passerelle (Mac/Linux), exécutez : Non connecté + + Gateway update required — incompatible version + Joindre diff --git a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw index fe766d944..87a49064b 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw @@ -2829,6 +2829,9 @@ Voer op uw gateway-host (Mac/Linux) uit: Niet verbonden + + Gateway update required — incompatible version + Bijvoegen diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw index c1b9c1ed9..5849c2968 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw @@ -2828,6 +2828,9 @@ 未连接 + + Gateway update required — incompatible version + 附加 diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw index c9218f538..8871ddbaf 100644 --- a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw +++ b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw @@ -2828,6 +2828,9 @@ 未連線 + + Gateway update required — incompatible version + 附加 diff --git a/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs b/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs index 40f5fc8ef..dc0048eb4 100644 --- a/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs +++ b/tests/OpenClaw.Tray.Tests/OpenClawChatDataProviderTests.cs @@ -460,6 +460,43 @@ public async Task LoadAsync_HandshakeKnown_ZeroSessions_ExposesReadyComposeTarge Assert.Equal("agent:main:main", snap.DefaultThreadId); } + [Fact] + public async Task LoadAsync_HandshakeComplete_NoSessionKey_SignalsIncompatibleGateway() + { + // When the gateway completes the handshake but does not advertise + // a mainSessionKey (or sessionDefaults.mainKey), the provider must surface + // an "Incompatible gateway" connection label and a NotReady compose target + // so the UI can show a clear "gateway update required" message rather than + // silently blocking send. Relates to issue #459. + var (bridge, provider, _, _) = CreateProvider(); + bridge.HasHandshakeSnapshot = true; + bridge.MainSessionKey = null; // incompatible gateway: no session key + bridge.RaiseStatus(ConnectionStatus.Connected); + var snap = await provider.LoadAsync(); + + Assert.Equal("Incompatible gateway", snap.ConnectionStatus); + Assert.False(snap.ComposeTarget.IsReady); + Assert.Null(snap.ComposeTarget.SessionKey); + } + + [Fact] + public async Task StatusChanged_IncompatibleGateway_IsReflectedInSnapshotConnectionLabel() + { + // Raise Connected with handshake present but no session key; the snapshot + // must use "Incompatible gateway" rather than the plain "Connected" label. + var (bridge, provider, snapshots, _) = CreateProvider(); + bridge.HasHandshakeSnapshot = true; + bridge.MainSessionKey = null; + await provider.LoadAsync(); + snapshots.Clear(); + + bridge.RaiseStatus(ConnectionStatus.Connected); + + Assert.NotEmpty(snapshots); + Assert.Equal("Incompatible gateway", snapshots[^1].ConnectionStatus); + Assert.False(snapshots[^1].ComposeTarget.IsReady); + } + // ── Parity additions: streaming, lifecycle, reasoning, history, abort ── [Fact] From 53300ecda3b4b16c786d3de613e89f835ad6c081 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:01 -0700 Subject: [PATCH 002/115] fix(chat): tighten composer padding symmetry and cap tool burst card 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> --- src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | 5 +++-- src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 3d94748ca..a7e509e5a 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1187,7 +1187,8 @@ string TaskFooter() Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) .Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) - .CornerRadius(8); + .CornerRadius(8) + .Set(b => { b.MaxWidth = 720; b.HorizontalAlignment = HorizontalAlignment.Left; }); // Build the per-step rows once — used by Plain, TaskHeader, and // CompactSummary (when expanded). @@ -1469,7 +1470,7 @@ string Truncate(string s, int max) ).Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) .CornerRadius(8) - .Set(b => { b.HorizontalAlignment = HorizontalAlignment.Left; }); + .Set(b => { b.MaxWidth = 720; b.HorizontalAlignment = HorizontalAlignment.Left; }); // Wrap with the assistant avatar slot so the burst visually // anchors to the agent that produced it (and lines up with the diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs index fc1a311c3..99846888d 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs @@ -248,8 +248,8 @@ public override Element Render() Element dropdownsRow = composerLayout switch { ChatComposerLayout.Minimal => Empty(), - ChatComposerLayout.InlinePill => (FlexRow(modelCombo) with { ColumnGap = 4 }), - _ => (FlexRow(channelCombo, modelCombo, reasoningCombo) with { ColumnGap = 4 }), + ChatComposerLayout.InlinePill => (FlexRow(modelCombo) with { ColumnGap = 6 }), + _ => (FlexRow(channelCombo, modelCombo, reasoningCombo) with { ColumnGap = 6 }), }; // ── Row 2: multi-line composer textbox ───────────────────────── @@ -745,7 +745,7 @@ Element IconButton(string glyph, string tip, Action onClick, Brush? foreground = permissionBanner, Border( VStack(8, textbox, attachmentChip, bottomRow) - ).Padding(14, 12, 8, 12) + ).Padding(14, 12, 14, 12) .Set(b => { b.BorderThickness = new Thickness(0, 1, 0, 0); @@ -770,7 +770,7 @@ Element IconButton(string glyph, string tip, Action onClick, Brush? foreground = permissionBanner2, Border( VStack(8, dropdownsRow, textbox, voiceIndicator, attachmentChip, actionsRow) - ).Padding(14, 12, 8, 12) + ).Padding(14, 12, 14, 12) .Set(b => { // Top divider only — mirrors Kenny's ChatShell ComposerBorder. From 53d6a3df4ce4e166caacac7fd366bce9532a1c92 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:01 -0700 Subject: [PATCH 003/115] ui(chat): unify margins, radius, drop tool footer, outline tool cards - 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> --- .../Chat/OpenClawChatTimeline.cs | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index a7e509e5a..c6e5e41ed 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -439,9 +439,10 @@ static Element TimelineInset(Element child, double top = 2, double bottom = 2) = ? themeBrush("TextFillColorTertiaryBrush") : themeBrush("TextFillColorSecondaryBrush"); var chatTextFg = themeBrush("TextFillColorPrimaryBrush"); - // Tool chips kept in a slightly cooler/dim shade so they read as - // secondary content next to the assistant bubble. - var toolCardBgBrush = themeBrush("SubtleFillColorTertiaryBrush"); + // Tool chips: outline-only so they clearly differ from the filled + // assistant bubble (per visual feedback — make tool cards distinct + // from chat bubbles instead of looking like a slightly dimmer copy). + var toolCardBgBrush = (Brush)new SolidColorBrush(Microsoft.UI.Colors.Transparent); var toolCardBorderBrush = themeBrush("ControlStrokeColorDefaultBrush"); // Avatar: 36×36 circle (Kenny uses circular avatars). Same constructor @@ -850,7 +851,7 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst VStack(2, bubbleRow, footer) .HAlign(HorizontalAlignment.Stretch) ).Background(new SolidColorBrush(Colors.Transparent)) - .Margin(gutter, topMargin, 8, bottomMargin), + .Margin(gutter, topMargin, 16, bottomMargin), entry.Id); } @@ -913,7 +914,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends .HAlign(HorizontalAlignment.Stretch) .AutomationName(entry.Text ?? "") ).Background(new SolidColorBrush(Colors.Transparent)) - .Margin(8, topMargin, gutter, bottomMargin), + .Margin(16, topMargin, gutter, bottomMargin), entry.Id); } @@ -1167,14 +1168,11 @@ Element BuildSection(string sectionLabel, string contentText) string? StepPrefix(int i) => showStepNumbers ? $"{i + 1}." : null; - // Footer reflects the *last* entry's timestamp — that's when the - // burst finished from the user's POV. + // Footer (when shown) reflects the *last* entry's timestamp — + // that's when the burst finished from the user's POV. var lastEntry = entries[entries.Count - 1]; var entryMeta = MetaFor(lastEntry.Id); var timeStr = FormatTime(entryMeta?.Timestamp); - var plainFooter = string.IsNullOrEmpty(timeStr) - ? LocalizationHelper.GetString("Chat_Tool_FooterLabel") - : string.Format(LocalizationHelper.GetString("Chat_Tool_FooterWithTimeFormat"), timeStr); // "Task · 3 steps · 8:16 PM" — used by FooterReframe + as the // companion line under the TaskHeader card. Keeps the time so // users still get the chronology. @@ -1187,8 +1185,7 @@ string TaskFooter() Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) .Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) - .CornerRadius(8) - .Set(b => { b.MaxWidth = 720; b.HorizontalAlignment = HorizontalAlignment.Left; }); + .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720; b.HorizontalAlignment = HorizontalAlignment.Left; }); // Build the per-step rows once — used by Plain, TaskHeader, and // CompactSummary (when expanded). @@ -1244,7 +1241,7 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) b.HorizontalAlignment = HorizontalAlignment.Stretch; b.HorizontalContentAlignment = HorizontalAlignment.Stretch; b.Padding = new Thickness(0); - b.CornerRadius = new CornerRadius(8, 8, summaryExpanded ? 0 : 8, summaryExpanded ? 0 : 8); + b.CornerRadius = new CornerRadius(bubbleRadius.TopLeft, bubbleRadius.TopRight, summaryExpanded ? 0 : bubbleRadius.BottomRight, summaryExpanded ? 0 : bubbleRadius.BottomLeft); }).Resources(r => r .Set("ButtonBackground", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x22, 0x00, 0x00, 0x00))) @@ -1440,7 +1437,7 @@ string Truncate(string s, int max) b.HorizontalAlignment = HorizontalAlignment.Stretch; b.HorizontalContentAlignment = HorizontalAlignment.Stretch; b.Padding = bubblePadding; - b.CornerRadius = new CornerRadius(8, 8, effectiveExpanded ? 0 : 8, effectiveExpanded ? 0 : 8); + b.CornerRadius = new CornerRadius(bubbleRadius.TopLeft, bubbleRadius.TopRight, effectiveExpanded ? 0 : bubbleRadius.BottomRight, effectiveExpanded ? 0 : bubbleRadius.BottomLeft); }).Resources(r => r .Set("ButtonBackground", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x22, 0x00, 0x00, 0x00))) @@ -1469,8 +1466,7 @@ string Truncate(string s, int max) VStack(0, cardChildren.ToArray()) ).Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) - .CornerRadius(8) - .Set(b => { b.MaxWidth = 720; b.HorizontalAlignment = HorizontalAlignment.Left; }); + .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720; b.HorizontalAlignment = HorizontalAlignment.Left; }); // Wrap with the assistant avatar slot so the burst visually // anchors to the agent that produced it (and lines up with the @@ -1490,23 +1486,26 @@ string Truncate(string s, int max) listCard.HAlign(HorizontalAlignment.Left).Grid(row: 0, column: 1) ).HAlign(HorizontalAlignment.Stretch); - // When avatar is present, drop the left margin to 0 so the - // avatar fills the indent slot. Otherwise keep the original 36. - // No trailing footer — assistant follow-up bubble below carries - // the timestamp for the whole turn. - var leftMargin = showAssistAvatar ? 8.0 : 36.0; - return burstRow.HAlign(HorizontalAlignment.Stretch).Margin(leftMargin, 6, gutter, 6); + // Match assistant bubble's outer inset so user/assistant/tool + // share the same left edge. Avatar slot lives inside burstRow. + return burstRow.HAlign(HorizontalAlignment.Stretch).Margin(16, 6, gutter, 6); } - // FooterReframe: rows unchanged, footer becomes "Task · N steps · time". - // Plain: original — "Tool · time". - var footerText = style == ToolBurstStyle.FooterReframe ? TaskFooter() : plainFooter; + // FooterReframe keeps the "Task · N steps · time" caption. + // Plain drops the footer entirely — the assistant follow-up + // bubble below carries the timestamp for the whole turn, and + // labelling each tool card with "Tool · time" added visual noise. + if (style == ToolBurstStyle.FooterReframe) + { + return VStack(2, + CardOf(rows), + FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) + ).HAlign(HorizontalAlignment.Stretch) + .Margin(16, 6, gutter, 6); + } - return VStack(2, - CardOf(rows), - FooterCaption(footerText, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch) - .Margin(36, 6, gutter, 6); + return CardOf(rows).HAlign(HorizontalAlignment.Left) + .Margin(16, 6, gutter, 6); } // Legacy single-entry RenderToolEntry removed — all ToolCall rendering From 43ed8a395219d38854548effe034b3837f8f5ed3 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:01 -0700 Subject: [PATCH 004/115] ui(chat): align assistant continuation with tool cards + add top breathing 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> --- .../Chat/OpenClawChatTimeline.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index c6e5e41ed..88ae65840 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -865,14 +865,12 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends return Empty(); // Avatar shown only on the FIRST entry of a contiguous agent-side - // run (Assistant + ToolCall task cards count as one agent block). - // Mid-run entries get a spacer so the bubble stays aligned with - // the first bubble that does carry the avatar. - Element leftSlot = !showAssistAvatar + // run. Continuation entries get no spacer — they align flush with + // the tool burst cards above (which also sit at the left inset), + // so the agent column reads as a single vertical edge. + Element leftSlot = !showAssistAvatar || !showAvatar ? Empty() - : (showAvatar - ? AssistantAvatar().VAlign(VerticalAlignment.Top) - : Border(Empty()).Size(36, 36)); + : AssistantAvatar().VAlign(VerticalAlignment.Top); // Assistant bubble — subtle gray with primary text. Radius/Padding // come from ChatExplorationState (BubbleCornerRadius + PaddingDensity). @@ -1683,8 +1681,9 @@ static bool IsAgentSide(ChatTimelineItemKind k) => // Page background matches dash-light --bg so bubbles stand out. Border( ScrollView( - Grid([GridSize.Star()], [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto], + Grid([GridSize.Star()], [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto], loadMoreButton.Grid(row: 0, column: 0), + Border(Empty()).Height(20).Grid(row: 1, column: 0), VStack(2, renderedEntries).Set(sp => { if (contentRef.Current != sp) @@ -1696,9 +1695,9 @@ static bool IsAgentSide(ChatTimelineItemKind k) => QueueScrollToBottom(sv, prevSessionIdRef.Current, disableAnimation: true); }; } - }).Grid(row: 1, column: 0), - thinkingIndicator.Grid(row: 2, column: 0), - Border(Empty()).Height(24).Grid(row: 3, column: 0) + }).Grid(row: 2, column: 0), + thinkingIndicator.Grid(row: 3, column: 0), + Border(Empty()).Height(24).Grid(row: 4, column: 0) ) ).Set(sv => { From 31c8dbe8a256689f5deee6f57dbca2947bc52c48 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:01 -0700 Subject: [PATCH 005/115] ui(chat): strip phantom avatar gap, unify tool row padding/height, align 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> --- src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | 8 ++++---- src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 88ae65840..9d6855148 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -886,7 +886,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends var bubbleRow = Grid( [GridSize.Auto, GridSize.Star()], [GridSize.Auto], - leftSlot.Grid(row: 0, column: 0).Margin(0, 0, bubbleSideMargin, 0), + leftSlot.Grid(row: 0, column: 0).Margin(0, 0, showAssistAvatar && showAvatar ? bubbleSideMargin : 0, 0), card.HAlign(HorizontalAlignment.Left).Grid(row: 0, column: 1) ).HAlign(HorizontalAlignment.Stretch); @@ -1031,8 +1031,8 @@ Element BuildRow(ChatTimelineItem entry, bool isFirst, bool isLast, string? step .VAlign(VerticalAlignment.Center) .HAlign(HorizontalAlignment.Right) .Grid(row: 0, column: 4) - ).HAlign(HorizontalAlignment.Stretch).Padding(12, 8, 12, 8) - ).Set(b => b.MinHeight = 22); + ).HAlign(HorizontalAlignment.Stretch).Padding(bubblePadding.Left, bubblePadding.Top, bubblePadding.Right, bubblePadding.Bottom) + ).Set(b => b.MinHeight = 32); Element body = Empty(); if (isExpanded) @@ -1480,7 +1480,7 @@ string Truncate(string s, int max) var burstRow = Grid( [GridSize.Auto, GridSize.Star()], [GridSize.Auto], - leftSlot.Grid(row: 0, column: 0).Margin(0, 0, bubbleSideMargin, 0), + leftSlot.Grid(row: 0, column: 0).Margin(0, 0, showAssistAvatar && showAvatar ? bubbleSideMargin : 0, 0), listCard.HAlign(HorizontalAlignment.Left).Grid(row: 0, column: 1) ).HAlign(HorizontalAlignment.Stretch); diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs index 99846888d..bfa798306 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs @@ -745,7 +745,7 @@ Element IconButton(string glyph, string tip, Action onClick, Brush? foreground = permissionBanner, Border( VStack(8, textbox, attachmentChip, bottomRow) - ).Padding(14, 12, 14, 12) + ).Padding(16, 12, 16, 12) .Set(b => { b.BorderThickness = new Thickness(0, 1, 0, 0); @@ -770,7 +770,7 @@ Element IconButton(string glyph, string tip, Action onClick, Brush? foreground = permissionBanner2, Border( VStack(8, dropdownsRow, textbox, voiceIndicator, attachmentChip, actionsRow) - ).Padding(14, 12, 14, 12) + ).Padding(16, 12, 16, 12) .Set(b => { // Top divider only — mirrors Kenny's ChatShell ComposerBorder. From 65ba516cc138ddfb3d752080f233badaf35428ee Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:02 -0700 Subject: [PATCH 006/115] ui(chat): align continuation footer, soften tool hover, light tool tint - 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> --- .../Chat/OpenClawChatTimeline.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 9d6855148..54de16850 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -439,10 +439,10 @@ static Element TimelineInset(Element child, double top = 2, double bottom = 2) = ? themeBrush("TextFillColorTertiaryBrush") : themeBrush("TextFillColorSecondaryBrush"); var chatTextFg = themeBrush("TextFillColorPrimaryBrush"); - // Tool chips: outline-only so they clearly differ from the filled - // assistant bubble (per visual feedback — make tool cards distinct - // from chat bubbles instead of looking like a slightly dimmer copy). - var toolCardBgBrush = (Brush)new SolidColorBrush(Microsoft.UI.Colors.Transparent); + // Tool chips: very subtle background tint + light border so they + // read as a secondary surface distinct from the filled assistant + // bubble without looking like an empty outlined box. + var toolCardBgBrush = themeBrush("LayerOnAcrylicFillColorDefaultBrush"); var toolCardBorderBrush = themeBrush("ControlStrokeColorDefaultBrush"); // Avatar: 36×36 circle (Kenny uses circular avatars). Same constructor @@ -900,7 +900,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends entryMeta?.InputTokens, entryMeta?.OutputTokens, entryMeta?.ResponseTokens, entryMeta?.ContextPercent, chatStampFg, entry.Id, entry.Text ?? ""); - var leftInset = showAssistAvatar ? (36 + bubbleSideMargin) : 0; + var leftInset = (showAssistAvatar && showAvatar) ? (36 + bubbleSideMargin) : 0; footer = footer.Margin(leftInset, 2, 0, 0); } @@ -1135,8 +1135,8 @@ Element BuildSection(string sectionLabel, string contentText) }) .Resources(r => r .Set("ButtonBackground", new SolidColorBrush(Colors.Transparent)) - .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x22, 0x00, 0x00, 0x00))) - .Set("ButtonBackgroundPressed", new SolidColorBrush(Color.FromArgb(0x33, 0x00, 0x00, 0x00))) + .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x10, 0x00, 0x00, 0x00))) + .Set("ButtonBackgroundPressed", new SolidColorBrush(Color.FromArgb(0x1C, 0x00, 0x00, 0x00))) .Set("ButtonBorderBrush", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBorderBrushPointerOver", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBorderBrushPressed", new SolidColorBrush(Colors.Transparent))); @@ -1242,8 +1242,8 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) b.CornerRadius = new CornerRadius(bubbleRadius.TopLeft, bubbleRadius.TopRight, summaryExpanded ? 0 : bubbleRadius.BottomRight, summaryExpanded ? 0 : bubbleRadius.BottomLeft); }).Resources(r => r .Set("ButtonBackground", new SolidColorBrush(Colors.Transparent)) - .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x22, 0x00, 0x00, 0x00))) - .Set("ButtonBackgroundPressed", new SolidColorBrush(Color.FromArgb(0x33, 0x00, 0x00, 0x00))) + .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x10, 0x00, 0x00, 0x00))) + .Set("ButtonBackgroundPressed", new SolidColorBrush(Color.FromArgb(0x1C, 0x00, 0x00, 0x00))) .Set("ButtonBorderBrush", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBorderBrushPointerOver", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBorderBrushPressed", new SolidColorBrush(Colors.Transparent))); @@ -1438,8 +1438,8 @@ string Truncate(string s, int max) b.CornerRadius = new CornerRadius(bubbleRadius.TopLeft, bubbleRadius.TopRight, effectiveExpanded ? 0 : bubbleRadius.BottomRight, effectiveExpanded ? 0 : bubbleRadius.BottomLeft); }).Resources(r => r .Set("ButtonBackground", new SolidColorBrush(Colors.Transparent)) - .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x22, 0x00, 0x00, 0x00))) - .Set("ButtonBackgroundPressed", new SolidColorBrush(Color.FromArgb(0x33, 0x00, 0x00, 0x00))) + .Set("ButtonBackgroundPointerOver", new SolidColorBrush(Color.FromArgb(0x10, 0x00, 0x00, 0x00))) + .Set("ButtonBackgroundPressed", new SolidColorBrush(Color.FromArgb(0x1C, 0x00, 0x00, 0x00))) .Set("ButtonBorderBrush", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBorderBrushPointerOver", new SolidColorBrush(Colors.Transparent)) .Set("ButtonBorderBrushPressed", new SolidColorBrush(Colors.Transparent))); From 003afae7ce85d615179e26b5b660d390c8881b1a Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:02 -0700 Subject: [PATCH 007/115] fix(chat): align timestamps with bubble inner text 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> --- src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 54de16850..43565b80a 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -840,6 +840,7 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst var entryMeta = MetaFor(entry.Id); var timeStr = FormatTime(entryMeta?.Timestamp); var rightInset = showUserAvatar ? (36 + bubbleSideMargin) : 0; + rightInset += (int)bubblePadding.Right; footer = BuildUserFooter(userSender, timeStr, chatStampFg, entry.Id, entry.Text ?? "") .Margin(0, 2, rightInset, 0); } @@ -901,6 +902,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends entryMeta?.ResponseTokens, entryMeta?.ContextPercent, chatStampFg, entry.Id, entry.Text ?? ""); var leftInset = (showAssistAvatar && showAvatar) ? (36 + bubbleSideMargin) : 0; + leftInset += (int)bubblePadding.Left; footer = footer.Margin(leftInset, 2, 0, 0); } From ab977832f79b2b3dab359ec05652f8734ace097e Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:02 -0700 Subject: [PATCH 008/115] fix(chat): align user timestamp with bubble text (no avatar case) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 43565b80a..fd10efbd5 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -831,7 +831,7 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst [GridSize.Star(), GridSize.Auto], [GridSize.Auto], content.HAlign(HorizontalAlignment.Right).Grid(row: 0, column: 0), - rightSlot.Grid(row: 0, column: 1).Margin(bubbleSideMargin, 0, 0, 0) + rightSlot.Grid(row: 0, column: 1).Margin(showUserAvatar ? bubbleSideMargin : 0, 0, 0, 0) ).HAlign(HorizontalAlignment.Stretch); Element footer = Empty(); From 85deafa99cdd20e25283ca570108190c0361b6c1 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:02 -0700 Subject: [PATCH 009/115] fix(chat): align Plain/FooterReframe tool bursts with assistant bubble 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> --- .../Chat/OpenClawChatTimeline.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index fd10efbd5..8883008f2 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1491,20 +1491,40 @@ string Truncate(string s, int max) return burstRow.HAlign(HorizontalAlignment.Stretch).Margin(16, 6, gutter, 6); } + // Helper: wrap a tool burst card with the same avatar/spacer slot + // that TaskHeader uses, so Plain/FooterReframe align with the + // assistant bubble's text edge instead of starting at the gutter. + Element WrapWithAvatarSlot(Element card) + { + Element leftSlot = !showAssistAvatar + ? Empty() + : (showAvatar + ? AssistantAvatar().VAlign(VerticalAlignment.Top) + : Border(Empty()).Size(36, 36)); + + return Grid( + [GridSize.Auto, GridSize.Star()], + [GridSize.Auto], + leftSlot.Grid(row: 0, column: 0).Margin(0, 0, showAssistAvatar && showAvatar ? bubbleSideMargin : 0, 0), + card.HAlign(HorizontalAlignment.Left).Grid(row: 0, column: 1) + ).HAlign(HorizontalAlignment.Stretch); + } + // FooterReframe keeps the "Task · N steps · time" caption. // Plain drops the footer entirely — the assistant follow-up // bubble below carries the timestamp for the whole turn, and // labelling each tool card with "Tool · time" added visual noise. if (style == ToolBurstStyle.FooterReframe) { - return VStack(2, - CardOf(rows), - FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch) - .Margin(16, 6, gutter, 6); + return WrapWithAvatarSlot( + VStack(2, + CardOf(rows), + FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) + ) + ).Margin(16, 6, gutter, 6); } - return CardOf(rows).HAlign(HorizontalAlignment.Left) + return WrapWithAvatarSlot(CardOf(rows)) .Margin(16, 6, gutter, 6); } From d70387fe8327ed0edf32cbda1d9a15dc802d66a5 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:02 -0700 Subject: [PATCH 010/115] feat(chat): default ToolBurstStyle to TaskList for collapsible bursts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../Chat/Explorations/ChatExplorationState.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs index 3abd1fe24..f0c5c8dd8 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs @@ -394,7 +394,13 @@ public static bool ShowContextPercent // ---- Tool burst (H) ---- - private static ToolBurstStyle _toolBurstStyle = ToolBurstStyle.Plain; + // Default to TaskList so tool bursts auto-collapse into a one-line + // summary when finished (with chevron to expand). Matches the "show + // work is happening, hide verbose logs unless asked" UX Scott + // requested — TaskList stays expanded while a step is InProgress and + // collapses to "Ran N steps" / result snippet when done. Per-step + // rows can still be expanded individually for full args/output. + private static ToolBurstStyle _toolBurstStyle = ToolBurstStyle.TaskList; private static bool _showStepNumbers; /// From 7d6a8b1ec1eb0f1d0a1f57dd1337d51827a9ea3f Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:03 -0700 Subject: [PATCH 011/115] Chat UI: tool card indent + token/ctx footer priority - 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> --- .../ChatExplorationPresetStore.cs | 8 +-- .../Chat/Explorations/ChatExplorationState.cs | 16 ++---- .../Chat/OpenClawChatTimeline.cs | 55 +++++++++---------- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationPresetStore.cs b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationPresetStore.cs index 16807a9d9..255ef1fb5 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationPresetStore.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationPresetStore.cs @@ -43,10 +43,10 @@ public sealed record ChatExplorationPreset public double BubbleSideMargin { get; init; } = 8; // Footer - public bool ShowSenderName { get; init; } = true; - public bool ShowModelName { get; init; } = true; - public bool ShowTokens { get; init; } - public bool ShowContextPercent { get; init; } + public bool ShowSenderName { get; init; } = false; + public bool ShowModelName { get; init; } = false; + public bool ShowTokens { get; init; } = true; + public bool ShowContextPercent { get; init; } = true; // Avatar public bool ShowAvatars { get; init; } = true; diff --git a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs index f0c5c8dd8..3327d1f58 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationState.cs @@ -155,10 +155,10 @@ public static class ChatExplorationState private static double _bubbleMaxWidth = 560d; private static double _bubbleSideMargin = 8d; - private static bool _showSenderName = true; - private static bool _showModelName = true; - private static bool _showTokens; - private static bool _showContextPercent; + private static bool _showSenderName = false; + private static bool _showModelName = false; + private static bool _showTokens = true; + private static bool _showContextPercent = true; // Default icon glyphs match production OpenClawComposer.cs. private static string _sendIconGlyph = "\uE724"; @@ -394,13 +394,7 @@ public static bool ShowContextPercent // ---- Tool burst (H) ---- - // Default to TaskList so tool bursts auto-collapse into a one-line - // summary when finished (with chevron to expand). Matches the "show - // work is happening, hide verbose logs unless asked" UX Scott - // requested — TaskList stays expanded while a step is InProgress and - // collapses to "Ran N steps" / result snippet when done. Per-step - // rows can still be expanded individually for full args/output. - private static ToolBurstStyle _toolBurstStyle = ToolBurstStyle.TaskList; + private static ToolBurstStyle _toolBurstStyle = ToolBurstStyle.Plain; private static bool _showStepNumbers; /// diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 8883008f2..ebd6267ce 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1151,6 +1151,18 @@ Element BuildSection(string sectionLabel, string contentText) var showStepNumbers = ChatExplorationState.ShowStepNumbers && entries.Count > 1; var stepCount = entries.Count; + // Tool burst alignment: align outer left to the assistant bubble's + // outer left edge + a small indent so the card visually reads as + // "owned by" the assistant bubble above. Width math: + // bubble outer left = gutter(16) + avatar(36) + sideMargin + // tool card outer left = bubble outer left + indent + // When avatars are globally hidden, drop the avatar slot but still + // keep the indent so the tool card sits inside the bubble's reading + // column rather than aligning to the gutter. + const int toolIndent = 16; + var toolAvatarSlot = showAssistAvatar ? (36 + (int)bubbleSideMargin) : 0; + var toolLeftMargin = 16 + toolAvatarSlot + toolIndent; + // Aggregate burst status: Error if any errored, Running if any // not-yet-finished, Interrupted if any interrupted (but not running), // otherwise Done. Drives the task header pill. @@ -1185,7 +1197,7 @@ string TaskFooter() Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) .Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) - .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720; b.HorizontalAlignment = HorizontalAlignment.Left; }); + .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720 - toolIndent; b.HorizontalAlignment = HorizontalAlignment.Left; }); // Build the per-step rows once — used by Plain, TaskHeader, and // CompactSummary (when expanded). @@ -1260,7 +1272,7 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) return VStack(2, CardOf(pieces.ToArray()), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(36, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); } // TaskHeader: prepend a non-clickable header row to the card. @@ -1292,7 +1304,7 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) return VStack(2, CardOf(combined), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(36, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); } // TaskList: per-step rows with a status icon (✓ / spinner / ✕) @@ -1491,41 +1503,24 @@ string Truncate(string s, int max) return burstRow.HAlign(HorizontalAlignment.Stretch).Margin(16, 6, gutter, 6); } - // Helper: wrap a tool burst card with the same avatar/spacer slot - // that TaskHeader uses, so Plain/FooterReframe align with the - // assistant bubble's text edge instead of starting at the gutter. - Element WrapWithAvatarSlot(Element card) - { - Element leftSlot = !showAssistAvatar - ? Empty() - : (showAvatar - ? AssistantAvatar().VAlign(VerticalAlignment.Top) - : Border(Empty()).Size(36, 36)); - - return Grid( - [GridSize.Auto, GridSize.Star()], - [GridSize.Auto], - leftSlot.Grid(row: 0, column: 0).Margin(0, 0, showAssistAvatar && showAvatar ? bubbleSideMargin : 0, 0), - card.HAlign(HorizontalAlignment.Left).Grid(row: 0, column: 1) - ).HAlign(HorizontalAlignment.Stretch); - } - // FooterReframe keeps the "Task · N steps · time" caption. // Plain drops the footer entirely — the assistant follow-up // bubble below carries the timestamp for the whole turn, and // labelling each tool card with "Tool · time" added visual noise. + // Both styles align the card to the bubble's text edge + indent + // (see toolLeftMargin) so the burst visually belongs to the + // assistant bubble it follows. if (style == ToolBurstStyle.FooterReframe) { - return WrapWithAvatarSlot( - VStack(2, - CardOf(rows), - FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ) - ).Margin(16, 6, gutter, 6); + return VStack(2, + CardOf(rows), + FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) + ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); } - return WrapWithAvatarSlot(CardOf(rows)) - .Margin(16, 6, gutter, 6); + return CardOf(rows) + .HAlign(HorizontalAlignment.Left) + .Margin(toolLeftMargin, 6, gutter, 6); } // Legacy single-entry RenderToolEntry removed — all ToolCall rendering From 1287011c9f5630851a0ca63a7282b78f55c35bde Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:03 -0700 Subject: [PATCH 012/115] Chat UI: tool cards below assistant/thinking, right-align with bubble MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- .../Chat/OpenClawChatTimeline.cs | 102 ++++++++++++++---- 1 file changed, 80 insertions(+), 22 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index ebd6267ce..5f74830f0 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1197,7 +1197,7 @@ string TaskFooter() Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) .Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) - .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720 - toolIndent; b.HorizontalAlignment = HorizontalAlignment.Left; }); + .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720 - toolIndent; b.HorizontalAlignment = HorizontalAlignment.Stretch; }); // Build the per-step rows once — used by Plain, TaskHeader, and // CompactSummary (when expanded). @@ -1272,7 +1272,7 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) return VStack(2, CardOf(pieces.ToArray()), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); } // TaskHeader: prepend a non-clickable header row to the card. @@ -1304,7 +1304,7 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) return VStack(2, CardOf(combined), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); } // TaskList: per-step rows with a status icon (✓ / spinner / ✕) @@ -1515,11 +1515,11 @@ string Truncate(string s, int max) return VStack(2, CardOf(rows), FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); } return CardOf(rows) - .HAlign(HorizontalAlignment.Left) + .HAlign(HorizontalAlignment.Stretch) .Margin(toolLeftMargin, 6, gutter, 6); } @@ -1628,11 +1628,42 @@ static bool IsAgentSide(ChatTimelineItemKind k) => k == ChatTimelineItemKind.Assistant || k == ChatTimelineItemKind.ToolCall; var renderedEntries = new Element[Props.Entries.Count]; - for (int i = 0; i < Props.Entries.Count; i++) + // Reorder display so that within each turn (segment between User entries) + // ToolCall bursts render AFTER the assistant reply (or the thinking + // indicator if no reply has streamed yet). Gateway emits tool_start + // before assistant_delta, but the desired visual flow is + // [User] → [Assistant reply / thinking] → [Tool burst] + // so the assistant message reads first and the tool work hangs below it. + var orderedIdx = new int[Props.Entries.Count]; { + int outPos = 0; + int turnStart = 0; + void Flush(int endExclusive) + { + for (int j = turnStart; j < endExclusive; j++) + if (Props.Entries[j].Kind != ChatTimelineItemKind.ToolCall) + orderedIdx[outPos++] = j; + for (int j = turnStart; j < endExclusive; j++) + if (Props.Entries[j].Kind == ChatTimelineItemKind.ToolCall) + orderedIdx[outPos++] = j; + } + for (int i = 0; i < Props.Entries.Count; i++) + { + if (Props.Entries[i].Kind == ChatTimelineItemKind.User && i > turnStart) + { + Flush(i); + turnStart = i; + } + } + Flush(Props.Entries.Count); + } + + for (int k = 0; k < orderedIdx.Length; k++) + { + int i = orderedIdx[k]; var entry = Props.Entries[i]; - var prevKind = i > 0 ? Props.Entries[i - 1].Kind : (ChatTimelineItemKind?)null; - var nextKind = i < Props.Entries.Count - 1 ? Props.Entries[i + 1].Kind : (ChatTimelineItemKind?)null; + var prevKind = k > 0 ? Props.Entries[orderedIdx[k - 1]].Kind : (ChatTimelineItemKind?)null; + var nextKind = k < orderedIdx.Length - 1 ? Props.Entries[orderedIdx[k + 1]].Kind : (ChatTimelineItemKind?)null; var startsBurst = prevKind is null || !SameBurstKind(prevKind.Value, entry.Kind); var endsBurst = nextKind is null || !SameBurstKind(entry.Kind, nextKind.Value); var showAvatar = !(prevKind is { } pk && IsAgentSide(pk) && IsAgentSide(entry.Kind)); @@ -1644,28 +1675,26 @@ static bool IsAgentSide(ChatTimelineItemKind k) => { if (!showToolCalls) { - renderedEntries[i] = Empty().WithKey(entry.Id); + renderedEntries[k] = Empty().WithKey(entry.Id); continue; } if (!startsBurst) { - // Non-start tool entries collapsed into the burst rendered - // at startsBurst; render Empty here to avoid duplication. - renderedEntries[i] = Empty().WithKey(entry.Id); + renderedEntries[k] = Empty().WithKey(entry.Id); continue; } var burst = new System.Collections.Generic.List { entry }; - int j = i + 1; - while (j < Props.Entries.Count && Props.Entries[j].Kind == ChatTimelineItemKind.ToolCall) + int kj = k + 1; + while (kj < orderedIdx.Length && Props.Entries[orderedIdx[kj]].Kind == ChatTimelineItemKind.ToolCall) { - burst.Add(Props.Entries[j]); - j++; + burst.Add(Props.Entries[orderedIdx[kj]]); + kj++; } - renderedEntries[i] = RenderToolBurst(burst, showAvatar).WithKey(entry.Id); + renderedEntries[k] = RenderToolBurst(burst, showAvatar).WithKey(entry.Id); continue; } - renderedEntries[i] = RenderEntry(entry, startsBurst, endsBurst, showAvatar).WithKey(entry.Id); + renderedEntries[k] = RenderEntry(entry, startsBurst, endsBurst, showAvatar).WithKey(entry.Id); } // Inline "thinking" indicator rendered just below the last entry @@ -1694,14 +1723,44 @@ static bool IsAgentSide(ChatTimelineItemKind k) => .LiveRegion(Microsoft.UI.Xaml.Automation.Peers.AutomationLiveSetting.Polite); } + // Build the final element list, splicing the thinking indicator + // inline RIGHT AFTER the most recent User entry so tool bursts that + // follow it visually hang below "Agent is thinking…" (and below the + // assistant reply once one streams in). + Element[] timelineRows; + if (Props.ShowThinkingIndicator && renderedEntries.Length > 0) + { + int insertAfter = -1; + for (int k = orderedIdx.Length - 1; k >= 0; k--) + { + if (Props.Entries[orderedIdx[k]].Kind == ChatTimelineItemKind.User) + { + insertAfter = k; + break; + } + } + var spliced = new System.Collections.Generic.List(renderedEntries.Length + 1); + for (int k = 0; k < renderedEntries.Length; k++) + { + spliced.Add(renderedEntries[k]); + if (k == insertAfter) spliced.Add(thinkingIndicator); + } + if (insertAfter < 0) spliced.Insert(0, thinkingIndicator); + timelineRows = spliced.ToArray(); + } + else + { + timelineRows = renderedEntries; + } + return Grid([GridSize.Star()], [GridSize.Star()], // Page background matches dash-light --bg so bubbles stand out. Border( ScrollView( - Grid([GridSize.Star()], [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto], + Grid([GridSize.Star()], [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Auto], loadMoreButton.Grid(row: 0, column: 0), Border(Empty()).Height(20).Grid(row: 1, column: 0), - VStack(2, renderedEntries).Set(sp => + VStack(2, timelineRows).Set(sp => { if (contentRef.Current != sp) { @@ -1713,8 +1772,7 @@ static bool IsAgentSide(ChatTimelineItemKind k) => }; } }).Grid(row: 2, column: 0), - thinkingIndicator.Grid(row: 3, column: 0), - Border(Empty()).Height(24).Grid(row: 4, column: 0) + Border(Empty()).Height(24).Grid(row: 3, column: 0) ) ).Set(sv => { From 6301d78b6beab4d0bd90799fed73b62697d9d22e Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:03 -0700 Subject: [PATCH 013/115] Chat UI: align assistant bubble + tool card right edges 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> --- src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 5f74830f0..c0fe4f181 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -875,6 +875,9 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends // Assistant bubble — subtle gray with primary text. Radius/Padding // come from ChatExplorationState (BubbleCornerRadius + PaddingDensity). + // MaxWidth+Stretch so the bubble's right stroke lines up with the + // tool burst card's right stroke (tool card sits indented inside + // the same column with HAlign=Stretch and MaxWidth = 720 - indent). var card = Border( SafeMarkdownText(entry.Text) ).Background(assistantBubbleBg) @@ -882,13 +885,14 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends { b.CornerRadius = bubbleRadius; b.Padding = bubblePadding; + b.MaxWidth = 720; }); var bubbleRow = Grid( [GridSize.Auto, GridSize.Star()], [GridSize.Auto], leftSlot.Grid(row: 0, column: 0).Margin(0, 0, showAssistAvatar && showAvatar ? bubbleSideMargin : 0, 0), - card.HAlign(HorizontalAlignment.Left).Grid(row: 0, column: 1) + card.HAlign(HorizontalAlignment.Stretch).Grid(row: 0, column: 1) ).HAlign(HorizontalAlignment.Stretch); Element footer = Empty(); From ccec251d247f30c017db8f30cc804ba0fd26c706 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:03 -0700 Subject: [PATCH 014/115] Chat UI: revert assistant bubble to HAlign=Left (keep avatar/timestamp 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> --- src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index c0fe4f181..4ff929c88 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -875,9 +875,9 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends // Assistant bubble — subtle gray with primary text. Radius/Padding // come from ChatExplorationState (BubbleCornerRadius + PaddingDensity). - // MaxWidth+Stretch so the bubble's right stroke lines up with the - // tool burst card's right stroke (tool card sits indented inside - // the same column with HAlign=Stretch and MaxWidth = 720 - indent). + // HAlign=Left keeps the bubble anchored next to the avatar/timestamp + // column. MaxWidth=720 caps the growth so long messages stop where + // the tool burst card's max right edge lands. var card = Border( SafeMarkdownText(entry.Text) ).Background(assistantBubbleBg) @@ -892,7 +892,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends [GridSize.Auto, GridSize.Star()], [GridSize.Auto], leftSlot.Grid(row: 0, column: 0).Margin(0, 0, showAssistAvatar && showAvatar ? bubbleSideMargin : 0, 0), - card.HAlign(HorizontalAlignment.Stretch).Grid(row: 0, column: 1) + card.HAlign(HorizontalAlignment.Left).Grid(row: 0, column: 1) ).HAlign(HorizontalAlignment.Stretch); Element footer = Empty(); From 00b4c598540245e2c697c7b72dff2997dfc066d1 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:04 -0700 Subject: [PATCH 015/115] Chat UI: revert tool card to HAlign=Left (avoid WinUI stretch-centers-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> --- src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 4ff929c88..9eb26266f 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1201,7 +1201,7 @@ string TaskFooter() Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) .Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) - .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720 - toolIndent; b.HorizontalAlignment = HorizontalAlignment.Stretch; }); + .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720 - toolIndent; b.HorizontalAlignment = HorizontalAlignment.Left; }); // Build the per-step rows once — used by Plain, TaskHeader, and // CompactSummary (when expanded). @@ -1276,7 +1276,7 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) return VStack(2, CardOf(pieces.ToArray()), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); } // TaskHeader: prepend a non-clickable header row to the card. @@ -1308,7 +1308,7 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) return VStack(2, CardOf(combined), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); } // TaskList: per-step rows with a status icon (✓ / spinner / ✕) @@ -1519,11 +1519,11 @@ string Truncate(string s, int max) return VStack(2, CardOf(rows), FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); } return CardOf(rows) - .HAlign(HorizontalAlignment.Stretch) + .HAlign(HorizontalAlignment.Left) .Margin(toolLeftMargin, 6, gutter, 6); } From 8add87d10c544aa0595774ec8d5417f6d55306c6 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:05 -0700 Subject: [PATCH 016/115] Chat UI: anchor tool card left while stretching to MaxWidth via Auto/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> --- .../Chat/OpenClawChatTimeline.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 9eb26266f..4c42db8b5 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1201,7 +1201,20 @@ string TaskFooter() Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) .Background(toolCardBgBrush) .WithBorder(toolCardBorderBrush, 1) - .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720 - toolIndent; b.HorizontalAlignment = HorizontalAlignment.Left; }); + .Set(b => { b.CornerRadius = bubbleRadius; b.MaxWidth = 720 - toolIndent; b.HorizontalAlignment = HorizontalAlignment.Stretch; }); + + // Wrap the card in a left-anchored Auto/Star Grid so its 704-wide + // slot stays pinned to toolLeftMargin instead of being centered + // inside the wider timeline column (WinUI's quirk: HAlign=Stretch + // with finite MaxWidth centers when the slot is bigger than the + // MaxWidth). The Auto column sizes to the card's MaxWidth, the + // Star column eats the remaining width on the right. + Element AnchorLeft(Element card) => Grid( + [GridSize.Auto, GridSize.Star()], + [GridSize.Auto], + card.Grid(row: 0, column: 0), + Empty().Grid(row: 0, column: 1) + ).HAlign(HorizontalAlignment.Stretch); // Build the per-step rows once — used by Plain, TaskHeader, and // CompactSummary (when expanded). @@ -1274,9 +1287,9 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) pieces.AddRange(rows); } return VStack(2, - CardOf(pieces.ToArray()), + AnchorLeft(CardOf(pieces.ToArray())), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); } // TaskHeader: prepend a non-clickable header row to the card. @@ -1306,9 +1319,9 @@ Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls)) Array.Copy(rows, 0, combined, 1, rows.Length); return VStack(2, - CardOf(combined), + AnchorLeft(CardOf(combined)), FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); } // TaskList: per-step rows with a status icon (✓ / spinner / ✕) @@ -1517,13 +1530,12 @@ string Truncate(string s, int max) if (style == ToolBurstStyle.FooterReframe) { return VStack(2, - CardOf(rows), + AnchorLeft(CardOf(rows)), FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Left).Margin(toolLeftMargin, 6, gutter, 6); + ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); } - return CardOf(rows) - .HAlign(HorizontalAlignment.Left) + return AnchorLeft(CardOf(rows)) .Margin(toolLeftMargin, 6, gutter, 6); } From fa6f7ad6daf575a7f634a18195229679a1e5f27c Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:05 -0700 Subject: [PATCH 017/115] Chat UI: bind tool card width to assistant bubble's ActualWidth 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> --- .../Chat/OpenClawChatTimeline.cs | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index 4c42db8b5..e631a5a76 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -856,7 +856,13 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst entry.Id); } - Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar) + // Per-turn shared reference between the assistant bubble and any + // tool cards rendered below it. The tool card binds its Width to + // bubble.ActualWidth - toolIndent so the two cards' right edges + // (and left indent) stay exactly parallel as the bubble grows + // with content. Single-element Border[] used as a mutable slot + // since these are local functions (no nested class allowed). + Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar, Microsoft.UI.Xaml.Controls.Border[]? bubbleSlot = null) { if (string.IsNullOrEmpty(entry.Text)) return Empty(); @@ -886,6 +892,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends b.CornerRadius = bubbleRadius; b.Padding = bubblePadding; b.MaxWidth = 720; + if (bubbleSlot != null) bubbleSlot[0] = b; }); var bubbleRow = Grid( @@ -928,7 +935,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends // into `▸ ⚡ · [Done]`; click expands the row // to reveal the original args + raw output (the previous chip body). // A single trailing `Tool · TaskList, + /// Smart default — picks per burst state: + /// running bursts render Plain (per-step status visible); + /// terminal multi-step bursts collapse to CompactSummary (1-line summary, + /// click chevron to expand); single-step bursts stay Plain. + /// Matches Scott's feedback: keep live progress visible, fold completed + /// work into a tidy one-liner once the turn finishes. + Auto, } /// @@ -394,7 +401,7 @@ public static bool ShowContextPercent // ---- Tool burst (H) ---- - private static ToolBurstStyle _toolBurstStyle = ToolBurstStyle.Plain; + private static ToolBurstStyle _toolBurstStyle = ToolBurstStyle.Auto; private static bool _showStepNumbers; /// diff --git a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationsPanel.cs b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationsPanel.cs index d9a555b8c..99792087f 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationsPanel.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/Explorations/ChatExplorationsPanel.cs @@ -212,6 +212,7 @@ public override Element Render() // ── H. Tool burst (multi-step task framing) ────────────────── // Variants explored here mirror competitor patterns: + // Auto — smart default: Plain while running, CompactSummary when done // Plain — current Cursor-lite (no task framing) // TaskHeader — Cursor's "Tool calls (N steps)" + per-row list // CompactSummary — single collapsed "Task · 3 steps" row, expands @@ -219,6 +220,7 @@ public override Element Render() var toolBurstSection = Section("H. Tool burst style", EnumCombo("Burst style", ChatExplorationState.ToolBurstStyle, v => ChatExplorationState.ToolBurstStyle = v, + ToolBurstStyle.Auto, ToolBurstStyle.Plain, ToolBurstStyle.TaskHeader, ToolBurstStyle.CompactSummary, diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index e631a5a76..d4b99c5a5 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1157,8 +1157,25 @@ Element BuildSection(string sectionLabel, string contentText) // ── Style-aware composition ────────────────────────────── // Read the live exploration state for the burst variant. - // Defaults to Plain when not explicitly toggled. + // Defaults to Auto, which picks the best variant per burst: + // - single-step → Plain (one inline row, nothing to fold) + // - all terminal → CompactSummary (1-line collapsed summary, + // click chevron to expand the steps) + // - any running → Plain (per-step status visible in real time) + // Matches Scott's feedback: keep live progress legible while + // executing, then tidy up to a one-liner once the turn finishes. var style = ChatExplorationState.ToolBurstStyle; + if (style == ToolBurstStyle.Auto) + { + bool allTerminal = true; + foreach (var e in entries) + { + if (e.ToolResult == ChatToolCallStatus.InProgress) { allTerminal = false; break; } + } + style = (entries.Count >= 2 && allTerminal) + ? ToolBurstStyle.CompactSummary + : ToolBurstStyle.Plain; + } var showStepNumbers = ChatExplorationState.ShowStepNumbers && entries.Count > 1; var stepCount = entries.Count; From 725c3907d73d9cf491f36809cfae849b12ecd9b8 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:06 -0700 Subject: [PATCH 019/115] Chat UI: align collapsed tool header with step rows, drop tool timestamp 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> --- .../Chat/OpenClawChatTimeline.cs | 68 ++++++++++++------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index d4b99c5a5..f199c66b6 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -1292,25 +1292,50 @@ Element AnchorLeft(Element card) => Grid( }; var summaryHeader = Border( - (FlexRow( + Grid( + [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Star(), GridSize.Auto], + [GridSize.Auto], Caption(summaryChevron).Foreground(TertiaryText) - .Set(t => { t.FontSize = 12; }).VAlign(VerticalAlignment.Center), + .Set(t => { t.FontSize = 12; }) + .VAlign(VerticalAlignment.Center) + .Grid(row: 0, column: 0), Caption("⚡").Foreground(taskStatusBg) - .Set(t => { t.FontSize = 12; }).VAlign(VerticalAlignment.Center), + .Set(t => { t.FontSize = 12; }) + .VAlign(VerticalAlignment.Center) + .Margin(6, 0, 0, 0) + .Grid(row: 0, column: 1), Caption($"Task · {stepCount} steps").Foreground(SecondaryText) - .Set(t => { t.FontSize = 12; t.FontWeight = Microsoft.UI.Text.FontWeights.SemiBold; }) - .VAlign(VerticalAlignment.Center), + .Set(t => + { + t.FontSize = 12; + t.FontWeight = Microsoft.UI.Text.FontWeights.SemiBold; + }) + .VAlign(VerticalAlignment.Center) + .Margin(6, 0, 0, 0) + .Grid(row: 0, column: 2), Caption("· " + toolList).Foreground(TertiaryText) - .Set(t => { t.FontSize = 12; t.TextTrimming = TextTrimming.CharacterEllipsis; t.MaxLines = 1; }) - .VAlign(VerticalAlignment.Center).Flex(grow: 1), + .Set(t => + { + t.FontSize = 12; + t.TextTrimming = TextTrimming.CharacterEllipsis; + t.MaxLines = 1; + }) + .VAlign(VerticalAlignment.Center) + .Margin(6, 0, 12, 0) + .Grid(row: 0, column: 3), Border( Caption(taskStatusText).Foreground(new SolidColorBrush(Colors.White)) .Set(t => { t.FontSize = 10; t.LineHeight = 16; }) .VAlign(VerticalAlignment.Center) - ).Background(taskStatusBg).CornerRadius(10).Padding(8, 0, 8, 0) - .Set(b => b.MinHeight = 18).VAlign(VerticalAlignment.Center) - ) with { ColumnGap = 6 }).Padding(12, 8, 12, 8) - ).Set(b => b.MinHeight = 22); + ).Background(taskStatusBg) + .CornerRadius(10) + .Padding(8, 0, 8, 0) + .Set(b => b.MinHeight = 18) + .VAlign(VerticalAlignment.Center) + .HAlign(HorizontalAlignment.Right) + .Grid(row: 0, column: 4) + ).HAlign(HorizontalAlignment.Stretch).Padding(bubblePadding.Left, bubblePadding.Top, bubblePadding.Right, bubblePadding.Bottom) + ).Set(b => b.MinHeight = 32); var summaryButton = Button(summaryHeader, toggleSummary).Set(b => { @@ -1333,10 +1358,9 @@ Element AnchorLeft(Element card) => Grid( // the summary header inside the same card. pieces.AddRange(rows); } - return VStack(2, - AnchorLeft(CardOf(pieces.ToArray())), - FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); + return AnchorLeft(CardOf(pieces.ToArray())) + .HAlign(HorizontalAlignment.Stretch) + .Margin(toolLeftMargin, 6, gutter, 6); } // TaskHeader: prepend a non-clickable header row to the card. @@ -1365,10 +1389,9 @@ Element AnchorLeft(Element card) => Grid( combined[0] = taskHeader; Array.Copy(rows, 0, combined, 1, rows.Length); - return VStack(2, - AnchorLeft(CardOf(combined)), - FooterCaption(timeStr ?? string.Empty, HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); + return AnchorLeft(CardOf(combined)) + .HAlign(HorizontalAlignment.Stretch) + .Margin(toolLeftMargin, 6, gutter, 6); } // TaskList: per-step rows with a status icon (✓ / spinner / ✕) @@ -1576,10 +1599,9 @@ string Truncate(string s, int max) // assistant bubble it follows. if (style == ToolBurstStyle.FooterReframe) { - return VStack(2, - AnchorLeft(CardOf(rows)), - FooterCaption(TaskFooter(), HorizontalAlignment.Left).Margin(0, 2, 0, 0) - ).HAlign(HorizontalAlignment.Stretch).Margin(toolLeftMargin, 6, gutter, 6); + return AnchorLeft(CardOf(rows)) + .HAlign(HorizontalAlignment.Stretch) + .Margin(toolLeftMargin, 6, gutter, 6); } return AnchorLeft(CardOf(rows)) From 5058dccac3552a45ee3d3471f557dc65265c992d Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:06 -0700 Subject: [PATCH 020/115] chore(chat-ui): move assistant timestamp below tool burst 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> --- .../Chat/OpenClawChatTimeline.cs | 84 ++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index f199c66b6..f7358fb28 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -862,7 +862,7 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst // (and left indent) stay exactly parallel as the bubble grows // with content. Single-element Border[] used as a mutable slot // since these are local functions (no nested class allowed). - Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar, Microsoft.UI.Xaml.Controls.Border[]? bubbleSlot = null) + Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar, Microsoft.UI.Xaml.Controls.Border[]? bubbleSlot = null, Element[]? footerSlot = null) { if (string.IsNullOrEmpty(entry.Text)) return Empty(); @@ -915,6 +915,15 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends var leftInset = (showAssistAvatar && showAvatar) ? (36 + bubbleSideMargin) : 0; leftInset += (int)bubblePadding.Left; footer = footer.Margin(leftInset, 2, 0, 0); + // When a footerSlot is supplied, the caller is deferring the + // timestamp/model/tokens line to render BELOW the tool burst + // (so the timestamp sits at the very bottom of the turn). + // Hand the footer over and emit Empty() in its inline slot. + if (footerSlot != null) + { + footerSlot[0] = footer; + footer = Empty(); + } } var topMargin = startsBurst ? 4.0 : 1.0; @@ -1747,6 +1756,28 @@ void Flush(int endExclusive) // rendered below it in the same turn. Reset at each User entry. Microsoft.UI.Xaml.Controls.Border[]? currentBubbleSlot = null; + // Pre-compute the last orderedIdx position of the turn containing each + // entry. Used to (a) decide whether to defer an assistant footer past + // a following tool burst and (b) know where to splice the deferred + // footer back into the timeline so the timestamp sits at the very + // bottom of the turn. + var turnEndAt = new int[orderedIdx.Length]; + { + int ts = 0; + for (int k = 1; k < orderedIdx.Length; k++) + { + if (Props.Entries[orderedIdx[k]].Kind == ChatTimelineItemKind.User) + { + for (int j = ts; j < k; j++) turnEndAt[j] = k - 1; + ts = k; + } + } + for (int j = ts; j < orderedIdx.Length; j++) turnEndAt[j] = orderedIdx.Length - 1; + } + + // Map: insert these deferred-footer elements right AFTER orderedIdx[k]. + var deferredFooterAfter = new System.Collections.Generic.Dictionary(); + for (int k = 0; k < orderedIdx.Length; k++) { int i = orderedIdx[k]; @@ -1791,7 +1822,31 @@ void Flush(int endExclusive) if (entry.Kind == ChatTimelineItemKind.Assistant) { currentBubbleSlot ??= new Microsoft.UI.Xaml.Controls.Border[1]; - renderedEntries[k] = RenderAssistantEntry(entry, startsBurst, endsBurst, showAvatar, currentBubbleSlot).WithKey(entry.Id); + + // Does a ToolCall follow this assistant entry in the same turn? + // If yes, defer the assistant footer (timestamp/model/tokens) + // so it renders BELOW the tool card(s), keeping the timestamp + // at the very bottom of the turn. + bool hasToolAfter = false; + int turnEnd = turnEndAt[k]; + for (int kj = k + 1; kj <= turnEnd; kj++) + { + if (Props.Entries[orderedIdx[kj]].Kind == ChatTimelineItemKind.ToolCall) + { + hasToolAfter = true; + break; + } + } + + Element[]? footerSlot = (hasToolAfter && endsBurst && showTimestamps) ? new Element[1] : null; + renderedEntries[k] = RenderAssistantEntry(entry, startsBurst, endsBurst, showAvatar, currentBubbleSlot, footerSlot).WithKey(entry.Id); + + if (footerSlot != null && footerSlot[0] != null) + { + // Splice the deferred footer in right after the last + // entry of this turn (which will be the tail tool burst). + deferredFooterAfter[turnEnd] = footerSlot[0]!; + } continue; } @@ -1827,26 +1882,33 @@ void Flush(int endExclusive) // Build the final element list, splicing the thinking indicator // inline RIGHT AFTER the most recent User entry so tool bursts that // follow it visually hang below "Agent is thinking…" (and below the - // assistant reply once one streams in). + // assistant reply once one streams in). Also splice any deferred + // assistant footers (timestamp/model/tokens) AFTER the last entry of + // their turn so the timestamp sits at the very bottom — under the + // tool card(s). Element[] timelineRows; - if (Props.ShowThinkingIndicator && renderedEntries.Length > 0) + if ((Props.ShowThinkingIndicator && renderedEntries.Length > 0) || deferredFooterAfter.Count > 0) { int insertAfter = -1; - for (int k = orderedIdx.Length - 1; k >= 0; k--) + if (Props.ShowThinkingIndicator) { - if (Props.Entries[orderedIdx[k]].Kind == ChatTimelineItemKind.User) + for (int k = orderedIdx.Length - 1; k >= 0; k--) { - insertAfter = k; - break; + if (Props.Entries[orderedIdx[k]].Kind == ChatTimelineItemKind.User) + { + insertAfter = k; + break; + } } } - var spliced = new System.Collections.Generic.List(renderedEntries.Length + 1); + var spliced = new System.Collections.Generic.List(renderedEntries.Length + 1 + deferredFooterAfter.Count); for (int k = 0; k < renderedEntries.Length; k++) { spliced.Add(renderedEntries[k]); - if (k == insertAfter) spliced.Add(thinkingIndicator); + if (Props.ShowThinkingIndicator && k == insertAfter) spliced.Add(thinkingIndicator); + if (deferredFooterAfter.TryGetValue(k, out var deferred)) spliced.Add(deferred); } - if (insertAfter < 0) spliced.Insert(0, thinkingIndicator); + if (Props.ShowThinkingIndicator && insertAfter < 0) spliced.Insert(0, thinkingIndicator); timelineRows = spliced.ToArray(); } else From e8a4c2e2312ee7cee9dd4560c5f0a871069ffb06 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:06 -0700 Subject: [PATCH 021/115] Revert "chore(chat-ui): move assistant timestamp below tool burst" This reverts commit 9f84ba9196e2d2fb414eda57c1b27cd5a6bf1fde. --- .../Chat/OpenClawChatTimeline.cs | 84 +++---------------- 1 file changed, 11 insertions(+), 73 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index f7358fb28..f199c66b6 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -862,7 +862,7 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst // (and left indent) stay exactly parallel as the bubble grows // with content. Single-element Border[] used as a mutable slot // since these are local functions (no nested class allowed). - Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar, Microsoft.UI.Xaml.Controls.Border[]? bubbleSlot = null, Element[]? footerSlot = null) + Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar, Microsoft.UI.Xaml.Controls.Border[]? bubbleSlot = null) { if (string.IsNullOrEmpty(entry.Text)) return Empty(); @@ -915,15 +915,6 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends var leftInset = (showAssistAvatar && showAvatar) ? (36 + bubbleSideMargin) : 0; leftInset += (int)bubblePadding.Left; footer = footer.Margin(leftInset, 2, 0, 0); - // When a footerSlot is supplied, the caller is deferring the - // timestamp/model/tokens line to render BELOW the tool burst - // (so the timestamp sits at the very bottom of the turn). - // Hand the footer over and emit Empty() in its inline slot. - if (footerSlot != null) - { - footerSlot[0] = footer; - footer = Empty(); - } } var topMargin = startsBurst ? 4.0 : 1.0; @@ -1756,28 +1747,6 @@ void Flush(int endExclusive) // rendered below it in the same turn. Reset at each User entry. Microsoft.UI.Xaml.Controls.Border[]? currentBubbleSlot = null; - // Pre-compute the last orderedIdx position of the turn containing each - // entry. Used to (a) decide whether to defer an assistant footer past - // a following tool burst and (b) know where to splice the deferred - // footer back into the timeline so the timestamp sits at the very - // bottom of the turn. - var turnEndAt = new int[orderedIdx.Length]; - { - int ts = 0; - for (int k = 1; k < orderedIdx.Length; k++) - { - if (Props.Entries[orderedIdx[k]].Kind == ChatTimelineItemKind.User) - { - for (int j = ts; j < k; j++) turnEndAt[j] = k - 1; - ts = k; - } - } - for (int j = ts; j < orderedIdx.Length; j++) turnEndAt[j] = orderedIdx.Length - 1; - } - - // Map: insert these deferred-footer elements right AFTER orderedIdx[k]. - var deferredFooterAfter = new System.Collections.Generic.Dictionary(); - for (int k = 0; k < orderedIdx.Length; k++) { int i = orderedIdx[k]; @@ -1822,31 +1791,7 @@ void Flush(int endExclusive) if (entry.Kind == ChatTimelineItemKind.Assistant) { currentBubbleSlot ??= new Microsoft.UI.Xaml.Controls.Border[1]; - - // Does a ToolCall follow this assistant entry in the same turn? - // If yes, defer the assistant footer (timestamp/model/tokens) - // so it renders BELOW the tool card(s), keeping the timestamp - // at the very bottom of the turn. - bool hasToolAfter = false; - int turnEnd = turnEndAt[k]; - for (int kj = k + 1; kj <= turnEnd; kj++) - { - if (Props.Entries[orderedIdx[kj]].Kind == ChatTimelineItemKind.ToolCall) - { - hasToolAfter = true; - break; - } - } - - Element[]? footerSlot = (hasToolAfter && endsBurst && showTimestamps) ? new Element[1] : null; - renderedEntries[k] = RenderAssistantEntry(entry, startsBurst, endsBurst, showAvatar, currentBubbleSlot, footerSlot).WithKey(entry.Id); - - if (footerSlot != null && footerSlot[0] != null) - { - // Splice the deferred footer in right after the last - // entry of this turn (which will be the tail tool burst). - deferredFooterAfter[turnEnd] = footerSlot[0]!; - } + renderedEntries[k] = RenderAssistantEntry(entry, startsBurst, endsBurst, showAvatar, currentBubbleSlot).WithKey(entry.Id); continue; } @@ -1882,33 +1827,26 @@ void Flush(int endExclusive) // Build the final element list, splicing the thinking indicator // inline RIGHT AFTER the most recent User entry so tool bursts that // follow it visually hang below "Agent is thinking…" (and below the - // assistant reply once one streams in). Also splice any deferred - // assistant footers (timestamp/model/tokens) AFTER the last entry of - // their turn so the timestamp sits at the very bottom — under the - // tool card(s). + // assistant reply once one streams in). Element[] timelineRows; - if ((Props.ShowThinkingIndicator && renderedEntries.Length > 0) || deferredFooterAfter.Count > 0) + if (Props.ShowThinkingIndicator && renderedEntries.Length > 0) { int insertAfter = -1; - if (Props.ShowThinkingIndicator) + for (int k = orderedIdx.Length - 1; k >= 0; k--) { - for (int k = orderedIdx.Length - 1; k >= 0; k--) + if (Props.Entries[orderedIdx[k]].Kind == ChatTimelineItemKind.User) { - if (Props.Entries[orderedIdx[k]].Kind == ChatTimelineItemKind.User) - { - insertAfter = k; - break; - } + insertAfter = k; + break; } } - var spliced = new System.Collections.Generic.List(renderedEntries.Length + 1 + deferredFooterAfter.Count); + var spliced = new System.Collections.Generic.List(renderedEntries.Length + 1); for (int k = 0; k < renderedEntries.Length; k++) { spliced.Add(renderedEntries[k]); - if (Props.ShowThinkingIndicator && k == insertAfter) spliced.Add(thinkingIndicator); - if (deferredFooterAfter.TryGetValue(k, out var deferred)) spliced.Add(deferred); + if (k == insertAfter) spliced.Add(thinkingIndicator); } - if (Props.ShowThinkingIndicator && insertAfter < 0) spliced.Insert(0, thinkingIndicator); + if (insertAfter < 0) spliced.Insert(0, thinkingIndicator); timelineRows = spliced.ToArray(); } else From 4c522a3252e01d0a5d847c24d50eb84ba8a3fd46 Mon Sep 17 00:00:00 2001 From: kenehong <22486640+kenehong@users.noreply.github.com> Date: Wed, 20 May 2026 20:39:06 -0700 Subject: [PATCH 022/115] feat(chat-ui): nest single/collapsed tool burst inside assistant bubble MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../Chat/OpenClawChatTimeline.cs | 133 ++++++++++++++++-- 1 file changed, 118 insertions(+), 15 deletions(-) diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs index f199c66b6..3e2a2ca8b 100644 --- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs +++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs @@ -862,7 +862,7 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst // (and left indent) stay exactly parallel as the bubble grows // with content. Single-element Border[] used as a mutable slot // since these are local functions (no nested class allowed). - Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar, Microsoft.UI.Xaml.Controls.Border[]? bubbleSlot = null) + Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst, bool showAvatar, Microsoft.UI.Xaml.Controls.Border[]? bubbleSlot = null, Element? nestedTool = null) { if (string.IsNullOrEmpty(entry.Text)) return Empty(); @@ -884,8 +884,17 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends // HAlign=Left keeps the bubble anchored next to the avatar/timestamp // column. MaxWidth=720 caps the growth so long messages stop where // the tool burst card's max right edge lands. + // When `nestedTool` is supplied, the tool burst (single chip OR + // collapsed multi-step summary) is rendered INSIDE the bubble's + // content area — directly below the assistant text with a small + // top gap — so it visually reads as a child of the bubble. + Element bubbleContent = SafeMarkdownText(entry.Text); + if (nestedTool != null) + { + bubbleContent = VStack(8, bubbleContent, nestedTool); + } var card = Border( - SafeMarkdownText(entry.Text) + bubbleContent ).Background(assistantBubbleBg) .Set(b => { @@ -935,7 +944,7 @@ Element RenderAssistantEntry(ChatTimelineItem entry, bool startsBurst, bool ends // into `▸ ⚡ · [Done]`; click expands the row // to reveal the original args + raw output (the previous chip body). // A single trailing `Tool ·