Skip to content

Commit ee349fa

Browse files
committed
🤖 fix: resolve storybook visual regression from ORPC migration
Stories were showing empty screens with welcome messages because story-defined project/workspace data never reached React components. Root cause: The ORPC migration (ca8a504) created a dual mock system where: 1. Stories set window.api via installMockAPI() (now ignored by ORPC) 2. AppLoader's inner APIProvider shadowed the global decorator 3. Inner provider created broken MessageChannel client in Storybook Fix: Update stories to pass properly configured ORPC client to AppLoader: - Add providersConfig/providersList to createMockORPCClient - Remove global APIProvider decorator from preview.tsx - Update AppWithMocks to accept setup returning APIClient - Update all story helpers to return APIClient instead of void - Remove dead code: createMockAPI() and installMockAPI()
1 parent ca8a504 commit ee349fa

13 files changed

+267
-362
lines changed

.storybook/mocks/orpc.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export interface MockORPCClientOptions {
2121
workspaceId: string,
2222
script: string
2323
) => Promise<{ success: true; output: string; exitCode: number; wall_duration_ms: number }>;
24+
/** Provider configuration (API keys, base URLs, etc.) */
25+
providersConfig?: Record<string, { apiKeySet: boolean; baseUrl?: string; models?: string[] }>;
26+
/** List of available provider names */
27+
providersList?: string[];
2428
}
2529

2630
/**
@@ -41,7 +45,14 @@ export interface MockORPCClientOptions {
4145
* ```
4246
*/
4347
export function createMockORPCClient(options: MockORPCClientOptions = {}): APIClient {
44-
const { projects = new Map(), workspaces = [], onChat, executeBash } = options;
48+
const {
49+
projects = new Map(),
50+
workspaces = [],
51+
onChat,
52+
executeBash,
53+
providersConfig = {},
54+
providersList = [],
55+
} = options;
4556

4657
const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));
4758

@@ -65,8 +76,8 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
6576
getLaunchProject: async () => null,
6677
},
6778
providers: {
68-
list: async () => [],
69-
getConfig: async () => ({}),
79+
list: async () => providersList,
80+
getConfig: async () => providersConfig,
7081
setProviderConfig: async () => ({ success: true, data: undefined }),
7182
setModels: async () => ({ success: true, data: undefined }),
7283
},

.storybook/preview.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import React, { useMemo } from "react";
1+
import React from "react";
22
import type { Preview } from "@storybook/react-vite";
33
import { ThemeProvider, type ThemeMode } from "../src/browser/contexts/ThemeContext";
4-
import { APIProvider } from "../src/browser/contexts/API";
5-
import { createMockORPCClient } from "./mocks/orpc";
64
import "../src/browser/styles/globals.css";
75
import { TUTORIAL_STATE_KEY, type TutorialState } from "../src/common/constants/storage";
86

