diff --git a/src/renderer/components/WorkbenchControls.tsx b/src/renderer/components/WorkbenchControls.tsx index 5cf1ddf..1fb297a 100644 --- a/src/renderer/components/WorkbenchControls.tsx +++ b/src/renderer/components/WorkbenchControls.tsx @@ -276,6 +276,50 @@ export function Composer({ ); } +export const TOOL_TEXT_PREVIEW_LIMIT = 2000; + +function ToolTextSection({ title, text }: { title: string; text: string }) { + const [showAll, setShowAll] = useState(false); + const [copied, setCopied] = useState(false); + const isLong = text.length > TOOL_TEXT_PREVIEW_LIMIT; + const visible = + isLong && !showAll ? text.slice(0, TOOL_TEXT_PREVIEW_LIMIT) : text; + const copy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + window.setTimeout(() => setCopied(false), 1500); + } catch { + setCopied(false); + } + }; + return ( +
+
+

{title}

+ +
+
{visible}
+ {isLong && ( + + )} +
+ ); +} + export const TranscriptRow = memo(function TranscriptRow({ entry, expanded, @@ -310,17 +354,14 @@ export const TranscriptRow = memo(function TranscriptRow({ {entry.running ? 'Running' : entry.ok ? 'Done' : 'Failed'} -
-

Input

-
{entry.input || 'No input.'}
-
-
-

Output

-
-            {entry.output ||
-              (entry.running ? 'Waiting for output…' : 'No output.')}
-          
-
+ + ); return ( diff --git a/src/renderer/workbench.css b/src/renderer/workbench.css index 8123754..e5d9eb7 100644 --- a/src/renderer/workbench.css +++ b/src/renderer/workbench.css @@ -715,6 +715,26 @@ html[data-reduced-motion='true'] .wb-shell *::after { text-transform: uppercase; color: var(--muted); } +.wb-tool-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding-right: 12px; +} +.wb-tool-copy, +.wb-tool-toggle { + border: 1px solid var(--line-soft); + background: transparent; + color: var(--copy); + font-size: 11px; + line-height: 16px; + padding: 2px 8px; + cursor: pointer; +} +.wb-tool-toggle { + margin: 0 12px 12px; +} .wb-tool-section + .wb-tool-section { border-top: 1px solid var(--line-soft); } diff --git a/tests/integration/ToolCard.test.tsx b/tests/integration/ToolCard.test.tsx index 534ba36..e83a617 100644 --- a/tests/integration/ToolCard.test.tsx +++ b/tests/integration/ToolCard.test.tsx @@ -1,5 +1,5 @@ -import { describe, expect, it, beforeEach } from 'vitest'; -import { render, cleanup } from '@testing-library/react'; +import { describe, expect, it, beforeEach, vi } from 'vitest'; +import { render, cleanup, fireEvent } from '@testing-library/react'; import { TranscriptRow } from '../../src/renderer/components/WorkbenchControls'; import type { Entry } from '../../src/renderer/timeline'; @@ -33,6 +33,43 @@ describe(' tool card', () => { expect(container.textContent).toContain('Done'); }); + it('copies the exact output text and shows Copied', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { + value: { writeText }, + configurable: true, + }); + const expanded = new Map([[toolEntry.key, true]]); + const { getByRole } = render( + , + ); + fireEvent.click(getByRole('button', { name: 'Copy output' })); + expect(writeText).toHaveBeenCalledWith('Edited /repo/src/app.ts'); + await vi.waitFor(() => + expect(getByRole('button', { name: 'Copy output' }).textContent).toBe( + 'Copied', + ), + ); + }); + + it('truncates long output and expands on Show more', () => { + const longOutput = `start-${'x'.repeat(2500)}-end-marker`; + const longEntry: Extract = { + ...toolEntry, + key: 'tool-3', + output: longOutput, + }; + const expanded = new Map([[longEntry.key, true]]); + const { container, getByRole } = render( + , + ); + expect(container.textContent).not.toContain('end-marker'); + fireEvent.click(getByRole('button', { name: 'Show more' })); + expect(container.textContent).toContain('end-marker'); + fireEvent.click(getByRole('button', { name: 'Show less' })); + expect(container.textContent).not.toContain('end-marker'); + }); + it('shows running state and waiting output while a tool runs', () => { const running: Extract = { ...toolEntry,