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,