@@ -37,15 +35,6 @@ const preview: Preview = {
3735
theme: "dark",
3836
},
3937
decorators: [
40-
// Global ORPC provider - ensures useORPC works in all stories
41-
(Story) => {
42-
const client = useMemo(() => createMockORPCClient(), []);
43-
return (
44-
<APIProvider client={client}>
45-
<Story />
46-
</APIProvider>
47-
);
48-
},
4938
// Theme provider
5039
(Story, context) => {
5140
// Default to dark if mode not set (e.g., Chromatic headless browser defaults to light)

src/browser/stories/App.chat.stories.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default {
2323
export const Conversation: AppStory = {
2424
render: () => (
2525
<AppWithMocks
26-
setup={() => {
26+
setup={() =>
2727
setupSimpleChatStory({
2828
messages: [
2929
createUserMessage("msg-1", "Add authentication to the user API endpoint", {
@@ -74,8 +74,8 @@ export const Conversation: AppStory = {
7474
],
7575
}),
7676
],
77-
});
78-
}}
77+
})
78+
}
7979
/>
8080
),
8181
};
@@ -84,7 +84,7 @@ export const Conversation: AppStory = {
8484
export const WithReasoning: AppStory = {
8585
render: () => (
8686
<AppWithMocks
87-
setup={() => {
87+
setup={() =>
8888
setupSimpleChatStory({
8989
workspaceId: "ws-reasoning",
9090
messages: [
@@ -112,8 +112,8 @@ export const WithReasoning: AppStory = {
112112
}
113113
),
114114
],
115-
});
116-
}}
115+
})
116+
}
117117
/>
118118
),
119119
};
@@ -122,7 +122,7 @@ export const WithReasoning: AppStory = {
122122
export const WithTerminal: AppStory = {
123123
render: () => (
124124
<AppWithMocks
125-
setup={() => {
125+
setup={() =>
126126
setupSimpleChatStory({
127127
workspaceId: "ws-terminal",
128128
messages: [
@@ -171,8 +171,8 @@ export const WithTerminal: AppStory = {
171171
],
172172
}),
173173
],
174-
});
175-
}}
174+
})
175+
}
176176
/>
177177
),
178178
};
@@ -181,7 +181,7 @@ export const WithTerminal: AppStory = {
181181
export const WithAgentStatus: AppStory = {
182182
render: () => (
183183
<AppWithMocks
184-
setup={() => {
184+
setup={() =>
185185
setupSimpleChatStory({
186186
workspaceId: "ws-status",
187187
messages: [
@@ -206,8 +206,8 @@ export const WithAgentStatus: AppStory = {
206206
}
207207
),
208208
],
209-
});
210-
}}
209+
})
210+
}
211211
/>
212212
),
213213
};
@@ -216,16 +216,16 @@ export const WithAgentStatus: AppStory = {
216216
export const VoiceInputNoApiKey: AppStory = {
217217
render: () => (
218218
<AppWithMocks
219-
setup={() => {
219+
setup={() =>
220220
setupSimpleChatStory({
221221
messages: [],
222222
// No OpenAI key configured - voice button should be disabled with tooltip
223223
providersConfig: {
224224
anthropic: { apiKeySet: true },
225225
// openai deliberately missing
226226
},
227-
});
228-
}}
227+
})
228+
}
229229
/>
230230
),
231231
parameters: {
@@ -242,7 +242,7 @@ export const VoiceInputNoApiKey: AppStory = {
242242
export const Streaming: AppStory = {
243243
render: () => (
244244
<AppWithMocks
245-
setup={() => {
245+
setup={() =>
246246
setupStreamingChatStory({
247247
messages: [
248248
createUserMessage("msg-1", "Refactor the database connection to use pooling", {
@@ -259,8 +259,8 @@ export const Streaming: AppStory = {
259259
args: { target_file: "src/db/connection.ts" },
260260
},
261261
gitStatus: { dirty: 1 },
262-
});
263-
}}
262+
})
263+
}
264264
/>
265265
),
266266
};

src/browser/stories/App.demo.stories.tsx

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,51 @@ import {
1515
createFileEditTool,
1616
createTerminalTool,
1717
createStatusTool,
18-
createMockAPI,
19-
installMockAPI,
2018
createStaticChatHandler,
2119
createStreamingChatHandler,
20+
createGitStatusOutput,
2221
type GitStatusFixture,
2322
} from "./mockFactory";
2423
import { selectWorkspace, setWorkspaceInput, setWorkspaceModel } from "./storyHelpers";
24+
import { createMockORPCClient } from "../../../.storybook/mocks/orpc";
25+
import type { WorkspaceChatMessage } from "@/common/orpc/types";
2526

2627
export default {
2728
...appMeta,
2829
title: "App/Demo",
2930
};
3031

32+
type ChatHandler = (callback: (event: WorkspaceChatMessage) => void) => () => void;
33+
34+
/** Adapts callback-based chat handlers to ORPC onChat format */
35+
function createOnChatAdapter(chatHandlers: Map<string, ChatHandler>) {
36+
return (workspaceId: string, emit: (msg: WorkspaceChatMessage) => void) => {
37+
const handler = chatHandlers.get(workspaceId);
38+
if (handler) {
39+
return handler(emit);
40+
}
41+
queueMicrotask(() => emit({ type: "caught-up" }));
42+
return undefined;
43+
};
44+
}
45+
46+
/** Creates an executeBash function that returns git status output for workspaces */
47+
function createGitStatusExecutor(gitStatus: Map<string, GitStatusFixture>) {
48+
return (workspaceId: string, script: string) => {
49+
if (script.includes("git status") || script.includes("git show-branch")) {
50+
const status = gitStatus.get(workspaceId) ?? {};
51+
const output = createGitStatusOutput(status);
52+
return Promise.resolve({ success: true as const, output, exitCode: 0, wall_duration_ms: 50 });
53+
}
54+
return Promise.resolve({
55+
success: true as const,
56+
output: "",
57+
exitCode: 0,
58+
wall_duration_ms: 0,
59+
});
60+
};
61+
}
62+
3163
/**
3264
* Comprehensive story showing all sidebar indicators and chat features.
3365
*
@@ -181,7 +213,7 @@ export const Comprehensive: AppStory = {
181213
}),
182214
];
183215

184-
const chatHandlers = new Map([
216+
const chatHandlers = new Map<string, ChatHandler>([
185217
[activeWorkspaceId, createStaticChatHandler(activeMessages)],
186218
[
187219
streamingWorkspaceId,
@@ -208,19 +240,17 @@ export const Comprehensive: AppStory = {
208240
["ws-ssh", { ahead: 1, headCommit: "Production deploy" }],
209241
]);
210242

211-
installMockAPI(
212-
createMockAPI({
213-
projects: groupWorkspacesByProject(workspaces),
214-
workspaces,
215-
chatHandlers,
216-
gitStatus,
217-
providersList: ["anthropic", "openai", "xai"],
218-
})
219-
);
220-
221243
selectWorkspace(workspaces[0]);
222244
setWorkspaceInput(activeWorkspaceId, "Add OAuth2 support with Google and GitHub");
223245
setWorkspaceModel(activeWorkspaceId, "anthropic:claude-sonnet-4-5");
246+
247+
return createMockORPCClient({
248+
projects: groupWorkspacesByProject(workspaces),
249+
workspaces,
250+
onChat: createOnChatAdapter(chatHandlers),
251+
executeBash: createGitStatusExecutor(gitStatus),
252+
providersList: ["anthropic", "openai", "xai"],
253+
});
224254
}}
225255
/>
226256
),

src/browser/stories/App.errors.stories.tsx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import {
1313
createAssistantMessage,
1414
createFileEditTool,
1515
createStaticChatHandler,
16-
createMockAPI,
17-
installMockAPI,
1816
} from "./mockFactory";
1917
import { selectWorkspace, setupSimpleChatStory, setupCustomChatStory } from "./storyHelpers";
18+
import { createMockORPCClient } from "../../../.storybook/mocks/orpc";
2019

2120
export default {
2221
...appMeta,
@@ -100,7 +99,7 @@ const LARGE_DIFF = [
10099
export const StreamError: AppStory = {
101100
render: () => (
102101
<AppWithMocks
103-
setup={() => {
102+
setup={() =>
104103
setupCustomChatStory({
105104
workspaceId: "ws-error",
106105
chatHandler: (callback: (event: WorkspaceChatMessage) => void) => {
@@ -124,8 +123,8 @@ export const StreamError: AppStory = {
124123
// eslint-disable-next-line @typescript-eslint/no-empty-function
125124
return () => {};
126125
},
127-
});
128-
}}
126+
})
127+
}
129128
/>
130129
),
131130
};
@@ -164,7 +163,7 @@ export const HiddenHistory: AppStory = {
164163
),
165164
];
166165

167-
setupCustomChatStory({
166+
return setupCustomChatStory({
168167
workspaceId: "ws-history",
169168
chatHandler: createStaticChatHandler(messages),
170169
});
@@ -193,15 +192,13 @@ export const IncompatibleWorkspace: AppStory = {
193192
}),
194193
];
195194

196-
installMockAPI(
197-
createMockAPI({
198-
projects: groupWorkspacesByProject(workspaces),
199-
workspaces,
200-
})
201-
);
202-
203195
// Select the incompatible workspace
204196
selectWorkspace(workspaces[1]);
197+
198+
return createMockORPCClient({
199+
projects: groupWorkspacesByProject(workspaces),
200+
workspaces,
201+
});
205202
}}
206203
/>
207204
),
@@ -211,7 +208,7 @@ export const IncompatibleWorkspace: AppStory = {
211208
export const LargeDiff: AppStory = {
212209
render: () => (
213210
<AppWithMocks
214-
setup={() => {
211+
setup={() =>
215212
setupSimpleChatStory({
216213
workspaceId: "ws-diff",
217214
messages: [
@@ -233,8 +230,8 @@ export const LargeDiff: AppStory = {
233230
}
234231
),
235232
],
236-
});
237-
}}
233+
})
234+
}
238235
/>
239236
),
240237
};

src/browser/stories/App.markdown.stories.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe('getUser', () => {
8181
export const Tables: AppStory = {
8282
render: () => (
8383
<AppWithMocks
84-
setup={() => {
84+
setup={() =>
8585
setupSimpleChatStory({
8686
workspaceId: "ws-tables",
8787
messages: [
@@ -94,8 +94,8 @@ export const Tables: AppStory = {
9494
timestamp: STABLE_TIMESTAMP - 90000,
9595
}),
9696
],
97-
});
98-
}}
97+
})
98+
}
9999
/>
100100
),
101101
};
@@ -104,7 +104,7 @@ export const Tables: AppStory = {
104104
export const CodeBlocks: AppStory = {
105105
render: () => (
106106
<AppWithMocks
107-
setup={() => {
107+
setup={() =>
108108
setupSimpleChatStory({
109109
workspaceId: "ws-code",
110110
messages: [
@@ -117,8 +117,8 @@ export const CodeBlocks: AppStory = {
117117
timestamp: STABLE_TIMESTAMP - 90000,
118118
}),
119119
],
120-
});
121-
}}
120+
})
121+
}
122122
/>
123123
),
124124
};

0 commit comments

Comments
 (0)