From 57ef5e480d35a49b151202a15cf077423385ff01 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 20 Oct 2025 19:02:32 -0400 Subject: [PATCH 1/5] Add stories for App.tsx --- .storybook/main.ts | 2 +- src/App.stories.tsx | 646 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 src/App.stories.tsx diff --git a/.storybook/main.ts b/.storybook/main.ts index 3b574f543..fee6b7bc3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -18,7 +18,7 @@ const config: StorybookConfig = { // Inherit project aliases resolve: { alias: { - "@": path.resolve(__dirname, "../src"), + "@": path.resolve(import.meta.dirname, "../src"), }, }, }); diff --git a/src/App.stories.tsx b/src/App.stories.tsx new file mode 100644 index 000000000..8e7d47ba7 --- /dev/null +++ b/src/App.stories.tsx @@ -0,0 +1,646 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useRef } from "react"; +import App from "./App"; +import type { ProjectConfig } from "./config"; +import type { FrontendWorkspaceMetadata } from "./types/workspace"; +import type { IPCApi } from "./types/ipc"; + +// Mock window.api for App component +function setupMockAPI(options: { + projects?: Map; + workspaces?: FrontendWorkspaceMetadata[]; + selectedWorkspaceId?: string; +}) { + const mockProjects = options.projects ?? new Map(); + const mockWorkspaces = options.workspaces ?? []; + + const mockApi: IPCApi = { + dialog: { + selectDirectory: () => Promise.resolve(null), + }, + providers: { + setProviderConfig: () => Promise.resolve({ success: true, data: undefined }), + list: () => Promise.resolve([]), + }, + workspace: { + create: (projectPath: string, branchName: string) => + Promise.resolve({ + success: true, + metadata: { + id: `${projectPath.split("/").pop() ?? "project"}-${branchName}`, + name: branchName, + projectPath, + projectName: projectPath.split("/").pop() ?? "project", + namedWorkspacePath: `/mock/workspace/${branchName}`, + }, + }), + list: () => Promise.resolve(mockWorkspaces), + 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: () => () => undefined, + 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 }, + }), + }, + projects: { + list: () => Promise.resolve(Array.from(mockProjects.entries())), + create: () => Promise.resolve({ success: true, data: { workspaces: [] } }), + remove: () => Promise.resolve({ success: true, data: undefined }), + listBranches: () => + Promise.resolve({ + branches: ["main", "develop", "feature/new-feature"], + recommendedTrunk: "main", + }), + secrets: { + get: () => Promise.resolve([]), + update: () => Promise.resolve({ success: true, data: undefined }), + }, + }, + window: { + setTitle: () => Promise.resolve(undefined), + }, + update: { + check: () => Promise.resolve(undefined), + download: () => Promise.resolve(undefined), + install: () => undefined, + onStatus: () => () => undefined, + }, + }; + + // @ts-expect-error - Assigning mock API to window for Storybook + window.api = mockApi; +} + +const meta = { + title: "App/Full Application", + component: App, + parameters: { + layout: "fullscreen", + backgrounds: { + default: "dark", + values: [{ name: "dark", value: "#1e1e1e" }], + }, + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Story wrapper that sets up mocks synchronously before rendering +const AppWithMocks: React.FC<{ + projects?: Map; + workspaces?: FrontendWorkspaceMetadata[]; + selectedWorkspaceId?: string; +}> = ({ projects, workspaces, selectedWorkspaceId }) => { + // Set up mock API only once per component instance (not on every render) + // Use useRef to ensure it runs synchronously before first render + const initialized = useRef(false); + if (!initialized.current) { + setupMockAPI({ projects, workspaces, selectedWorkspaceId }); + initialized.current = true; + } + + return ; +}; + +export const WelcomeScreen: Story = { + render: () => , +}; + +export const SingleProject: Story = { + render: () => { + const projects = new Map([ + [ + "/home/user/projects/my-app", + { + workspaces: [ + { path: "/home/user/.cmux/src/my-app/main", id: "my-app-main", name: "main" }, + { + path: "/home/user/.cmux/src/my-app/feature-auth", + id: "my-app-feature-auth", + name: "feature/auth", + }, + { + path: "/home/user/.cmux/src/my-app/bugfix", + id: "my-app-bugfix", + name: "bugfix/memory-leak", + }, + ], + }, + ], + ]); + + const workspaces: FrontendWorkspaceMetadata[] = [ + { + id: "my-app-main", + name: "main", + projectPath: "/home/user/projects/my-app", + projectName: "my-app", + namedWorkspacePath: "/home/user/.cmux/src/my-app/main", + }, + { + id: "my-app-feature-auth", + name: "feature/auth", + projectPath: "/home/user/projects/my-app", + projectName: "my-app", + namedWorkspacePath: "/home/user/.cmux/src/my-app/feature-auth", + }, + { + id: "my-app-bugfix", + name: "bugfix/memory-leak", + projectPath: "/home/user/projects/my-app", + projectName: "my-app", + namedWorkspacePath: "/home/user/.cmux/src/my-app/bugfix", + }, + ]; + + return ; + }, +}; + +export const MultipleProjects: Story = { + render: () => { + const projects = new Map([ + [ + "/home/user/projects/frontend", + { + workspaces: [ + { path: "/home/user/.cmux/src/frontend/main", id: "frontend-main", name: "main" }, + { + path: "/home/user/.cmux/src/frontend/redesign", + id: "frontend-redesign", + name: "redesign", + }, + ], + }, + ], + [ + "/home/user/projects/backend", + { + workspaces: [ + { path: "/home/user/.cmux/src/backend/main", id: "backend-main", name: "main" }, + { path: "/home/user/.cmux/src/backend/api-v2", id: "backend-api-v2", name: "api-v2" }, + { + path: "/home/user/.cmux/src/backend/db-migration", + id: "backend-db-migration", + name: "db-migration", + }, + ], + }, + ], + [ + "/home/user/projects/mobile", + { + workspaces: [ + { path: "/home/user/.cmux/src/mobile/main", id: "mobile-main", name: "main" }, + ], + }, + ], + ]); + + const workspaces: FrontendWorkspaceMetadata[] = [ + { + id: "frontend-main", + name: "main", + projectPath: "/home/user/projects/frontend", + projectName: "frontend", + namedWorkspacePath: "/home/user/.cmux/src/frontend/main", + }, + { + id: "frontend-redesign", + name: "redesign", + projectPath: "/home/user/projects/frontend", + projectName: "frontend", + namedWorkspacePath: "/home/user/.cmux/src/frontend/redesign", + }, + { + id: "backend-main", + name: "main", + projectPath: "/home/user/projects/backend", + projectName: "backend", + namedWorkspacePath: "/home/user/.cmux/src/backend/main", + }, + { + id: "backend-api-v2", + name: "api-v2", + projectPath: "/home/user/projects/backend", + projectName: "backend", + namedWorkspacePath: "/home/user/.cmux/src/backend/api-v2", + }, + { + id: "backend-db-migration", + name: "db-migration", + projectPath: "/home/user/projects/backend", + projectName: "backend", + namedWorkspacePath: "/home/user/.cmux/src/backend/db-migration", + }, + { + id: "mobile-main", + name: "main", + projectPath: "/home/user/projects/mobile", + projectName: "mobile", + namedWorkspacePath: "/home/user/.cmux/src/mobile/main", + }, + ]; + + return ; + }, +}; + +export const ManyWorkspaces: Story = { + render: () => { + const workspaceNames = [ + "main", + "develop", + "staging", + "feature/authentication", + "feature/dashboard", + "feature/notifications", + "feature/search", + "bugfix/memory-leak", + "bugfix/login-redirect", + "refactor/components", + "experiment/new-ui", + "release/v1.2.0", + ]; + + const projects = new Map([ + [ + "/home/user/projects/big-app", + { + workspaces: workspaceNames.map((name) => ({ + path: `/home/user/.cmux/src/big-app/${name}`, + id: `big-app-${name}`, + name, + })), + }, + ], + ]); + + const workspaces: FrontendWorkspaceMetadata[] = workspaceNames.map((name) => ({ + id: `big-app-${name}`, + name, + projectPath: "/home/user/projects/big-app", + projectName: "big-app", + namedWorkspacePath: `/home/user/.cmux/src/big-app/${name}`, + })); + + return ; + }, +}; + +export const ActiveWorkspaceWithChat: Story = { + render: () => { + const workspaceId = "demo-workspace"; + const projects = new Map([ + [ + "/home/user/projects/my-app", + { + workspaces: [ + { path: "/home/user/.cmux/src/my-app/feature", id: workspaceId, name: "feature/auth" }, + ], + }, + ], + ]); + + const workspaces: FrontendWorkspaceMetadata[] = [ + { + id: workspaceId, + name: "feature/auth", + projectPath: "/home/user/projects/my-app", + projectName: "my-app", + namedWorkspacePath: "/home/user/.cmux/src/my-app/feature", + }, + ]; + + const AppWithChatMocks: React.FC = () => { + // Set up mock API only once per component instance (not on every render) + const initialized = useRef(false); + if (!initialized.current) { + const mockProjects = projects; + const mockWorkspaces = workspaces; + + const mockApi: IPCApi = { + dialog: { + selectDirectory: () => Promise.resolve(null), + }, + providers: { + setProviderConfig: () => Promise.resolve({ success: true, data: undefined }), + list: () => Promise.resolve(["anthropic", "openai"]), + }, + workspace: { + create: (projectPath: string, branchName: string) => + Promise.resolve({ + success: true, + metadata: { + id: `${projectPath.split("/").pop() ?? "project"}-${branchName}`, + name: branchName, + projectPath, + projectName: projectPath.split("/").pop() ?? "project", + namedWorkspacePath: `/mock/workspace/${branchName}`, + }, + }), + list: () => Promise.resolve(mockWorkspaces), + 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) => { + // Send chat history immediately when subscribed + setTimeout(() => { + // User message + callback({ + id: "msg-1", + role: "user", + parts: [{ type: "text", text: "Add authentication to the user API endpoint" }], + metadata: { + historySequence: 1, + timestamp: Date.now() - 300000, + }, + }); + + // Assistant message with tool calls + callback({ + id: "msg-2", + role: "assistant", + parts: [ + { + type: "text", + text: "I'll help you add authentication to the user API endpoint. Let me first check the current implementation.", + }, + { + type: "dynamic-tool", + toolCallId: "call-1", + toolName: "read_file", + state: "output-available", + input: { target_file: "src/api/users.ts" }, + output: { + success: true, + content: + "export function getUser(req, res) {\n const user = db.users.find(req.params.id);\n res.json(user);\n}", + }, + }, + ], + metadata: { + historySequence: 2, + timestamp: Date.now() - 290000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 1250, + outputTokens: 450, + totalTokens: 1700, + }, + duration: 3500, + }, + }); + + // User response + callback({ + id: "msg-3", + role: "user", + parts: [{ type: "text", text: "Yes, add JWT token validation" }], + metadata: { + historySequence: 3, + timestamp: Date.now() - 280000, + }, + }); + + // Assistant message with file edit + callback({ + id: "msg-4", + role: "assistant", + parts: [ + { + type: "text", + text: "I'll add JWT token validation to the endpoint. Let me update the file.", + }, + { + type: "dynamic-tool", + toolCallId: "call-2", + toolName: "search_replace", + state: "output-available", + input: { + file_path: "src/api/users.ts", + old_string: "export function getUser(req, res) {", + new_string: + "import { verifyToken } from '../auth/jwt';\n\nexport function getUser(req, res) {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }", + }, + output: { + success: true, + message: "File updated successfully", + }, + }, + ], + metadata: { + historySequence: 4, + timestamp: Date.now() - 270000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 2100, + outputTokens: 680, + totalTokens: 2780, + }, + duration: 4200, + }, + }); + + // User asking to run tests + callback({ + id: "msg-5", + role: "user", + parts: [{ type: "text", text: "Can you run the tests to make sure it works?" }], + metadata: { + historySequence: 5, + timestamp: Date.now() - 240000, + }, + }); + + // Assistant running tests + callback({ + id: "msg-6", + role: "assistant", + parts: [ + { + type: "text", + text: "I'll run the tests to verify the authentication is working correctly.", + }, + { + type: "dynamic-tool", + toolCallId: "call-3", + toolName: "run_terminal_cmd", + state: "output-available", + input: { + command: "npm test src/api/users.test.ts", + explanation: "Running tests for the users API endpoint", + }, + output: { + success: true, + stdout: + "PASS src/api/users.test.ts\n ✓ should return user when authenticated (24ms)\n ✓ should return 401 when no token (18ms)\n ✓ should return 401 when invalid token (15ms)\n\nTest Suites: 1 passed, 1 total\nTests: 3 passed, 3 total", + exitCode: 0, + }, + }, + ], + metadata: { + historySequence: 6, + timestamp: Date.now() - 230000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 2800, + outputTokens: 420, + totalTokens: 3220, + }, + duration: 5100, + }, + }); + + // User follow-up about error handling + callback({ + id: "msg-7", + role: "user", + parts: [ + { + type: "text", + text: "Great! What about error handling if the JWT library throws?", + }, + ], + metadata: { + historySequence: 7, + timestamp: Date.now() - 180000, + }, + }); + + // Assistant response with thinking (reasoning) + callback({ + id: "msg-8", + role: "assistant", + parts: [ + { + type: "reasoning", + text: "The user is asking about error handling for JWT verification. The verifyToken function could throw if the token is malformed or if there's an issue with the secret. I should wrap it in a try-catch block and return a proper error response.", + }, + { + type: "text", + text: "Good catch! We should add try-catch error handling around the JWT verification. Let me update that.", + }, + { + type: "dynamic-tool", + toolCallId: "call-4", + toolName: "search_replace", + state: "output-available", + input: { + file_path: "src/api/users.ts", + old_string: + " const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }", + new_string: + " try {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }\n } catch (err) {\n console.error('Token verification failed:', err);\n return res.status(401).json({ error: 'Invalid token' });\n }", + }, + output: { + success: true, + message: "File updated successfully", + }, + }, + ], + metadata: { + historySequence: 8, + timestamp: Date.now() - 170000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 3500, + outputTokens: 520, + totalTokens: 4020, + reasoningTokens: 150, + }, + duration: 6200, + }, + }); + + // 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 }, + }), + }, + projects: { + list: () => Promise.resolve(Array.from(mockProjects.entries())), + create: () => Promise.resolve({ success: true, data: { workspaces: [] } }), + remove: () => Promise.resolve({ success: true, data: undefined }), + listBranches: () => + Promise.resolve({ + branches: ["main", "develop", "feature/auth"], + recommendedTrunk: "main", + }), + secrets: { + get: () => Promise.resolve([]), + update: () => Promise.resolve({ success: true, data: undefined }), + }, + }, + window: { + setTitle: () => Promise.resolve(undefined), + }, + update: { + check: () => Promise.resolve(undefined), + download: () => Promise.resolve(undefined), + install: () => undefined, + onStatus: () => () => undefined, + }, + }; + + // @ts-expect-error - Assigning mock API to window for Storybook + window.api = mockApi; + + // 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 ; + }, +}; From bba1c844848f28137ee9ae4b730e76d4f2d4419b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 20 Oct 2025 19:51:12 -0400 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=A4=96=20Fix=20storybook=20build=20an?= =?UTF-8?q?d=20refactor=20to=20use=20setupMockAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix .storybook/main.ts to use __dirname instead of import.meta.dirname for CJS compatibility - Refactor ActiveWorkspaceWithChat story to use setupMockAPI with apiOverrides instead of duplicating the entire mock API - Add apiOverrides parameter to setupMockAPI for story customization _Generated with `cmux`_ --- .storybook/main.ts | 5 +- src/App.stories.tsx | 118 +++++++++++++++++--------------------------- 2 files changed, 50 insertions(+), 73 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index fee6b7bc3..72776bb3e 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,6 +1,9 @@ import type { StorybookConfig } from "@storybook/react-vite"; import { mergeConfig } from "vite"; import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const config: StorybookConfig = { stories: ["../src/**/*.stories.@(ts|tsx)"], @@ -18,7 +21,7 @@ const config: StorybookConfig = { // Inherit project aliases resolve: { alias: { - "@": path.resolve(import.meta.dirname, "../src"), + "@": path.resolve(__dirname, "../src"), }, }, }); diff --git a/src/App.stories.tsx b/src/App.stories.tsx index 8e7d47ba7..b09d9606a 100644 --- a/src/App.stories.tsx +++ b/src/App.stories.tsx @@ -10,6 +10,7 @@ function setupMockAPI(options: { projects?: Map; workspaces?: FrontendWorkspaceMetadata[]; selectedWorkspaceId?: string; + apiOverrides?: Partial; }) { const mockProjects = options.projects ?? new Map(); const mockWorkspaces = options.workspaces ?? []; @@ -80,6 +81,7 @@ function setupMockAPI(options: { install: () => undefined, onStatus: () => () => undefined, }, + ...options.apiOverrides, }; // @ts-expect-error - Assigning mock API to window for Storybook @@ -333,40 +335,37 @@ export const ActiveWorkspaceWithChat: Story = { // Set up mock API only once per component instance (not on every render) const initialized = useRef(false); if (!initialized.current) { - const mockProjects = projects; - const mockWorkspaces = workspaces; - - const mockApi: IPCApi = { - dialog: { - selectDirectory: () => Promise.resolve(null), - }, - providers: { - setProviderConfig: () => Promise.resolve({ success: true, data: undefined }), - list: () => Promise.resolve(["anthropic", "openai"]), - }, - workspace: { - create: (projectPath: string, branchName: string) => - Promise.resolve({ - success: true, - metadata: { - id: `${projectPath.split("/").pop() ?? "project"}-${branchName}`, - name: branchName, - projectPath, - projectName: projectPath.split("/").pop() ?? "project", - namedWorkspacePath: `/mock/workspace/${branchName}`, - }, - }), - list: () => Promise.resolve(mockWorkspaces), - 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) => { - // Send chat history immediately when subscribed + setupMockAPI({ + projects, + workspaces, + apiOverrides: { + providers: { + setProviderConfig: () => Promise.resolve({ success: true, data: undefined }), + list: () => Promise.resolve(["anthropic", "openai"]), + }, + workspace: { + create: (projectPath: string, branchName: string) => + Promise.resolve({ + success: true, + metadata: { + id: `${projectPath.split("/").pop() ?? "project"}-${branchName}`, + 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) => { + // Send chat history immediately when subscribed setTimeout(() => { // User message callback({ @@ -583,46 +582,21 @@ export const ActiveWorkspaceWithChat: Story = { // 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 }, - }), - }, - projects: { - list: () => Promise.resolve(Array.from(mockProjects.entries())), - create: () => Promise.resolve({ success: true, data: { workspaces: [] } }), - remove: () => Promise.resolve({ success: true, data: undefined }), - listBranches: () => - Promise.resolve({ - branches: ["main", "develop", "feature/auth"], - recommendedTrunk: "main", - }), - secrets: { - get: () => Promise.resolve([]), - update: () => Promise.resolve({ success: true, data: undefined }), + 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 }, + }), }, }, - window: { - setTitle: () => Promise.resolve(undefined), - }, - update: { - check: () => Promise.resolve(undefined), - download: () => Promise.resolve(undefined), - install: () => undefined, - onStatus: () => () => undefined, - }, - }; - - // @ts-expect-error - Assigning mock API to window for Storybook - window.api = mockApi; + }); // Set initial workspace selection localStorage.setItem( From a04df653f82b3d145057ab6bf9201b51f99b6861 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 20 Oct 2025 19:53:46 -0400 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A4=96=20Fix=20formatting=20in=20App.?= =?UTF-8?q?stories.tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Generated with `cmux`_ --- src/App.stories.tsx | 398 ++++++++++++++++++++++---------------------- 1 file changed, 199 insertions(+), 199 deletions(-) diff --git a/src/App.stories.tsx b/src/App.stories.tsx index b09d9606a..96a2b7c0b 100644 --- a/src/App.stories.tsx +++ b/src/App.stories.tsx @@ -366,222 +366,222 @@ export const ActiveWorkspaceWithChat: Story = { openTerminal: () => Promise.resolve(undefined), onChat: (workspaceId, callback) => { // Send chat history immediately when subscribed - setTimeout(() => { - // User message - callback({ - id: "msg-1", - role: "user", - parts: [{ type: "text", text: "Add authentication to the user API endpoint" }], - metadata: { - historySequence: 1, - timestamp: Date.now() - 300000, - }, - }); - - // Assistant message with tool calls - callback({ - id: "msg-2", - role: "assistant", - parts: [ - { - type: "text", - text: "I'll help you add authentication to the user API endpoint. Let me first check the current implementation.", + setTimeout(() => { + // User message + callback({ + id: "msg-1", + role: "user", + parts: [{ type: "text", text: "Add authentication to the user API endpoint" }], + metadata: { + historySequence: 1, + timestamp: Date.now() - 300000, }, - { - type: "dynamic-tool", - toolCallId: "call-1", - toolName: "read_file", - state: "output-available", - input: { target_file: "src/api/users.ts" }, - output: { - success: true, - content: - "export function getUser(req, res) {\n const user = db.users.find(req.params.id);\n res.json(user);\n}", + }); + + // Assistant message with tool calls + callback({ + id: "msg-2", + role: "assistant", + parts: [ + { + type: "text", + text: "I'll help you add authentication to the user API endpoint. Let me first check the current implementation.", }, - }, - ], - metadata: { - historySequence: 2, - timestamp: Date.now() - 290000, - model: "claude-sonnet-4-20250514", - usage: { - inputTokens: 1250, - outputTokens: 450, - totalTokens: 1700, - }, - duration: 3500, - }, - }); - - // User response - callback({ - id: "msg-3", - role: "user", - parts: [{ type: "text", text: "Yes, add JWT token validation" }], - metadata: { - historySequence: 3, - timestamp: Date.now() - 280000, - }, - }); - - // Assistant message with file edit - callback({ - id: "msg-4", - role: "assistant", - parts: [ - { - type: "text", - text: "I'll add JWT token validation to the endpoint. Let me update the file.", - }, - { - type: "dynamic-tool", - toolCallId: "call-2", - toolName: "search_replace", - state: "output-available", - input: { - file_path: "src/api/users.ts", - old_string: "export function getUser(req, res) {", - new_string: - "import { verifyToken } from '../auth/jwt';\n\nexport function getUser(req, res) {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }", + { + type: "dynamic-tool", + toolCallId: "call-1", + toolName: "read_file", + state: "output-available", + input: { target_file: "src/api/users.ts" }, + output: { + success: true, + content: + "export function getUser(req, res) {\n const user = db.users.find(req.params.id);\n res.json(user);\n}", + }, }, - output: { - success: true, - message: "File updated successfully", + ], + metadata: { + historySequence: 2, + timestamp: Date.now() - 290000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 1250, + outputTokens: 450, + totalTokens: 1700, }, + duration: 3500, }, - ], - metadata: { - historySequence: 4, - timestamp: Date.now() - 270000, - model: "claude-sonnet-4-20250514", - usage: { - inputTokens: 2100, - outputTokens: 680, - totalTokens: 2780, - }, - duration: 4200, - }, - }); - - // User asking to run tests - callback({ - id: "msg-5", - role: "user", - parts: [{ type: "text", text: "Can you run the tests to make sure it works?" }], - metadata: { - historySequence: 5, - timestamp: Date.now() - 240000, - }, - }); - - // Assistant running tests - callback({ - id: "msg-6", - role: "assistant", - parts: [ - { - type: "text", - text: "I'll run the tests to verify the authentication is working correctly.", + }); + + // User response + callback({ + id: "msg-3", + role: "user", + parts: [{ type: "text", text: "Yes, add JWT token validation" }], + metadata: { + historySequence: 3, + timestamp: Date.now() - 280000, }, - { - type: "dynamic-tool", - toolCallId: "call-3", - toolName: "run_terminal_cmd", - state: "output-available", - input: { - command: "npm test src/api/users.test.ts", - explanation: "Running tests for the users API endpoint", + }); + + // Assistant message with file edit + callback({ + id: "msg-4", + role: "assistant", + parts: [ + { + type: "text", + text: "I'll add JWT token validation to the endpoint. Let me update the file.", }, - output: { - success: true, - stdout: - "PASS src/api/users.test.ts\n ✓ should return user when authenticated (24ms)\n ✓ should return 401 when no token (18ms)\n ✓ should return 401 when invalid token (15ms)\n\nTest Suites: 1 passed, 1 total\nTests: 3 passed, 3 total", - exitCode: 0, + { + type: "dynamic-tool", + toolCallId: "call-2", + toolName: "search_replace", + state: "output-available", + input: { + file_path: "src/api/users.ts", + old_string: "export function getUser(req, res) {", + new_string: + "import { verifyToken } from '../auth/jwt';\n\nexport function getUser(req, res) {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }", + }, + output: { + success: true, + message: "File updated successfully", + }, }, + ], + metadata: { + historySequence: 4, + timestamp: Date.now() - 270000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 2100, + outputTokens: 680, + totalTokens: 2780, + }, + duration: 4200, }, - ], - metadata: { - historySequence: 6, - timestamp: Date.now() - 230000, - model: "claude-sonnet-4-20250514", - usage: { - inputTokens: 2800, - outputTokens: 420, - totalTokens: 3220, - }, - duration: 5100, - }, - }); - - // User follow-up about error handling - callback({ - id: "msg-7", - role: "user", - parts: [ - { - type: "text", - text: "Great! What about error handling if the JWT library throws?", + }); + + // User asking to run tests + callback({ + id: "msg-5", + role: "user", + parts: [{ type: "text", text: "Can you run the tests to make sure it works?" }], + metadata: { + historySequence: 5, + timestamp: Date.now() - 240000, }, - ], - metadata: { - historySequence: 7, - timestamp: Date.now() - 180000, - }, - }); - - // Assistant response with thinking (reasoning) - callback({ - id: "msg-8", - role: "assistant", - parts: [ - { - type: "reasoning", - text: "The user is asking about error handling for JWT verification. The verifyToken function could throw if the token is malformed or if there's an issue with the secret. I should wrap it in a try-catch block and return a proper error response.", + }); + + // Assistant running tests + callback({ + id: "msg-6", + role: "assistant", + parts: [ + { + type: "text", + text: "I'll run the tests to verify the authentication is working correctly.", + }, + { + type: "dynamic-tool", + toolCallId: "call-3", + toolName: "run_terminal_cmd", + state: "output-available", + input: { + command: "npm test src/api/users.test.ts", + explanation: "Running tests for the users API endpoint", + }, + output: { + success: true, + stdout: + "PASS src/api/users.test.ts\n ✓ should return user when authenticated (24ms)\n ✓ should return 401 when no token (18ms)\n ✓ should return 401 when invalid token (15ms)\n\nTest Suites: 1 passed, 1 total\nTests: 3 passed, 3 total", + exitCode: 0, + }, + }, + ], + metadata: { + historySequence: 6, + timestamp: Date.now() - 230000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 2800, + outputTokens: 420, + totalTokens: 3220, + }, + duration: 5100, }, - { - type: "text", - text: "Good catch! We should add try-catch error handling around the JWT verification. Let me update that.", + }); + + // User follow-up about error handling + callback({ + id: "msg-7", + role: "user", + parts: [ + { + type: "text", + text: "Great! What about error handling if the JWT library throws?", + }, + ], + metadata: { + historySequence: 7, + timestamp: Date.now() - 180000, }, - { - type: "dynamic-tool", - toolCallId: "call-4", - toolName: "search_replace", - state: "output-available", - input: { - file_path: "src/api/users.ts", - old_string: - " const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }", - new_string: - " try {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }\n } catch (err) {\n console.error('Token verification failed:', err);\n return res.status(401).json({ error: 'Invalid token' });\n }", + }); + + // Assistant response with thinking (reasoning) + callback({ + id: "msg-8", + role: "assistant", + parts: [ + { + type: "reasoning", + text: "The user is asking about error handling for JWT verification. The verifyToken function could throw if the token is malformed or if there's an issue with the secret. I should wrap it in a try-catch block and return a proper error response.", }, - output: { - success: true, - message: "File updated successfully", + { + type: "text", + text: "Good catch! We should add try-catch error handling around the JWT verification. Let me update that.", }, + { + type: "dynamic-tool", + toolCallId: "call-4", + toolName: "search_replace", + state: "output-available", + input: { + file_path: "src/api/users.ts", + old_string: + " const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }", + new_string: + " try {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token || !verifyToken(token)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }\n } catch (err) {\n console.error('Token verification failed:', err);\n return res.status(401).json({ error: 'Invalid token' });\n }", + }, + output: { + success: true, + message: "File updated successfully", + }, + }, + ], + metadata: { + historySequence: 8, + timestamp: Date.now() - 170000, + model: "claude-sonnet-4-20250514", + usage: { + inputTokens: 3500, + outputTokens: 520, + totalTokens: 4020, + reasoningTokens: 150, + }, + duration: 6200, }, - ], - metadata: { - historySequence: 8, - timestamp: Date.now() - 170000, - model: "claude-sonnet-4-20250514", - usage: { - inputTokens: 3500, - outputTokens: 520, - totalTokens: 4020, - reasoningTokens: 150, - }, - duration: 6200, - }, - }); + }); - // Mark as caught up - callback({ type: "caught-up" }); - }, 100); + // Mark as caught up + callback({ type: "caught-up" }); + }, 100); - return () => { - // Cleanup - }; - }, + return () => { + // Cleanup + }; + }, onMetadata: () => () => undefined, sendMessage: () => Promise.resolve({ success: true, data: undefined }), resumeStream: () => Promise.resolve({ success: true, data: undefined }), From 10b01a549a2374eb66519b841076cb35942a2c2b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 20 Oct 2025 19:59:22 -0400 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=A4=96=20Use=20process.cwd()=20instea?= =?UTF-8?q?d=20of=20import.meta=20for=20path=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using process.cwd() avoids ES module vs CJS conflicts in Storybook's transpilation process. _Generated with `cmux`_ --- .storybook/main.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 72776bb3e..db423e756 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,9 +1,6 @@ import type { StorybookConfig } from "@storybook/react-vite"; import { mergeConfig } from "vite"; import path from "path"; -import { fileURLToPath } from "url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); const config: StorybookConfig = { stories: ["../src/**/*.stories.@(ts|tsx)"], @@ -21,7 +18,7 @@ const config: StorybookConfig = { // Inherit project aliases resolve: { alias: { - "@": path.resolve(__dirname, "../src"), + "@": path.join(process.cwd(), "src"), }, }, }); From fab2d6eabd887fd03ec8b9a4ff37096173f7255a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 20 Oct 2025 20:03:52 -0400 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=A4=96=20Add=20src/version.ts=20depen?= =?UTF-8?q?dency=20to=20storybook-build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensures version.ts is generated before building Storybook, fixing CI build failures. _Generated with `cmux`_ --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ccb698f0e..ebe9cbb23 100644 --- a/Makefile +++ b/Makefile @@ -245,7 +245,7 @@ docs-watch: ## Watch and rebuild documentation storybook: node_modules/.installed ## Start Storybook development server @bun x storybook dev -p 6006 -storybook-build: node_modules/.installed ## Build static Storybook +storybook-build: node_modules/.installed src/version.ts ## Build static Storybook @bun x storybook build test-storybook: node_modules/.installed ## Run Storybook interaction tests (requires Storybook to be running or built)