Skip to content

improve(chat): make prose and simple lists one selectable text block#997

Open
karkarl wants to merge 2 commits into
openclaw:mainfrom
karkarl:karkarl-chat-bubble-richtextblock
Open

improve(chat): make prose and simple lists one selectable text block#997
karkarl wants to merge 2 commits into
openclaw:mainfrom
karkarl:karkarl-chat-bubble-richtextblock

Conversation

@karkarl

@karkarl karkarl commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Related: #995
Related: #996

What Problem This Solves

In the tray chat timeline, a message's text was fragmented into many separate text controls: each paragraph, heading, and list item was its own box. A user could not drag-select a whole assistant reply in one gesture, which was awkward and annoying when trying to copy part of an answer.

Why This Change Was Made

Each chat message now renders through a single per-message RichTextBlock, and consecutive prose (paragraphs + headings) plus simple text-only lists (bullet / ordered / task, including nested) coalesce into that one control, so the whole run is one continuous drag-selection scope.

Blocks that need their own chrome stay as separate selectable islands: code blocks, tables, block quotes, thematic breaks, and any list whose items contain one of those. This split is a WinUI constraint, not a preference: InlineUIContainer is excluded from SelectedText and breaks selection at its boundary, and there is no built-in way to run one selection across multiple controls. So chrome-bearing content cannot be embedded inline without losing selection.

Supporting pieces:

  • A RichTextBlock primitive added to the in-house OpenClawTray.FunctionalUI layer (factory, Set, modifiers).
  • Simple lists render as hanging-indent paragraphs inside the shared RichTextBlock (per-item marker, fixed hanging indent so wrapped lines align at the content column, per-level nesting, heading formatting preserved).
  • A structural block-equality cache so a re-render or AST cache reparse of unchanged content does not clear and rebuild the blocks, which would wipe the user's active selection.
  • The existing per-message Copy button remains the whole-message grab across islands.

