Skip to content

Copy and run a chat code block from a hover strip - #985

Merged
alexeyzimarev merged 2 commits into
mainfrom
capacitor/agent-57e63ed1d4054e
Sep 17, 2026
Merged

alexeyzimarev merged 2 commits into
mainfrom
capacitor/agent-57e63ed1d4054e

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Closes #984 — AI-2902

What & why

Text in a fenced code block cannot be lifted out of it, which bites hardest when the agent writes a
command it wants run: the only way into the composer was to retype it. Hovering a block now reveals
Copy, and Run it when the block's text begins with ! — the bang being what makes the line a
command rather than prose, so it is the whole condition. Run sends the text verbatim, bang included,
through the composer's own send path, so a block's prompt is queued, recalled and cleared exactly as
a typed one is; whatever draft was sitting there is replaced.

Where to look

The strip wraps the block MarkView's own renderer wrote instead of replacing that renderer, so
syntax highlighting and its theme switch stay with the package. A block reads RunCode as it is
built, which is why the view builds the document again when a binding delivers one after the first
render — the pull request reader, which never gets one, offers copy alone.

Verification

dotnet run --project test/Capacitor.App.Tests.Unit/... — 2270/2270 passed.
dotnet build src/Capacitor.App/Capacitor.App.csproj --no-incremental — 0 warnings.

The late-binding path was checked by removing the re-render and watching
A_run_command_set_after_the_text_still_reaches_the_block fail, then restoring it.

Not checked: the strip's colours and padding in a real window.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-17T17:32:31.266733Z 6ccb3c8 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add hover actions to copy and run chat code blocks

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds hover controls to copy code blocks and run bang-prefixed commands.
• Routes runnable blocks through the composer, preserving queue, history, and clearing behavior.
• Re-renders late-bound commands and tests visibility, clipboard, gating, and sending.
Diagram

graph TD
  A["Markdown View"] --> B["Code Renderer"] --> C["Hover Actions"]
  C -->|Copy| E["System Clipboard"]
  C --> D{"Bang Prefix?"}
  D -->|Yes| F["Run Code Command"] --> G["Composer Send Path"] --> H["PTY Channel"]
Loading
High-Level Assessment

The current approach is appropriate: wrapping MarkView's TextMate renderer preserves package-owned syntax highlighting and theme behavior, while routing execution through the composer preserves queue, recall, availability, and clearing semantics. Replacing the renderer or sending directly to the channel would duplicate rendering behavior or bypass established chat invariants.

Files changed (9) +365 / -4

Enhancement (6) +166 / -4
ChatTabViewModel.csRoute runnable code blocks through the composer +14/-0

Route runnable code blocks through the composer

• Adds a reactive command that replaces the current draft with code block text and executes the normal send command. Availability follows text acceptance and read-only participant state.

src/Capacitor.App/ViewModels/ChatTabViewModel.cs

ChatTabView.axamlBind code block execution into chat markdown views +6/-3

Bind code block execution into chat markdown views

• Passes the chat tab's run command into user, assistant, and system-note markdown surfaces.

src/Capacitor.App/Views/ChatTabView.axaml

CodeBlockActionRenderer.csOverlay copy and run controls on highlighted code blocks +83/-0

Overlay copy and run controls on highlighted code blocks

• Wraps MarkView's TextMate-rendered block with a hover action strip. It always provides clipboard copying with status feedback and conditionally provides execution for bang-prefixed blocks.

src/Capacitor.App/Views/CodeBlockActionRenderer.cs

CodeBlockActions.csRegister the code block action renderer +15/-0

Register the code block action renderer

• Adds a per-view MarkView extension that replaces the TextMate code renderer with the action-aware wrapper while retaining syntax highlighting.

src/Capacitor.App/Views/CodeBlockActions.cs

MarkdownStyles.axamlStyle hover controls for markdown code blocks +25/-0

Style hover controls for markdown code blocks

• Hides code block actions at rest, reveals them for the hovered block, and styles compact opaque action buttons using existing theme resources.

src/Capacitor.App/Views/MarkdownStyles.axaml

MarkdownView.csExpose run commands and rebuild late-bound code blocks +23/-1

Expose run commands and rebuild late-bound code blocks

• Adds an optional RunCode property and installs the code block action extension after syntax highlighting. Rebuilds markdown when text or the run binding changes so commands arriving after initial rendering reach existing blocks.

src/Capacitor.App/Views/MarkdownView.cs

Tests (2) +183 / -0
ChatComposerTests.csTest composer routing and execution availability +30/-0

Test composer routing and execution availability

• Verifies that running a block sends its text through the composer, clears the input, and becomes unavailable after the session ends.

test/Capacitor.App.Tests.Unit/ChatComposerTests.cs

MarkdownViewTests.csTest code block hover, copy, and run interactions +153/-0

