From 623ee2503f8debe5ce2d725eb7f8c8ed6f41b657 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 8 Nov 2025 17:02:08 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20disable=20disruptive=20ta?= =?UTF-8?q?ble=20copy/download=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streamdown's built-in table controls were adding copy/download buttons that disrupted table spacing. Disabled via controls prop while keeping useful code/mermaid controls. Also added MarkdownTables storybook demonstrating various table formats. Generated with `cmux` --- src/App.stories.tsx | 194 +++++++++++++++++++++++ src/components/Messages/MarkdownCore.tsx | 1 + 2 files changed, 195 insertions(+) diff --git a/src/App.stories.tsx b/src/App.stories.tsx index baacef3a9..bd5152eab 100644 --- a/src/App.stories.tsx +++ b/src/App.stories.tsx @@ -676,3 +676,197 @@ export const ActiveWorkspaceWithChat: Story = { return ; }, }; + +/** + * Story demonstrating markdown table rendering + * Shows various table formats without disruptive copy/download actions + */ +export const MarkdownTables: Story = { + render: () => { + const AppWithTableMocks = () => { + const initialized = useRef(false); + + if (!initialized.current) { + const workspaceId = "my-app-feature"; + + const workspaces: FrontendWorkspaceMetadata[] = [ + { + id: workspaceId, + name: "feature", + projectPath: "/home/user/projects/my-app", + projectName: "my-app", + namedWorkspacePath: "/home/user/.cmux/src/my-app/feature", + }, + ]; + + setupMockAPI({ + projects: new Map([ + [ + "/home/user/projects/my-app", + { + workspaces: [ + { path: "/home/user/.cmux/src/my-app/feature", id: workspaceId, name: "feature" }, + ], + }, + ], + ]), + workspaces, + selectedWorkspaceId: workspaceId, + apiOverrides: { + workspace: { + create: (projectPath: string, branchName: string) => + Promise.resolve({ + success: true, + metadata: { + id: Math.random().toString(36).substring(2, 12), + name: branchName, + projectPath, + projectName: projectPath.split("/").pop() ?? "project", + namedWorkspacePath: `/mock/workspace/${branchName}`, + }, + }), + list: () => Promise.resolve(workspaces), + rename: (workspaceId: string) => + Promise.resolve({ + success: true, + data: { newWorkspaceId: workspaceId }, + }), + remove: () => Promise.resolve({ success: true }), + fork: () => Promise.resolve({ success: false, error: "Not implemented in mock" }), + openTerminal: () => Promise.resolve(undefined), + onChat: (workspaceId, callback) => { + setTimeout(() => { + // User message + callback({ + id: "msg-1", + role: "user", + parts: [{ type: "text", text: "Show me some table examples" }], + metadata: { + historySequence: 1, + timestamp: STABLE_TIMESTAMP, + }, + }); + + // Assistant message with tables + callback({ + id: "msg-2", + role: "assistant", + parts: [ + { + type: "text", + text: `Here are various markdown table examples: + +## Simple Table + +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Value A | Value B | Value C | +| Value D | Value E | Value F | +| Value G | Value H | Value I | + +## Table with Different Alignments + +| Left Aligned | Center Aligned | Right Aligned | +|:-------------|:--------------:|--------------:| +| Left | Center | Right | +| Text | Text | Text | +| More | Data | Here | + +## Code and Links in Tables + +| Feature | Status | Notes | +|---------|--------|-------| +| \`markdown\` support | ✅ Done | Full GFM support | +| [Links](https://example.com) | ✅ Done | Opens externally | +| **Bold** and _italic_ | ✅ Done | Standard formatting | + +## Large Table with Many Rows + +| ID | Name | Email | Status | Role | Last Login | +|----|------|-------|--------|------|------------| +| 1 | Alice Smith | alice@example.com | Active | Admin | 2024-01-20 | +| 2 | Bob Jones | bob@example.com | Active | User | 2024-01-19 | +| 3 | Carol White | carol@example.com | Inactive | User | 2024-01-15 | +| 4 | David Brown | david@example.com | Active | Moderator | 2024-01-21 | +| 5 | Eve Wilson | eve@example.com | Active | User | 2024-01-18 | +| 6 | Frank Miller | frank@example.com | Pending | User | 2024-01-10 | +| 7 | Grace Lee | grace@example.com | Active | Admin | 2024-01-22 | +| 8 | Henry Davis | henry@example.com | Active | User | 2024-01-17 | + +## Narrow Table + +| # | Item | +|----|------| +| 1 | First | +| 2 | Second | +| 3 | Third | + +## Wide Table with Long Content + +| Configuration Key | Default Value | Description | Environment Variable | +|-------------------|---------------|-------------|---------------------| +| \`api.timeout\` | 30000 | Request timeout in milliseconds | \`API_TIMEOUT\` | +| \`cache.enabled\` | true | Enable response caching | \`CACHE_ENABLED\` | +| \`logging.level\` | info | Log verbosity level (debug, info, warn, error) | \`LOG_LEVEL\` | +| \`server.port\` | 3000 | Port number for HTTP server | \`PORT\` | + +These tables should render cleanly without any disruptive copy or download actions.`, + }, + ], + metadata: { + historySequence: 2, + timestamp: STABLE_TIMESTAMP + 1000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 100, + outputTokens: 500, + totalTokens: 600, + }, + duration: 2000, + }, + }); + + // Mark as caught up + callback({ type: "caught-up" }); + }, 100); + + return () => { + // Cleanup + }; + }, + onMetadata: () => () => undefined, + sendMessage: () => Promise.resolve({ success: true, data: undefined }), + resumeStream: () => Promise.resolve({ success: true, data: undefined }), + interruptStream: () => Promise.resolve({ success: true, data: undefined }), + truncateHistory: () => Promise.resolve({ success: true, data: undefined }), + replaceChatHistory: () => Promise.resolve({ success: true, data: undefined }), + getInfo: () => Promise.resolve(null), + executeBash: () => + Promise.resolve({ + success: true, + data: { success: true, output: "", exitCode: 0, wall_duration_ms: 0 }, + }), + }, + }, + }); + + // Set initial workspace selection + localStorage.setItem( + "selectedWorkspace", + JSON.stringify({ + workspaceId: workspaceId, + projectPath: "/home/user/projects/my-app", + projectName: "my-app", + namedWorkspacePath: "/home/user/.cmux/src/my-app/feature", + }) + ); + + initialized.current = true; + } + + return ; + }; + + return ; + }, +}; diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index 652d5f05b..fbe25b3af 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -54,6 +54,7 @@ export const MarkdownCore = React.memo(({ content, children } rehypePlugins={REHYPE_PLUGINS} parseIncompleteMarkdown={true} className="space-y-2" // Reduce from default space-y-4 (16px) to space-y-2 (8px) + controls={{ table: false, code: true, mermaid: true }} // Disable table copy/download, keep code/mermaid controls > {normalizedContent}