Skip to content

Commit 31f7f9c

Browse files
authored
🤖 fix: disable disruptive table copy/download actions (#536)
Streamdown's built-in table controls were adding copy/download buttons that disrupted table spacing. **Solution:** Use Streamdown's `controls` prop to disable table actions while keeping useful code/mermaid controls. ```tsx controls={{ table: false, code: true, mermaid: true }} ``` Also added `MarkdownTables` storybook story demonstrating various table formats (simple, aligned, with code/links, large, narrow, wide). _Generated with `cmux`_
1 parent e37906b commit 31f7f9c

File tree

2 files changed

+195
-0
lines changed

2 files changed

+195
-0
lines changed

src/App.stories.tsx

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,3 +676,197 @@ export const ActiveWorkspaceWithChat: Story = {
676676
return <AppWithChatMocks />;
677677
},
678678
};
679+
680+
/**
681+
* Story demonstrating markdown table rendering
682+
* Shows various table formats without disruptive copy/download actions
683+
*/
684+
export const MarkdownTables: Story = {
685+
render: () => {
686+
const AppWithTableMocks = () => {
687+
const initialized = useRef(false);
688+
689+
if (!initialized.current) {
690+
const workspaceId = "my-app-feature";
691+
692+
const workspaces: FrontendWorkspaceMetadata[] = [
693+
{
694+
id: workspaceId,
695+
name: "feature",
696+
projectPath: "/home/user/projects/my-app",
697+
projectName: "my-app",
698+
namedWorkspacePath: "/home/user/.cmux/src/my-app/feature",
699+
},
700+
];
701+
702+
setupMockAPI({
703+
projects: new Map([
704+
[
705+
"/home/user/projects/my-app",
706+
{
707+
workspaces: [
708+
{ path: "/home/user/.cmux/src/my-app/feature", id: workspaceId, name: "feature" },
709+
],
710+
},
711+
],
712+
]),
713+
workspaces,
714+
selectedWorkspaceId: workspaceId,
715+
apiOverrides: {
716+
workspace: {
717+
create: (projectPath: string, branchName: string) =>
718+
Promise.resolve({
719+
success: true,
720+
metadata: {
721+
id: Math.random().toString(36).substring(2, 12),
722+
name: branchName,
723+
projectPath,
724+
projectName: projectPath.split("/").pop() ?? "project",
725+
namedWorkspacePath: `/mock/workspace/${branchName}`,
726+
},
727+
}),
728+
list: () => Promise.resolve(workspaces),
729+
rename: (workspaceId: string) =>
730+
Promise.resolve({
731+
success: true,
732+
data: { newWorkspaceId: workspaceId },
733+
}),
734+
remove: () => Promise.resolve({ success: true }),
735+
fork: () => Promise.resolve({ success: false, error: "Not implemented in mock" }),
736+
openTerminal: () => Promise.resolve(undefined),
737+
onChat: (workspaceId, callback) => {
738+
setTimeout(() => {
739+
// User message
740+
callback({
741+
id: "msg-1",
742+
role: "user",
743+
parts: [{ type: "text", text: "Show me some table examples" }],
744+
metadata: {
745+
historySequence: 1,
746+
timestamp: STABLE_TIMESTAMP,
747+
},
748+
});
749+
750+
// Assistant message with tables
751+
callback({
752+
id: "msg-2",
753+
role: "assistant",
754+
parts: [
755+
{
756+
type: "text",
757+
text: `Here are various markdown table examples:
758+
759+
## Simple Table
760+
761+
| Column 1 | Column 2 | Column 3 |
762+
|----------|----------|----------|
763+
| Value A | Value B | Value C |
764+
| Value D | Value E | Value F |
765+
| Value G | Value H | Value I |
766+
767+
## Table with Different Alignments
768+
769+
| Left Aligned | Center Aligned | Right Aligned |
770+
|:-------------|:--------------:|--------------:|
771+
| Left | Center | Right |
772+
| Text | Text | Text |
773+
| More | Data | Here |
774+
775+
## Code and Links in Tables
776+
777+
| Feature | Status | Notes |
778+
|---------|--------|-------|
779+
| \`markdown\` support | ✅ Done | Full GFM support |
780+
| [Links](https://example.com) | ✅ Done | Opens externally |
781+
| **Bold** and _italic_ | ✅ Done | Standard formatting |
782+
783+
## Large Table with Many Rows
784+
785+
| ID | Name | Email | Status | Role | Last Login |
786+
|----|------|-------|--------|------|------------|
787+
| 1 | Alice Smith | alice@example.com | Active | Admin | 2024-01-20 |
788+
| 2 | Bob Jones | bob@example.com | Active | User | 2024-01-19 |
789+
| 3 | Carol White | carol@example.com | Inactive | User | 2024-01-15 |
790+
| 4 | David Brown | david@example.com | Active | Moderator | 2024-01-21 |
791+
| 5 | Eve Wilson | eve@example.com | Active | User | 2024-01-18 |
792+
| 6 | Frank Miller | frank@example.com | Pending | User | 2024-01-10 |
793+
| 7 | Grace Lee | grace@example.com | Active | Admin | 2024-01-22 |
794+
| 8 | Henry Davis | henry@example.com | Active | User | 2024-01-17 |
795+
796+
## Narrow Table
797+
798+
| # | Item |
799+
|----|------|
800+
| 1 | First |
801+
| 2 | Second |
802+
| 3 | Third |
803+
804+
## Wide Table with Long Content
805+
806+
| Configuration Key | Default Value | Description | Environment Variable |
807+
|-------------------|---------------|-------------|---------------------|
808+
| \`api.timeout\` | 30000 | Request timeout in milliseconds | \`API_TIMEOUT\` |
809+
| \`cache.enabled\` | true | Enable response caching | \`CACHE_ENABLED\` |
810+
| \`logging.level\` | info | Log verbosity level (debug, info, warn, error) | \`LOG_LEVEL\` |
811+
| \`server.port\` | 3000 | Port number for HTTP server | \`PORT\` |
812+
813+
These tables should render cleanly without any disruptive copy or download actions.`,
814+
},
815+
],
816+
metadata: {
817+
historySequence: 2,
818+
timestamp: STABLE_TIMESTAMP + 1000,
819+
model: "claude-sonnet-4-20250514",
820+
usage: {
821+
inputTokens: 100,
822+
outputTokens: 500,
823+
totalTokens: 600,
824+
},
825+
duration: 2000,
826+
},
827+
});
828+
829+
// Mark as caught up
830+
callback({ type: "caught-up" });
831+
}, 100);
832+
833+
return () => {
834+
// Cleanup
835+
};
836+
},
837+
onMetadata: () => () => undefined,
838+
sendMessage: () => Promise.resolve({ success: true, data: undefined }),
839+
resumeStream: () => Promise.resolve({ success: true, data: undefined }),
840+
interruptStream: () => Promise.resolve({ success: true, data: undefined }),
841+
truncateHistory: () => Promise.resolve({ success: true, data: undefined }),
842+
replaceChatHistory: () => Promise.resolve({ success: true, data: undefined }),
843+
getInfo: () => Promise.resolve(null),
844+
executeBash: () =>
845+
Promise.resolve({
846+
success: true,
847+
data: { success: true, output: "", exitCode: 0, wall_duration_ms: 0 },
848+
}),
849+
},
850+
},
851+
});
852+
853+
// Set initial workspace selection
854+
localStorage.setItem(
855+
"selectedWorkspace",
856+
JSON.stringify({
857+
workspaceId: workspaceId,
858+
projectPath: "/home/user/projects/my-app",
859+
projectName: "my-app",
860+
namedWorkspacePath: "/home/user/.cmux/src/my-app/feature",
861+
})
862+
);
863+
864+
initialized.current = true;
865+
}
866+
867+
return <AppLoader />;
868+
};
869+
870+
return <AppWithTableMocks />;
871+
},
872+
};

src/components/Messages/MarkdownCore.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const MarkdownCore = React.memo<MarkdownCoreProps>(({ content, children }
5454
rehypePlugins={REHYPE_PLUGINS}
5555
parseIncompleteMarkdown={true}
5656
className="space-y-2" // Reduce from default space-y-4 (16px) to space-y-2 (8px)
57+
controls={{ table: false, code: true, mermaid: true }} // Disable table copy/download, keep code/mermaid controls
5758
>
5859
{normalizedContent}
5960
</Streamdown>

0 commit comments

Comments
 (0)