The larger follow-ups are tracked separately: a custom cross-control selection engine for whole-transcript selection (#995), and porting FunctionalUI to Reactor to stop hand-maintaining bespoke primitives (#996).

User Impact

Users can drag-select an assistant reply's prose and bullet / numbered lists as one continuous selection instead of fighting per-line boxes. Code blocks and tables stay distinctly styled and individually selectable. No change to how messages are authored or sent.

Evidence

Record-level renderer tests assert the Element shape directly: prose + a simple list collapses to one RichTextBlockElement, while a list containing a non-text block stays its own island. Full required suites plus focused renderer tests pass (counts below).

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Docs or instructions
  • Tests or validation
  • Security hardening
  • Chore or infrastructure

Scope

  • Tray or WinUI UX
  • Windows node capability
  • Local MCP or winnode
  • Gateway, connection, or pairing
  • Setup or onboarding
  • Permissions, privacy, or security
  • Tests, CI, or docs

Validation

All run on branch head 47b335d2 (ARM64 host, -p:Platform=x64 for WinUI/UITests):

  • ./build.ps1 -> all 5 projects built successfully
  • dotnet test ./tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj -> Passed 2754, Skipped 31, Failed 0
  • dotnet test ./tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj -> Passed 1705, Failed 0
  • dotnet test ./tests/OpenClawTray.FunctionalUI.Tests/... -> Passed 14, Failed 0
  • dotnet test ./tests/OpenClaw.Tray.UITests/... -> Passed 106, Failed 0
  • Focused: --filter FullyQualifiedName~MarkdownRenderer -> Passed 12, Failed 0

Real Behavior Proof

image
  • Environment tested: isolated dev app (Release identity, ARM64 Debug) against the managed OpenClawGateway WSL distro; focused renderer tests headless.
  • PR head or commit tested: 47b335d2
  • Exact steps or command run: (1) dotnet test --filter FullyQualifiedName~MarkdownRenderer -p:Platform=x64; (2) relaunched the isolated app (run-app-local.ps1 -NoBuild -Isolated -AllowNonMain, PID 34000) to drag-select a message containing prose + a bullet list + a fenced code block.
  • Evidence after fix: MarkdownRendererCoalesceTests / MarkdownRendererListTests assert prose + simple list -> one RichTextBlockElement, lone simple list -> RichTextBlockElement, and a list containing a block quote -> separate StackElement island. 12/12 pass.
  • Observed result: prose and simple lists render into one selectable RichTextBlock; code blocks stay their own styled island; Copy button grabs the whole message.
  • Screenshot or artifact links verified? No
  • Not verified or blocked: Live drag-selection screenshot/video not captured in this session (app relaunched for manual verification). Second-model autoreview could not run because codex/claude/pi CLIs are not installed in this environment; a rubber-duck review did run and both findings (heading-in-list formatting, true hanging indent) were fixed.

Security Impact

  • New permissions or capabilities? No
  • Secrets or tokens handling changed? No
  • New or changed network calls? No
  • Command or tool execution surface changed? No
  • Data access scope changed? No
  • Inert-markdown posture preserved: no Hyperlink / BitmapImage introduced, AppendInlines unchanged, rendering stays non-interactive.

Compatibility and Migration

  • Backward compatible? Yes
  • Config or environment changes? No
  • Migration needed? No

Review Conversations

  • I replied to or resolved every bot review conversation addressed by this PR.
  • I left unresolved only conversations that still need maintainer judgment.

karkarl and others added 2 commits July 14, 2026 16:56
…selection

Chat prose was rendered as many separate TextBlocks, so a user could not drag-select text across paragraphs of one message. Add a poolable RichTextBlock primitive to the in-house FunctionalUI reconciler and route both assistant and user message text through one RichTextBlock per message (a Paragraph per block), giving each message a single continuous, selection-friendly text scope while keeping chat virtualizable.

- FunctionalUI: RichTextBlockElement record, RichTextBlock() factory, Set(Action<RichTextBlock>), reconciler arm, ConfigureRichTextBlock (leaves Blocks to the setter), ApplyModifiers case.

- ChatMarkdownRenderer: coalesce consecutive paragraph/heading blocks into one RichTextBlock; lone text blocks stay TextBlocks; lists/code/tables/etc stay separate selectable siblings. Structural block-run cache preserves active selection across re-renders independent of AST-cache eviction. Inert posture preserved (AppendInlines only).

- OpenClawChatTimeline: user bubble renders as a single-Paragraph RichTextBlock preserving font, foreground, wrap and SelectionHighlightColor.

- Tests: FunctionalUI primitive tests, renderer coalescing tests, updated user-bubble contract + virtualization proof text collection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Extend the assistant-bubble text coalescing so simple (text-only,
possibly nested) bullet/ordered/task lists flow into the same per-message
RichTextBlock as surrounding prose, making paragraphs + headings + list
items one continuous drag-selection scope. Lists whose items carry a code
block, table, blockquote, thematic break, or raw block stay as their own
Grid-based selectable island (RenderList), preserving that chrome.

List items render as hanging-indent Paragraphs inside the shared
RichTextBlock: a per-item marker (task box, ordered number, bullet),
fixed hanging indent so wrapped lines / continuation paragraphs align at
the content column, per-level indentation for nesting, and heading
formatting preserved for heading list items.

Extend the selection-preservation cache (BlockEqual) with structural
ListEqual so the bounded AST cache evicting and re-parsing an unchanged
message does not force a Blocks rebuild that would wipe the active
selection.

Update MarkdownRendererCoalesceTests / MarkdownRendererListTests: simple
lists now assert RichTextBlock coalescing; the issue openclaw#636 Grid wrap
guards retarget to complex (blockquote-bearing) lists that still use the
Grid island path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@clawsweeper clawsweeper Bot added proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. labels Jul 16, 2026
@clawsweeper

clawsweeper Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed July 15, 2026, 7:43 PM ET / 23:43 UTC.

Summary
The PR renders assistant prose, headings, simple lists, and user text through per-message RichTextBlock controls, adds selection-preserving render caches and a FunctionalUI primitive, and expands renderer and virtualization tests.

Reproducibility: yes. at the source level: recycle the same RichTextBlock through assistant content A, user content B, then assistant content A again; the assistant cache can match A while the live Blocks still contain B. A real WinUI execution of that sequence was not supplied.

Review metrics: 3 noteworthy metrics.

  • Changed surface: 8 files: 3 source, 5 tests. The patch changes the renderer, timeline control lifecycle, and the in-house FunctionalUI reconciler rather than only presentation markup.
  • Diff size: 635 added, 41 removed. The sizeable net-new rendering and cache logic warrants focused virtualization and real-UI proof before merge.
  • Reported focused coverage: 12 MarkdownRenderer tests passed. The renderer has useful structural coverage, but the pooled sender-transition sequence remains untested.

Merge readiness
Overall: 🦪 silver shellfish
Proof: 🦪 silver shellfish
Patch quality: 🦪 silver shellfish
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Unify RichTextBlock cache ownership and add a virtualization regression covering assistant-to-user-to-assistant reuse.
  • [P1] Post a current-head recording showing one drag selection spanning prose and a simple list while a code block remains a separate island, with private details redacted.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The static screenshot shows the chat UI but not an active drag selection spanning prose and list items; add a short current-head native recording or visibly selected screenshot, redact private information, update the PR body, and request @clawsweeper re-review if automatic review does not run.

Risk before merge

  • [P1] Merging as written can display stale or mis-associated chat text after virtualization reuses one RichTextBlock across assistant and user rendering paths.
  • [P1] The current proof does not show that one native drag selection actually spans prose and simple list items while stopping at a styled island.

Maintainer options:

  1. Unify cache ownership before merge (recommended)
    Replace the independent assistant and user cache decisions with shared render-state invalidation, add a recycled-control regression test, and then capture visible current-head selection proof.
  2. Pause the RichTextBlock pooling change
    Keep the existing renderer until the selection-preservation cache can distinguish every writer of a pooled RichTextBlock safely.

Next step before merge

  • [P1] The contributor should repair the pooled-control cache defect and provide native visible proof; the missing real-setup proof prevents routing this through an automated repair lane.

Security
Cleared: The diff changes local WinUI rendering and tests without adding dependencies, network access, permissions, secret handling, downloaded code, or interactive markdown capabilities.

Review findings

  • [P1] Invalidate the other writer's cache when reusing RichTextBlock — src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs:325-328
Review details

Best possible solution:

Use one authoritative per-control render-state cache shared by every RichTextBlock population path, cover assistant-to-user-to-assistant pool reuse with a regression test, and provide a current-head native recording showing the intended selection boundary.

Do we have a high-confidence way to reproduce the issue?

Yes at the source level: recycle the same RichTextBlock through assistant content A, user content B, then assistant content A again; the assistant cache can match A while the live Blocks still contain B. A real WinUI execution of that sequence was not supplied.

Is this the best way to solve the issue?

No, not as written: coalescing into RichTextBlock is a reasonable design, but selection preservation must use cache state shared across all writers of a pooled control rather than independent ConditionalWeakTables.

Full review comments:

  • [P1] Invalidate the other writer's cache when reusing RichTextBlock — src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs:325-328
    Because RichTextBlock controls are poolable, one instance can move from assistant rendering to user rendering and back. These paths keep separate ConditionalWeakTable entries, so after assistant A → user B → assistant A, the assistant cache can report A unchanged while the live Blocks still contain B, causing the wrong message text to render. Use one shared render-state cache or validate the live block owner/content before returning, and add a recycled sender-transition test.
    Confidence: 0.9

Overall correctness: patch is incorrect
Overall confidence: 0.86

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against bf0fa8a9bde4.

Label changes

Label changes:

  • add P2: This is a useful, bounded chat UX improvement with a concrete merge-blocking stale-render risk, but it is not an active production emergency.
  • add merge-risk: 🚨 session-state: A pooled control can retain blocks from a different message-rendering path, potentially showing text associated with the wrong chat row.
  • add proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The static screenshot shows the chat UI but not an active drag selection spanning prose and list items; add a short current-head native recording or visibly selected screenshot, redact private information, update the PR body, and request @clawsweeper re-review if automatic review does not run.
  • add rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🦪 silver shellfish.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The static screenshot shows the chat UI but not an active drag selection spanning prose and list items; add a short current-head native recording or visibly selected screenshot, redact private information, update the PR body, and request @clawsweeper re-review if automatic review does not run.

Label justifications:

  • P2: This is a useful, bounded chat UX improvement with a concrete merge-blocking stale-render risk, but it is not an active production emergency.
  • merge-risk: 🚨 session-state: A pooled control can retain blocks from a different message-rendering path, potentially showing text associated with the wrong chat row.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🦪 silver shellfish.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The static screenshot shows the chat UI but not an active drag selection spanning prose and list items; add a short current-head native recording or visibly selected screenshot, redact private information, update the PR body, and request @clawsweeper re-review if automatic review does not run.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The static screenshot shows the chat UI but not an active drag selection spanning prose and list items; add a short current-head native recording or visibly selected screenshot, redact private information, update the PR body, and request @clawsweeper re-review if automatic review does not run.
Evidence reviewed

What I checked:

Likely related people:

  • samanthamsong: This person opened the repository's chat virtualization work, which is directly relevant to the pooled-row lifecycle affected by the new RichTextBlock caches; the available history signal is indirect, so routing confidence is low. (role: adjacent feature owner; confidence: low; files: src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs, tests/OpenClaw.Tray.UITests/ChatTimelineVirtualizationProofTests.cs)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper

clawsweeper Bot commented Jul 16, 2026

Copy link
Copy Markdown

ClawSweeper status: review started.

I am starting a fresh review of this pull request: improve(chat): make prose and simple lists one selectable text block This is item 1/1 in the current shard. Shard 0/1.

This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking.

Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. P2 Normal priority bug or improvement with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant