Skip to content

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

Merged
shanselman merged 3 commits into
openclaw:mainfrom
karkarl:karkarl-chat-bubble-richtextblock
Jul 17, 2026
Merged

improve(chat): make prose and simple lists one selectable text block#997
shanselman merged 3 commits into
openclaw:mainfrom
karkarl:karkarl-chat-bubble-richtextblock

Conversation

@karkarl

@karkarl karkarl commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

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. A live-runtime UI-thread test (ChatBubbleSelectionScopeProofTests) additionally proves the shipping renderer on a real WinRT thread. 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 d9e2b995 (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 107, Failed 0 (adds the deterministic selection-scope proof on head d9e2b995)
  • Focused: --filter FullyQualifiedName~MarkdownRenderer -> Passed 12, Failed 0

Real Behavior Proof

image

Deterministic renderer proof (head d9e2b995). tests/OpenClaw.Tray.UITests/ChatBubbleSelectionScopeProofTests.cs mounts the real OpenClawChatTimeline through the shipping FunctionalUI reconciler on a live WinRT UI thread and renders one assistant message (two prose paragraphs, a three-item bullet list, a trailing paragraph, a fenced code block, and a table). It asserts: prose + list + trailing paragraph land in ONE RichTextBlock (one continuous selection scope); the code and table text are absent from that RichTextBlock; and each renders in its own separate TextBlock island. Run: dotnet test .\tests\OpenClaw.Tray.UITests\OpenClaw.Tray.UITests.csproj -p:Platform=x64 --filter FullyQualifiedName~ChatBubbleSelectionScopeProof -> Passed 1/1. This is the automated stand-in for the interactive native drag recording, which is a human-captured artifact.

  • Environment tested: isolated dev app (Release identity, ARM64 Debug) against the managed OpenClawGateway WSL distro; focused renderer tests headless; deterministic proof on a live WinRT UI-thread fixture.
  • PR head or commit tested: d9e2b995
  • Exact steps or command run: (1) dotnet test --filter FullyQualifiedName~MarkdownRenderer -p:Platform=x64; (2) dotnet test --filter FullyQualifiedName~ChatBubbleSelectionScopeProof -p:Platform=x64; (3) relaunched the isolated app (run-app-local.ps1 -NoBuild -Isolated -AllowNonMain) 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). ChatBubbleSelectionScopeProofTests proves the same contract end-to-end through the shipping renderer on a real UI thread including code/table islands (1/1).
  • Observed result: prose and simple lists render into one selectable RichTextBlock; code blocks and tables stay their own styled islands; Copy button grabs the whole message.
  • Screenshot or artifact links verified? Yes (screenshot above)
  • Not verified or blocked: Live drag-selection video not captured in this automated session; the six-step recipe in the reviewer thread reproduces it manually and the deterministic UI-thread test is the automated substitute. 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 maintainer review before merge. Reviewed July 17, 2026, 3:25 PM ET / 19:25 UTC.

Summary
The PR adds a FunctionalUI RichTextBlock primitive and renders adjacent chat prose and simple lists in one selectable text block while retaining code and tables as separate selectable islands.

Reproducibility: not applicable. as a bug reproduction: this PR proposes a new selection-scope capability. The submitted live WinRT UI-thread fixture provides a high-confidence verification path for the intended renderer structure and island boundaries.

Review metrics: 2 noteworthy metrics.

  • Diff surface: 9 files; 830 added, 41 removed. The change spans renderer behavior, the shared reconciler primitive, and focused regression/UI-runtime coverage.
  • Runtime selection proof: 1 live WinRT UI-thread test added. The proof exercises the shipping timeline and reconciler rather than only an element-record mock.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

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

Rank-up moves:

  • [P2] Obtain an explicit maintainer decision on retaining the new FunctionalUI primitive before merge.

Risk before merge

  • [P1] Merging introduces a reusable RichTextBlock primitive and imperative block-population pattern into the bespoke FunctionalUI reconciler; maintainers should explicitly decide whether that scoped extension is acceptable while the broader Reactor direction remains open.

Maintainer options:

  1. Accept the scoped FunctionalUI extension (recommended)
    Merge only after a maintainer explicitly accepts the new primitive as an intentional bounded addition to the existing reconciler.
  2. Pause for architecture direction
    Close or defer the branch if maintainers want new control primitives to wait for the Reactor decision rather than extend FunctionalUI.

Next step before merge

  • [P2] A maintainer must choose the UI abstraction direction; there is no narrow mechanical repair to dispatch.

Maintainer decision needed

  • Question: Should the tray retain a scoped first-class RichTextBlock primitive in FunctionalUI for this selection improvement, or should this UX change wait for an agreed Reactor/alternative UI direction?
  • Rationale: The patch appears bounded and tested, but it deliberately expands a bespoke UI abstraction while the related architecture direction remains an open maintainer decision.
  • Likely owner: shanselman — The recorded review already calibrated the native-proof requirement and is the strongest available signal on acceptance of this UI direction.
  • Options:
    • Accept the scoped primitive (recommended): Merge the focused renderer and reconciler addition, with the new UI-thread contract test preserving selection and island boundaries.
    • Defer to UI architecture work: Pause this PR and decide the Reactor or replacement abstraction first, then reapply the selection behavior through that approved surface.

Security
Cleared: The supplied diff changes WinUI rendering and tests only; it adds no dependencies, permissions, network calls, secret handling, or command-execution surface.

Review details

Best possible solution:

Accept or reject the narrowly scoped FunctionalUI extension explicitly; if accepted, merge with the current renderer and live-UI contract tests intact, leaving whole-transcript selection and any broader Reactor migration to their linked follow-ups.

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

Not applicable as a bug reproduction: this PR proposes a new selection-scope capability. The submitted live WinRT UI-thread fixture provides a high-confidence verification path for the intended renderer structure and island boundaries.

Is this the best way to solve the issue?

Unclear pending maintainer direction. The renderer approach is narrowly implemented and covered, but adding a reusable primitive to FunctionalUI must be weighed against the open Reactor/architecture follow-up.

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add merge-risk: 🚨 other: The PR deliberately expands the bespoke FunctionalUI abstraction, a maintainer-visible architectural risk not settled by ordinary tests alone.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The contributor added a current-head live WinRT UI-thread fixture that mounts the shipping renderer and validates the requested shared selection scope and separate code/table islands; this matches the prior review’s accepted deterministic-fixture path.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The contributor added a current-head live WinRT UI-thread fixture that mounts the shipping renderer and validates the requested shared selection scope and separate code/table islands; this matches the prior review’s accepted deterministic-fixture path.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove merge-risk: 🚨 session-state: Current PR review merge-risk labels are merge-risk: 🚨 other.

Label justifications:

  • P2: This is a bounded chat usability improvement with no evidence of a release-blocking failure.
  • merge-risk: 🚨 other: The PR deliberately expands the bespoke FunctionalUI abstraction, a maintainer-visible architectural risk not settled by ordinary tests alone.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The contributor added a current-head live WinRT UI-thread fixture that mounts the shipping renderer and validates the requested shared selection scope and separate code/table islands; this matches the prior review’s accepted deterministic-fixture path.
  • proof: sufficient: Contributor real behavior proof is sufficient. The contributor added a current-head live WinRT UI-thread fixture that mounts the shipping renderer and validates the requested shared selection scope and separate code/table islands; this matches the prior review’s accepted deterministic-fixture path.
Evidence reviewed

What I checked:

