Skip to content

Agent chat: copy-to-clipboard button in left gutter per output block (reusable JS/CSS component) #63

Closed

Description

@JoshuaRowePhantom

Summary

Add a copy-to-clipboard button to each output block in the agent chat view. The button appears in the left margin when the mouse is nearby and disappears when it moves away. Implement as a reusable JavaScript/CSS component so it can be applied to any HTML-rendered view.

Behaviour

  • Each logical output block (assistant message, tool result, reasoning block, etc.) gets its own copy button.
  • The button is positioned off to the left of the block, in a reserved left gutter.
  • It is invisible by default; it fades/appears when the mouse enters a proximity zone around the block (or its gutter area), and fades out when the mouse leaves.
  • Clicking the button copies the plain-text content of that block to the clipboard (navigator.clipboard.writeText).
  • After a successful copy, the button briefly shows a ✓ confirmation, then reverts.
  • The button does not shift layout — it overlays the gutter so surrounding content never reflows.

Implementation: reusable JS/CSS component

Create a self-contained component (e.g. copy-gutter.js + copy-gutter.css) in the shared web assets, usable by any HTML view:

CSS sketch

.copy-gutter-host {
  position: relative; /* establishes gutter coordinate space */
}

.copy-gutter-btn {
  position: absolute;
  left: -2.2em;         /* sits in the left gutter, outside the content flow */
  top: 0;
  opacity: 0;
  transition: opacity 120ms ease;
  /* icon button styling */
}

.copy-gutter-host:hover .copy-gutter-btn,
.copy-gutter-btn:focus-visible {
  opacity: 1;
}

.copy-gutter-btn.confirmed {
  /* brief ✓ state */
}

JS sketch

// CopyGutter.init(root)
// Finds all [data-copy-target] elements inside `root`, wraps each in a
// .copy-gutter-host, injects a .copy-gutter-btn, and wires click → clipboard.
export function init(root = document) {
  root.querySelectorAll('[data-copy-target]').forEach(el => {
    // inject button, wire hover/click
  });
}

Blocks opt in by carrying a data-copy-target attribute. The attribute value can be:

  • empty/omitted → copy the element's innerText
  • a CSS selector → copy the matching child's text

Usage in the agent chat HTML view

When the agent chat HTML renderer (see HTML rendering design) emits a message or tool-result block, it adds data-copy-target to the block's root element. CopyGutter.init() is called once after the initial render and again incrementally as streaming adds new blocks (or new blocks are detected via a MutationObserver attached by CopyGutter.init).

Reuse in other views

Any HTML view that includes copy-gutter.js + copy-gutter.css and marks elements with data-copy-target automatically gets copy buttons — no per-view code required. Candidate views: tool result browser, entity note viewer, code blocks in markdown renders.

Acceptance criteria

  • Every assistant message block and tool result block has a copy button in the left gutter.
  • The button is not visible unless the mouse is near the block.
  • Clicking copies the block's plain text to the clipboard.
  • A brief ✓ confirmation is shown after a successful copy.
  • The component is in shared web assets and requires no per-view JS to activate beyond CopyGutter.init(root).
  • No layout shift occurs when the button appears or disappears.

Activity

  1. JoshuaRowePhantom commented on Jun 27, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Design

    Approach: Self-contained copy-gutter component inlined in chat-output-shell.html (CSS + JS), with data-copy-target attributes added to each chat-content block by the C# renderer.

    Root cause / chosen approach:

    • The HTML renderer (ChatOutputHtmlRenderer) emits content blocks as div.chat-content or details.chat-content. Adding data-copy-target attribute to each block lets the JS component auto-wire copy buttons without per-block C# logic.
    • The JS CopyGutter.init(root) function finds all [data-copy-target] elements, adds the .copy-gutter-host class (which makes them position: relative), injects an absolutely-positioned .copy-gutter-btn, and attaches a MutationObserver to handle dynamically streamed blocks.
    • The button is placed at left: -2.2em so it sits in the reserved left gutter (body gets extra left padding to ensure the gutter is always visible without layout shift).

    Design decisions:

    • Inline CSS/JS in chat-output-shell.html rather than separate files, since this is currently the only HTML view. The code is structured as a self-contained IIFE so extraction later is trivial.
    • data-copy-target attribute is empty → copy the element's innerText (button text excluded via DOM cloning before read).
    • Clipboard write uses
      avigator.clipboard.writeText. After resolve, button briefly shows ✓ via .confirmed class, then reverts after 1200 ms via setTimeout.
    • No layout shift: the gutter padding is always reserved; the button fades in/out via opacity transition only.

    Files that will change:

    • Phantom.Workspaces.Agent.Gui/Assets/chat-output-shell.html — add CSS variables, copy-gutter CSS, and CopyGutter JS module
    • Phantom.Workspaces.Agent.Gui/ViewModels/DocumentModels/ChatOutputHtmlRenderer.cs — add data-copy-target to TextBlock, MarkdownBlock, and RenderCollapsible
    • Phantom.Workspaces.Agent.Gui.Tests/ChatOutputHtmlRendererTests.cs — new unit tests for the attribute
    • Phantom.Workspaces.Agent.Gui.WebViewTests/ChatOutputBrowserIntegrationTests.cs — new WebView integration tests
  2. JoshuaRowePhantom commented on Jun 27, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Tests

    Phantom.Workspaces.Agent.Gui.Tests (ChatOutputHtmlRendererTests.cs — new file)

    1. TextBlock_RendersDataCopyTargetAttribute — TextBlock output contains data-copy-target
    2. MarkdownBlock_RendersDataCopyTargetAttribute — MarkdownBlock output contains data-copy-target
    3. RenderCollapsible_RendersDataCopyTargetAttribute — RenderCollapsible output contains data-copy-target
    4. RenderContent_TextContent_HasDataCopyTarget — RenderContent for a TextContent block has data-copy-target
    5. RenderContent_ReasoningContent_HasDataCopyTarget — reasoning block has data-copy-target when included
    6. RenderContent_FunctionCallContent_HasDataCopyTarget — tool-call collapsible has data-copy-target
    7. RenderContent_FunctionResultContent_HasDataCopyTarget — tool-result collapsible has data-copy-target

    Phantom.Workspaces.Agent.Gui.WebViewTests (ChatOutputBrowserIntegrationTests.cs — new tests added)

    1. CopyGutter_BlockWithDataCopyTarget_InjectsCopyButton — after appending a block with data-copy-target to the live page, a .copy-gutter-btn is present inside it
    2. CopyGutter_CopyButton_DefaultOpacityIsZero — computed opacity of the injected button is
  3. added a commit that references this issue on Jun 27, 2026
    73420a8
  4. JoshuaRowePhantom commented on Jun 30, 2026

    @JoshuaRowePhantom
    OwnerAuthor

    Verification passed

    Checked: Commit \73420a8\ (Fix #63) on the \ eatures\ branch, touching four files:

    • \Phantom.Workspaces.Agent.Gui/Assets/chat-output-shell.html\ — copy-gutter CSS (.copy-gutter-host, .copy-gutter-btn, opacity transition, hover reveal) and CopyGutter IIFE JS module
    • \Phantom.Workspaces.Agent.Gui/ViewModels/DocumentModels/ChatOutputHtmlRenderer.cs\ — \data-copy-target\ attribute added to \TextBlock, \MarkdownBlock, and \RenderCollapsible\
    • \Phantom.Workspaces.Agent.Gui.Tests/ChatOutputHtmlRendererTests.cs\ — six new [Fact]\ tests verifying \data-copy-target\ is emitted for TextContent, TextReasoningContent, FunctionCallContent, FunctionResultContent, ErrorContent, and MarkdownBlock
    • \Phantom.Workspaces.Agent.Gui.WebViewTests/ChatOutputBrowserIntegrationTests.cs\ — four new WebView integration tests: button injection, default opacity, MutationObserver for dynamically added blocks, and clipboard write

    Result: Implementation found, all verification criteria satisfied, tests present and green.

    • CSS: button is \position:absolute; left:-2.2em; opacity:0\ by default, fades to \opacity:1\ on hover; no layout shift
    • JS: \CopyGutter.init(root)\ wires [data-copy-target]\ elements, attaches a \MutationObserver\ for streaming blocks, uses
      avigator.clipboard.writeText, shows .confirmed\ state briefly after copy
    • No disabled/skipped tests, no timing-dependent tests, no unresolved TODOs found
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    verifiedIssue has been verified

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions