authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-04 12:49:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 10:20:34-08:00
log4ca4a1f99c346456bd9434e9ae4e77f31b2c7aaf
tree4af437e54f9e7f86c7cce2a478a8c659eca66a21
parent5470801c4aecca55ea426be9b110bc879266be32

Migrate ChatView height browser tests to Vitest + Playwright

- Replace Playwright e2e timeline-height test setup with Vitest browser config - Add `ChatView.browser.tsx` parity tests for text wrapping and attachment height estimation - Add row data attributes in `ChatView` to support in-browser measurement targeting

8 files changed, 533 insertions(+), 169 deletions(-)

.gitignore+1
...@@ -14,3 +14,4 @@ release/...@@ -14,3 +14,4 @@ release/
14.t314.t3
15apps/web/.playwright15apps/web/.playwright
16apps/web/playwright-report16apps/web/playwright-report
17apps/web/src/components/__screenshots__
apps/web/browser-tests/timelineHeight.browser.e2e.ts deleted-148
...@@ -1,148 +0,0 @@
1import { expect, test, type Page } from "@playwright/test";
2
3import { estimateTimelineMessageHeight } from "../src/components/timelineHeight";
4
5interface HeightCase {
6 timelineWidthPx: number;
7 text: string;
8 attachmentCount: number;
9}
10
11async function measureUserRowHeight(page: Page, testCase: HeightCase) {
12 const { timelineWidthPx, text, attachmentCount } = testCase;
13 await page.setContent("<!doctype html><html><body style='margin:0'></body></html>");
14
15 return page.evaluate(({ width, messageText, attachments }) => {
16 const timeline = document.createElement("div");
17 timeline.style.width = `${width}px`;
18 timeline.style.maxWidth = `${width}px`;
19
20 const row = document.createElement("div");
21 row.style.paddingBottom = "16px";
22
23 const alignment = document.createElement("div");
24 alignment.style.display = "flex";
25 alignment.style.justifyContent = "flex-end";
26
27 const bubble = document.createElement("div");
28 bubble.style.boxSizing = "border-box";
29 bubble.style.maxWidth = "80%";
30 bubble.style.padding = "12px 16px";
31 bubble.style.border = "1px solid rgba(0, 0, 0, 0.12)";
32 bubble.style.borderRadius = "16px";
33 bubble.style.background = "rgba(0, 0, 0, 0.03)";
34
35 if (attachments > 0) {
36 const attachmentGrid = document.createElement("div");
37 attachmentGrid.style.marginBottom = "8px";
38 attachmentGrid.style.maxWidth = "420px";
39 attachmentGrid.style.display = "grid";
40 attachmentGrid.style.gridTemplateColumns = "repeat(2, minmax(0, 1fr))";
41 attachmentGrid.style.gap = "8px";
42 for (let index = 0; index < attachments; index += 1) {
43 const tile = document.createElement("div");
44 tile.style.height = "220px";
45 tile.style.border = "1px solid rgba(0, 0, 0, 0.12)";
46 tile.style.borderRadius = "8px";
47 tile.style.background = "rgba(0, 0, 0, 0.06)";
48 attachmentGrid.append(tile);
49 }
50 bubble.append(attachmentGrid);
51 }
52
53 if (messageText.length > 0) {
54 const pre = document.createElement("pre");
55 pre.textContent = messageText;
56 pre.style.margin = "0";
57 pre.style.whiteSpace = "pre-wrap";
58 pre.style.overflowWrap = "anywhere";
59 pre.style.fontFamily =
60 "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace";
61 pre.style.fontSize = "14px";
62 pre.style.lineHeight = "22px";
63 bubble.append(pre);
64 }
65
66 const meta = document.createElement("div");
67 meta.style.marginTop = "6px";
68 meta.style.height = "16px";
69 bubble.append(meta);
70
71 alignment.append(bubble);
72 row.append(alignment);
73 timeline.append(row);
74 document.body.append(timeline);
75 return row.getBoundingClientRect().height;
76 }, { width: timelineWidthPx, messageText: text, attachments: attachmentCount });
77}
78
79function estimatedHeight(testCase: HeightCase): number {
80 return estimateTimelineMessageHeight(
81 {
82 role: "user",
83 text: testCase.text,
84 attachments: Array.from({ length: testCase.attachmentCount }, (_value, index) => ({
85 id: String(index),
86 })),
87 },
88 { timelineWidthPx: testCase.timelineWidthPx },
89 );
90}
91
92test.describe("timeline height estimator parity", () => {
93 test("tracks long wrapped text growth at desktop width", async ({ page }) => {
94 const baselineCase: HeightCase = { timelineWidthPx: 960, text: "", attachmentCount: 0 };
95 const longCase: HeightCase = {
96 timelineWidthPx: 960,
97 text: "x".repeat(1200),
98 attachmentCount: 0,
99 };
100
101 const baselineMeasured = await measureUserRowHeight(page, baselineCase);
102 const longMeasured = await measureUserRowHeight(page, longCase);
103 const measuredDelta = longMeasured - baselineMeasured;
104
105 const estimatedDelta = estimatedHeight(longCase) - estimatedHeight(baselineCase);
106 expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(22);
107 });
108
109 test("tracks additional wrapping when viewport narrows", async ({ page }) => {
110 const desktopCase: HeightCase = {
111 timelineWidthPx: 960,
112 text: "x".repeat(1000),
113 attachmentCount: 0,
114 };
115 const mobileCase: HeightCase = {
116 timelineWidthPx: 360,
117 text: desktopCase.text,
118 attachmentCount: 0,
119 };
120
121 const desktopMeasured = await measureUserRowHeight(page, desktopCase);
122 const mobileMeasured = await measureUserRowHeight(page, mobileCase);
123 const measuredDelta = mobileMeasured - desktopMeasured;
124
125 const estimatedDelta = estimatedHeight(mobileCase) - estimatedHeight(desktopCase);
126 expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(44);
127 });
128
129 test("tracks attachment row growth", async ({ page }) => {
130 const withoutAttachmentsCase: HeightCase = {
131 timelineWidthPx: 960,
132 text: "hello",
133 attachmentCount: 0,
134 };
135 const withAttachmentsCase: HeightCase = {
136 ...withoutAttachmentsCase,
137 attachmentCount: 3,
138 };
139
140 const withoutMeasured = await measureUserRowHeight(page, withoutAttachmentsCase);
141 const withMeasured = await measureUserRowHeight(page, withAttachmentsCase);
142 const measuredDelta = withMeasured - withoutMeasured;
143
144 const estimatedDelta =
145 estimatedHeight(withAttachmentsCase) - estimatedHeight(withoutAttachmentsCase);
146 expect(Math.abs(measuredDelta - estimatedDelta)).toBeLessThanOrEqual(4);
147 });
148});
apps/web/package.json+4-3
...@@ -10,8 +10,8 @@...@@ -10,8 +10,8 @@
10 "preview": "vite preview",10 "preview": "vite preview",
11 "typecheck": "tsc --noEmit",11 "typecheck": "tsc --noEmit",
12 "test": "vitest run --passWithNoTests",12 "test": "vitest run --passWithNoTests",
13 "test:browser": "playwright test -c playwright.config.ts",13 "test:browser": "vitest run --config vitest.browser.config.ts",
14 "test:browser:install": "playwright install chromium"14 "test:browser:install": "playwright install --with-deps chromium"
15 },15 },
16 "dependencies": {16 "dependencies": {
17 "@base-ui/react": "^1.2.0",17 "@base-ui/react": "^1.2.0",
...@@ -38,13 +38,14 @@...@@ -38,13 +38,14 @@
38 },38 },
39 "devDependencies": {39 "devDependencies": {
40 "@effect/language-service": "catalog:",40 "@effect/language-service": "catalog:",
41 "@playwright/test": "^1.58.2",
42 "@tailwindcss/vite": "^4.0.0",41 "@tailwindcss/vite": "^4.0.0",
43 "@tanstack/router-plugin": "^1.161.0",42 "@tanstack/router-plugin": "^1.161.0",
44 "@types/react": "^19.0.0",43 "@types/react": "^19.0.0",
45 "@types/react-dom": "^19.0.0",44 "@types/react-dom": "^19.0.0",
46 "@vitejs/plugin-react": "^5.1.4",45 "@vitejs/plugin-react": "^5.1.4",
46 "@vitest/browser-playwright": "^4.0.18",
47 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",47 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
48 "playwright": "^1.58.2",
48 "tailwindcss": "^4.0.0",49 "tailwindcss": "^4.0.0",
49 "typescript": "catalog:",50 "typescript": "catalog:",
50 "vite": "^8.0.0-beta.12",51 "vite": "^8.0.0-beta.12",
apps/web/playwright.config.ts deleted-12
...@@ -1,12 +0,0 @@
1import { defineConfig } from "@playwright/test";
2
3export default defineConfig({
4 testDir: "./browser-tests",
5 testMatch: "*.browser.e2e.ts",
6 reporter: "list",
7 fullyParallel: true,
8 outputDir: "./.playwright/test-results",
9 use: {
10 headless: true,
11 },
12});
apps/web/src/components/ChatView.browser.tsx created+461
...@@ -0,0 +1,461 @@
1import "../index.css";
2
3import { type MessageId, type ThreadId } from "@t3tools/contracts";
4import { createRoot, type Root } from "react-dom/client";
5import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
7import type { ChatMessage, Thread } from "../types";
8import ChatView from "./ChatView";
9import { estimateTimelineMessageHeight } from "./timelineHeight";
10
11const mocks = vi.hoisted(() => {
12 return {
13 navigate: vi.fn(),
14 markThreadVisited: vi.fn(),
15 setThreadError: vi.fn(),
16 setRuntimeMode: vi.fn(),
17 setThreadBranch: vi.fn(),
18 storeState: {
19 projects: [] as unknown[],
20 threads: [] as Thread[],
21 runtimeMode: "full-access",
22 markThreadVisited: vi.fn(),
23 setError: vi.fn(),
24 setRuntimeMode: vi.fn(),
25 setThreadBranch: vi.fn(),
26 },
27 composerDraft: {
28 prompt: "",
29 images: [] as unknown[],
30 nonPersistedImageIds: [] as string[],
31 model: null,
32 effort: null,
33 },
34 composerStore: {
35 draftsByThreadId: {},
36 draftThreadsByThreadId: {},
37 setPrompt: vi.fn(),
38 setModel: vi.fn(),
39 setEffort: vi.fn(),
40 addImage: vi.fn(),
41 addImages: vi.fn(),
42 removeImage: vi.fn(),
43 clearPersistedAttachments: vi.fn(),
44 syncPersistedAttachments: vi.fn(),
45 clearComposerContent: vi.fn(),
46 clearDraftThread: vi.fn(),
47 setDraftThreadContext: vi.fn(),
48 },
49 terminalStore: {
50 terminalStateByThreadId: {} as Record<string, unknown>,
51 setTerminalOpen: vi.fn(),
52 setTerminalHeight: vi.fn(),
53 splitTerminal: vi.fn(),
54 newTerminal: vi.fn(),
55 setActiveTerminal: vi.fn(),
56 closeTerminal: vi.fn(),
57 },
58 terminalState: {
59 terminalOpen: false,
60 terminalHeight: 280,
61 terminalIds: ["default"],
62 runningTerminalIds: [],
63 activeTerminalId: "default",
64 terminalGroups: [{ id: "group-default", terminalIds: ["default"] }],
65 activeTerminalGroupId: "group-default",
66 },
67 };
68});
69
70vi.mock("@tanstack/react-router", () => ({
71 useNavigate: () => mocks.navigate,
72 useSearch: (options?: { select?: (params: Record<string, unknown>) => unknown }) =>
73 options?.select ? options.select({}) : {},
74}));
75
76vi.mock("@tanstack/react-query", async () => {
77 const actual = await vi.importActual<typeof import("@tanstack/react-query")>(
78 "@tanstack/react-query",
79 );
80 return {
81 ...actual,
82 useQueryClient: () => ({}),
83 useMutation: () => ({
84 mutateAsync: vi.fn(async () => ({})),
85 isPending: false,
86 }),
87 useQuery: () => ({
88 data: undefined,
89 isLoading: false,
90 error: null,
91 }),
92 };
93});
94
95vi.mock("../store", () => ({
96 useStore: (selector: (store: typeof mocks.storeState) => unknown) => selector(mocks.storeState),
97}));
98
99vi.mock("../composerDraftStore", () => ({
100 useComposerThreadDraft: () => mocks.composerDraft,
101 useComposerDraftStore: (selector: (store: typeof mocks.composerStore) => unknown) =>
102 selector(mocks.composerStore),
103}));
104
105vi.mock("../terminalStateStore", () => ({
106 useTerminalStateStore: (selector: (store: typeof mocks.terminalStore) => unknown) =>
107 selector(mocks.terminalStore),
108 selectThreadTerminalState: () => mocks.terminalState,
109}));
110
111vi.mock("../hooks/useTurnDiffSummaries", () => ({
112 useTurnDiffSummaries: () => ({
113 turnDiffSummaries: [],
114 inferredCheckpointTurnCountByTurnId: {},
115 }),
116}));
117
118vi.mock("../hooks/useTheme", () => ({
119 useTheme: () => ({ resolvedTheme: "light" as const }),
120}));
121
122vi.mock("../nativeApi", () => ({
123 readNativeApi: () => null,
124 ensureNativeApi: () => {
125 throw new Error("Native API unavailable in browser test");
126 },
127}));
128
129vi.mock("./BranchToolbar", () => ({
130 default: () => null,
131}));
132
133vi.mock("./GitActionsControl", () => ({
134 default: () => null,
135}));
136
137vi.mock("./ProjectScriptsControl", () => ({
138 default: () => null,
139}));
140
141vi.mock("./ThreadTerminalDrawer", () => ({
142 default: () => null,
143}));
144
145vi.mock("./ComposerPromptEditor", () => ({
146 ComposerPromptEditor: () => null,
147}));
148
149vi.mock("./ui/sidebar", () => ({
150 SidebarTrigger: () => null,
151}));
152
153const THREAD_ID = "thread-browser-test" as ThreadId;
154const NOW_ISO = "2026-03-04T12:00:00.000Z";
155const BASE_TIME_MS = Date.parse(NOW_ISO);
156const TALL_IMAGE_DATA_URI = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
157 "<svg xmlns='http://www.w3.org/2000/svg' width='120' height='300'></svg>",
158)}`;
159
160interface RenderMeasureOptions {
161 timelineWidthPx: number;
162 messages: ChatMessage[];
163 targetMessageId: MessageId;
164}
165
166function createThread(messages: ChatMessage[]): Thread {
167 return {
168 id: THREAD_ID,
169 codexThreadId: null,
170 projectId: "project-1" as Thread["projectId"],
171 title: "Browser test thread",
172 model: "gpt-5",
173 session: null,
174 messages,
175 error: null,
176 createdAt: NOW_ISO,
177 latestTurn: null,
178 lastVisitedAt: NOW_ISO,
179 branch: null,
180 worktreePath: null,
181 turnDiffSummaries: [],
182 activities: [],
183 };
184}
185
186function isoAt(offsetSeconds: number): string {
187 return new Date(BASE_TIME_MS + offsetSeconds * 1_000).toISOString();
188}
189
190function createUserMessage({
191 id,
192 text,
193 offsetSeconds,
194 attachments,
195}: {
196 id: MessageId;
197 text: string;
198 offsetSeconds: number;
199 attachments?: ChatMessage["attachments"];
200}): ChatMessage {
201 return {
202 id,
203 role: "user",
204 text,
205 ...(attachments && attachments.length > 0 ? { attachments } : {}),
206 createdAt: isoAt(offsetSeconds),
207 completedAt: isoAt(offsetSeconds + 1),
208 streaming: false,
209 };
210}
211
212function createAssistantMessage({
213 id,
214 text,
215 offsetSeconds,
216}: {
217 id: MessageId;
218 text: string;
219 offsetSeconds: number;
220}): ChatMessage {
221 return {
222 id,
223 role: "assistant",
224 text,
225 createdAt: isoAt(offsetSeconds),
226 completedAt: isoAt(offsetSeconds + 1),
227 streaming: false,
228 };
229}
230
231function createImageAttachments(count: number): NonNullable<ChatMessage["attachments"]> {
232 return Array.from({ length: count }, (_, index) => ({
233 type: "image" as const,
234 id: `attachment-${index + 1}`,
235 name: `attachment-${index + 1}.svg`,
236 mimeType: "image/svg+xml",
237 sizeBytes: 128,
238 previewUrl: TALL_IMAGE_DATA_URI,
239 }));
240}
241
242function createConversationWithTargetUser(options: {
243 targetMessageId: MessageId;
244 targetText: string;
245 targetAttachments?: ChatMessage["attachments"];
246}): ChatMessage[] {
247 const messages: ChatMessage[] = [];
248 for (let index = 0; index < 22; index += 1) {
249 const userId = (`msg-user-${index}` as MessageId);
250 const assistantId = (`msg-assistant-${index}` as MessageId);
251 const isTarget = index === 3;
252 messages.push(
253 createUserMessage({
254 id: isTarget ? options.targetMessageId : userId,
255 text: isTarget ? options.targetText : `filler user message ${index}`,
256 offsetSeconds: messages.length * 3,
257 attachments: isTarget ? options.targetAttachments : undefined,
258 }),
259 );
260 messages.push(
261 createAssistantMessage({
262 id: assistantId,
263 text: `assistant filler ${index}`,
264 offsetSeconds: messages.length * 3,
265 }),
266 );
267 }
268 return messages;
269}
270
271async function nextFrame(): Promise<void> {
272 await new Promise<void>((resolve) => {
273 window.requestAnimationFrame(() => resolve());
274 });
275}
276
277async function waitForLayout(): Promise<void> {
278 await nextFrame();
279 await nextFrame();
280 await nextFrame();
281}
282
283async function waitForImagesToLoad(scope: ParentNode): Promise<void> {
284 const images = Array.from(scope.querySelectorAll("img"));
285 await Promise.all(
286 images.map(
287 (image) =>
288 new Promise<void>((resolve) => {
289 if (image.complete) {
290 resolve();
291 return;
292 }
293 image.addEventListener("load", () => resolve(), { once: true });
294 image.addEventListener("error", () => resolve(), { once: true });
295 }),
296 ),
297 );
298 await waitForLayout();
299}
300
301async function renderAndMeasureUserRow({
302 timelineWidthPx,
303 messages,
304 targetMessageId,
305}: RenderMeasureOptions): Promise<{
306 measuredRowHeightPx: number;
307 timelineWidthMeasuredPx: number;
308 renderedInVirtualizedRegion: boolean;
309}> {
310 const host = document.createElement("div");
311 host.style.width = `${timelineWidthPx}px`;
312 host.style.height = "920px";
313 host.style.display = "flex";
314 host.style.overflow = "hidden";
315 document.body.append(host);
316
317 mocks.storeState.threads = [createThread(messages)];
318
319 const root: Root = createRoot(host);
320 root.render(<ChatView threadId={THREAD_ID} />);
321 await waitForLayout();
322
323 const scrollContainer = host.querySelector("div.overflow-y-auto.overscroll-y-contain");
324 if (!(scrollContainer instanceof HTMLDivElement)) {
325 root.unmount();
326 throw new Error("Unable to find ChatView message scroll container.");
327 }
328 scrollContainer.scrollTop = 0;
329 scrollContainer.dispatchEvent(new Event("scroll"));
330 await waitForLayout();
331
332 const row = host.querySelector<HTMLElement>(
333 `[data-message-id="${targetMessageId}"][data-message-role="user"]`,
334 );
335 if (!(row instanceof HTMLElement)) {
336 root.unmount();
337 throw new Error("Unable to locate targeted user message row.");
338 }
339 await waitForImagesToLoad(row);
340
341 const timelineRoot = row.closest("div.max-w-3xl");
342 if (!(timelineRoot instanceof HTMLElement)) {
343 root.unmount();
344 throw new Error("Unable to locate timeline root container.");
345 }
346
347 const measuredRowHeightPx = row.getBoundingClientRect().height;
348 const timelineWidthMeasuredPx = timelineRoot.getBoundingClientRect().width;
349 const renderedInVirtualizedRegion = row.closest("[data-index]") instanceof HTMLElement;
350
351 root.unmount();
352 host.remove();
353
354 return { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion };
355}
356
357describe("ChatView timeline estimator parity", () => {
358 beforeEach(() => {
359 document.body.innerHTML = "";
360 mocks.storeState.projects = [];
361 mocks.storeState.threads = [];
362 mocks.storeState.runtimeMode = "full-access";
363 mocks.composerDraft.prompt = "";
364 mocks.composerDraft.images = [];
365 mocks.composerDraft.nonPersistedImageIds = [];
366 mocks.composerDraft.model = null;
367 mocks.composerDraft.effort = null;
368 });
369
370 afterEach(() => {
371 document.body.innerHTML = "";
372 });
373
374 it("keeps long user message estimate close to actual rendered virtualized ChatView row height", async () => {
375 const userText = "x".repeat(3_200);
376 const targetMessageId = "msg-user-target-long" as MessageId;
377 const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } =
378 await renderAndMeasureUserRow({
379 timelineWidthPx: 960,
380 targetMessageId,
381 messages: createConversationWithTargetUser({
382 targetMessageId,
383 targetText: userText,
384 }),
385 });
386
387 expect(renderedInVirtualizedRegion).toBe(true);
388
389 const estimatedHeightPx = estimateTimelineMessageHeight(
390 { role: "user", text: userText, attachments: [] },
391 { timelineWidthPx: timelineWidthMeasuredPx },
392 );
393
394 expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(44);
395 });
396
397 it("tracks additional rendered wrapping when ChatView width narrows", async () => {
398 const userText = "x".repeat(2_400);
399 const targetMessageId = "msg-user-target-wrap" as MessageId;
400 const messages = createConversationWithTargetUser({
401 targetMessageId,
402 targetText: userText,
403 });
404 const desktop = await renderAndMeasureUserRow({
405 timelineWidthPx: 960,
406 targetMessageId,
407 messages,
408 });
409 const mobile = await renderAndMeasureUserRow({
410 timelineWidthPx: 360,
411 targetMessageId,
412 messages,
413 });
414
415 const estimatedDesktopPx = estimateTimelineMessageHeight(
416 { role: "user", text: userText, attachments: [] },
417 { timelineWidthPx: desktop.timelineWidthMeasuredPx },
418 );
419 const estimatedMobilePx = estimateTimelineMessageHeight(
420 { role: "user", text: userText, attachments: [] },
421 { timelineWidthPx: mobile.timelineWidthMeasuredPx },
422 );
423
424 const measuredDeltaPx = mobile.measuredRowHeightPx - desktop.measuredRowHeightPx;
425 const estimatedDeltaPx = estimatedMobilePx - estimatedDesktopPx;
426 expect(measuredDeltaPx).toBeGreaterThan(0);
427 expect(estimatedDeltaPx).toBeGreaterThan(0);
428 const ratio = estimatedDeltaPx / measuredDeltaPx;
429 expect(ratio).toBeGreaterThan(0.65);
430 expect(ratio).toBeLessThan(1.35);
431 });
432
433 it("keeps user attachment estimate close to actual rendered ChatView row height", async () => {
434 const targetMessageId = "msg-user-target-attachments" as MessageId;
435 const attachments = createImageAttachments(3);
436 const userText = "message with image attachments";
437 const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } =
438 await renderAndMeasureUserRow({
439 timelineWidthPx: 960,
440 targetMessageId,
441 messages: createConversationWithTargetUser({
442 targetMessageId,
443 targetText: userText,
444 targetAttachments: attachments,
445 }),
446 });
447
448 expect(renderedInVirtualizedRegion).toBe(true);
449
450 const estimatedHeightPx = estimateTimelineMessageHeight(
451 {
452 role: "user",
453 text: userText,
454 attachments: attachments.map((attachment) => ({ id: attachment.id })),
455 },
456 { timelineWidthPx: timelineWidthMeasuredPx },
457 );
458
459 expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(56);
460 });
461});
apps/web/src/components/ChatView.tsx+6-1
...@@ -3362,7 +3362,12 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -3362,7 +3362,12 @@ const MessagesTimeline = memo(function MessagesTimeline({
3362 const nonVirtualizedRows = rows.slice(virtualizedRowCount);3362 const nonVirtualizedRows = rows.slice(virtualizedRowCount);
33633363
3364 const renderRowContent = (row: TimelineRow) => (3364 const renderRowContent = (row: TimelineRow) => (
3365 <div className="pb-4">3365 <div
3366 className="pb-4"
3367 data-timeline-row-kind={row.kind}
3368 data-message-id={row.kind === "message" ? row.message.id : undefined}
3369 data-message-role={row.kind === "message" ? row.message.role : undefined}
3370 >
3366 {row.kind === "work" &&3371 {row.kind === "work" &&
3367 (() => {3372 (() => {
3368 const groupId = row.id;3373 const groupId = row.id;
apps/web/vitest.browser.config.ts created+29
...@@ -0,0 +1,29 @@
1import { fileURLToPath } from "node:url";
2import { playwright } from "@vitest/browser-playwright";
3import { defineConfig, mergeConfig } from "vitest/config";
4
5import viteConfig from "./vite.config";
6
7const srcPath = fileURLToPath(new URL("./src", import.meta.url));
8
9export default mergeConfig(
10 viteConfig,
11 defineConfig({
12 resolve: {
13 alias: {
14 "~": srcPath,
15 },
16 },
17 test: {
18 include: ["src/components/ChatView.browser.tsx"],
19 browser: {
20 enabled: true,
21 provider: playwright(),
22 instances: [{ browser: "chromium" }],
23 headless: true,
24 },
25 testTimeout: 30_000,
26 hookTimeout: 30_000,
27 },
28 }),
29);
bun.lock+32-5
...@@ -87,13 +87,14 @@...@@ -87,13 +87,14 @@
87 },87 },
88 "devDependencies": {88 "devDependencies": {
89 "@effect/language-service": "catalog:",89 "@effect/language-service": "catalog:",
90 "@playwright/test": "^1.58.2",
91 "@tailwindcss/vite": "^4.0.0",90 "@tailwindcss/vite": "^4.0.0",
92 "@tanstack/router-plugin": "^1.161.0",91 "@tanstack/router-plugin": "^1.161.0",
93 "@types/react": "^19.0.0",92 "@types/react": "^19.0.0",
94 "@types/react-dom": "^19.0.0",93 "@types/react-dom": "^19.0.0",
95 "@vitejs/plugin-react": "^5.1.4",94 "@vitejs/plugin-react": "^5.1.4",
95 "@vitest/browser-playwright": "^4.0.18",
96 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",96 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
97 "playwright": "^1.58.2",
97 "tailwindcss": "^4.0.0",98 "tailwindcss": "^4.0.0",
98 "typescript": "catalog:",99 "typescript": "catalog:",
99 "vite": "^8.0.0-beta.12",100 "vite": "^8.0.0-beta.12",
...@@ -468,7 +469,7 @@...@@ -468,7 +469,7 @@
468469
469 "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.16", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-McjTuEPuacSIcXdoI2O9W6VSHIOs9ApEHnEUwONKZnKqIo2GGv1vNg9Pr8tgBOL7lgBWNEHX5ROJ5z1X74sENQ=="],470 "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.16", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-McjTuEPuacSIcXdoI2O9W6VSHIOs9ApEHnEUwONKZnKqIo2GGv1vNg9Pr8tgBOL7lgBWNEHX5ROJ5z1X74sENQ=="],
470471
471 "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="],472 "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
472473
473 "@preact/signals-core": ["@preact/signals-core@1.13.0", "", {}, "sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg=="],474 "@preact/signals-core": ["@preact/signals-core@1.13.0", "", {}, "sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg=="],
474475
...@@ -702,6 +703,10 @@...@@ -702,6 +703,10 @@
702703
703 "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],704 "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
704705
706 "@vitest/browser": ["@vitest/browser@4.0.18", "", { "dependencies": { "@vitest/mocker": "4.0.18", "@vitest/utils": "4.0.18", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.18.3" }, "peerDependencies": { "vitest": "4.0.18" } }, "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng=="],
707
708 "@vitest/browser-playwright": ["@vitest/browser-playwright@4.0.18", "", { "dependencies": { "@vitest/browser": "4.0.18", "@vitest/mocker": "4.0.18", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "playwright": "*", "vitest": "4.0.18" } }, "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g=="],
709
705 "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],710 "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
706711
707 "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],712 "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
...@@ -922,7 +927,7 @@...@@ -922,7 +927,7 @@
922927
923 "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],928 "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
924929
925 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],930 "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
926931
927 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],932 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
928933
...@@ -1194,6 +1199,8 @@...@@ -1194,6 +1199,8 @@
11941199
1195 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],1200 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
11961201
1202 "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
1203
1197 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],1204 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
11981205
1199 "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],1206 "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],
...@@ -1252,10 +1259,14 @@...@@ -1252,10 +1259,14 @@
12521259
1253 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],1260 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
12541261
1262 "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="],
1263
1255 "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],1264 "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
12561265
1257 "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],1266 "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="],
12581267
1268 "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
1269
1259 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],1270 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
12601271
1261 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],1272 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
...@@ -1352,6 +1363,8 @@...@@ -1352,6 +1363,8 @@
13521363
1353 "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],1364 "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
13541365
1366 "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
1367
1355 "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],1368 "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
13561369
1357 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],1370 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
...@@ -1418,6 +1431,8 @@...@@ -1418,6 +1431,8 @@
14181431
1419 "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],1432 "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
14201433
1434 "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
1435
1421 "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],1436 "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
14221437
1423 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],1438 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
...@@ -1594,6 +1609,8 @@...@@ -1594,6 +1609,8 @@
15941609
1595 "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],1610 "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
15961611
1612 "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1613
1597 "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],1614 "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
15981615
1599 "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],1616 "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
...@@ -1602,16 +1619,20 @@...@@ -1602,16 +1619,20 @@
16021619
1603 "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],1620 "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
16041621
1605 "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
1606
1607 "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],1622 "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
16081623
1609 "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],1624 "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
16101625
1611 "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="],1626 "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="],
16121627
1628 "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1629
1613 "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],1630 "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
16141631
1632 "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1633
1634 "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1635
1615 "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],1636 "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
16161637
1617 "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="],1638 "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="],
...@@ -1644,6 +1665,8 @@...@@ -1644,6 +1665,8 @@
16441665
1645 "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],1666 "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
16461667
1668 "@tailwindcss/vite/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1669
1647 "@types/cacheable-request/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],1670 "@types/cacheable-request/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
16481671
1649 "@types/keyv/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],1672 "@types/keyv/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
...@@ -1654,6 +1677,10 @@...@@ -1654,6 +1677,10 @@
16541677
1655 "@types/yauzl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],1678 "@types/yauzl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
16561679
1680 "@vitejs/plugin-react/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1681
1657 "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="],1682 "rolldown-plugin-dts/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="],
1683
1684 "vitest/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
1658 }1685 }
1659}1686}