// Production CSS is part of the behavior under test because row height depends on it. import "../index.css"; import { EventId, ORCHESTRATION_WS_METHODS, EnvironmentId, type EnvironmentApi, type MessageId, type OrchestrationReadModel, type ProjectId, type ServerConfig, type ServerLifecycleWelcomePayload, type ThreadId, type TurnId, WS_METHODS, OrchestrationSessionStatus, DEFAULT_SERVER_SETTINGS, } from "@t3tools/contracts"; import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime"; import { createModelCapabilities, createModelSelection } from "@t3tools/shared/model"; import { RouterProvider, createMemoryHistory } from "@tanstack/react-router"; import { HttpResponse, http, ws } from "msw"; import { setupWorker } from "msw/browser"; import { page } from "vitest/browser"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { render } from "vitest-browser-react"; import { useCommandPaletteStore } from "../commandPaletteStore"; import { useComposerDraftStore, DraftId } from "../composerDraftStore"; import { __resetEnvironmentApiOverridesForTests, __setEnvironmentApiOverrideForTests, } from "../environmentApi"; import { resetSavedEnvironmentRegistryStoreForTests, resetSavedEnvironmentRuntimeStoreForTests, useSavedEnvironmentRegistryStore, useSavedEnvironmentRuntimeStore, } from "../environments/runtime"; import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER, removeInlineTerminalContextPlaceholder, type TerminalContextDraft, } from "../lib/terminalContext"; import { isMacPlatform } from "../lib/utils"; import { __resetLocalApiForTests } from "../localApi"; import { AppAtomRegistryProvider } from "../rpc/atomRegistry"; import { getServerConfig } from "../rpc/serverState"; import { getRouter } from "../router"; import { deriveLogicalProjectKeyFromSettings } from "../logicalProject"; import { selectBootstrapCompleteForActiveEnvironment, useStore } from "../store"; import { useTerminalStateStore } from "../terminalStateStore"; import { useUiStateStore } from "../uiStateStore"; import { createAuthenticatedSessionHandlers } from "../../test/authHttpHandlers"; import { BrowserWsRpcHarness, type NormalizedWsRpcRequestBody } from "../../test/wsRpcHarness"; import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; vi.mock("../lib/gitStatusState", () => ({ useGitStatus: () => ({ data: null, error: null, cause: null, isPending: false }), useGitStatuses: () => new Map(), refreshGitStatus: () => Promise.resolve(null), resetGitStatusStateForTests: () => undefined, })); const THREAD_ID = "thread-browser-test" as ThreadId; const THREAD_TITLE = "Browser test thread"; const ARCHIVED_SECONDARY_THREAD_ID = "thread-secondary-project-archived" as ThreadId; const PROJECT_ID = "project-1" as ProjectId; const SECOND_PROJECT_ID = "project-2" as ProjectId; const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("environment-local"); const REMOTE_ENVIRONMENT_ID = EnvironmentId.make("environment-remote"); const THREAD_REF = scopeThreadRef(LOCAL_ENVIRONMENT_ID, THREAD_ID); const THREAD_KEY = scopedThreadKey(THREAD_REF); const UUID_ROUTE_RE = /^\/draft\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const PROJECT_DRAFT_KEY = `${LOCAL_ENVIRONMENT_ID}:${PROJECT_ID}`; const PROJECT_LOGICAL_KEY = deriveLogicalProjectKeyFromSettings( { environmentId: LOCAL_ENVIRONMENT_ID, id: PROJECT_ID, cwd: "/repo/project", repositoryIdentity: null, }, { sidebarProjectGroupingMode: DEFAULT_CLIENT_SETTINGS.sidebarProjectGroupingMode, sidebarProjectGroupingOverrides: DEFAULT_CLIENT_SETTINGS.sidebarProjectGroupingOverrides, }, ); const NOW_ISO = "2026-03-04T12:00:00.000Z"; const BASE_TIME_MS = Date.parse(NOW_ISO); const ATTACHMENT_SVG = ""; const ADD_PROJECT_SUBMENU_PLACEHOLDER = "Enter path (e.g. ~/projects/my-app)"; interface TestFixture { snapshot: OrchestrationReadModel; serverConfig: ServerConfig; welcome: ServerLifecycleWelcomePayload; } let fixture: TestFixture; const rpcHarness = new BrowserWsRpcHarness(); const wsRequests = rpcHarness.requests; let customWsRpcResolver: ((body: NormalizedWsRpcRequestBody) => unknown | undefined) | null = null; const wsLink = ws.link(/ws(s)?:\/\/.*/); interface ViewportSpec { name: string; width: number; height: number; textTolerancePx: number; attachmentTolerancePx: number; } const DEFAULT_VIEWPORT: ViewportSpec = { name: "desktop", width: 960, height: 1_100, textTolerancePx: 44, attachmentTolerancePx: 56, }; const WIDE_FOOTER_VIEWPORT: ViewportSpec = { name: "wide-footer", width: 1_400, height: 1_100, textTolerancePx: 44, attachmentTolerancePx: 56, }; const COMPACT_FOOTER_VIEWPORT: ViewportSpec = { name: "compact-footer", width: 430, height: 932, textTolerancePx: 56, attachmentTolerancePx: 56, }; interface MountedChatView { [Symbol.asyncDispose]: () => Promise; cleanup: () => Promise; setViewport: (viewport: ViewportSpec) => Promise; setContainerSize: (viewport: Pick) => Promise; router: ReturnType; } function isoAt(offsetSeconds: number): string { return new Date(BASE_TIME_MS + offsetSeconds * 1_000).toISOString(); } function createBaseServerConfig(): ServerConfig { return { environment: { environmentId: EnvironmentId.make("environment-local"), label: "Local environment", platform: { os: "darwin" as const, arch: "arm64" as const }, serverVersion: "0.0.0-test", capabilities: { repositoryIdentity: true }, }, auth: { policy: "loopback-browser", bootstrapMethods: ["one-time-token"], sessionMethods: ["browser-session-cookie", "bearer-session-token"], sessionCookieName: "t3_session", }, cwd: "/repo/project", keybindingsConfigPath: "/repo/project/.t3code-keybindings.json", keybindings: [], issues: [], providers: [ { provider: "codex", enabled: true, installed: true, version: "0.116.0", status: "ready", auth: { status: "authenticated" }, checkedAt: NOW_ISO, models: [], slashCommands: [], skills: [], }, ], availableEditors: [], observability: { logsDirectoryPath: "/repo/project/.t3/logs", localTracingEnabled: true, otlpTracesEnabled: false, otlpMetricsEnabled: false, }, settings: { ...DEFAULT_SERVER_SETTINGS, ...DEFAULT_CLIENT_SETTINGS, }, }; } function createMockEnvironmentApi(input: { browse: EnvironmentApi["filesystem"]["browse"]; dispatchCommand: EnvironmentApi["orchestration"]["dispatchCommand"]; }): EnvironmentApi { return { terminal: {} as EnvironmentApi["terminal"], projects: {} as EnvironmentApi["projects"], filesystem: { browse: input.browse, }, git: {} as EnvironmentApi["git"], orchestration: { dispatchCommand: input.dispatchCommand, getTurnDiff: (() => { throw new Error("Not implemented in browser test."); }) as EnvironmentApi["orchestration"]["getTurnDiff"], getFullThreadDiff: (() => { throw new Error("Not implemented in browser test."); }) as EnvironmentApi["orchestration"]["getFullThreadDiff"], subscribeShell: (() => () => undefined) as EnvironmentApi["orchestration"]["subscribeShell"], subscribeThread: (() => () => undefined) as EnvironmentApi["orchestration"]["subscribeThread"], }, }; } function createUserMessage(options: { id: MessageId; text: string; offsetSeconds: number; attachments?: Array<{ type: "image"; id: string; name: string; mimeType: string; sizeBytes: number; }>; }) { return { id: options.id, role: "user" as const, text: options.text, ...(options.attachments ? { attachments: options.attachments } : {}), turnId: null, streaming: false, createdAt: isoAt(options.offsetSeconds), updatedAt: isoAt(options.offsetSeconds + 1), }; } function createAssistantMessage(options: { id: MessageId; text: string; offsetSeconds: number }) { return { id: options.id, role: "assistant" as const, text: options.text, turnId: null, streaming: false, createdAt: isoAt(options.offsetSeconds), updatedAt: isoAt(options.offsetSeconds + 1), }; } function createTerminalContext(input: { id: string; terminalLabel: string; lineStart: number; lineEnd: number; text: string; }): TerminalContextDraft { return { id: input.id, threadId: THREAD_ID, terminalId: `terminal-${input.id}`, terminalLabel: input.terminalLabel, lineStart: input.lineStart, lineEnd: input.lineEnd, text: input.text, createdAt: NOW_ISO, }; } function createSnapshotForTargetUser(options: { targetMessageId: MessageId; targetText: string; targetAttachmentCount?: number; sessionStatus?: OrchestrationSessionStatus; }): OrchestrationReadModel { const messages: Array = []; for (let index = 0; index < 22; index += 1) { const isTarget = index === 3; const userId = `msg-user-${index}` as MessageId; const assistantId = `msg-assistant-${index}` as MessageId; const attachments = isTarget && (options.targetAttachmentCount ?? 0) > 0 ? Array.from({ length: options.targetAttachmentCount ?? 0 }, (_, attachmentIndex) => ({ type: "image" as const, id: `attachment-${attachmentIndex + 1}`, name: `attachment-${attachmentIndex + 1}.png`, mimeType: "image/png", sizeBytes: 128, previewUrl: `/attachments/attachment-${attachmentIndex + 1}`, })) : undefined; messages.push( createUserMessage({ id: isTarget ? options.targetMessageId : userId, text: isTarget ? options.targetText : `filler user message ${index}`, offsetSeconds: messages.length * 3, ...(attachments ? { attachments } : {}), }), ); messages.push( createAssistantMessage({ id: assistantId, text: `assistant filler ${index}`, offsetSeconds: messages.length * 3, }), ); } return { snapshotSequence: 1, projects: [ { id: PROJECT_ID, title: "Project", workspaceRoot: "/repo/project", defaultModelSelection: { provider: "codex", model: "gpt-5", }, scripts: [], createdAt: NOW_ISO, updatedAt: NOW_ISO, deletedAt: null, }, ], threads: [ { id: THREAD_ID, projectId: PROJECT_ID, title: THREAD_TITLE, modelSelection: { provider: "codex", model: "gpt-5", }, interactionMode: "default", runtimeMode: "full-access", branch: "main", worktreePath: null, latestTurn: null, createdAt: NOW_ISO, updatedAt: NOW_ISO, archivedAt: null, deletedAt: null, messages, activities: [], proposedPlans: [], checkpoints: [], session: { threadId: THREAD_ID, status: options.sessionStatus ?? "ready", providerName: "codex", runtimeMode: "full-access", activeTurnId: null, lastError: null, updatedAt: NOW_ISO, }, }, ], updatedAt: NOW_ISO, }; } function buildFixture(snapshot: OrchestrationReadModel): TestFixture { return { snapshot, serverConfig: createBaseServerConfig(), welcome: { environment: { environmentId: EnvironmentId.make("environment-local"), label: "Local environment", platform: { os: "darwin" as const, arch: "arm64" as const }, serverVersion: "0.0.0-test", capabilities: { repositoryIdentity: true }, }, cwd: "/repo/project", projectName: "Project", bootstrapProjectId: PROJECT_ID, bootstrapThreadId: THREAD_ID, }, }; } function addThreadToSnapshot( snapshot: OrchestrationReadModel, threadId: ThreadId, ): OrchestrationReadModel { return { ...snapshot, snapshotSequence: snapshot.snapshotSequence + 1, threads: [ ...snapshot.threads, { id: threadId, projectId: PROJECT_ID, title: "New thread", modelSelection: { provider: "codex", model: "gpt-5", }, interactionMode: "default", runtimeMode: "full-access", branch: "main", worktreePath: null, latestTurn: null, createdAt: NOW_ISO, updatedAt: NOW_ISO, archivedAt: null, deletedAt: null, messages: [], activities: [], proposedPlans: [], checkpoints: [], session: { threadId, status: "ready", providerName: "codex", runtimeMode: "full-access", activeTurnId: null, lastError: null, updatedAt: NOW_ISO, }, }, ], }; } function toShellThread(thread: OrchestrationReadModel["threads"][number]) { return { id: thread.id, projectId: thread.projectId, title: thread.title, modelSelection: thread.modelSelection, runtimeMode: thread.runtimeMode, interactionMode: thread.interactionMode, branch: thread.branch, worktreePath: thread.worktreePath, latestTurn: thread.latestTurn, createdAt: thread.createdAt, updatedAt: thread.updatedAt, archivedAt: thread.archivedAt, session: thread.session, latestUserMessageAt: thread.messages.findLast((message) => message.role === "user")?.createdAt ?? null, hasPendingApprovals: false, hasPendingUserInput: false, hasActionableProposedPlan: false, }; } function toShellSnapshot(snapshot: OrchestrationReadModel) { return { snapshotSequence: snapshot.snapshotSequence, projects: snapshot.projects.map((project) => ({ id: project.id, title: project.title, workspaceRoot: project.workspaceRoot, repositoryIdentity: project.repositoryIdentity ?? null, defaultModelSelection: project.defaultModelSelection, scripts: project.scripts, createdAt: project.createdAt, updatedAt: project.updatedAt, })), threads: snapshot.threads.map(toShellThread), updatedAt: snapshot.updatedAt, }; } function updateThreadSessionInSnapshot( snapshot: OrchestrationReadModel, threadId: ThreadId, session: OrchestrationReadModel["threads"][number]["session"], ): OrchestrationReadModel { return { ...snapshot, snapshotSequence: snapshot.snapshotSequence + 1, threads: snapshot.threads.map((thread) => thread.id === threadId ? { ...thread, session, updatedAt: NOW_ISO, } : thread, ), }; } function sendShellThreadUpsert( threadId: ThreadId, options?: { readonly session?: OrchestrationReadModel["threads"][number]["session"]; }, ): void { const thread = fixture.snapshot.threads.find((entry) => entry.id === threadId); if (!thread) { throw new Error(`Expected thread ${threadId} in snapshot.`); } const shellThread = options?.session !== undefined ? toShellThread({ ...thread, session: options.session }) : toShellThread(thread); rpcHarness.emitStreamValue(ORCHESTRATION_WS_METHODS.subscribeShell, { kind: "thread-upserted", sequence: fixture.snapshot.snapshotSequence, thread: shellThread, }); } async function waitForWsClient(): Promise { await vi.waitFor( () => { expect( wsRequests.some((request) => request._tag === ORCHESTRATION_WS_METHODS.subscribeShell), ).toBe(true); expect( wsRequests.some((request) => request._tag === WS_METHODS.subscribeServerLifecycle), ).toBe(true); expect(wsRequests.some((request) => request._tag === WS_METHODS.subscribeServerConfig)).toBe( true, ); }, { timeout: 8_000, interval: 16 }, ); } function threadRefFor(threadId: ThreadId) { return scopeThreadRef(LOCAL_ENVIRONMENT_ID, threadId); } function threadKeyFor(threadId: ThreadId): string { return scopedThreadKey(threadRefFor(threadId)); } function composerDraftFor(target: string) { const { draftsByThreadKey } = useComposerDraftStore.getState(); return draftsByThreadKey[target] ?? draftsByThreadKey[threadKeyFor(target as ThreadId)]; } function draftIdFromPath(pathname: string) { const segments = pathname.split("/"); const draftId = segments[segments.length - 1]; if (!draftId) { throw new Error(`Expected thread path, received "${pathname}".`); } return DraftId.make(draftId); } function draftThreadIdFor(draftId: ReturnType): ThreadId { const draftSession = useComposerDraftStore.getState().getDraftSession(draftId); if (!draftSession) { throw new Error(`Expected draft session for "${draftId}".`); } return draftSession.threadId; } function serverThreadPath(threadId: ThreadId): string { return `/${LOCAL_ENVIRONMENT_ID}/${threadId}`; } async function waitForAppBootstrap(): Promise { await vi.waitFor( () => { expect(getServerConfig()).not.toBeNull(); expect(selectBootstrapCompleteForActiveEnvironment(useStore.getState())).toBe(true); }, { timeout: 8_000, interval: 16 }, ); } async function materializePromotedDraftThreadViaDomainEvent(threadId: ThreadId): Promise { await waitForWsClient(); fixture.snapshot = addThreadToSnapshot(fixture.snapshot, threadId); fixture.snapshot = updateThreadSessionInSnapshot(fixture.snapshot, threadId, null); sendShellThreadUpsert(threadId, { session: null }); } async function startPromotedServerThreadViaDomainEvent(threadId: ThreadId): Promise { fixture.snapshot = updateThreadSessionInSnapshot(fixture.snapshot, threadId, { threadId, status: "running", providerName: "codex", runtimeMode: "full-access", activeTurnId: `turn-${threadId}` as TurnId, lastError: null, updatedAt: NOW_ISO, }); sendShellThreadUpsert(threadId); } async function promoteDraftThreadViaDomainEvent(threadId: ThreadId): Promise { await materializePromotedDraftThreadViaDomainEvent(threadId); await startPromotedServerThreadViaDomainEvent(threadId); await vi.waitFor( () => { expect(useComposerDraftStore.getState().draftThreadsByThreadKey[threadKeyFor(threadId)]).toBe( undefined, ); }, { timeout: 8_000, interval: 16 }, ); } function createDraftOnlySnapshot(): OrchestrationReadModel { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-draft-target" as MessageId, targetText: "draft thread", }); return { ...snapshot, threads: [], }; } function createProjectlessSnapshot(): OrchestrationReadModel { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-projectless-target" as MessageId, targetText: "projectless", }); return { ...snapshot, projects: [], threads: [], }; } function withProjectScripts( snapshot: OrchestrationReadModel, scripts: OrchestrationReadModel["projects"][number]["scripts"], ): OrchestrationReadModel { return { ...snapshot, projects: snapshot.projects.map((project) => project.id === PROJECT_ID ? { ...project, scripts: Array.from(scripts) } : project, ), }; } function setDraftThreadWithoutWorktree(): void { useComposerDraftStore.setState({ draftThreadsByThreadKey: { [THREAD_KEY]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, envMode: "local", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [PROJECT_DRAFT_KEY]: THREAD_KEY, }, }); } function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-plan-target" as MessageId, targetText: "plan thread", }); const planMarkdown = [ "# Ship plan mode follow-up", "", "- Step 1: capture the thread-open trace", "- Step 2: identify the main-thread bottleneck", "- Step 3: keep collapsed cards cheap", "- Step 4: render the full markdown only on demand", "- Step 5: preserve export and save actions", "- Step 6: add regression coverage", "- Step 7: verify route transitions stay responsive", "- Step 8: confirm no server-side work changed", "- Step 9: confirm short plans still render normally", "- Step 10: confirm long plans stay collapsed by default", "- Step 11: confirm preview text is still useful", "- Step 12: confirm plan follow-up flow still works", "- Step 13: confirm timeline virtualization still behaves", "- Step 14: confirm theme styling still looks correct", "- Step 15: confirm save dialog behavior is unchanged", "- Step 16: confirm download behavior is unchanged", "- Step 17: confirm code fences do not parse until expand", "- Step 18: confirm preview truncation ends cleanly", "- Step 19: confirm markdown links still open in editor after expand", "- Step 20: confirm deep hidden detail only appears after expand", "", "```ts", "export const hiddenPlanImplementationDetail = 'deep hidden detail only after expand';", "```", ].join("\n"); return { ...snapshot, threads: snapshot.threads.map((thread) => thread.id === THREAD_ID ? Object.assign({}, thread, { proposedPlans: [ { id: "plan-browser-test", turnId: null, planMarkdown, implementedAt: null, implementationThreadId: null, createdAt: isoAt(1_000), updatedAt: isoAt(1_001), }, ], updatedAt: isoAt(1_001), }) : thread, ), }; } function createSnapshotWithSecondaryProject(options?: { includeSecondaryThread?: boolean; includeArchivedSecondaryThread?: boolean; }): OrchestrationReadModel { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-secondary-project-target" as MessageId, targetText: "secondary project", }); const includeSecondaryThread = options?.includeSecondaryThread ?? true; const includeArchivedSecondaryThread = options?.includeArchivedSecondaryThread ?? true; const secondaryThreads: OrchestrationReadModel["threads"] = includeSecondaryThread ? [ { id: "thread-secondary-project" as ThreadId, projectId: SECOND_PROJECT_ID, title: "Release checklist", modelSelection: { provider: "codex", model: "gpt-5" }, interactionMode: "default", runtimeMode: "full-access", branch: "release/docs-portal", worktreePath: null, latestTurn: null, createdAt: isoAt(30), updatedAt: isoAt(31), deletedAt: null, messages: [], activities: [], proposedPlans: [], checkpoints: [], session: { threadId: "thread-secondary-project" as ThreadId, status: "ready", providerName: "codex", runtimeMode: "full-access", activeTurnId: null, lastError: null, updatedAt: isoAt(31), }, archivedAt: null, }, ] : []; const archivedSecondaryThreads: OrchestrationReadModel["threads"] = includeArchivedSecondaryThread ? [ { id: ARCHIVED_SECONDARY_THREAD_ID, projectId: SECOND_PROJECT_ID, title: "Archived Docs Notes", modelSelection: { provider: "codex", model: "gpt-5" }, interactionMode: "default", runtimeMode: "full-access", branch: "release/docs-archive", worktreePath: null, latestTurn: null, createdAt: isoAt(24), updatedAt: isoAt(25), deletedAt: null, messages: [], activities: [], proposedPlans: [], checkpoints: [], session: { threadId: ARCHIVED_SECONDARY_THREAD_ID, status: "ready", providerName: "codex", runtimeMode: "full-access", activeTurnId: null, lastError: null, updatedAt: isoAt(25), }, archivedAt: isoAt(26), }, ] : []; return { ...snapshot, projects: [ ...snapshot.projects, { id: SECOND_PROJECT_ID, title: "Docs Portal", workspaceRoot: "/repo/clients/docs-portal", defaultModelSelection: { provider: "codex", model: "gpt-5" }, scripts: [], createdAt: NOW_ISO, updatedAt: NOW_ISO, deletedAt: null, }, ], threads: [...snapshot.threads, ...secondaryThreads, ...archivedSecondaryThreads], }; } function createSnapshotWithPendingUserInput(): OrchestrationReadModel { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-pending-input-target" as MessageId, targetText: "question thread", }); return { ...snapshot, threads: snapshot.threads.map((thread) => thread.id === THREAD_ID ? Object.assign({}, thread, { interactionMode: "plan", activities: [ { id: EventId.make("activity-user-input-requested"), tone: "info", kind: "user-input.requested", summary: "User input requested", payload: { requestId: "req-browser-user-input", questions: [ { id: "scope", header: "Scope", question: "What should this change cover?", options: [ { label: "Tight", description: "Touch only the footer layout logic.", }, { label: "Broad", description: "Also adjust the related composer controls.", }, ], }, { id: "risk", header: "Risk", question: "How aggressive should the imaginary plan be?", options: [ { label: "Conservative", description: "Favor reliability and low-risk changes.", }, { label: "Balanced", description: "Mix quick wins with one structural improvement.", }, ], }, ], }, turnId: null, sequence: 1, createdAt: isoAt(1_000), }, ], updatedAt: isoAt(1_000), }) : thread, ), }; } function createSnapshotWithPlanFollowUpPrompt(options?: { modelSelection?: { provider: "codex"; model: string }; planMarkdown?: string; }): OrchestrationReadModel { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-plan-follow-up-target" as MessageId, targetText: "plan follow-up thread", }); const modelSelection = options?.modelSelection ?? { provider: "codex" as const, model: "gpt-5", }; const planMarkdown = options?.planMarkdown ?? "# Follow-up plan\n\n- Keep the composer footer stable on resize."; return { ...snapshot, projects: snapshot.projects.map((project) => project.id === PROJECT_ID ? { ...project, defaultModelSelection: modelSelection } : project, ), threads: snapshot.threads.map((thread) => thread.id === THREAD_ID ? Object.assign({}, thread, { modelSelection, interactionMode: "plan", latestTurn: { turnId: "turn-plan-follow-up" as TurnId, state: "completed", requestedAt: isoAt(1_000), startedAt: isoAt(1_001), completedAt: isoAt(1_010), assistantMessageId: null, }, proposedPlans: [ { id: "plan-follow-up-browser-test", turnId: "turn-plan-follow-up" as TurnId, planMarkdown, implementedAt: null, implementationThreadId: null, createdAt: isoAt(1_002), updatedAt: isoAt(1_003), }, ], session: { ...thread.session, status: "ready", updatedAt: isoAt(1_010), }, updatedAt: isoAt(1_010), }) : thread, ), }; } function resolveWsRpc(body: NormalizedWsRpcRequestBody): unknown { const customResult = customWsRpcResolver?.(body); if (customResult !== undefined) { return customResult; } const tag = body._tag; if (tag === WS_METHODS.serverGetConfig) { return fixture.serverConfig; } if (tag === WS_METHODS.gitListBranches) { return { isRepo: true, hasOriginRemote: true, nextCursor: null, totalCount: 1, branches: [ { name: "main", current: true, isDefault: true, worktreePath: null, }, ], }; } if (tag === WS_METHODS.projectsSearchEntries) { return { entries: [], truncated: false, }; } if (tag === WS_METHODS.shellOpenInEditor) { return null; } if (tag === WS_METHODS.terminalOpen) { return { threadId: typeof body.threadId === "string" ? body.threadId : THREAD_ID, terminalId: typeof body.terminalId === "string" ? body.terminalId : "default", cwd: typeof body.cwd === "string" ? body.cwd : "/repo/project", worktreePath: typeof body.worktreePath === "string" ? body.worktreePath : body.worktreePath === null ? null : null, status: "running", pid: 123, history: "", exitCode: null, exitSignal: null, updatedAt: NOW_ISO, }; } return {}; } const worker = setupWorker( wsLink.addEventListener("connection", ({ client }) => { void rpcHarness.connect(client); client.addEventListener("message", (event) => { const rawData = event.data; if (typeof rawData !== "string") return; void rpcHarness.onMessage(rawData); }); }), ...createAuthenticatedSessionHandlers(() => fixture.serverConfig.auth), http.get("*/attachments/:attachmentId", () => HttpResponse.text(ATTACHMENT_SVG, { headers: { "Content-Type": "image/svg+xml", }, }), ), http.get("*/api/project-favicon", () => new HttpResponse(null, { status: 204 })), ); async function nextFrame(): Promise { await new Promise((resolve) => { window.requestAnimationFrame(() => resolve()); }); } async function waitForLayout(): Promise { await nextFrame(); await nextFrame(); await nextFrame(); } async function setViewport(viewport: ViewportSpec): Promise { await page.viewport(viewport.width, viewport.height); await waitForLayout(); } async function waitForProductionStyles(): Promise { await vi.waitFor( () => { expect( getComputedStyle(document.documentElement).getPropertyValue("--background").trim(), ).not.toBe(""); expect(getComputedStyle(document.body).marginTop).toBe("0px"); }, { timeout: 4_000, interval: 16, }, ); } async function waitForElement( query: () => T | null, errorMessage: string, ): Promise { let element: T | null = null; await vi.waitFor( () => { element = query(); expect(element, errorMessage).toBeTruthy(); }, { timeout: 8_000, interval: 16, }, ); if (!element) { throw new Error(errorMessage); } return element; } async function waitForURL( router: ReturnType, predicate: (pathname: string) => boolean, errorMessage: string, ): Promise { let pathname = ""; await vi.waitFor( () => { pathname = router.state.location.pathname; expect(predicate(pathname), errorMessage).toBe(true); }, { timeout: 8_000, interval: 16 }, ); return pathname; } async function waitForComposerEditor(): Promise { return waitForElement( () => document.querySelector('[contenteditable="true"]'), "Unable to find composer editor.", ); } async function pressComposerKey(key: string): Promise { const composerEditor = await waitForComposerEditor(); composerEditor.focus(); const keydownEvent = new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, }); composerEditor.dispatchEvent(keydownEvent); if (keydownEvent.defaultPrevented) { await waitForLayout(); return; } const beforeInputEvent = new InputEvent("beforeinput", { data: key, inputType: "insertText", bubbles: true, cancelable: true, }); composerEditor.dispatchEvent(beforeInputEvent); if (beforeInputEvent.defaultPrevented) { await waitForLayout(); return; } if ( typeof document.execCommand === "function" && document.execCommand("insertText", false, key) ) { await waitForLayout(); return; } const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { throw new Error("Unable to resolve composer selection for text input."); } const range = selection.getRangeAt(0); range.deleteContents(); const textNode = document.createTextNode(key); range.insertNode(textNode); range.setStartAfter(textNode); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); composerEditor.dispatchEvent( new InputEvent("input", { data: key, inputType: "insertText", bubbles: true, }), ); await waitForLayout(); } async function pressComposerUndo(): Promise { const composerEditor = await waitForComposerEditor(); const useMetaForMod = isMacPlatform(navigator.platform); composerEditor.focus(); composerEditor.dispatchEvent( new KeyboardEvent("keydown", { key: "z", metaKey: useMetaForMod, ctrlKey: !useMetaForMod, bubbles: true, cancelable: true, }), ); await waitForLayout(); } async function waitForComposerText(expectedText: string): Promise { await vi.waitFor( () => { expect(useComposerDraftStore.getState().draftsByThreadKey[THREAD_KEY]?.prompt ?? "").toBe( expectedText, ); }, { timeout: 8_000, interval: 16 }, ); } async function setComposerSelectionByTextOffsets(options: { start: number; end: number; direction?: "forward" | "backward"; }): Promise { const composerEditor = await waitForComposerEditor(); composerEditor.focus(); const resolvePoint = (targetOffset: number) => { const traversedRef = { value: 0 }; const visitNode = (node: Node): { node: Node; offset: number } | null => { if (node.nodeType === Node.TEXT_NODE) { const textLength = node.textContent?.length ?? 0; if (targetOffset <= traversedRef.value + textLength) { return { node, offset: Math.max(0, Math.min(targetOffset - traversedRef.value, textLength)), }; } traversedRef.value += textLength; return null; } if (node instanceof HTMLBRElement) { const parent = node.parentNode; if (!parent) { return null; } const siblingIndex = Array.prototype.indexOf.call(parent.childNodes, node); if (targetOffset <= traversedRef.value) { return { node: parent, offset: siblingIndex }; } if (targetOffset <= traversedRef.value + 1) { return { node: parent, offset: siblingIndex + 1 }; } traversedRef.value += 1; return null; } if (node instanceof Element || node instanceof DocumentFragment) { for (const child of node.childNodes) { const point = visitNode(child); if (point) { return point; } } } return null; }; return ( visitNode(composerEditor) ?? { node: composerEditor, offset: composerEditor.childNodes.length, } ); }; const startPoint = resolvePoint(options.start); const endPoint = resolvePoint(options.end); const selection = window.getSelection(); if (!selection) { throw new Error("Unable to resolve window selection."); } selection.removeAllRanges(); if (options.direction === "backward" && "setBaseAndExtent" in selection) { selection.setBaseAndExtent(endPoint.node, endPoint.offset, startPoint.node, startPoint.offset); await waitForLayout(); return; } const range = document.createRange(); range.setStart(startPoint.node, startPoint.offset); range.setEnd(endPoint.node, endPoint.offset); selection.addRange(range); await waitForLayout(); } async function selectAllComposerContent(): Promise { const composerEditor = await waitForComposerEditor(); composerEditor.focus(); const selection = window.getSelection(); if (!selection) { throw new Error("Unable to resolve window selection."); } selection.removeAllRanges(); const range = document.createRange(); range.selectNodeContents(composerEditor); selection.addRange(range); await waitForLayout(); } async function waitForComposerMenuItem(itemId: string): Promise { return waitForElement( () => document.querySelector(`[data-composer-item-id="${itemId}"]`), `Unable to find composer menu item "${itemId}".`, ); } async function waitForSendButton(): Promise { return waitForElement( () => document.querySelector('button[aria-label="Send message"]'), "Unable to find send button.", ); } function findComposerProviderModelPicker(): HTMLButtonElement | null { return document.querySelector('[data-chat-provider-model-picker="true"]'); } function findButtonByText(text: string): HTMLButtonElement | null { return (Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === text, ) ?? null) as HTMLButtonElement | null; } async function waitForButtonByText(text: string): Promise { return waitForElement(() => findButtonByText(text), `Unable to find "${text}" button.`); } function findButtonContainingText(text: string): HTMLButtonElement | null { return (Array.from(document.querySelectorAll("button")).find((button) => button.textContent?.includes(text), ) ?? null) as HTMLButtonElement | null; } async function waitForButtonContainingText(text: string): Promise { return waitForElement( () => findButtonContainingText(text), `Unable to find button containing "${text}".`, ); } async function waitForSelectItemContainingText(text: string): Promise { return waitForElement( () => Array.from(document.querySelectorAll('[data-slot="select-item"]')).find((item) => item.textContent?.includes(text), ) ?? null, `Unable to find select item containing "${text}".`, ); } async function expectComposerActionsContained(): Promise { const footer = await waitForElement( () => document.querySelector('[data-chat-composer-footer="true"]'), "Unable to find composer footer.", ); const actions = await waitForElement( () => document.querySelector('[data-chat-composer-actions="right"]'), "Unable to find composer actions container.", ); await vi.waitFor( () => { const footerRect = footer.getBoundingClientRect(); const actionButtons = Array.from(actions.querySelectorAll("button")); expect(actionButtons.length).toBeGreaterThanOrEqual(1); const buttonRects = actionButtons.map((button) => button.getBoundingClientRect()); const firstTop = buttonRects[0]?.top ?? 0; for (const rect of buttonRects) { expect(rect.right).toBeLessThanOrEqual(footerRect.right + 0.5); expect(rect.bottom).toBeLessThanOrEqual(footerRect.bottom + 0.5); expect(Math.abs(rect.top - firstTop)).toBeLessThanOrEqual(1.5); } }, { timeout: 8_000, interval: 16 }, ); } async function waitForInteractionModeButton( expectedLabel: "Build" | "Plan", ): Promise { return waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === expectedLabel, ) as HTMLButtonElement | null, `Unable to find ${expectedLabel} interaction mode button.`, ); } async function waitForServerConfigToApply(): Promise { await vi.waitFor( () => { expect(wsRequests.some((request) => request._tag === WS_METHODS.subscribeServerConfig)).toBe( true, ); }, { timeout: 8_000, interval: 16 }, ); await waitForLayout(); } function dispatchChatNewShortcut(): void { const useMetaForMod = isMacPlatform(navigator.platform); window.dispatchEvent( new KeyboardEvent("keydown", { key: "o", shiftKey: true, metaKey: useMetaForMod, ctrlKey: !useMetaForMod, bubbles: true, cancelable: true, }), ); } function releaseModShortcut(key?: string): void { window.dispatchEvent( new KeyboardEvent("keyup", { key: key ?? (isMacPlatform(navigator.platform) ? "Meta" : "Control"), metaKey: false, ctrlKey: false, bubbles: true, cancelable: true, }), ); } async function triggerChatNewShortcutUntilPath( router: ReturnType, predicate: (pathname: string) => boolean, errorMessage: string, ): Promise { let pathname = router.state.location.pathname; const deadline = Date.now() + 8_000; while (Date.now() < deadline) { dispatchChatNewShortcut(); await waitForLayout(); pathname = router.state.location.pathname; if (predicate(pathname)) { return pathname; } } throw new Error(`${errorMessage} Last path: ${pathname}`); } async function openCommandPaletteFromTrigger(): Promise { const trigger = page.getByTestId("command-palette-trigger"); await expect.element(trigger).toBeInTheDocument(); await trigger.click(); await waitForElement( () => document.querySelector('[data-testid="command-palette"]'), "Command palette should have opened from the sidebar trigger.", ); } async function waitForNewThreadShortcutLabel(): Promise { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.hover(); const shortcutLabel = isMacPlatform(navigator.platform) ? "New thread (⇧⌘O)" : "New thread (Ctrl+Shift+O)"; await expect.element(page.getByText(shortcutLabel)).toBeInTheDocument(); } async function waitForCommandPaletteShortcutLabel(): Promise { await waitForElement( () => document.querySelector('[data-testid="command-palette-trigger"] kbd'), "Command palette shortcut label did not render.", ); } async function waitForCommandPaletteInput(placeholder: string): Promise { return waitForElement( () => document.querySelector(`input[placeholder="${placeholder}"]`) as HTMLInputElement | null, `Command palette input with placeholder "${placeholder}" did not render.`, ); } function getCommandPaletteLegendEntries(): string[] { const footer = document.querySelector('[data-slot="command-footer"]'); if (!footer) { return []; } return Array.from(footer.querySelectorAll('[data-slot="kbd-group"]')) .map((group) => Array.from(group.children) .map((child) => child.textContent?.trim() ?? "") .filter((value) => value.length > 0) .join(" "), ) .filter((value) => value.length > 0); } async function dispatchInputKey( input: HTMLInputElement, init: Pick, ): Promise { input.focus(); input.dispatchEvent( new KeyboardEvent("keydown", { bubbles: true, cancelable: true, ...init, }), ); await waitForLayout(); } async function mountChatView(options: { viewport: ViewportSpec; snapshot: OrchestrationReadModel; configureFixture?: (fixture: TestFixture) => void; resolveRpc?: (body: NormalizedWsRpcRequestBody) => unknown | undefined; initialPath?: string; }): Promise { fixture = buildFixture(options.snapshot); options.configureFixture?.(fixture); customWsRpcResolver = options.resolveRpc ?? null; await setViewport(options.viewport); await waitForProductionStyles(); const host = document.createElement("div"); host.style.position = "fixed"; host.style.top = "0"; host.style.left = "0"; host.style.width = "100vw"; host.style.height = "100vh"; host.style.display = "grid"; host.style.overflow = "hidden"; document.body.append(host); const router = getRouter( createMemoryHistory({ initialEntries: [options.initialPath ?? `/${LOCAL_ENVIRONMENT_ID}/${THREAD_ID}`], }), ); const screen = await render( , { container: host, }, ); await waitForWsClient(); await waitForAppBootstrap(); await waitForLayout(); const cleanup = async () => { customWsRpcResolver = null; await screen.unmount(); host.remove(); await waitForLayout(); }; return { [Symbol.asyncDispose]: cleanup, cleanup, setViewport: async (viewport: ViewportSpec) => { await setViewport(viewport); await waitForProductionStyles(); }, setContainerSize: async (viewport) => { host.style.width = `${viewport.width}px`; host.style.height = `${viewport.height}px`; await waitForLayout(); }, router, }; } describe("ChatView timeline estimator parity (full app)", () => { beforeAll(async () => { fixture = buildFixture( createSnapshotForTargetUser({ targetMessageId: "msg-user-bootstrap" as MessageId, targetText: "bootstrap", }), ); await worker.start({ onUnhandledRequest: "bypass", quiet: true, serviceWorker: { url: "/mockServiceWorker.js", }, }); }); afterAll(async () => { await rpcHarness.disconnect(); await worker.stop(); }); beforeEach(async () => { await rpcHarness.reset({ resolveUnary: resolveWsRpc, getInitialStreamValues: (request) => { if (request._tag === WS_METHODS.subscribeServerLifecycle) { return [ { version: 1, sequence: 1, type: "welcome", payload: fixture.welcome, }, ]; } if (request._tag === WS_METHODS.subscribeServerConfig) { return [ { version: 1, type: "snapshot", config: fixture.serverConfig, }, ]; } if (request._tag === ORCHESTRATION_WS_METHODS.subscribeShell) { return [ { kind: "snapshot", snapshot: toShellSnapshot(fixture.snapshot), }, ]; } if (request._tag === ORCHESTRATION_WS_METHODS.subscribeThread) { const thread = fixture.snapshot.threads.find((entry) => entry.id === request.threadId); return thread ? [ { kind: "snapshot", snapshot: { snapshotSequence: fixture.snapshot.snapshotSequence, thread, }, }, ] : []; } return []; }, }); await __resetLocalApiForTests(); await setViewport(DEFAULT_VIEWPORT); localStorage.clear(); document.body.innerHTML = ""; wsRequests.length = 0; customWsRpcResolver = null; __resetEnvironmentApiOverridesForTests(); resetSavedEnvironmentRegistryStoreForTests(); resetSavedEnvironmentRuntimeStoreForTests(); Reflect.deleteProperty(window, "desktopBridge"); useComposerDraftStore.setState({ draftsByThreadKey: {}, draftThreadsByThreadKey: {}, logicalProjectDraftThreadKeyByLogicalProjectKey: {}, stickyModelSelectionByProvider: {}, stickyActiveProvider: null, }); useCommandPaletteStore.setState({ open: false, openIntent: null, }); useStore.setState({ activeEnvironmentId: null, environmentStateById: {}, }); useUiStateStore.setState({ projectExpandedById: {}, projectOrder: [], threadLastVisitedAtById: {}, }); useTerminalStateStore.persist.clearStorage(); useTerminalStateStore.setState({ terminalStateByThreadKey: {}, terminalLaunchContextByThreadKey: {}, terminalEventEntriesByKey: {}, nextTerminalEventId: 1, }); }); afterEach(() => { customWsRpcResolver = null; document.body.innerHTML = ""; }); it("re-expands the bootstrap project using its logical key", async () => { useUiStateStore.setState({ projectExpandedById: { [PROJECT_LOGICAL_KEY]: false, }, projectOrder: [PROJECT_LOGICAL_KEY], threadLastVisitedAtById: {}, }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-bootstrap-project-expand" as MessageId, targetText: "bootstrap project expand", }), }); try { await vi.waitFor( () => { expect(useUiStateStore.getState().projectExpandedById[PROJECT_LOGICAL_KEY]).toBe(true); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("shows an explicit empty state for projects without threads in the sidebar", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), }); try { await expect.element(page.getByText("No threads yet")).toBeInTheDocument(); } finally { await mounted.cleanup(); } }); it("opens the project cwd for draft threads without a worktree path", async () => { setDraftThreadWithoutWorktree(); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, availableEditors: ["vscode"], }; }, }); try { await waitForServerConfigToApply(); const openButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "Open", ) as HTMLButtonElement | null, "Unable to find Open button.", ); await vi.waitFor(() => { expect(openButton.disabled).toBe(false); }); openButton.click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.shellOpenInEditor, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.shellOpenInEditor, cwd: "/repo/project", editor: "vscode", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("does not leak a server worktree path into drawer runtime env when launch context clears it", async () => { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-launch-context-target" as MessageId, targetText: "launch context worktree override", }); const targetThread = snapshot.threads.find((thread) => thread.id === THREAD_ID); if (targetThread) { Object.assign(targetThread, { branch: "feature/branch", worktreePath: "/repo/worktrees/feature-branch", }); } useTerminalStateStore.setState({ terminalStateByThreadKey: { [THREAD_KEY]: { terminalOpen: true, terminalHeight: 280, terminalIds: ["default"], runningTerminalIds: [], activeTerminalId: "default", terminalGroups: [{ id: "group-default", terminalIds: ["default"] }], activeTerminalGroupId: "group-default", }, }, terminalLaunchContextByThreadKey: { [THREAD_KEY]: { cwd: "/repo/project", worktreePath: null, }, }, }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot, }); try { await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.terminalOpen, ) as | { _tag: string; cwd?: string; worktreePath?: string | null; env?: Record; } | undefined; expect(openRequest).toMatchObject({ _tag: WS_METHODS.terminalOpen, cwd: "/repo/project", worktreePath: null, env: { T3CODE_PROJECT_ROOT: "/repo/project", }, }); expect(openRequest?.env?.T3CODE_WORKTREE_PATH).toBeUndefined(); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("opens the project cwd with VS Code Insiders when it is the only available editor", async () => { setDraftThreadWithoutWorktree(); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, availableEditors: ["vscode-insiders"], }; }, }); try { await waitForServerConfigToApply(); const openButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "Open", ) as HTMLButtonElement | null, "Unable to find Open button.", ); await vi.waitFor(() => { expect(openButton.disabled).toBe(false); }); openButton.click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.shellOpenInEditor, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.shellOpenInEditor, cwd: "/repo/project", editor: "vscode-insiders", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("opens the project cwd with Trae when it is the only available editor", async () => { setDraftThreadWithoutWorktree(); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, availableEditors: ["trae"], }; }, }); try { await waitForServerConfigToApply(); const openButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "Open", ) as HTMLButtonElement | null, "Unable to find Open button.", ); await vi.waitFor(() => { expect(openButton.disabled).toBe(false); }); openButton.click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.shellOpenInEditor, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.shellOpenInEditor, cwd: "/repo/project", editor: "trae", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("shows Kiro in the open picker menu and opens the project cwd with it", async () => { setDraftThreadWithoutWorktree(); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, availableEditors: ["kiro"], }; }, }); try { await waitForServerConfigToApply(); const menuButton = await waitForElement( () => document.querySelector('button[aria-label="Copy options"]'), "Unable to find Open picker button.", ); (menuButton as HTMLButtonElement).click(); const kiroItem = await waitForElement( () => Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) => item.textContent?.includes("Kiro"), ) ?? null, "Unable to find Kiro menu item.", ); (kiroItem as HTMLElement).click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.shellOpenInEditor, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.shellOpenInEditor, cwd: "/repo/project", editor: "kiro", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("filters the open picker menu and opens VSCodium from the menu", async () => { setDraftThreadWithoutWorktree(); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, availableEditors: ["vscode-insiders", "vscodium"], }; }, }); try { await waitForServerConfigToApply(); const menuButton = await waitForElement( () => document.querySelector('button[aria-label="Copy options"]'), "Unable to find Open picker button.", ); (menuButton as HTMLButtonElement).click(); await waitForElement( () => Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) => item.textContent?.includes("VS Code Insiders"), ) ?? null, "Unable to find VS Code Insiders menu item.", ); expect( Array.from(document.querySelectorAll('[data-slot="menu-item"]')).some((item) => item.textContent?.includes("Zed"), ), ).toBe(false); const vscodiumItem = await waitForElement( () => Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) => item.textContent?.includes("VSCodium"), ) ?? null, "Unable to find VSCodium menu item.", ); (vscodiumItem as HTMLElement).click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.shellOpenInEditor, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.shellOpenInEditor, cwd: "/repo/project", editor: "vscodium", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("falls back to the first installed editor when the stored favorite is unavailable", async () => { localStorage.setItem("t3code:last-editor", JSON.stringify("vscodium")); setDraftThreadWithoutWorktree(); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, availableEditors: ["vscode-insiders"], }; }, }); try { await waitForServerConfigToApply(); const openButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "Open", ) as HTMLButtonElement | null, "Unable to find Open button.", ); await vi.waitFor(() => { expect(openButton.disabled).toBe(false); }); openButton.click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.shellOpenInEditor, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.shellOpenInEditor, cwd: "/repo/project", editor: "vscode-insiders", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("runs project scripts from local draft threads at the project cwd", async () => { useComposerDraftStore.setState({ draftThreadsByThreadKey: { [THREAD_KEY]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, envMode: "local", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [PROJECT_DRAFT_KEY]: THREAD_KEY, }, }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: withProjectScripts(createDraftOnlySnapshot(), [ { id: "lint", name: "Lint", command: "bun run lint", icon: "lint", runOnWorktreeCreate: false, }, ]), }); try { const runButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.title === "Run Lint", ) as HTMLButtonElement | null, "Unable to find Run Lint button.", ); runButton.click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.terminalOpen, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.terminalOpen, threadId: THREAD_ID, cwd: "/repo/project", env: { T3CODE_PROJECT_ROOT: "/repo/project", }, }); }, { timeout: 8_000, interval: 16 }, ); await vi.waitFor( () => { const writeRequest = wsRequests.find( (request) => request._tag === WS_METHODS.terminalWrite, ); expect(writeRequest).toMatchObject({ _tag: WS_METHODS.terminalWrite, threadId: THREAD_ID, data: "bun run lint\r", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("runs project scripts from worktree draft threads at the worktree cwd", async () => { useComposerDraftStore.setState({ draftThreadsByThreadKey: { [THREAD_KEY]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: "feature/draft", worktreePath: "/repo/worktrees/feature-draft", envMode: "worktree", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [PROJECT_DRAFT_KEY]: THREAD_KEY, }, }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: withProjectScripts(createDraftOnlySnapshot(), [ { id: "test", name: "Test", command: "bun run test", icon: "test", runOnWorktreeCreate: false, }, ]), }); try { const runButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.title === "Run Test", ) as HTMLButtonElement | null, "Unable to find Run Test button.", ); runButton.click(); await vi.waitFor( () => { const openRequest = wsRequests.find( (request) => request._tag === WS_METHODS.terminalOpen, ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.terminalOpen, threadId: THREAD_ID, cwd: "/repo/worktrees/feature-draft", env: { T3CODE_PROJECT_ROOT: "/repo/project", T3CODE_WORKTREE_PATH: "/repo/worktrees/feature-draft", }, }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("lets the server own setup after preparing a pull request worktree thread", async () => { useComposerDraftStore.setState({ draftThreadsByThreadKey: { [THREAD_KEY]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, envMode: "local", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [PROJECT_DRAFT_KEY]: THREAD_KEY, }, }); const mounted = await mountChatView({ viewport: WIDE_FOOTER_VIEWPORT, snapshot: withProjectScripts(createDraftOnlySnapshot(), [ { id: "setup", name: "Setup", command: "bun install", icon: "configure", runOnWorktreeCreate: true, }, ]), resolveRpc: (body) => { if (body._tag === WS_METHODS.gitResolvePullRequest) { return { pullRequest: { number: 1359, title: "Add thread archiving and settings navigation", url: "https://github.com/pingdotgg/t3code/pull/1359", baseBranch: "main", headBranch: "archive-settings-overhaul", state: "open", }, }; } if (body._tag === WS_METHODS.gitPreparePullRequestThread) { return { pullRequest: { number: 1359, title: "Add thread archiving and settings navigation", url: "https://github.com/pingdotgg/t3code/pull/1359", baseBranch: "main", headBranch: "archive-settings-overhaul", state: "open", }, branch: "archive-settings-overhaul", worktreePath: "/repo/worktrees/pr-1359", }; } return undefined; }, }); try { const branchButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "main", ) as HTMLButtonElement | null, "Unable to find branch selector button.", ); branchButton.click(); const branchInput = await waitForElement( () => document.querySelector('input[placeholder="Search branches..."]'), "Unable to find branch search input.", ); branchInput.focus(); await page.getByPlaceholder("Search branches...").fill("1359"); const checkoutItem = await waitForElement( () => Array.from(document.querySelectorAll("span")).find( (element) => element.textContent?.trim() === "Checkout Pull Request", ) as HTMLSpanElement | null, "Unable to find checkout pull request option.", ); checkoutItem.click(); const worktreeButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "Worktree", ) as HTMLButtonElement | null, "Unable to find Worktree button.", ); worktreeButton.click(); await vi.waitFor( () => { const prepareRequest = wsRequests.find( (request) => request._tag === WS_METHODS.gitPreparePullRequestThread, ); expect(prepareRequest).toMatchObject({ _tag: WS_METHODS.gitPreparePullRequestThread, cwd: "/repo/project", reference: "1359", mode: "worktree", threadId: THREAD_ID, }); }, { timeout: 8_000, interval: 16 }, ); expect( wsRequests.some( (request) => request._tag === WS_METHODS.terminalWrite && request.data === "bun install\r", ), ).toBe(false); } finally { await mounted.cleanup(); } }); it("sends bootstrap turn-starts and waits for server setup on first-send worktree drafts", async () => { useTerminalStateStore.setState({ terminalStateByThreadKey: {}, }); useComposerDraftStore.setState({ draftThreadsByThreadKey: { [THREAD_KEY]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, envMode: "worktree", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [PROJECT_DRAFT_KEY]: THREAD_KEY, }, }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: withProjectScripts(createDraftOnlySnapshot(), [ { id: "setup", name: "Setup", command: "bun install", icon: "configure", runOnWorktreeCreate: true, }, ]), resolveRpc: (body) => { if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { useComposerDraftStore.getState().setPrompt(THREAD_REF, "Ship it"); await waitForLayout(); const sendButton = await waitForSendButton(); expect(sendButton.disabled).toBe(false); sendButton.click(); await vi.waitFor( () => { const dispatchRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand, ) as | { _tag: string; type?: string; bootstrap?: { createThread?: { projectId?: string }; prepareWorktree?: { projectCwd?: string; baseBranch?: string; branch?: string }; runSetupScript?: boolean; }; } | undefined; expect(dispatchRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "thread.turn.start", bootstrap: { createThread: { projectId: PROJECT_ID, }, prepareWorktree: { projectCwd: "/repo/project", baseBranch: "main", branch: expect.stringMatching(/^t3code\/[0-9a-f]{8}$/), }, runSetupScript: true, }, }); }, { timeout: 8_000, interval: 16 }, ); expect(wsRequests.some((request) => request._tag === WS_METHODS.gitCreateWorktree)).toBe( false, ); expect( wsRequests.some( (request) => request._tag === WS_METHODS.terminalWrite && request.threadId === THREAD_ID && request.data === "bun install\r", ), ).toBe(false); } finally { await mounted.cleanup(); } }); it("keeps new-worktree mode on empty server threads and bootstraps the first send", async () => { const snapshot = addThreadToSnapshot(createDraftOnlySnapshot(), THREAD_ID); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: { ...snapshot, threads: snapshot.threads.map((thread) => thread.id === THREAD_ID ? Object.assign({}, thread, { session: null }) : thread, ), }, resolveRpc: (body) => { if (body._tag === WS_METHODS.gitListBranches) { return { isRepo: true, hasOriginRemote: true, nextCursor: null, totalCount: 1, branches: [ { name: "main", current: true, isDefault: true, worktreePath: null, }, ], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { (await waitForButtonByText("Current checkout")).click(); await page.getByText("New worktree", { exact: true }).click(); await vi.waitFor( () => { expect(findButtonByText("New worktree")).toBeTruthy(); }, { timeout: 8_000, interval: 16 }, ); useComposerDraftStore.getState().setPrompt(THREAD_REF, "Ship it"); await waitForLayout(); const sendButton = await waitForSendButton(); expect(sendButton.disabled).toBe(false); sendButton.click(); await vi.waitFor( () => { const turnStartRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "thread.turn.start", ) as | { _tag: string; type?: string; bootstrap?: { createThread?: { projectId?: string }; prepareWorktree?: { projectCwd?: string; baseBranch?: string; branch?: string }; runSetupScript?: boolean; }; } | undefined; expect(turnStartRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "thread.turn.start", bootstrap: { prepareWorktree: { projectCwd: "/repo/project", baseBranch: "main", branch: expect.stringMatching(/^t3code\/[0-9a-f]{8}$/), }, runSetupScript: true, }, }); expect(turnStartRequest?.bootstrap?.createThread).toBeUndefined(); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("updates the selected worktree base branch on empty server threads", async () => { const snapshot = addThreadToSnapshot(createDraftOnlySnapshot(), THREAD_ID); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: { ...snapshot, threads: snapshot.threads.map((thread) => thread.id === THREAD_ID ? Object.assign({}, thread, { session: null }) : thread, ), }, resolveRpc: (body) => { if (body._tag === WS_METHODS.gitListBranches) { return { isRepo: true, hasOriginRemote: true, nextCursor: null, totalCount: 2, branches: [ { name: "main", current: true, isDefault: true, worktreePath: null, }, { name: "release/next", current: false, isDefault: false, worktreePath: null, }, ], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { (await waitForButtonByText("Current checkout")).click(); await page.getByText("New worktree", { exact: true }).click(); await page.getByText("From main", { exact: true }).click(); await page.getByText("release/next", { exact: true }).click(); await vi.waitFor( () => { expect(findButtonByText("From release/next")).toBeTruthy(); }, { timeout: 8_000, interval: 16 }, ); useComposerDraftStore.getState().setPrompt(THREAD_REF, "Ship it"); await waitForLayout(); const sendButton = await waitForSendButton(); expect(sendButton.disabled).toBe(false); sendButton.click(); await vi.waitFor( () => { const turnStartRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "thread.turn.start", ) as | { _tag: string; type?: string; bootstrap?: { prepareWorktree?: { baseBranch?: string }; }; } | undefined; expect(turnStartRequest?.bootstrap?.prepareWorktree?.baseBranch).toBe("release/next"); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("clears pending worktree overrides when switching empty server threads", async () => { const secondThreadId = "thread-browser-test-second" as ThreadId; const snapshot = addThreadToSnapshot(createDraftOnlySnapshot(), THREAD_ID); const snapshotWithSecondThread = addThreadToSnapshot(snapshot, secondThreadId); const snapshotWithTwoThreads = { ...snapshotWithSecondThread, threads: snapshotWithSecondThread.threads.map((thread) => { if (thread.id === THREAD_ID) { return Object.assign({}, thread, { session: null, title: "Thread alpha" }); } if (thread.id === secondThreadId) { return Object.assign({}, thread, { session: null, title: "Thread beta" }); } return thread; }), }; const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: snapshotWithTwoThreads, resolveRpc: (body) => { if (body._tag === WS_METHODS.gitListBranches) { return { isRepo: true, hasOriginRemote: true, nextCursor: null, totalCount: 2, branches: [ { name: "main", current: true, isDefault: true, worktreePath: null, }, { name: "release/next", current: false, isDefault: false, worktreePath: null, }, ], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { (await waitForButtonByText("Current checkout")).click(); await page.getByText("New worktree", { exact: true }).click(); await page.getByText("From main", { exact: true }).click(); await page.getByText("release/next", { exact: true }).click(); await vi.waitFor( () => { expect(findButtonByText("From release/next")).toBeTruthy(); }, { timeout: 8_000, interval: 16 }, ); await mounted.router.navigate({ to: "/$environmentId/$threadId", params: { environmentId: LOCAL_ENVIRONMENT_ID, threadId: secondThreadId, }, }); await waitForURL( mounted.router, (path) => path === serverThreadPath(secondThreadId), "Route should switch to the second empty server thread.", ); await vi.waitFor( () => { expect(findButtonByText("Current checkout")).toBeTruthy(); expect(findButtonByText("From release/next")).toBeNull(); }, { timeout: 8_000, interval: 16 }, ); (await waitForButtonByText("Current checkout")).click(); await page.getByText("New worktree", { exact: true }).click(); await vi.waitFor( () => { expect(findButtonByText("From main")).toBeTruthy(); expect(findButtonByText("From release/next")).toBeNull(); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("shows the send state once bootstrap dispatch is in flight", async () => { useTerminalStateStore.setState({ terminalStateByThreadKey: {}, }); useComposerDraftStore.setState({ draftThreadsByThreadKey: { [THREAD_KEY]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, envMode: "worktree", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [PROJECT_DRAFT_KEY]: THREAD_KEY, }, }); let resolveDispatch!: (value: { sequence: number }) => void; const dispatchPromise = new Promise<{ sequence: number }>((resolve) => { resolveDispatch = resolve; }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: withProjectScripts(createDraftOnlySnapshot(), [ { id: "setup", name: "Setup", command: "bun install", icon: "configure", runOnWorktreeCreate: true, }, ]), resolveRpc: (body) => { if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return dispatchPromise; } return undefined; }, }); try { useComposerDraftStore.getState().setPrompt(THREAD_REF, "Ship it"); await waitForLayout(); const sendButton = await waitForSendButton(); expect(sendButton.disabled).toBe(false); sendButton.click(); await vi.waitFor( () => { expect( wsRequests.some((request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand), ).toBe(true); expect(document.querySelector('button[aria-label="Sending"]')).toBeTruthy(); expect(document.querySelector('button[aria-label="Preparing worktree"]')).toBeNull(); }, { timeout: 8_000, interval: 16 }, ); } finally { resolveDispatch({ sequence: fixture.snapshot.snapshotSequence + 1 }); await mounted.cleanup(); } }); it("toggles plan mode with Shift+Tab only while the composer is focused", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-target-hotkey" as MessageId, targetText: "hotkey target", }), }); try { const initialModeButton = await waitForInteractionModeButton("Build"); expect(initialModeButton.title).toContain("enter plan mode"); window.dispatchEvent( new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true, cancelable: true, }), ); await waitForLayout(); expect((await waitForInteractionModeButton("Build")).title).toContain("enter plan mode"); const composerEditor = await waitForComposerEditor(); composerEditor.focus(); composerEditor.dispatchEvent( new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true, cancelable: true, }), ); await vi.waitFor( async () => { expect((await waitForInteractionModeButton("Plan")).title).toContain( "return to normal build mode", ); }, { timeout: 8_000, interval: 16 }, ); composerEditor.dispatchEvent( new KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true, cancelable: true, }), ); await vi.waitFor( async () => { expect((await waitForInteractionModeButton("Build")).title).toContain("enter plan mode"); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("uses the active draft route session when changing the base branch", async () => { const staleDraftId = draftIdFromPath("/draft/draft-stale-branch-session"); const activeDraftId = draftIdFromPath("/draft/draft-active-branch-session"); useComposerDraftStore.setState({ draftThreadsByThreadKey: { [staleDraftId]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: `${PROJECT_DRAFT_KEY}:stale`, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, envMode: "worktree", }, [activeDraftId]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, envMode: "worktree", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [`${PROJECT_DRAFT_KEY}:stale`]: staleDraftId, [PROJECT_DRAFT_KEY]: activeDraftId, }, }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), initialPath: `/draft/${activeDraftId}`, resolveRpc: (body) => { if (body._tag === WS_METHODS.gitListBranches) { return { isRepo: true, hasOriginRemote: true, nextCursor: null, totalCount: 2, branches: [ { name: "main", current: true, isDefault: true, worktreePath: null, }, { name: "release/next", current: false, isDefault: false, worktreePath: null, }, ], }; } return undefined; }, }); try { const branchButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "From main", ) as HTMLButtonElement | null, 'Unable to find branch selector button with "From main".', ); branchButton.click(); const branchOption = await waitForElement( () => Array.from(document.querySelectorAll("span")).find( (element) => element.textContent?.trim() === "release/next", ) as HTMLSpanElement | null, 'Unable to find the "release/next" branch option.', ); branchOption.click(); await vi.waitFor( () => { expect(useComposerDraftStore.getState().getDraftSession(activeDraftId)?.branch).toBe( "release/next", ); expect(useComposerDraftStore.getState().getDraftSession(staleDraftId)?.branch).toBe( "main", ); }, { timeout: 8_000, interval: 16 }, ); await vi.waitFor( () => { const updatedButton = Array.from(document.querySelectorAll("button")).find((button) => button.textContent?.trim().includes("From release/next"), ); expect(updatedButton).toBeTruthy(); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("keeps the new worktree branch picker anchored at the top when opening with a preselected branch", async () => { const draftId = DraftId.make("draft-branch-picker-scroll-regression"); const branches = [ { name: "feature/current", current: true, isDefault: false, worktreePath: null, }, { name: "main", current: false, isDefault: true, worktreePath: null, }, ...Array.from({ length: 48 }, (_, index) => ({ name: `feature/${String(index).padStart(2, "0")}`, current: false, isDefault: false, worktreePath: null, })), { name: "feature/selected", current: false, isDefault: false, worktreePath: null, }, ]; useComposerDraftStore.setState({ draftThreadsByThreadKey: { [draftId]: { threadId: THREAD_ID, environmentId: LOCAL_ENVIRONMENT_ID, projectId: PROJECT_ID, logicalProjectKey: PROJECT_DRAFT_KEY, createdAt: NOW_ISO, runtimeMode: "full-access", interactionMode: "default", branch: "feature/selected", worktreePath: null, envMode: "worktree", }, }, logicalProjectDraftThreadKeyByLogicalProjectKey: { [PROJECT_DRAFT_KEY]: draftId, }, }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createDraftOnlySnapshot(), initialPath: `/draft/${draftId}`, resolveRpc: (body) => { if (body._tag === WS_METHODS.gitListBranches) { return { isRepo: true, hasOriginRemote: true, nextCursor: null, totalCount: branches.length, branches, }; } return undefined; }, }); try { const branchButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "From feature/selected", ) as HTMLButtonElement | null, 'Unable to find branch selector button with "From feature/selected".', ); branchButton.click(); await waitForElement( () => document.querySelector('input[placeholder="Search branches..."]'), "Unable to find branch search input.", ); const popup = await waitForElement( () => document.querySelector('[data-slot="combobox-popup"]'), "Unable to find the branch picker popup.", ); await vi.waitFor( () => { const popupSpans = Array.from(popup.querySelectorAll("span")); expect( popupSpans.some((element) => element.textContent?.trim() === "feature/current"), ).toBe(true); expect(popupSpans.some((element) => element.textContent?.trim() === "main")).toBe(true); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("surrounds selected plain text and preserves the inner selection for repeated wrapping", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-surround-basic" as MessageId, targetText: "surround basic", }), }); try { useComposerDraftStore.getState().setPrompt(THREAD_REF, "selected"); await waitForComposerText("selected"); await setComposerSelectionByTextOffsets({ start: 0, end: "selected".length }); await pressComposerKey("("); await waitForComposerText("(selected)"); await pressComposerKey("["); await waitForComposerText("([selected])"); } finally { await mounted.cleanup(); } }); it("leaves collapsed-caret typing unchanged for surround symbols", async () => { useComposerDraftStore.getState().setPrompt(THREAD_REF, "selected"); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-surround-collapsed" as MessageId, targetText: "surround collapsed", }), }); try { await waitForComposerText("selected"); await setComposerSelectionByTextOffsets({ start: "selected".length, end: "selected".length, }); await pressComposerKey("("); await waitForComposerText("selected("); } finally { await mounted.cleanup(); } }); it("supports symmetric and backward-selection surrounds", async () => { useComposerDraftStore.getState().setPrompt(THREAD_REF, "backward"); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-surround-backward" as MessageId, targetText: "surround backward", }), }); try { await waitForComposerText("backward"); await setComposerSelectionByTextOffsets({ start: 0, end: "backward".length, direction: "backward", }); await pressComposerKey("*"); await waitForComposerText("*backward*"); } finally { await mounted.cleanup(); } }); it("supports option-produced surround symbols like guillemets", async () => { useComposerDraftStore.getState().setPrompt(THREAD_REF, "quoted"); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-surround-guillemet" as MessageId, targetText: "surround guillemet", }), }); try { await waitForComposerText("quoted"); await setComposerSelectionByTextOffsets({ start: 0, end: "quoted".length }); await pressComposerKey("«"); await waitForComposerText("«quoted»"); } finally { await mounted.cleanup(); } }); it("supports dead-key composition that resolves to another surround symbol without an extra undo step", async () => { useComposerDraftStore.getState().setPrompt(THREAD_REF, "quoted"); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-surround-dead-quote" as MessageId, targetText: "surround dead quote", }), }); try { await waitForComposerText("quoted"); await setComposerSelectionByTextOffsets({ start: 0, end: "quoted".length }); const composerEditor = await waitForComposerEditor(); composerEditor.focus(); composerEditor.dispatchEvent( new KeyboardEvent("keydown", { key: "Dead", bubbles: true, cancelable: true, }), ); composerEditor.dispatchEvent( new InputEvent("beforeinput", { data: "'", inputType: "insertCompositionText", bubbles: true, cancelable: true, }), ); const resolvedInputEvent = new InputEvent("beforeinput", { data: "'", inputType: "insertText", bubbles: true, cancelable: true, }); composerEditor.dispatchEvent(resolvedInputEvent); expect(resolvedInputEvent.defaultPrevented).toBe(true); await waitForComposerText("'quoted'"); await pressComposerUndo(); await waitForComposerText("quoted"); } finally { await mounted.cleanup(); } }); it("surrounds text after a mention using the correct expanded offsets", async () => { useComposerDraftStore.getState().setPrompt(THREAD_REF, "hi @package.json there"); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-surround-after-mention" as MessageId, targetText: "surround after mention", }), }); try { await vi.waitFor( () => { expect(document.body.textContent).toContain("package.json"); }, { timeout: 8_000, interval: 16 }, ); await waitForComposerText("hi @package.json there"); await setComposerSelectionByTextOffsets({ start: "hi package.json ".length, end: "hi package.json there".length, }); await pressComposerKey("("); await waitForComposerText("hi @package.json (there)"); } finally { await mounted.cleanup(); } }); it("falls back to normal replacement when the selection includes a mention token", async () => { useComposerDraftStore.getState().setPrompt(THREAD_REF, "hi @package.json there "); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-surround-token" as MessageId, targetText: "surround token", }), }); try { await vi.waitFor( () => { expect(document.body.textContent).toContain("package.json"); }, { timeout: 8_000, interval: 16 }, ); await selectAllComposerContent(); await pressComposerKey("("); await waitForComposerText("("); } finally { await mounted.cleanup(); } }); it("shows runtime mode descriptions in the desktop composer access select", async () => { setDraftThreadWithoutWorktree(); const mounted = await mountChatView({ viewport: WIDE_FOOTER_VIEWPORT, snapshot: createDraftOnlySnapshot(), }); try { const runtimeModeSelect = await waitForButtonByText("Full access"); runtimeModeSelect.click(); expect((await waitForSelectItemContainingText("Supervised")).textContent).toContain( "Ask before commands and file changes", ); const autoAcceptItem = await waitForSelectItemContainingText("Auto-accept edits"); expect(autoAcceptItem.textContent).toContain("Auto-approve edits"); expect((await waitForSelectItemContainingText("Full access")).textContent).toContain( "Allow commands and edits without prompts", ); } finally { await mounted.cleanup(); } }); it("keeps removed terminal context pills removed when a new one is added", async () => { const removedLabel = "Terminal 1 lines 1-2"; const addedLabel = "Terminal 2 lines 9-10"; useComposerDraftStore.getState().addTerminalContext( THREAD_REF, createTerminalContext({ id: "ctx-removed", terminalLabel: "Terminal 1", lineStart: 1, lineEnd: 2, text: "bun i\nno changes", }), ); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-terminal-pill-backspace" as MessageId, targetText: "terminal pill backspace target", }), }); try { await vi.waitFor( () => { expect(document.body.textContent).toContain(removedLabel); }, { timeout: 8_000, interval: 16 }, ); const store = useComposerDraftStore.getState(); const currentPrompt = store.draftsByThreadKey[THREAD_KEY]?.prompt ?? ""; const nextPrompt = removeInlineTerminalContextPlaceholder(currentPrompt, 0); store.setPrompt(THREAD_REF, nextPrompt.prompt); store.removeTerminalContext(THREAD_REF, "ctx-removed"); await vi.waitFor( () => { expect(useComposerDraftStore.getState().draftsByThreadKey[THREAD_KEY]).toBeUndefined(); expect(document.body.textContent).not.toContain(removedLabel); }, { timeout: 8_000, interval: 16 }, ); useComposerDraftStore.getState().addTerminalContext( THREAD_REF, createTerminalContext({ id: "ctx-added", terminalLabel: "Terminal 2", lineStart: 9, lineEnd: 10, text: "git status\nOn branch main", }), ); await vi.waitFor( () => { const draft = useComposerDraftStore.getState().draftsByThreadKey[THREAD_KEY]; expect(draft?.terminalContexts.map((context) => context.id)).toEqual(["ctx-added"]); expect(document.body.textContent).toContain(addedLabel); expect(document.body.textContent).not.toContain(removedLabel); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("disables send when the composer only contains an expired terminal pill", async () => { const expiredLabel = "Terminal 1 line 4"; useComposerDraftStore.getState().addTerminalContext( THREAD_REF, createTerminalContext({ id: "ctx-expired-only", terminalLabel: "Terminal 1", lineStart: 4, lineEnd: 4, text: "", }), ); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-expired-pill-disabled" as MessageId, targetText: "expired pill disabled target", }), }); try { await vi.waitFor( () => { expect(document.body.textContent).toContain(expiredLabel); }, { timeout: 8_000, interval: 16 }, ); const sendButton = await waitForSendButton(); expect(sendButton.disabled).toBe(true); } finally { await mounted.cleanup(); } }); it("warns when sending text while omitting expired terminal pills", async () => { const expiredLabel = "Terminal 1 line 4"; useComposerDraftStore.getState().addTerminalContext( THREAD_REF, createTerminalContext({ id: "ctx-expired-send-warning", terminalLabel: "Terminal 1", lineStart: 4, lineEnd: 4, text: "", }), ); useComposerDraftStore .getState() .setPrompt(THREAD_REF, `yoo${INLINE_TERMINAL_CONTEXT_PLACEHOLDER}waddup`); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-expired-pill-warning" as MessageId, targetText: "expired pill warning target", }), }); try { await vi.waitFor( () => { expect(document.body.textContent).toContain(expiredLabel); }, { timeout: 8_000, interval: 16 }, ); const sendButton = await waitForSendButton(); expect(sendButton.disabled).toBe(false); sendButton.click(); await vi.waitFor( () => { expect(document.body.textContent).toContain( "Expired terminal context omitted from message", ); expect(document.body.textContent).not.toContain(expiredLabel); expect(document.body.textContent).toContain("yoowaddup"); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("shows a pointer cursor for the running stop button", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-stop-button-cursor" as MessageId, targetText: "stop button cursor target", sessionStatus: "running", }), }); try { const stopButton = await waitForElement( () => document.querySelector('button[aria-label="Stop generation"]'), "Unable to find stop generation button.", ); expect(getComputedStyle(stopButton).cursor).toBe("pointer"); } finally { await mounted.cleanup(); } }); it("hides the archive action when the pointer leaves a thread row", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-archive-hover-test" as MessageId, targetText: "archive hover target", }), }); try { const threadRow = page.getByTestId(`thread-row-${THREAD_ID}`); await expect.element(threadRow).toBeInTheDocument(); const archiveButton = await waitForElement( () => document.querySelector(`[data-testid="thread-archive-${THREAD_ID}"]`), "Unable to find archive button.", ); const archiveAction = archiveButton.parentElement; expect( archiveAction, "Archive button should render inside a visibility wrapper.", ).not.toBeNull(); expect(getComputedStyle(archiveAction!).opacity).toBe("0"); await threadRow.hover(); await vi.waitFor( () => { expect(getComputedStyle(archiveAction!).opacity).toBe("1"); }, { timeout: 4_000, interval: 16 }, ); await page.getByTestId("composer-editor").hover(); await vi.waitFor( () => { expect(getComputedStyle(archiveAction!).opacity).toBe("0"); }, { timeout: 4_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("exposes the full thread title on the sidebar row tooltip", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-thread-tooltip-target" as MessageId, targetText: "thread tooltip target", }), }); try { const threadTitle = page.getByTestId(`thread-title-${THREAD_ID}`); await expect.element(threadTitle).toBeInTheDocument(); await threadTitle.hover(); await vi.waitFor( () => { const tooltip = document.querySelector('[data-slot="tooltip-popup"]'); expect(tooltip).not.toBeNull(); expect(tooltip?.textContent).toContain(THREAD_TITLE); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("shows the confirm archive action after clicking the archive button", async () => { localStorage.setItem( "t3code:client-settings:v1", JSON.stringify({ ...DEFAULT_CLIENT_SETTINGS, confirmThreadArchive: true, }), ); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-archive-confirm-test" as MessageId, targetText: "archive confirm target", }), }); try { const threadRow = page.getByTestId(`thread-row-${THREAD_ID}`); await expect.element(threadRow).toBeInTheDocument(); await threadRow.hover(); const archiveButton = page.getByTestId(`thread-archive-${THREAD_ID}`); await expect.element(archiveButton).toBeInTheDocument(); await archiveButton.click(); const confirmButton = page.getByTestId(`thread-archive-confirm-${THREAD_ID}`); await expect.element(confirmButton).toBeInTheDocument(); await expect.element(confirmButton).toBeVisible(); } finally { localStorage.removeItem("t3code:client-settings:v1"); await mounted.cleanup(); } }); it("canonicalizes promoted draft threads to the server thread route", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-new-thread-test" as MessageId, targetText: "new thread selection test", }), }); try { // Wait for the sidebar to render with the project. const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); // The route should change to a new draft thread ID. const newThreadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread UUID.", ); const newDraftId = draftIdFromPath(newThreadPath); const newThreadId = draftThreadIdFor(newDraftId); // The composer editor should be present for the new draft thread. await waitForComposerEditor(); // `thread.created` should only mark the draft as promoting; it should // not navigate away until the server thread has actual runtime state. await materializePromotedDraftThreadViaDomainEvent(newThreadId); expect(mounted.router.state.location.pathname).toBe(newThreadPath); await expect.element(page.getByTestId("composer-editor")).toBeInTheDocument(); // Once the server thread starts, the route should canonicalize. await startPromotedServerThreadViaDomainEvent(newThreadId); await vi.waitFor( () => { expect(useComposerDraftStore.getState().draftThreadsByThreadKey[newDraftId]).toBe( undefined, ); }, { timeout: 8_000, interval: 16 }, ); // The route should switch to the canonical server thread path. await waitForURL( mounted.router, (path) => path === serverThreadPath(newThreadId), "Promoted drafts should canonicalize to the server thread route.", ); // The composer should remain usable after canonicalization, regardless of // whether the promoted thread is still visibly empty or has already // entered the running state. await expect.element(page.getByTestId("composer-editor")).toBeInTheDocument(); } finally { await mounted.cleanup(); } }); it("canonicalizes stale promoted draft routes to the server thread route", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-draft-hydration-race-test" as MessageId, targetText: "draft hydration race test", }), }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); const newThreadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread UUID.", ); const newDraftId = draftIdFromPath(newThreadPath); const newThreadId = draftThreadIdFor(newDraftId); await promoteDraftThreadViaDomainEvent(newThreadId); await mounted.router.navigate({ to: "/draft/$draftId", params: { draftId: newDraftId }, }); await waitForURL( mounted.router, (path) => path === serverThreadPath(newThreadId), "Stale promoted draft routes should canonicalize to the server thread path.", ); await expect.element(page.getByTestId("composer-editor")).toBeInTheDocument(); } finally { await mounted.cleanup(); } }); it("creates a fresh worktree draft from an existing worktree thread when the default mode is worktree", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: { ...createSnapshotForTargetUser({ targetMessageId: "msg-user-new-thread-worktree-default-test" as MessageId, targetText: "new thread worktree default test", }), threads: createSnapshotForTargetUser({ targetMessageId: "msg-user-new-thread-worktree-default-test" as MessageId, targetText: "new thread worktree default test", }).threads.map((thread) => thread.id === THREAD_ID ? Object.assign({}, thread, { branch: "feature/existing", worktreePath: "/repo/.t3/worktrees/existing", }) : thread, ), }, configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, settings: { ...nextFixture.serverConfig.settings, defaultThreadEnvMode: "worktree", }, }; }, }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); const newThreadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should change to a new draft thread.", ); const newDraftId = draftIdFromPath(newThreadPath); expect(useComposerDraftStore.getState().getDraftSession(newDraftId)).toMatchObject({ envMode: "worktree", worktreePath: null, }); } finally { await mounted.cleanup(); } }); it("creates a new draft instead of reusing a promoting draft thread", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-promoting-draft-new-thread-test" as MessageId, targetText: "promoting draft new thread test", }), }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); const firstDraftPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should change to the first draft thread.", ); const firstDraftId = draftIdFromPath(firstDraftPath); const firstThreadId = draftThreadIdFor(firstDraftId); await materializePromotedDraftThreadViaDomainEvent(firstThreadId); expect(mounted.router.state.location.pathname).toBe(firstDraftPath); await newThreadButton.click(); const secondDraftPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path) && path !== firstDraftPath, "Route should change to a second draft thread instead of reusing the promoting draft.", ); expect(draftIdFromPath(secondDraftPath)).not.toBe(firstDraftId); } finally { await mounted.cleanup(); } }); it("snapshots sticky codex settings into a new draft thread", async () => { useComposerDraftStore.setState({ stickyModelSelectionByProvider: { codex: createModelSelection("codex", "gpt-5.3-codex", [ { id: "reasoningEffort", value: "medium" }, { id: "fastMode", value: true }, ]), }, stickyActiveProvider: "codex", }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-sticky-codex-traits-test" as MessageId, targetText: "sticky codex traits test", }), }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); const newThreadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread UUID.", ); const newDraftId = draftIdFromPath(newThreadPath); // `toMatchObject` matches objects loosely (extras ignored) but compares // arrays strictly, so wrap `options` in `arrayContaining` to keep the // assertion focused on sticky `fastMode` carrying over without asserting // on exactly which other options are preserved. expect(composerDraftFor(newDraftId)).toMatchObject({ modelSelectionByProvider: { codex: { provider: "codex", model: "gpt-5.3-codex", options: expect.arrayContaining([{ id: "fastMode", value: true }]), }, }, activeProvider: "codex", }); } finally { await mounted.cleanup(); } }); it("hydrates the provider alongside a sticky claude model", async () => { useComposerDraftStore.setState({ stickyModelSelectionByProvider: { claudeAgent: createModelSelection("claudeAgent", "claude-opus-4-6", [ { id: "effort", value: "max" }, { id: "fastMode", value: true }, ]), }, stickyActiveProvider: "claudeAgent", }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-sticky-claude-model-test" as MessageId, targetText: "sticky claude model test", }), }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); const newThreadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new sticky claude draft thread UUID.", ); const newDraftId = draftIdFromPath(newThreadPath); expect(composerDraftFor(newDraftId)).toMatchObject({ modelSelectionByProvider: { claudeAgent: createModelSelection("claudeAgent", "claude-opus-4-6", [ { id: "effort", value: "max" }, { id: "fastMode", value: true }, ]), }, activeProvider: "claudeAgent", }); } finally { await mounted.cleanup(); } }); it("falls back to defaults when no sticky composer settings exist", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-default-codex-traits-test" as MessageId, targetText: "default codex traits test", }), }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); const newThreadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread UUID.", ); const newDraftId = draftIdFromPath(newThreadPath); expect(composerDraftFor(newDraftId)).toBe(undefined); } finally { await mounted.cleanup(); } }); it("prefers draft state over sticky composer settings and defaults", async () => { useComposerDraftStore.setState({ stickyModelSelectionByProvider: { codex: createModelSelection("codex", "gpt-5.3-codex", [ { id: "reasoningEffort", value: "medium" }, { id: "fastMode", value: true }, ]), }, stickyActiveProvider: "codex", }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-draft-codex-traits-precedence-test" as MessageId, targetText: "draft codex traits precedence test", }), }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await newThreadButton.click(); const threadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a sticky draft thread UUID.", ); const draftId = draftIdFromPath(threadPath); // See the note on the sibling sticky-codex test: arrays match strictly // under `toMatchObject`, so use `arrayContaining` to keep the assertion // scoped to the sticky trait (`fastMode`) that must carry over. expect(composerDraftFor(draftId)).toMatchObject({ modelSelectionByProvider: { codex: { provider: "codex", model: "gpt-5.3-codex", options: expect.arrayContaining([{ id: "fastMode", value: true }]), }, }, activeProvider: "codex", }); useComposerDraftStore.getState().setModelSelection( draftId, createModelSelection("codex", "gpt-5.4", [ { id: "reasoningEffort", value: "low" }, { id: "fastMode", value: true }, ]), ); await newThreadButton.click(); await waitForURL( mounted.router, (path) => path === threadPath, "New-thread should reuse the existing project draft thread.", ); expect(composerDraftFor(draftId)).toMatchObject({ modelSelectionByProvider: { codex: createModelSelection("codex", "gpt-5.4", [ { id: "reasoningEffort", value: "low" }, { id: "fastMode", value: true }, ]), }, activeProvider: "codex", }); } finally { await mounted.cleanup(); } }); it("creates a new thread from the global chat.new shortcut", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-chat-shortcut-test" as MessageId, targetText: "chat shortcut test", }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "chat.new", shortcut: { key: "o", metaKey: false, ctrlKey: false, shiftKey: true, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, { command: "thread.jump.1", shortcut: { key: "1", metaKey: true, ctrlKey: false, shiftKey: false, altKey: false, modKey: false, }, }, { command: "modelPicker.jump.1", shortcut: { key: "1", metaKey: true, ctrlKey: false, shiftKey: false, altKey: false, modKey: false, }, whenAst: { type: "identifier", name: "modelPickerOpen" }, }, ], }; }, }); try { await waitForNewThreadShortcutLabel(); await waitForServerConfigToApply(); const composerEditor = await waitForComposerEditor(); composerEditor.focus(); await waitForLayout(); await triggerChatNewShortcutUntilPath( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread UUID from the shortcut.", ); } finally { await mounted.cleanup(); } }); it("does not consume chat.new when there is no project context", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createProjectlessSnapshot(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "chat.new", shortcut: { key: "o", metaKey: false, ctrlKey: false, shiftKey: true, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { await waitForServerConfigToApply(); dispatchChatNewShortcut(); await waitForLayout(); expect(mounted.router.state.location.pathname).toBe(serverThreadPath(THREAD_ID)); expect(Object.keys(useComposerDraftStore.getState().draftThreadsByThreadKey)).toHaveLength(0); } finally { await mounted.cleanup(); } }); it("renders the configurable shortcut and runs a command from the sidebar trigger", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-shortcut-test" as MessageId, targetText: "command palette shortcut test", }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await expect .element(palette.getByText("New thread in Project", { exact: true })) .toBeInTheDocument(); await palette.getByText("New thread in Project", { exact: true }).click(); await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread UUID from the command palette.", ); } finally { await mounted.cleanup(); } }); it("filters command palette results as the user types", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-search-test" as MessageId, targetText: "command palette search test", }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await page.getByPlaceholder("Search commands, projects, and threads...").fill("settings"); await expect.element(palette.getByText("Open settings", { exact: true })).toBeInTheDocument(); await expect .element(palette.getByText("New thread in Project", { exact: true })) .not.toBeInTheDocument(); } finally { await mounted.cleanup(); } }); it("adds a project from browse mode with Enter when no directory is highlighted", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-add-project-enter" as MessageId, targetText: "command palette add project enter", }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, resolveRpc: (body) => { if (body._tag === WS_METHODS.filesystemBrowse) { if (body.partialPath === "~/Development/") { return { parentPath: "~/Development/", entries: [ { name: "alpha", fullPath: "~/Development/alpha" }, { name: "beta", fullPath: "~/Development/beta" }, ], }; } return { parentPath: "~/", entries: [{ name: "Development", fullPath: "~/Development" }], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await palette.getByText("Add project", { exact: true }).click(); const browseInput = await waitForCommandPaletteInput(ADD_PROJECT_SUBMENU_PLACEHOLDER); await page.getByPlaceholder(ADD_PROJECT_SUBMENU_PLACEHOLDER).fill("~/Development/"); await expect.element(palette.getByText("alpha", { exact: true })).toBeInTheDocument(); await expect .element(palette.getByRole("button", { name: "Add (Enter)" })) .toBeInTheDocument(); await dispatchInputKey(browseInput, { key: "Enter" }); await vi.waitFor( () => { const dispatchRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "project.create", ) as | { _tag: string; type?: string; workspaceRoot?: string; title?: string; } | undefined; expect(dispatchRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "project.create", workspaceRoot: "~/Development", title: "Development", }); }, { timeout: 8_000, interval: 16 }, ); await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread after adding a project with Enter.", ); } finally { await mounted.cleanup(); } }); it("opens add project browse mode from the sidebar add button", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-sidebar-add-project-trigger" as MessageId, targetText: "sidebar add project trigger", }), resolveRpc: (body) => { if (body._tag === WS_METHODS.filesystemBrowse) { return { parentPath: "~/", entries: [{ name: "Development", fullPath: "~/Development" }], }; } return undefined; }, }); try { await waitForServerConfigToApply(); await page.getByTestId("sidebar-add-project-trigger").click(); const palette = page.getByTestId("command-palette"); await expect.element(palette).toBeInTheDocument(); const browseInput = await waitForCommandPaletteInput(ADD_PROJECT_SUBMENU_PLACEHOLDER); await expect.element(browseInput).toHaveValue("~/"); await vi.waitFor( () => { expect( wsRequests.some( (request) => request._tag === WS_METHODS.filesystemBrowse && request.partialPath === "~/", ), ).toBe(true); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("starts add project browse mode from the configured base directory", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-sidebar-add-project-custom-base-dir" as MessageId, targetText: "sidebar add project custom base directory", }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, settings: { ...nextFixture.serverConfig.settings, addProjectBaseDirectory: "~/Development", }, }; }, resolveRpc: (body) => { if (body._tag === WS_METHODS.filesystemBrowse) { if (body.partialPath === "~/Development/") { return { parentPath: "~/Development/", entries: [{ name: "codething", fullPath: "~/Development/codething" }], }; } return { parentPath: "~/", entries: [{ name: "Development", fullPath: "~/Development" }], }; } return undefined; }, }); try { await waitForServerConfigToApply(); await page.getByTestId("sidebar-add-project-trigger").click(); const palette = page.getByTestId("command-palette"); await expect.element(palette).toBeInTheDocument(); const browseInput = await waitForCommandPaletteInput(ADD_PROJECT_SUBMENU_PLACEHOLDER); await expect.element(browseInput).toHaveValue("~/Development/"); await vi.waitFor( () => { expect( wsRequests.some( (request) => request._tag === WS_METHODS.filesystemBrowse && request.partialPath === "~/Development/", ), ).toBe(true); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("shows create-folder affordances for missing project paths", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-create-missing-project" as MessageId, targetText: "command palette create missing project", }), resolveRpc: (body) => { if (body._tag === WS_METHODS.filesystemBrowse) { if (body.partialPath === "~/Desktop/") { return { parentPath: "~/Desktop/", entries: [{ name: "existing", fullPath: "~/Desktop/existing" }], }; } return { parentPath: "~/", entries: [{ name: "Desktop", fullPath: "~/Desktop" }], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { await waitForServerConfigToApply(); const palette = page.getByTestId("command-palette"); await page.getByTestId("sidebar-add-project-trigger").click(); await expect.element(palette).toBeInTheDocument(); const browseInput = await waitForCommandPaletteInput(ADD_PROJECT_SUBMENU_PLACEHOLDER); await page.getByPlaceholder(ADD_PROJECT_SUBMENU_PLACEHOLDER).fill("~/Desktop/fresh-project"); await expect .element(palette.getByRole("button", { name: "Create & Add (Enter)" })) .toBeInTheDocument(); await expect.element(palette.getByText("Will create this folder")).not.toBeInTheDocument(); await dispatchInputKey(browseInput, { key: "Enter" }); await vi.waitFor( () => { const dispatchRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "project.create", ) as | { _tag: string; type?: string; workspaceRoot?: string; title?: string; createWorkspaceRootIfMissing?: boolean; } | undefined; expect(dispatchRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "project.create", workspaceRoot: "~/Desktop/fresh-project", title: "fresh-project", createWorkspaceRootIfMissing: true, }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("does not show create affordances for an existing directory with a trailing slash", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-existing-trailing-directory" as MessageId, targetText: "command palette existing trailing directory", }), resolveRpc: (body) => { if (body._tag === WS_METHODS.filesystemBrowse) { if (body.partialPath === "~/Development/codex/") { return { parentPath: "~/Development/codex/", entries: [{ name: "Codex.app", fullPath: "~/Development/codex/Codex.app" }], }; } return { parentPath: "~/", entries: [{ name: "Development", fullPath: "~/Development" }], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { await waitForServerConfigToApply(); const palette = page.getByTestId("command-palette"); await page.getByTestId("sidebar-add-project-trigger").click(); await expect.element(palette).toBeInTheDocument(); const browseInput = await waitForCommandPaletteInput(ADD_PROJECT_SUBMENU_PLACEHOLDER); await page.getByPlaceholder(ADD_PROJECT_SUBMENU_PLACEHOLDER).fill("~/Development/codex/"); await vi.waitFor( () => { expect( wsRequests.some( (request) => request._tag === WS_METHODS.filesystemBrowse && request.partialPath === "~/Development/codex/", ), ).toBe(true); }, { timeout: 8_000, interval: 16 }, ); await expect .element(palette.getByRole("button", { name: "Add (Enter)" })) .toBeInTheDocument(); await expect .element(palette.getByRole("button", { name: "Create & Add (Enter)" })) .not.toBeInTheDocument(); await dispatchInputKey(browseInput, { key: "Enter" }); await vi.waitFor( () => { const dispatchRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "project.create", ) as | { _tag: string; type?: string; workspaceRoot?: string; title?: string; } | undefined; expect(dispatchRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "project.create", workspaceRoot: "~/Development/codex", title: "codex", }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("selects an environment before browsing when multiple environments are available", async () => { const remoteBrowseMock = vi.fn(async ({ partialPath }: { partialPath: string }) => { if (partialPath === "~/workspaces/") { return { parentPath: "~/workspaces/", entries: [{ name: "codething", fullPath: "~/workspaces/codething" }], }; } return { parentPath: "~/", entries: [{ name: "workspaces", fullPath: "~/workspaces" }], }; }); const remoteDispatchMock = vi.fn(async () => ({ sequence: fixture.snapshot.snapshotSequence + 1, })); __setEnvironmentApiOverrideForTests( REMOTE_ENVIRONMENT_ID, createMockEnvironmentApi({ browse: remoteBrowseMock, dispatchCommand: remoteDispatchMock, }), ); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-add-project-multi-env" as MessageId, targetText: "command palette add project multi env", }), }); try { await waitForServerConfigToApply(); useSavedEnvironmentRegistryStore.getState().upsert({ environmentId: REMOTE_ENVIRONMENT_ID, label: "Staging", httpBaseUrl: "https://staging.example.test", wsBaseUrl: "wss://staging.example.test/ws", createdAt: NOW_ISO, lastConnectedAt: NOW_ISO, }); useSavedEnvironmentRuntimeStore.getState().patch(REMOTE_ENVIRONMENT_ID, { connectionState: "connected", authState: "authenticated", descriptor: { ...fixture.serverConfig.environment, environmentId: REMOTE_ENVIRONMENT_ID, label: "Staging", }, serverConfig: { ...fixture.serverConfig, environment: { ...fixture.serverConfig.environment, environmentId: REMOTE_ENVIRONMENT_ID, label: "Staging", }, settings: { ...fixture.serverConfig.settings, addProjectBaseDirectory: "~/workspaces", }, }, connectedAt: NOW_ISO, }); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await palette.getByText("Add project", { exact: true }).click(); await expect.element(palette.getByText("Environments", { exact: true })).toBeInTheDocument(); await expect .element(palette.getByText("This device", { exact: true }).first()) .toBeInTheDocument(); await palette.getByText("Staging", { exact: true }).click(); const browseInput = await waitForCommandPaletteInput(ADD_PROJECT_SUBMENU_PLACEHOLDER); await expect.element(browseInput).toHaveValue("~/workspaces/"); await vi.waitFor( () => { expect(remoteBrowseMock).toHaveBeenCalledWith({ partialPath: "~/workspaces/" }); }, { timeout: 8_000, interval: 16 }, ); await page.getByPlaceholder(ADD_PROJECT_SUBMENU_PLACEHOLDER).fill("~/workspaces/"); await vi.waitFor( () => { expect(remoteBrowseMock).toHaveBeenCalledWith({ partialPath: "~/workspaces/" }); }, { timeout: 8_000, interval: 16 }, ); await expect.element(palette.getByText("codething", { exact: true })).toBeInTheDocument(); await expect .element(palette.getByRole("button", { name: "Add (Enter)" })) .toBeInTheDocument(); await dispatchInputKey(browseInput, { key: "Enter" }); await vi.waitFor( () => { expect(remoteDispatchMock).toHaveBeenCalledWith( expect.objectContaining({ type: "project.create", workspaceRoot: "~/workspaces", title: "workspaces", }), ); }, { timeout: 8_000, interval: 16 }, ); await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread after adding a remote project.", ); } finally { await mounted.cleanup(); } }); it("picks a local project from the native file manager", async () => { const pickFolder = vi.fn().mockResolvedValue("/Users/julius/Projects/finder-picked"); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-add-project-file-manager" as MessageId, targetText: "command palette add project file manager", }), resolveRpc: (body) => { if (body._tag === WS_METHODS.filesystemBrowse) { if (body.partialPath === "~/Applications/") { return { parentPath: "~/Applications/", entries: [{ name: "Utilities", fullPath: "~/Applications/Utilities" }], }; } return { parentPath: "~/", entries: [{ name: "Applications", fullPath: "~/Applications" }], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { await waitForServerConfigToApply(); window.desktopBridge = { pickFolder, setTheme: vi.fn().mockResolvedValue(undefined), } as unknown as NonNullable; await page.getByTestId("sidebar-add-project-trigger").click(); const palette = page.getByTestId("command-palette"); await expect.element(palette).toBeInTheDocument(); const browseInput = palette.getByPlaceholder(ADD_PROJECT_SUBMENU_PLACEHOLDER); await browseInput.fill("~/Applications/access"); const fileManagerLabel = isMacPlatform(navigator.platform) ? "Open in Finder" : navigator.platform.toLowerCase().startsWith("win") ? "Open in Explorer" : "Open in Files"; await palette.getByRole("button", { name: fileManagerLabel }).click(); await vi.waitFor( () => { expect(pickFolder).toHaveBeenCalledWith({ initialPath: "~/Applications" }); }, { timeout: 8_000, interval: 16 }, ); await vi.waitFor( () => { const dispatchRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "project.create", ) as | { _tag: string; type?: string; workspaceRoot?: string; title?: string; } | undefined; expect(dispatchRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "project.create", workspaceRoot: "/Users/julius/Projects/finder-picked", title: "finder-picked", }); }, { timeout: 8_000, interval: 16 }, ); await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread after adding a project from the native file manager.", ); } finally { await mounted.cleanup(); } }); it("adds a project from browse mode with Mod+Enter when a directory is highlighted", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-palette-add-project-mod-enter" as MessageId, targetText: "command palette add project mod enter", }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, resolveRpc: (body) => { if (body._tag === WS_METHODS.filesystemBrowse) { if (body.partialPath === "~/Development/") { return { parentPath: "~/Development/", entries: [ { name: "alpha", fullPath: "~/Development/alpha" }, { name: "beta", fullPath: "~/Development/beta" }, ], }; } return { parentPath: "~/", entries: [{ name: "Development", fullPath: "~/Development" }], }; } if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await palette.getByText("Add project", { exact: true }).click(); const browseInput = await waitForCommandPaletteInput(ADD_PROJECT_SUBMENU_PLACEHOLDER); await page.getByPlaceholder(ADD_PROJECT_SUBMENU_PLACEHOLDER).fill("~/Development/"); await expect.element(palette.getByText("alpha", { exact: true })).toBeInTheDocument(); await dispatchInputKey(browseInput, { key: "ArrowDown" }); const addButtonLabel = isMacPlatform(navigator.platform) ? "Add (\u2318 Enter)" : "Add (Ctrl Enter)"; await vi.waitFor( () => { const legendEntries = getCommandPaletteLegendEntries(); expect(legendEntries).toContain("Enter Select"); }, { timeout: 8_000, interval: 16 }, ); await expect .element(palette.getByRole("button", { name: addButtonLabel })) .toBeInTheDocument(); await dispatchInputKey(browseInput, { key: "Enter", metaKey: isMacPlatform(navigator.platform), ctrlKey: !isMacPlatform(navigator.platform), }); await vi.waitFor( () => { const dispatchRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "project.create", ) as | { _tag: string; type?: string; workspaceRoot?: string; title?: string; } | undefined; expect(dispatchRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "project.create", workspaceRoot: "~/Development", title: "Development", }); }, { timeout: 8_000, interval: 16 }, ); await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread after adding a project with Mod+Enter.", ); } finally { await mounted.cleanup(); } }); it("keeps project-context thread matches available when searching by project name", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotWithSecondaryProject(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await page.getByPlaceholder("Search commands, projects, and threads...").fill("docs"); await expect.element(palette.getByText("Docs Portal", { exact: true })).toBeInTheDocument(); await expect .element(palette.getByText("Release checklist", { exact: true })) .toBeInTheDocument(); } finally { await mounted.cleanup(); } }); it("searches projects by path and opens the latest thread for that project", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotWithSecondaryProject(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, settings: { ...nextFixture.serverConfig.settings, defaultThreadEnvMode: "worktree", }, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await page.getByPlaceholder("Search commands, projects, and threads...").fill("clients/docs"); await expect.element(palette.getByText("Docs Portal", { exact: true })).toBeInTheDocument(); await expect .element(palette.getByText("/repo/clients/docs-portal", { exact: true })) .toBeInTheDocument(); await palette.getByText("Docs Portal", { exact: true }).click(); const nextPath = await waitForURL( mounted.router, (path) => path === serverThreadPath("thread-secondary-project" as ThreadId), "Route should have changed to the latest thread for the selected project.", ); expect(nextPath).toBe(serverThreadPath("thread-secondary-project" as ThreadId)); expect( useComposerDraftStore .getState() .getDraftThread(threadRefFor("thread-secondary-project" as ThreadId)), ).toBeNull(); } finally { await mounted.cleanup(); } }); it("creates a new thread from project search when no active project thread exists", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotWithSecondaryProject({ includeSecondaryThread: false }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, settings: { ...nextFixture.serverConfig.settings, defaultThreadEnvMode: "worktree", }, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await page.getByPlaceholder("Search commands, projects, and threads...").fill("clients/docs"); await expect.element(palette.getByText("Docs Portal", { exact: true })).toBeInTheDocument(); await expect .element(palette.getByText("/repo/clients/docs-portal", { exact: true })) .toBeInTheDocument(); await palette.getByText("Docs Portal", { exact: true }).click(); const nextPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a new draft thread UUID from the project search result.", ); const nextDraftId = draftIdFromPath(nextPath); const draftThread = useComposerDraftStore.getState().getDraftSession(nextDraftId); expect(draftThread?.projectId).toBe(SECOND_PROJECT_ID); expect(draftThread?.envMode).toBe("worktree"); } finally { await mounted.cleanup(); } }); it("filters archived threads out of command palette search results", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotWithSecondaryProject(), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "commandPalette.toggle", shortcut: { key: "k", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { await waitForServerConfigToApply(); await waitForCommandPaletteShortcutLabel(); const palette = page.getByTestId("command-palette"); await openCommandPaletteFromTrigger(); await expect.element(palette).toBeInTheDocument(); await page.getByPlaceholder("Search commands, projects, and threads...").fill("docs-archive"); await expect .element(palette.getByText("Archived Docs Notes", { exact: true })) .not.toBeInTheDocument(); } finally { await mounted.cleanup(); } }); it("creates a fresh draft after the previous draft thread is promoted", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-promoted-draft-shortcut-test" as MessageId, targetText: "promoted draft shortcut test", }), configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "chat.new", shortcut: { key: "o", metaKey: false, ctrlKey: false, shiftKey: true, altKey: false, modKey: true, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, ], }; }, }); try { const newThreadButton = page.getByTestId("new-thread-button"); await expect.element(newThreadButton).toBeInTheDocument(); await waitForServerConfigToApply(); await newThreadButton.click(); const promotedThreadPath = await waitForURL( mounted.router, (path) => UUID_ROUTE_RE.test(path), "Route should have changed to a promoted draft thread UUID.", ); const promotedDraftId = draftIdFromPath(promotedThreadPath); const promotedThreadId = draftThreadIdFor(promotedDraftId); await promoteDraftThreadViaDomainEvent(promotedThreadId); await waitForURL( mounted.router, (path) => path === serverThreadPath(promotedThreadId), "Promoted drafts should canonicalize to the server thread route before a fresh draft is created.", ); await vi.waitFor( () => { expect(useComposerDraftStore.getState().getDraftThread(promotedDraftId)).toBeNull(); }, { timeout: 8_000, interval: 16 }, ); const composerEditor = await waitForComposerEditor(); composerEditor.focus(); await waitForLayout(); const freshThreadPath = await triggerChatNewShortcutUntilPath( mounted.router, (path) => UUID_ROUTE_RE.test(path) && path !== promotedThreadPath, "Shortcut should create a fresh draft instead of reusing the promoted thread.", ); expect(freshThreadPath).not.toBe(promotedThreadPath); } finally { await mounted.cleanup(); } }); it("keeps long proposed plans lightweight until the user expands them", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotWithLongProposedPlan(), }); try { await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "Expand plan", ) as HTMLButtonElement | null, "Unable to find Expand plan button.", ); expect(document.body.textContent).not.toContain("deep hidden detail only after expand"); const expandButton = await waitForElement( () => Array.from(document.querySelectorAll("button")).find( (button) => button.textContent?.trim() === "Expand plan", ) as HTMLButtonElement | null, "Unable to find Expand plan button.", ); expandButton.click(); await vi.waitFor( () => { expect(document.body.textContent).toContain("deep hidden detail only after expand"); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("uses the active worktree path when saving a proposed plan to the workspace", async () => { const snapshot = createSnapshotWithLongProposedPlan(); const threads = snapshot.threads.slice(); const targetThreadIndex = threads.findIndex((thread) => thread.id === THREAD_ID); const targetThread = targetThreadIndex >= 0 ? threads[targetThreadIndex] : undefined; if (targetThread) { threads[targetThreadIndex] = { ...targetThread, worktreePath: "/repo/worktrees/plan-thread", }; } const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: { ...snapshot, threads, }, }); try { const planActionsButton = await waitForElement( () => document.querySelector('button[aria-label="Plan actions"]'), "Unable to find proposed plan actions button.", ); planActionsButton.click(); const saveToWorkspaceItem = await waitForElement( () => (Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find( (item) => item.textContent?.trim() === "Save to workspace", ) ?? null) as HTMLElement | null, 'Unable to find "Save to workspace" menu item.', ); saveToWorkspaceItem.click(); await vi.waitFor( () => { expect(document.body.textContent).toContain( "Enter a path relative to /repo/worktrees/plan-thread.", ); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("keeps pending-question footer actions inside the composer after a real resize", async () => { const mounted = await mountChatView({ viewport: WIDE_FOOTER_VIEWPORT, snapshot: createSnapshotWithPendingUserInput(), }); try { const firstOption = await waitForButtonContainingText("Tight"); firstOption.click(); await waitForButtonByText("Previous"); await waitForButtonByText("Submit answers"); await mounted.setContainerSize(COMPACT_FOOTER_VIEWPORT); await expectComposerActionsContained(); } finally { await mounted.cleanup(); } }); it("submits pending user input after the final option selection resolves the draft answers", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotWithPendingUserInput(), resolveRpc: (body) => { if (body._tag === ORCHESTRATION_WS_METHODS.dispatchCommand) { return { sequence: fixture.snapshot.snapshotSequence + 1, }; } return undefined; }, }); try { const firstOption = await waitForButtonContainingText("Tight"); firstOption.click(); const finalOption = await waitForButtonContainingText("Conservative"); finalOption.click(); await vi.waitFor( () => { const dispatchRequest = wsRequests.find( (request) => request._tag === ORCHESTRATION_WS_METHODS.dispatchCommand && request.type === "thread.user-input.respond", ) as | { _tag: string; type?: string; requestId?: string; answers?: Record; } | undefined; expect(dispatchRequest).toMatchObject({ _tag: ORCHESTRATION_WS_METHODS.dispatchCommand, type: "thread.user-input.respond", requestId: "req-browser-user-input", answers: { scope: "Tight", risk: "Conservative", }, }); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("keeps plan follow-up footer actions fused and aligned after a real resize", async () => { const mounted = await mountChatView({ viewport: WIDE_FOOTER_VIEWPORT, snapshot: createSnapshotWithPlanFollowUpPrompt(), }); try { const footer = await waitForElement( () => document.querySelector('[data-chat-composer-footer="true"]'), "Unable to find composer footer.", ); const initialModelPicker = await waitForElement( findComposerProviderModelPicker, "Unable to find provider model picker.", ); const initialModelPickerOffset = initialModelPicker.getBoundingClientRect().left - footer.getBoundingClientRect().left; const initialImplementButton = await waitForButtonByText("Implement"); const initialImplementWidth = initialImplementButton.getBoundingClientRect().width; await waitForElement( () => document.querySelector('button[aria-label="Implementation actions"]'), "Unable to find implementation actions trigger.", ); await mounted.setContainerSize({ width: 440, height: WIDE_FOOTER_VIEWPORT.height, }); await expectComposerActionsContained(); const implementButton = await waitForButtonByText("Implement"); const implementActionsButton = await waitForElement( () => document.querySelector('button[aria-label="Implementation actions"]'), "Unable to find implementation actions trigger.", ); await vi.waitFor( () => { const implementRect = implementButton.getBoundingClientRect(); const implementActionsRect = implementActionsButton.getBoundingClientRect(); const compactModelPicker = findComposerProviderModelPicker(); expect(compactModelPicker).toBeTruthy(); const compactModelPickerOffset = compactModelPicker!.getBoundingClientRect().left - footer.getBoundingClientRect().left; expect(Math.abs(implementRect.right - implementActionsRect.left)).toBeLessThanOrEqual(1); expect(Math.abs(implementRect.top - implementActionsRect.top)).toBeLessThanOrEqual(1); expect(Math.abs(implementRect.width - initialImplementWidth)).toBeLessThanOrEqual(1); expect(Math.abs(compactModelPickerOffset - initialModelPickerOffset)).toBeLessThanOrEqual( 1, ); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("keeps the wide desktop follow-up layout expanded when the footer still fits", async () => { const mounted = await mountChatView({ viewport: WIDE_FOOTER_VIEWPORT, snapshot: createSnapshotWithPlanFollowUpPrompt({ modelSelection: { provider: "codex", model: "gpt-5.3-codex-spark" }, planMarkdown: "# Imaginary Long-Range Plan: T3 Code Adaptive Orchestration and Safe-Delay Execution Initiative", }), }); try { await waitForButtonByText("Implement"); await vi.waitFor( () => { const footer = document.querySelector('[data-chat-composer-footer="true"]'); const actions = document.querySelector( '[data-chat-composer-actions="right"]', ); expect(footer?.dataset.chatComposerFooterCompact).toBe("false"); expect(actions?.dataset.chatComposerPrimaryActionsCompact).toBe("false"); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("compacts the footer when a wide desktop follow-up layout starts overflowing", async () => { const mounted = await mountChatView({ viewport: WIDE_FOOTER_VIEWPORT, snapshot: createSnapshotWithPlanFollowUpPrompt({ modelSelection: { provider: "codex", model: "gpt-5.3-codex-spark" }, planMarkdown: "# Imaginary Long-Range Plan: T3 Code Adaptive Orchestration and Safe-Delay Execution Initiative", }), }); try { await waitForButtonByText("Implement"); await mounted.setContainerSize({ width: 804, height: WIDE_FOOTER_VIEWPORT.height, }); await expectComposerActionsContained(); await vi.waitFor( () => { const footer = document.querySelector('[data-chat-composer-footer="true"]'); const actions = document.querySelector( '[data-chat-composer-actions="right"]', ); expect(footer?.dataset.chatComposerFooterCompact).toBe("true"); expect(actions?.dataset.chatComposerPrimaryActionsCompact).toBe("true"); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("keeps the slash-command menu visible above the composer", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-command-menu-target" as MessageId, targetText: "command menu thread", }), }); try { await waitForComposerEditor(); await page.getByTestId("composer-editor").fill("/"); const menuItem = await waitForComposerMenuItem("slash:model"); const composerForm = await waitForElement( () => document.querySelector('[data-chat-composer-form="true"]'), "Unable to find composer form.", ); await vi.waitFor( () => { const menuRect = menuItem.getBoundingClientRect(); const composerRect = composerForm.getBoundingClientRect(); const hitTarget = document.elementFromPoint( menuRect.left + menuRect.width / 2, menuRect.top + menuRect.height / 2, ); expect(menuRect.width).toBeGreaterThan(0); expect(menuRect.height).toBeGreaterThan(0); expect(menuRect.bottom).toBeLessThanOrEqual(composerRect.bottom); expect(hitTarget instanceof Element && menuItem.contains(hitTarget)).toBe(true); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); it("opens the model picker when selecting /model", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-model-command-target" as MessageId, targetText: "model command thread", }), }); try { await waitForComposerEditor(); await page.getByTestId("composer-editor").fill("/mod"); const menuItem = await waitForComposerMenuItem("slash:model"); await menuItem.click(); await vi.waitFor(() => { expect(document.querySelector(".model-picker-list")).not.toBeNull(); expect(findComposerProviderModelPicker()?.textContent).not.toContain("/model"); }); await new Promise((resolve) => { requestAnimationFrame(() => { requestAnimationFrame(() => resolve()); }); }); await vi.waitFor(() => { const searchInput = document.querySelector( 'input[placeholder="Search models..."]', ); expect(searchInput).not.toBeNull(); expect(document.activeElement).toBe(searchInput); }); } finally { await mounted.cleanup(); } }); it("toggles the model picker and shows jump keys immediately from the shortcut", async () => { const snapshot = createSnapshotForTargetUser({ targetMessageId: "msg-user-model-picker-shortcut-target" as MessageId, targetText: "model picker shortcut thread", }); const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: { ...snapshot, projects: snapshot.projects.map((project) => project.id === PROJECT_ID ? Object.assign({}, project, { defaultModelSelection: { provider: "codex", model: "gpt-5.4" }, }) : project, ), threads: snapshot.threads.map((thread) => thread.id === THREAD_ID ? Object.assign({}, thread, { modelSelection: { provider: "codex", model: "gpt-5.4" }, }) : thread, ), }, configureFixture: (nextFixture) => { nextFixture.serverConfig = { ...nextFixture.serverConfig, keybindings: [ { command: "modelPicker.toggle", shortcut: { key: "m", metaKey: false, ctrlKey: true, shiftKey: true, altKey: false, modKey: false, }, whenAst: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, { command: "thread.jump.1", shortcut: { key: "1", metaKey: false, ctrlKey: true, shiftKey: false, altKey: false, modKey: false, }, }, { command: "modelPicker.jump.1", shortcut: { key: "1", metaKey: false, ctrlKey: true, shiftKey: false, altKey: false, modKey: false, }, whenAst: { type: "identifier", name: "modelPickerOpen" }, }, ], providers: [ { ...nextFixture.serverConfig.providers[0]!, provider: "codex", models: [ { slug: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ { id: "fastMode", label: "Fast Mode", type: "boolean" as const }, ], }), }, { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ { id: "fastMode", label: "Fast Mode", type: "boolean" as const }, ], }), }, { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [ { id: "fastMode", label: "Fast Mode", type: "boolean" as const }, ], }), }, ], }, ], }; }, }); try { await waitForServerConfigToApply(); await waitForComposerEditor(); const initialPath = mounted.router.state.location.pathname; window.dispatchEvent( new KeyboardEvent("keydown", { key: "m", ctrlKey: true, shiftKey: true, bubbles: true, cancelable: true, }), ); await vi.waitFor(() => { expect(document.querySelector(".model-picker-list")).not.toBeNull(); }); const jumpLabel = isMacPlatform(navigator.platform) ? "⌃1" : "Ctrl+1"; await vi.waitFor(() => { expect( Array.from( document.querySelectorAll('.model-picker-list [data-slot="kbd"]'), ).some((element) => element.textContent?.trim() === jumpLabel), ).toBe(true); }); expect(mounted.router.state.location.pathname).toBe(initialPath); window.dispatchEvent( new KeyboardEvent("keydown", { key: "m", ctrlKey: true, shiftKey: true, bubbles: true, cancelable: true, }), ); await vi.waitFor(() => { expect(document.querySelector(".model-picker-list")).toBeNull(); }); } finally { releaseModShortcut("Control"); await mounted.cleanup(); } }); it("shows a tooltip with the skill description when hovering a skill pill", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, snapshot: createSnapshotForTargetUser({ targetMessageId: "msg-user-skill-tooltip-target" as MessageId, targetText: "skill tooltip thread", }), configureFixture: (nextFixture) => { const provider = nextFixture.serverConfig.providers[0]; if (!provider) { throw new Error("Expected default provider in test fixture."); } ( provider as { skills: ServerConfig["providers"][number]["skills"]; } ).skills = [ { name: "agent-browser", displayName: "Agent Browser", description: "Open pages, click around, and inspect web apps.", path: "/Users/test/.agents/skills/agent-browser/SKILL.md", enabled: true, }, ]; }, }); try { useComposerDraftStore.getState().setPrompt(THREAD_REF, "use the $agent-browser "); await waitForComposerText("use the $agent-browser "); await waitForElement( () => document.querySelector('[data-composer-skill-chip="true"]'), "Unable to find rendered composer skill chip.", ); await page.getByText("Agent Browser").hover(); await vi.waitFor( () => { const tooltip = document.querySelector('[data-slot="tooltip-popup"]'); expect(tooltip).not.toBeNull(); expect(tooltip?.textContent).toContain("Open pages, click around, and inspect web apps."); }, { timeout: 8_000, interval: 16 }, ); } finally { await mounted.cleanup(); } }); });