Test code block hover, copy, and run interactions

• Adds headless UI coverage for per-block hover visibility, bang-prefix eligibility, verbatim execution, multiline clipboard copying, late-bound commands, and copy-only surfaces.

test/Capacitor.App.Tests.Unit/MarkdownViewTests.cs

Documentation (1) +16 / -0
CHANGES.mdDocument code block copy and run behavior +16/-0

Document code block copy and run behavior

• Describes the hover actions, bang-prefix run condition, composer routing semantics, renderer wrapping, and late-binding behavior.

docs/CHANGES.md

@qodo-code-review

qodo-code-review Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Indented blocks get a false Run offer ✓ Resolved 🐞 Bug ≡ Correctness
Description
Host tests text.TrimStart().StartsWith('!'), but RunButton forwards the original untrimmed
block text and every input path sends it unchanged. A block whose first character is whitespace
therefore qualifies even though its transmitted text does not begin with the bang required to
distinguish a command from prose, so the advertised action can submit an ordinary prompt instead.
Code

src/Capacitor.App/Views/CodeBlockActionRenderer.cs[R41-43]

+        // The bang is what makes the block a command the composer can run rather than prose, and a
+        // view with no RunCode — the pull request reader — has nowhere to run one.
+        if (view.RunCode is not null && text.TrimStart().StartsWith('!')) strip.Children.Add(RunButton(text));
Relevance

●●● Strong

Clear correctness bug contradicting the PR’s bang-prefixed command intent; normalization is a
trivial deterministic fix.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The renderer preserves leading whitespace in the extracted block text, ignores it only for
eligibility, and then passes the original text through the view model and input implementations
without normalization.

src/Capacitor.App/Views/CodeBlockActionRenderer.cs[20-29]
src/Capacitor.App/Views/CodeBlockActionRenderer.cs[41-43]
src/Capacitor.App/Views/CodeBlockActionRenderer.cs[69-73]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[497-500]
src/Capacitor.App/ViewModels/TerminalChatInput.cs[61-71]
src/Capacitor.App/ViewModels/LocalFrameChatInput.cs[87-94]
src/Capacitor.App/ViewModels/ServerChatInput.cs[68-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Leading whitespace is ignored when deciding whether to show Run, but preserved when sending, allowing text that does not actually begin with a bang to receive the command affordance.

## Fix Focus Areas
- src/Capacitor.App/Views/CodeBlockActionRenderer.cs[29-43]
- test/Capacitor.App.Tests.Unit/MarkdownViewTests.cs[332-359]

## Recommended Fix
Check `text.StartsWith('!')` without trimming so eligibility matches the verbatim payload. Add a test showing that a whitespace-prefixed bang block remains copyable but does not offer Run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Keyboard users cannot reach actions ✓ Resolved 🐞 Bug ≡ Correctness
Description
MarkdownStyles.axaml sets the action strip's IsVisible to false and reveals it only under the
host's :pointerover state. When a user navigates by keyboard without first hovering each block,
the hidden Copy and Run buttons cannot receive focus, so neither new action is available.
Code

src/Capacitor.App/Views/MarkdownStyles.axaml[R56-61]

+    <Style Selector="mv|MarkdownViewer Panel.markdown-code-host > StackPanel.markdown-code-actions">
+        <Setter Property="IsVisible" Value="False" />
+        <Setter Property="Margin" Value="0,7,7,0" />
+    </Style>
+    <Style Selector="mv|MarkdownViewer Panel.markdown-code-host:pointerover > StackPanel.markdown-code-actions">
+        <Setter Property="IsVisible" Value="True" />
Evidence
The resting selector hides the entire action strip, while the only selector that restores visibility
requires pointer hover; both buttons are children of that hidden strip.

src/Capacitor.App/Views/MarkdownStyles.axaml[54-62]
src/Capacitor.App/Views/CodeBlockActionRenderer.cs[32-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The code-block action strip is removed from keyboard navigation until pointer hover, preventing keyboard-only users from reaching Copy or Run.

## Fix Focus Areas
- src/Capacitor.App/Views/MarkdownStyles.axaml[54-62]
- test/Capacitor.App.Tests.Unit/MarkdownViewTests.cs[312-330]

## Recommended Fix
Keep the action buttons in keyboard navigation and reveal the strip when it or its host receives keyboard focus, while retaining the existing pointer-hover behavior. Add a headless test that reaches and activates an action without first moving the mouse.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Unavailable commands still look runnable ✓ Resolved 🐞 Bug ≡ Correctness
Description
RunButton creates an ordinary enabled button and consults CanExecute only inside its click
handler instead of binding the command state. When the session is ended, busy, or read-only,
RunCodeCommand is disabled but every bang-prefixed block still presents an enabled “Run it”
control whose click silently does nothing.
Code

src/Capacitor.App/Views/CodeBlockActionRenderer.cs[R70-72]

+        var button = ActionButton("Run it", "markdown-code-run");
+        button.Click += (_, _) => {
+            if (view.RunCode is { } run && run.CanExecute(text)) run.Execute(text);
Relevance

● Weak

The repository previously rejected requiring button enablement to reflect changing command
availability.

PR-#653

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The button is always constructed as enabled and merely checks command availability after a click,
while the view model explicitly disables the command whenever text cannot be accepted or the
participant is read-only.

src/Capacitor.App/Views/CodeBlockActionRenderer.cs[69-80]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[493-500]
src/Capacitor.App/ViewModels/ChatTabViewModel.cs[400-406]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Run button does not reflect `RunCodeCommand.CanExecute`, so unavailable commands remain enabled-looking controls that silently ignore clicks.

## Fix Focus Areas
- src/Capacitor.App/Views/CodeBlockActionRenderer.cs[69-74]
- src/Capacitor.App/ViewModels/ChatTabViewModel.cs[493-500]
- test/Capacitor.App.Tests.Unit/MarkdownViewTests.cs[332-361]

## Recommended Fix
Assign the current run command and block text to the button's `Command` and `CommandParameter` properties so Avalonia updates its enabled state automatically. Add coverage proving the button becomes disabled when command execution is unavailable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
Review mode: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 15/18, lines 369/200; both must reach the floor). Router rationale: This adds substantial interactive UI, rendering-extension, command-routing, binding-lifecycle, and styling logic across multiple files, creating many independent defect opportunities that benefit from redundant review.

Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.App/Views/MarkdownStyles.axaml Outdated
Comment thread src/Capacitor.App/Views/CodeBlockActionRenderer.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23d1d4e247

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +493 to +496
var canRun = Observable.CombineLatest(
_input.WhenAnyValue(i => i.CanAcceptText),
this.WhenAnyValue(x => x.IsReadOnlyParticipant),
(can, readOnly) => can && !readOnly);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Block Run while another send is uploading

During an attachment upload, _input.CanAcceptText remains true because the input send has not started, even though SendCommand is already executing. This leaves Run enabled, and clicking it invokes SendCommand.Execute() directly, bypassing that command's CanExecute gate and starting another upload from the same tray. Whichever upload completes first can consume the attachments while the other send is rejected, so either the original prompt or the requested command can be lost; include the active send/upload state in this gate.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 6ccb3c8. ScriptedInput.CanAcceptText is Pending is null, and SendAsync has not been called during the upload, so the gate really did stay open there. canRun now also takes SendCommand.IsExecuting.

Running_a_code_block_is_refused_while_a_send_is_uploading asserts the channel still reports CanAcceptText in that window while the run command refuses. I checked it fails without the new term.

var block = scratch.Children[0];
// A control carries one parent: the scratch panel has to let go before the host adopts it.
scratch.Children.Clear();
renderer.WriteBlock(Host(block, obj.Lines.ToString().TrimEnd('\n', '\r')));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve intentional trailing blank lines in code blocks

For a fenced block that intentionally ends with one or more blank lines, TrimEnd('\n', '\r') removes every trailing line break rather than only the single terminator added by StringLineGroup.ToString(). Copy and Run therefore receive different text from the displayed block, which can change pasted multiline content; remove only the renderer-added final line ending.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 6ccb3c8, though not for the stated reason — worth recording because it changes the fix.

StringLineGroup.ToString() adds no terminator at all. It delegates to ToSlice, which appends the newline between lines only (if (i > 0) { Append(newLine); }) and nothing after the last. So there was no renderer-added final line ending to remove selectively: the TrimEnd was pure over-trimming, and the fix is to drop it outright rather than narrow it.

The conclusion held regardless, so: obj.Lines.ToString() now goes through untouched. A_blank_last_line_survives_the_copy pins ```bash\nls -la\n\n``` copying as ls -la\n, and the existing multi-line test still copies cd /tmp\nls -la with no trailing newline — which is the evidence that nothing was being appended.

The strip wraps the block MarkView's own renderer wrote rather than replacing that renderer, so syntax highlighting and its theme switch stay with the package across an upgrade. A block reads the run command as it is built, so the view builds the document again when a binding delivers one after the first render.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alexeyzimarev
alexeyzimarev force-pushed the capacitor/agent-57e63ed1d4054e branch from 23d1d4e to 44e51e1 Compare September 17, 2026 14:53
The run offer now matches the verbatim payload: the bang must be the text's own first character, and a blank last line the block shows survives the copy. Run carries the command itself, so an unavailable composer greys it rather than swallowing a click, and its gate takes in the send's executing state — through an upload the channel still reports it can take text. The strip hides by going transparent because an invisible one is skipped by tab navigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alexeyzimarev

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 6ccb3c8977

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copy and run a chat code block from a hover strip

1 participant