Likely related people:

  • shanselman: The recorded review discussion set the proof contract, examined the cache-isolation rationale, and identified the remaining acceptance question around native selection evidence. (role: reviewer; confidence: medium; files: src/OpenClaw.Tray.WinUI/Chat/Markdown/ChatMarkdownRenderer.cs, src/OpenClawTray.FunctionalUI/FunctionalUI.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.
Review history (3 earlier review cycles)
  • reviewed 2026-07-15T23:43:35.949Z sha 47b335d :: needs real behavior proof before merge. :: [P1] Invalidate the other writer's cache when reusing RichTextBlock
  • reviewed 2026-07-16T04:17:28.531Z sha 47b335d :: needs real behavior proof before merge. :: [P1] Invalidate the other writer's cache before reusing the control
  • reviewed 2026-07-17T18:43:42.770Z sha d9e2b99 :: needs real behavior proof before merge. :: none

@karkarl

karkarl commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

[P1] Invalidate the other writer's cache before reusing the control — src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs:324-327

Thanks for the careful read. I traced this on its merits and it does not apply here, because the premise -- one pooled RichTextBlock shared across the user-text writer and the assistant-markdown writer -- cannot occur with the FunctionalUI keying model.

Every timeline row is emitted with .WithKey(RowKey(entry)), where RowKey is thread:{session}|generation:{gen}|kind:{entry.Kind}|id:{entry.Id} (OpenClawChatTimeline.cs:831). FunctionalUI turns that Key into the control path (ChildPath / ResolveElementPath), and GetOrCreate<T>(path) pools controls by path plus type. Because both kind: and id: are baked into the key, a User entry and an Assistant entry never resolve to the same path, so their RichTextBlock controls are always distinct instances. s_userParagraphCache keys on the user-entry control; the assistant s_richBlockRunCache (ChatMarkdownRenderer.cs:318) keys on the assistant-entry control. They can never key the same object, so the early-return in ApplyPlainSelectableParagraph cannot ever short-circuit over assistant-owned blocks.

Two additional backstops make the handoff unreachable even under recycling:

  • An entry's Kind is immutable; any kind change is a new key, hence a new path and a fresh control.
  • On virtualization recycle, RemoveCachedSubtree(item.Path) evicts the entire _controls subtree for that row, so a re-realized row builds a new RichTextBlock rather than inheriting a stale one.

Given that, unifying or cross-invalidating the two caches would add complexity to guard a state that isn't reachable, so I'm intentionally not making that change. If a maintainer wants the invariant pinned down defensively, I'm happy to add an alternating user/assistant regression test that asserts the two paths never share a control instance.

On the real-behavior-proof ask: the PR body documents honestly that live drag-selection video/screenshot was not captured in this automated session (the isolated app was relaunched for manual verification). The behavior is covered by the record-level renderer tests (prose plus simple list coalesce into one RichTextBlockElement; a list containing a non-text block stays a separate island), 12/12 passing. A maintainer running the isolated build can capture the native selection recording if that gate needs to be closed before merge.

@shanselman

Copy link
Copy Markdown
Collaborator

GitHub Copilot maintainer-assistant review — this is automated feedback, not a comment written personally by Scott.

Reviewed head: 47b335d25428a7f4f099b3f150dae93c2cdd3fe0

The code review was favorable. In particular, the reported cross-writer cache contamination does not appear reachable: RowKey includes kind, FunctionalUI pools by full path and control type, and both weak-table caches key by control identity. The remaining merge gate is proof of the exact native UX claim.

Please add a current-head screenshot or short recording that visibly shows an active drag selection spanning:

  1. an assistant prose paragraph,
  2. a simple bullet or ordered list,
  3. a trailing prose paragraph,

all as one continuous selection. Please also show that a fenced code block or table remains a separate selection island, because preserving those boundaries is part of the implementation contract.

A useful self-test recipe for the PR body:

  1. Launch this PR head with ./run-app-local.ps1 -Isolated and record the printed isolated data directory.
  2. Open native chat and produce one deterministic assistant message containing two prose paragraphs, a three-item bullet list, a trailing paragraph, a fenced code block, and a small table.
  3. Drag from the first paragraph through the list into the trailing paragraph; capture the selection highlight while the mouse button is released.
  4. Copy the selection into Notepad and show that prose/list text copies as one contiguous range.
  5. Separately select inside the code block/table and show that selection does not cross into surrounding prose.
  6. Include the tested PR SHA and verify the uploaded screenshot/video link resolves.

If reproducing that message through a live gateway is inconvenient, please provide the exact prompt/session fixture you used. A maintainer can then open the PR branch in an isolated session and run the same steps locally. A small deterministic FunctionalUI/demo fixture would also be acceptable if it exercises the shipping renderer rather than a mock rendering path.

The fixed hanging indent for very large ordered markers and reduced screen-reader item granularity are reasonable follow-up concerns, not blockers for this focused change. The visible current-head selection proof is the item needed now.

Add a deterministic UI-thread proof that mounts the shipping
OpenClawChatTimeline through the real FunctionalUI reconciler and renders
one assistant message mixing prose paragraphs, a bullet list, a trailing
paragraph, a fenced code block, and a table. It asserts prose + list +
trailing coalesce into a single RichTextBlock (one continuous selection
scope) while the code block and table remain their own island TextBlocks.

This closes the record-level gap where headless WinRT could not activate
FontFamily for code/table islands, and serves as the automated stand-in
for the interactive native drag-select recording requested in review.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@karkarl

karkarl commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

A small deterministic FunctionalUI/demo fixture would also be acceptable if it exercises the shipping renderer rather than a mock rendering path.

Thanks for confirming the cross-writer cache path is unreachable, and for offering the deterministic fixture as an acceptable substitute. The live native drag-select recording is an interactive pointer gesture that this automated session cannot capture, so I have taken you up on the deterministic option.

New proof (current head d9e2b995): tests/OpenClaw.Tray.UITests/ChatBubbleSelectionScopeProofTests.cs -> AssistantMessage_ProseAndListShareOneSelectableBlock_CodeAndTableStayIslands.

This is not a mock rendering path. It mounts the real OpenClawChatTimeline through the shipping FunctionalUI reconciler on a live STA/WinRT UI thread (the same UIThreadFixture the virtualization proof uses), renders one assistant message, then walks the realized visual tree. Because it runs on a real UI thread, code/table islands activate FontFamily for real -- which is exactly the case the headless record-level tests could not cover.

Deterministic message fixture (NATO tokens so the tree-walk is unambiguous):

Alpha paragraph one describing the change.

Bravo paragraph two with more detail.

- Charlie first list item
- Delta second list item
- Echo third list item

Foxtrot trailing paragraph after the list.

```csharp
var golfCode = 1;
Hotel India
Juliet Kilo

**What it asserts (your contract, points 1-3 plus the island boundary):**

1. Exactly one `RichTextBlock` contains all of `Alpha`/`Bravo` paragraphs + all three list items + the `Foxtrot` trailing paragraph -> one continuous selection/copy scope spanning prose -> list -> trailing prose.
2. That same `RichTextBlock` does NOT contain `golfCode`, `Juliet`, or `Kilo` -> selection cannot bleed prose into the code/table.
3. `golfCode` and `Juliet` each appear in their own separate `TextBlock` -> the fenced code block and table render as distinct selectable islands, boundaries preserved.

Green on head `d9e2b995`: `dotnet test .\tests\OpenClaw.Tray.UITests\OpenClaw.Tray.UITests.csproj -p:Platform=x64 --filter FullyQualifiedName~ChatBubbleSelectionScopeProof` -> Passed 1/1. Full UITests project 107/107; required subset (build.ps1, Shared 2754, Tray 1705) all green.

**For a maintainer who wants the live recording**, your six-step recipe reproduces it exactly -- paste the fixture above as the assistant message in step 2, then drag from `Alpha` through the list into `Foxtrot` (one contiguous copy), and separately select inside the code block/table to show the boundary holds. I have left that human-captured screenshot/video as the remaining manual artifact; the deterministic test above is the automated, current-head stand-in.

Agreed the hanging-indent and screen-reader-granularity items are follow-ups, not blockers -- tracked under the cross-control selection engine follow-up (#995).

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. and removed 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. merge-risk: 🚨 session-state 🚨 Merging this PR could lose, corrupt, stale, or mis-associate session or agent state. labels Jul 17, 2026
@shanselman
shanselman merged commit 4518dd8 into openclaw:main Jul 17, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants