Skip to content

Add copy button for markdown write details#1647

Open
ipangdz wants to merge 1 commit into
getpaseo:mainfrom
ipangdz:codex/add-copy-markdown-write
Open

Add copy button for markdown write details#1647
ipangdz wants to merge 1 commit into
getpaseo:mainfrom
ipangdz:codex/add-copy-markdown-write

Conversation

@ipangdz

@ipangdz ipangdz commented Jun 21, 2026

Copy link
Copy Markdown

Summary

  • add a copy button to markdown Write tool detail panels
  • copy the full written markdown content to the clipboard
  • keep the new button scoped to .md, .mdx, and .markdown files

Why

Markdown write results can be long, and copying them manually requires selecting the rendered content inside a scrollable tool detail. This adds the same kind of one-click copy affordance already used elsewhere in the app.

Verification

  • npm --prefix packages/app test -- src/components/tool-call-details.test.tsx
  • npm run lint -- packages/app/src/components/tool-call-details.tsx packages/app/src/components/tool-call-details.test.tsx
  • npm run typecheck --workspace=@getpaseo/app
  • npm run typecheck --workspaces --if-present

@ipangdz ipangdz changed the title [codex] Add copy button for markdown write details Add copy button for markdown write details Jun 21, 2026
@ipangdz ipangdz marked this pull request as ready for review June 21, 2026 14:59
@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a one-click copy button to the Write tool detail panel in tool-call-details.tsx, scoped to .md, .mdx, and .markdown files. It introduces WriteDetailSection, CopyTextButton, and isMarkdownPath alongside a new test file.

  • CopyTextButton uses a useRef-managed timer for the "Copied" feedback reset, a cleanup-only useEffect for unmount safety, and expo-clipboard — the implementation logic is correct.
  • The callsite wrapper <View key=\"write\" style={ds.sectionFillStyle}> is preserved unchanged while WriteDetailSection internally re-applies the same ds.sectionFillStyle, creating a redundant nested View layer.
  • The accompanying test file uses the full set of patterns the project bans (JSDOM, vi.mock, vi.hoisted, component mounting, stubbed globals); the pure logic is extractable and the component-level flow belongs in an E2E harness.

Confidence Score: 4/5

The production code change is safe to merge; the copy-button feature is self-contained and the clipboard/timer logic is correct.

The WriteDetailSection component correctly positions and cleans up the copy button. The only structural concern is the redundant ds.sectionFillStyle applied on both the callsite wrapper and the inner View. The test file is the weaker part of the PR, mounting a component tree with 8 vi.mock calls and JSDOM in patterns the team explicitly bans.

The test file tool-call-details.test.tsx warrants the most attention due to its use of banned testing patterns. The tool-call-details.tsx callsite at the write branch is worth a second look for the double sectionFillStyle nesting.

Important Files Changed

Filename Overview
packages/app/src/components/tool-call-details.tsx Adds WriteDetailSection and CopyTextButton for markdown write panels. The copy button logic (timer ref, useCallback, cleanup useEffect) is correct. The main structural concern is that ds.sectionFillStyle is applied both on the outer callsite wrapper and again inside WriteDetailSection, producing a redundant nested View layer.
packages/app/src/components/tool-call-details.test.tsx New test file for the copy button feature. Uses a wide set of test patterns the project explicitly bans: vi.hoisted, vi.mock (×8), JSDOM, component mounting, and vi.stubGlobal. The extractable pure logic (isMarkdownPath, clipboard call) would be better tested directly; component-level integration belongs in an E2E harness.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildDetailSections] -->|detail.type === write| B["&lt;View key=write style=ds.sectionFillStyle&gt;"]
    B --> C{detail.content?}
    C -->|yes| D[WriteDetailSection]
    C -->|no| E[null]
    D --> F["&lt;View style=[ds.sectionFillStyle, copyableSection]&gt;"]
    F --> G{isMarkdownPath?}
    G -->|yes| H[CopyTextButton]
    G -->|no| I[no button]
    F --> J[ScrollableTextSection]
    H --> K{copied?}
    K -->|yes| L["Check icon, label=Copied"]
    K -->|no| M["Copy icon, label=Copy"]
    H --> N[handlePress]
    N --> O[Clipboard.setStringAsync]
    O --> P[setCopied true]
    P --> Q[setTimeout 1500ms]
    Q --> R[setCopied false]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[buildDetailSections] -->|detail.type === write| B["&lt;View key=write style=ds.sectionFillStyle&gt;"]
    B --> C{detail.content?}
    C -->|yes| D[WriteDetailSection]
    C -->|no| E[null]
    D --> F["&lt;View style=[ds.sectionFillStyle, copyableSection]&gt;"]
    F --> G{isMarkdownPath?}
    G -->|yes| H[CopyTextButton]
    G -->|no| I[no button]
    F --> J[ScrollableTextSection]
    H --> K{copied?}
    K -->|yes| L["Check icon, label=Copied"]
    K -->|no| M["Copy icon, label=Copy"]
    H --> N[handlePress]
    N --> O[Clipboard.setStringAsync]
    O --> P[setCopied true]
    P --> Q[setTimeout 1500ms]
    Q --> R[setCopied false]
Loading

Comments Outside Diff (1)

  1. packages/app/src/components/tool-call-details.tsx, line 776-783 (link)

    P2 Double application of sectionFillStyle

    The outer <View key="write" style={ds.sectionFillStyle}> already applies [styles.section, shouldFill && styles.fillHeight]. Inside, WriteDetailSection creates another <View style={[ds.sectionFillStyle, styles.copyableSection]}> with the exact same base style. Every markdown write panel now has two stacked Views carrying gap: spacing[2] (and flex: 1 / minHeight: 0 when shouldFill is true). WriteDetailSection should own its own style entirely; the outer wrapper at the callsite should be removed (or only carry the React key).

Reviews (1): Last reviewed commit: "Add copy button for markdown write detai..." | Re-trigger Greptile

Comment on lines +1 to +13
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

const { theme, setStringAsync } = vi.hoisted(() => ({
theme: {
spacing: { 1: 4, 2: 8, 3: 12, 4: 16 },
borderWidth: { 1: 1 },
borderRadius: { sm: 4, md: 6, lg: 8, xl: 12, full: 999 },
fontFamily: { mono: "monospace", ui: "system-ui" },
fontSize: { xs: 11, sm: 13, base: 15, code: 13 },
fontWeight: { normal: "400", medium: "500", semibold: "600" },

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 Banned test patterns throughout this file

The project's test discipline rules explicitly ban vi.hoisted, vi.mock, JSDOM as a substitute for app behavior, component mounting tests, and monkey-patched globals (vi.stubGlobal). This file uses all of them. The logic under test — isMarkdownPath and the clipboard call — is pure enough to extract from the component and test directly without mounting a component tree. The component behavior itself belongs in an E2E test with a real browser harness, not a JSDOM simulation. The mock surface here (6+ vi.mock calls, a full synthetic theme, a patched Pressable, and stubbed globals) is wider than the behavior being verified.

Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +489 to +491
const handlePress = useCallback(async () => {
if (!content) return;
await Clipboard.setStringAsync(content);

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 The content prop is typed string (non-optional), so the !content guard is unnecessary defensive code. Validations belong at the boundary; inside a typed interface the type system should be trusted.

Suggested change
const handlePress = useCallback(async () => {
if (!content) return;
await Clipboard.setStringAsync(content);
const handlePress = useCallback(async () => {
await Clipboard.setStringAsync(content);

Rule Used: # Code Review Pattern Reference: Slop, Tests, Feat... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

1 participant