Просмотр исходного кода

delete all e2e tests (#22501)

Cherry-picked from ea463e604cdd2a3e83e1c286e39b789455f0d413
Dax Raad 2 дней назад
Родитель
Сommit
627159acac
82 измененных файлов с 28 добавлено и 8880 удалено
  1. 5 5
      bun.lock
  2. 1 1
      package.json
  3. 2 3
      packages/app/README.md
  4. 0 225
      packages/app/e2e/AGENTS.md
  5. 0 949
      packages/app/e2e/actions.ts
  6. 0 24
      packages/app/e2e/app/home.spec.ts
  7. 0 10
      packages/app/e2e/app/navigation.spec.ts
  8. 0 20
      packages/app/e2e/app/palette.spec.ts
  9. 0 58
      packages/app/e2e/app/server-default.spec.ts
  10. 0 16
      packages/app/e2e/app/session.spec.ts
  11. 0 120
      packages/app/e2e/app/titlebar-history.spec.ts
  12. 0 141
      packages/app/e2e/backend.ts
  13. 0 15
      packages/app/e2e/commands/input-focus.spec.ts
  14. 0 33
      packages/app/e2e/commands/panels.spec.ts
  15. 0 32
      packages/app/e2e/commands/tab-close.spec.ts
  16. 0 31
      packages/app/e2e/files/file-open.spec.ts
  17. 0 56
      packages/app/e2e/files/file-tree.spec.ts
  18. 0 156
      packages/app/e2e/files/file-viewer.spec.ts
  19. 0 604
      packages/app/e2e/fixtures.ts
  20. 0 48
      packages/app/e2e/models/model-picker.spec.ts
  21. 0 61
      packages/app/e2e/models/models-visibility.spec.ts
  22. 0 49
      packages/app/e2e/projects/project-edit.spec.ts
  23. 0 49
      packages/app/e2e/projects/projects-close.spec.ts
  24. 0 94
      packages/app/e2e/projects/projects-switch.spec.ts
  25. 0 78
      packages/app/e2e/projects/workspace-new-session.spec.ts
  26. 0 368
      packages/app/e2e/projects/workspaces.spec.ts
  27. 0 95
      packages/app/e2e/prompt/context.spec.ts
  28. 0 15
      packages/app/e2e/prompt/mock.ts
  29. 0 54
      packages/app/e2e/prompt/prompt-async.spec.ts
  30. 0 22
      packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts
  31. 0 30
      packages/app/e2e/prompt/prompt-drop-file.spec.ts
  32. 0 88
      packages/app/e2e/prompt/prompt-footer-focus.spec.ts
  33. 0 146
      packages/app/e2e/prompt/prompt-history.spec.ts
  34. 0 26
      packages/app/e2e/prompt/prompt-mention.spec.ts
  35. 0 24
      packages/app/e2e/prompt/prompt-multiline.spec.ts
  36. 0 74
      packages/app/e2e/prompt/prompt-shell.spec.ts
  37. 0 22
      packages/app/e2e/prompt/prompt-slash-open.spec.ts
  38. 0 66
      packages/app/e2e/prompt/prompt-slash-share.spec.ts
  39. 0 18
      packages/app/e2e/prompt/prompt-slash-terminal.spec.ts
  40. 0 28
      packages/app/e2e/prompt/prompt.spec.ts
  41. 0 65
      packages/app/e2e/selectors.ts
  42. 0 64
      packages/app/e2e/session/session-child-navigation.spec.ts
  43. 0 655
      packages/app/e2e/session/session-composer-dock.spec.ts
  44. 0 362
      packages/app/e2e/session/session-model-persistence.spec.ts
  45. 0 440
      packages/app/e2e/session/session-review.spec.ts
  46. 0 233
      packages/app/e2e/session/session-undo-redo.spec.ts
  47. 0 182
      packages/app/e2e/session/session.spec.ts
  48. 0 389
      packages/app/e2e/settings/settings-keybinds.spec.ts
  49. 0 122
      packages/app/e2e/settings/settings-models.spec.ts
  50. 0 136
      packages/app/e2e/settings/settings-providers.spec.ts
  51. 0 718
      packages/app/e2e/settings/settings.spec.ts
  52. 0 109
      packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts
  53. 0 30
      packages/app/e2e/sidebar/sidebar-session-links.spec.ts
  54. 0 40
      packages/app/e2e/sidebar/sidebar.spec.ts
  55. 0 94
      packages/app/e2e/status/status-popover.spec.ts
  56. 0 28
      packages/app/e2e/terminal/terminal-init.spec.ts
  57. 0 45
      packages/app/e2e/terminal/terminal-reconnect.spec.ts
  58. 0 165
      packages/app/e2e/terminal/terminal-tabs.spec.ts
  59. 0 18
      packages/app/e2e/terminal/terminal.spec.ts
  60. 0 25
      packages/app/e2e/thinking-level.spec.ts
  61. 11 0
      packages/app/e2e/todo.spec.ts
  62. 1 1
      packages/app/e2e/tsconfig.json
  63. 0 63
      packages/app/e2e/utils.ts
  64. 2 2
      packages/app/package.json
  65. 0 180
      packages/app/script/e2e-local.ts
  66. 0 17
      packages/app/src/components/prompt-input.tsx
  67. 0 3
      packages/app/src/components/prompt-input/submit.ts
  68. 0 16
      packages/app/src/components/terminal.tsx
  69. 1 49
      packages/app/src/context/local.tsx
  70. 1 9
      packages/app/src/pages/error.tsx
  71. 2 53
      packages/app/src/pages/session/composer/session-composer-state.ts
  72. 1 21
      packages/app/src/pages/session/composer/session-todo-dock.tsx
  73. 0 6
      packages/app/src/pages/session/terminal-panel.tsx
  74. 0 109
      packages/app/src/testing/model-selection.ts
  75. 0 83
      packages/app/src/testing/prompt.ts
  76. 0 84
      packages/app/src/testing/session-composer.ts
  77. 0 119
      packages/app/src/testing/terminal.ts
  78. 0 66
      packages/app/test/e2e/mock.test.ts
  79. 0 27
      packages/app/test/e2e/no-real-llm.test.ts
  80. 0 76
      packages/opencode/script/seed-e2e.ts
  81. 0 15
      packages/opencode/src/provider/provider.ts
  82. 1 7
      packages/opencode/src/tool/registry.ts

+ 5 - 5
bun.lock

@@ -66,7 +66,7 @@
       },
       "devDependencies": {
         "@happy-dom/global-registrator": "20.0.11",
-        "@playwright/test": "1.57.0",
+        "@playwright/test": "catalog:",
         "@tailwindcss/vite": "catalog:",
         "@tsconfig/bun": "1.0.9",
         "@types/bun": "catalog:",
@@ -669,7 +669,7 @@
     "@octokit/rest": "22.0.0",
     "@openauthjs/openauth": "0.0.0-20250322224806",
     "@pierre/diffs": "1.1.0-beta.18",
-    "@playwright/test": "1.51.0",
+    "@playwright/test": "1.59.1",
     "@solid-primitives/storage": "4.3.3",
     "@solidjs/meta": "0.29.4",
     "@solidjs/router": "0.15.4",
@@ -1732,7 +1732,7 @@
 
     "@planetscale/database": ["@planetscale/[email protected]", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="],
 
-    "@playwright/test": ["@playwright/[email protected]7.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="],
+    "@playwright/test": ["@playwright/[email protected]9.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
 
     "@poppinss/colors": ["@poppinss/[email protected]", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
 
@@ -4174,9 +4174,9 @@
 
     "planck": ["[email protected]", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-dlvqJE+FscZgrGUXJ5ybd0o5bvZ5XXyZNbm08xGsXp9WjXeAyWSFT6n9s/1PQcUBo4546fDXA5RMA4wbDyZw6g=="],
 
-    "playwright": ["[email protected]7.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
+    "playwright": ["[email protected]9.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
 
-    "playwright-core": ["[email protected]7.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
+    "playwright-core": ["[email protected]9.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
 
     "plist": ["[email protected]", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
 

+ 1 - 1
package.json

@@ -58,7 +58,7 @@
       "marked": "17.0.1",
       "marked-shiki": "1.2.1",
       "remend": "1.3.0",
-      "@playwright/test": "1.51.0",
+      "@playwright/test": "1.59.1",
       "typescript": "5.8.2",
       "@typescript/native-preview": "7.0.0-dev.20251207.1",
       "zod": "4.1.8",

+ 2 - 3
packages/app/README.md

@@ -31,11 +31,10 @@ Your app is ready to be deployed!
 
 ## E2E Testing
 
-Playwright starts the Vite dev server automatically via `webServer`, and UI tests need an opencode backend (defaults to `localhost:4096`).
-Use the local runner to create a temp sandbox, seed data, and run the tests.
+Playwright starts the Vite dev server automatically via `webServer`, and UI tests expect an opencode backend at `localhost:4096` by default.
 
 ```bash
-bunx playwright install
+bunx playwright install chromium
 bun run test:e2e:local
 bun run test:e2e:local -- --grep "settings"
 ```

+ 0 - 225
packages/app/e2e/AGENTS.md

@@ -1,225 +0,0 @@
-# E2E Testing Guide
-
-## Build/Lint/Test Commands
-
-```bash
-# Run all e2e tests
-bun test:e2e
-
-# Run specific test file
-bun test:e2e -- app/home.spec.ts
-
-# Run single test by title
-bun test:e2e -- -g "home renders and shows core entrypoints"
-
-# Run tests with UI mode (for debugging)
-bun test:e2e:ui
-
-# Run tests locally with full server setup
-bun test:e2e:local
-
-# View test report
-bun test:e2e:report
-
-# Typecheck
-bun typecheck
-```
-
-## Test Structure
-
-All tests live in `packages/app/e2e/`:
-
-```
-e2e/
-├── fixtures.ts       # Test fixtures (test, expect, gotoSession, sdk)
-├── actions.ts        # Reusable action helpers
-├── selectors.ts      # DOM selectors
-├── utils.ts          # Utilities (serverUrl, modKey, path helpers)
-└── [feature]/
-    └── *.spec.ts     # Test files
-```
-
-## Test Patterns
-
-### Basic Test Structure
-
-```typescript
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { withSession } from "../actions"
-
-test("test description", async ({ page, sdk, gotoSession }) => {
-  await gotoSession() // or gotoSession(sessionID)
-
-  // Your test code
-  await expect(page.locator(promptSelector)).toBeVisible()
-})
-```
-
-### Using Fixtures
-
-- `page` - Playwright page
-- `llm` - Mock LLM server for queuing responses (`text`, `tool`, `toolMatch`, `textMatch`, etc.)
-- `project` - Golden-path project fixture (call `project.open()` first, then use `project.sdk`, `project.prompt(...)`, `project.gotoSession(...)`, `project.trackSession(...)`)
-- `sdk` - OpenCode SDK client for API calls (worker-scoped, shared directory)
-- `gotoSession(sessionID?)` - Navigate to session (worker-scoped, shared directory)
-
-### Helper Functions
-
-**Actions** (`actions.ts`):
-
-- `openPalette(page)` - Open command palette
-- `openSettings(page)` - Open settings dialog
-- `closeDialog(page, dialog)` - Close any dialog
-- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
-- `waitTerminalReady(page, { term? })` - Wait for a mounted terminal to connect and finish rendering output
-- `runTerminal(page, { cmd, token, term?, timeout? })` - Type into the terminal via the browser and wait for rendered output
-- `withSession(sdk, title, callback)` - Create temp session
-- `sessionIDFromUrl(url)` - Read session ID from URL
-- `slugFromUrl(url)` - Read workspace slug from URL
-- `waitSlug(page, skip?)` - Wait for resolved workspace slug
-- `clickListItem(container, filter)` - Click list item by key/text
-
-**Selectors** (`selectors.ts`):
-
-- `promptSelector` - Prompt input
-- `terminalSelector` - Terminal panel
-- `sessionItemSelector(id)` - Session in sidebar
-- `listItemSelector` - Generic list items
-
-**Utils** (`utils.ts`):
-
-- `modKey` - Meta (Mac) or Control (Linux/Win)
-- `serverUrl` - Backend server URL
-- `sessionPath(dir, id?)` - Build session URL
-
-## Code Style Guidelines
-
-### Imports
-
-Always import from `../fixtures`, not `@playwright/test`:
-
-```typescript
-// ✅ Good
-import { test, expect } from "../fixtures"
-
-// ❌ Bad
-import { test, expect } from "@playwright/test"
-```
-
-### Naming Conventions
-
-- Test files: `feature-name.spec.ts`
-- Test names: lowercase, descriptive: `"sidebar can be toggled"`
-- Variables: camelCase
-- Constants: SCREAMING_SNAKE_CASE
-
-### Error Handling
-
-Tests should clean up after themselves. Prefer fixture-managed cleanup:
-
-```typescript
-test("test with cleanup", async ({ page, sdk, gotoSession }) => {
-  await withSession(sdk, "test session", async (session) => {
-    await gotoSession(session.id)
-    // Test code...
-  }) // Auto-deletes session
-})
-```
-
-- Prefer the `project` fixture for tests that need a dedicated project with LLM mocking — call `project.open()` then use `project.prompt(...)`, `project.trackSession(...)`, etc.
-- Use `withSession(sdk, title, callback)` for lightweight temp sessions on the shared worker directory
-- Call `project.trackSession(sessionID, directory?)` and `project.trackDirectory(directory)` for any resources created outside the fixture so teardown can clean them up
-- Avoid calling `sdk.session.delete(...)` directly
-
-### Timeouts
-
-Default: 60s per test, 10s per assertion. Override when needed:
-
-```typescript
-test.setTimeout(120_000) // For long LLM operations
-test("slow test", async () => {
-  await expect.poll(() => check(), { timeout: 90_000 }).toBe(true)
-})
-```
-
-### Selectors
-
-Use `data-component`, `data-action`, or semantic roles:
-
-```typescript
-// ✅ Good
-await page.locator('[data-component="prompt-input"]').click()
-await page.getByRole("button", { name: "Open settings" }).click()
-
-// ❌ Bad
-await page.locator(".css-class-name").click()
-await page.locator("#id-name").click()
-```
-
-### Keyboard Shortcuts
-
-Use `modKey` for cross-platform compatibility:
-
-```typescript
-import { modKey } from "../utils"
-
-await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
-await page.keyboard.press(`${modKey}+Comma`) // Open settings
-```
-
-### Terminal Tests
-
-- In terminal tests, type through the browser. Do not write to the PTY through the SDK.
-- Use `waitTerminalReady(page, { term? })` and `runTerminal(page, { cmd, token, term?, timeout? })` from `actions.ts`.
-- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
-- After opening the terminal, use `waitTerminalFocusIdle(...)` before the next keyboard action when prompt focus or keyboard routing matters.
-- This avoids racing terminal mount, focus handoff, and prompt readiness when the next step types or sends shortcuts.
-- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
-
-### Wait on state
-
-- Never use wall-clock waits like `page.waitForTimeout(...)` to make a test pass
-- Avoid race-prone flows that assume work is finished after an action
-- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers
-- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state
-- Prefer semantic app state over transient DOM visibility when behavior depends on active selection, focus ownership, or async retry loops
-- Do not treat a visible element as proof that the app will route the next action to it
-- When fixing a flake, validate with `--repeat-each` and multiple workers when practical
-
-### Add hooks
-
-- If required state is not observable from the UI, add a small test-only driver or probe in app code instead of sleeps or fragile DOM checks
-- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts`
-- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
-- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
-- Add minimal test-only probes for semantic state like the active list item or selected command when DOM intermediates are unstable
-- Prefer probing committed app state over asserting on transient highlight, visibility, or animation states
-
-### Prefer helpers
-
-- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
-- Use direct locators when the interaction is simple and a helper would not add clarity
-- Prefer helpers that both perform an action and verify the app consumed it
-- Avoid composing helpers redundantly when one already includes the other or already waits for the resulting state
-- If a helper already covers the required wait or verification, use it directly instead of layering extra clicks, keypresses, or assertions
-
-## Writing New Tests
-
-1. Choose appropriate folder or create new one
-2. Import from `../fixtures`
-3. Use helper functions from `../actions` and `../selectors`
-4. When validating routing, use shared helpers from `../actions`. Workspace URL slugs can be canonicalized on Windows, so assert against canonical or resolved workspace slugs.
-5. Clean up any created resources
-6. Use specific selectors (avoid CSS classes)
-7. Test one feature per test file
-
-## Local Development
-
-For UI debugging, use:
-
-```bash
-bun test:e2e:ui
-```
-
-This opens Playwright's interactive UI for step-through debugging.

+ 0 - 949
packages/app/e2e/actions.ts

@@ -1,949 +0,0 @@
-import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
-import { expect, type Locator, type Page } from "@playwright/test"
-import fs from "node:fs/promises"
-import os from "node:os"
-import path from "node:path"
-import { execSync } from "node:child_process"
-import { terminalAttr, type E2EWindow } from "../src/testing/terminal"
-import { createSdk, modKey, resolveDirectory, serverUrl } from "./utils"
-import {
-  dropdownMenuContentSelector,
-  projectSwitchSelector,
-  projectMenuTriggerSelector,
-  projectCloseMenuSelector,
-  projectWorkspacesToggleSelector,
-  titlebarRightSelector,
-  popoverBodySelector,
-  listItemSelector,
-  listItemKeySelector,
-  listItemKeyStartsWithSelector,
-  promptSelector,
-  terminalSelector,
-  workspaceItemSelector,
-  workspaceMenuTriggerSelector,
-} from "./selectors"
-
-const phase = new WeakMap<Page, "test" | "cleanup">()
-
-export function setHealthPhase(page: Page, value: "test" | "cleanup") {
-  phase.set(page, value)
-}
-
-export function healthPhase(page: Page) {
-  return phase.get(page) ?? "test"
-}
-
-export async function defocus(page: Page) {
-  await page
-    .evaluate(() => {
-      const el = document.activeElement
-      if (el instanceof HTMLElement) el.blur()
-    })
-    .catch(() => undefined)
-}
-
-async function terminalID(term: Locator) {
-  const id = await term.getAttribute(terminalAttr)
-  if (id) return id
-  throw new Error(`Active terminal missing ${terminalAttr}`)
-}
-
-export async function terminalConnects(page: Page, input?: { term?: Locator }) {
-  const term = input?.term ?? page.locator(terminalSelector).first()
-  const id = await terminalID(term)
-  return page.evaluate((id) => {
-    return (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]?.connects ?? 0
-  }, id)
-}
-
-export async function disconnectTerminal(page: Page, input?: { term?: Locator }) {
-  const term = input?.term ?? page.locator(terminalSelector).first()
-  const id = await terminalID(term)
-  await page.evaluate((id) => {
-    ;(window as E2EWindow).__opencode_e2e?.terminal?.controls?.[id]?.disconnect?.()
-  }, id)
-}
-
-async function terminalReady(page: Page, term?: Locator) {
-  const next = term ?? page.locator(terminalSelector).first()
-  const id = await terminalID(next)
-  return page.evaluate((id) => {
-    const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]
-    return !!state?.connected && (state.settled ?? 0) > 0
-  }, id)
-}
-
-async function terminalFocusIdle(page: Page, term?: Locator) {
-  const next = term ?? page.locator(terminalSelector).first()
-  const id = await terminalID(next)
-  return page.evaluate((id) => {
-    const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]
-    return (state?.focusing ?? 0) === 0
-  }, id)
-}
-
-async function terminalHas(page: Page, input: { term?: Locator; token: string }) {
-  const next = input.term ?? page.locator(terminalSelector).first()
-  const id = await terminalID(next)
-  return page.evaluate(
-    (input) => {
-      const state = (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[input.id]
-      return state?.rendered.includes(input.token) ?? false
-    },
-    { id, token: input.token },
-  )
-}
-
-async function promptSlashActive(page: Page, id: string) {
-  return page.evaluate((id) => {
-    const state = (window as E2EWindow).__opencode_e2e?.prompt?.current
-    if (state?.popover !== "slash") return false
-    if (!state.slash.ids.includes(id)) return false
-    return state.slash.active === id
-  }, id)
-}
-
-async function promptSlashSelects(page: Page) {
-  return page.evaluate(() => {
-    return (window as E2EWindow).__opencode_e2e?.prompt?.current?.selects ?? 0
-  })
-}
-
-async function promptSlashSelected(page: Page, input: { id: string; count: number }) {
-  return page.evaluate((input) => {
-    const state = (window as E2EWindow).__opencode_e2e?.prompt?.current
-    if (!state) return false
-    return state.selected === input.id && state.selects >= input.count
-  }, input)
-}
-
-export async function waitTerminalReady(page: Page, input?: { term?: Locator; timeout?: number }) {
-  const term = input?.term ?? page.locator(terminalSelector).first()
-  const timeout = input?.timeout ?? 10_000
-  await expect(term).toBeVisible()
-  await expect(term.locator("textarea")).toHaveCount(1)
-  await expect.poll(() => terminalReady(page, term), { timeout }).toBe(true)
-}
-
-export async function waitTerminalFocusIdle(page: Page, input?: { term?: Locator; timeout?: number }) {
-  const term = input?.term ?? page.locator(terminalSelector).first()
-  const timeout = input?.timeout ?? 10_000
-  await waitTerminalReady(page, { term, timeout })
-  await expect.poll(() => terminalFocusIdle(page, term), { timeout }).toBe(true)
-}
-
-export async function showPromptSlash(
-  page: Page,
-  input: { id: string; text: string; prompt?: Locator; timeout?: number },
-) {
-  const prompt = input.prompt ?? page.locator(promptSelector)
-  const timeout = input.timeout ?? 10_000
-  await expect
-    .poll(
-      async () => {
-        await prompt.click().catch(() => false)
-        await prompt.fill(input.text).catch(() => false)
-        return promptSlashActive(page, input.id).catch(() => false)
-      },
-      { timeout },
-    )
-    .toBe(true)
-}
-
-export async function runPromptSlash(
-  page: Page,
-  input: { id: string; text: string; prompt?: Locator; timeout?: number },
-) {
-  const prompt = input.prompt ?? page.locator(promptSelector)
-  const timeout = input.timeout ?? 10_000
-  const count = await promptSlashSelects(page)
-  await showPromptSlash(page, input)
-  await prompt.press("Enter")
-  await expect.poll(() => promptSlashSelected(page, { id: input.id, count: count + 1 }), { timeout }).toBe(true)
-}
-
-export async function runTerminal(page: Page, input: { cmd: string; token: string; term?: Locator; timeout?: number }) {
-  const term = input.term ?? page.locator(terminalSelector).first()
-  const timeout = input.timeout ?? 10_000
-  await waitTerminalReady(page, { term, timeout })
-  const textarea = term.locator("textarea")
-  await term.click()
-  await expect(textarea).toBeFocused()
-  await page.keyboard.type(input.cmd)
-  await page.keyboard.press("Enter")
-  await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true)
-}
-
-export async function openPalette(page: Page, key = "K") {
-  await defocus(page)
-  await page.keyboard.press(`${modKey}+${key}`)
-
-  const dialog = page.getByRole("dialog")
-  await expect(dialog).toBeVisible()
-  await expect(dialog.getByRole("textbox").first()).toBeVisible()
-  return dialog
-}
-
-export async function closeDialog(page: Page, dialog: Locator) {
-  await page.keyboard.press("Escape")
-  const closed = await dialog
-    .waitFor({ state: "detached", timeout: 1500 })
-    .then(() => true)
-    .catch(() => false)
-
-  if (closed) return
-
-  await page.keyboard.press("Escape")
-  const closedSecond = await dialog
-    .waitFor({ state: "detached", timeout: 1500 })
-    .then(() => true)
-    .catch(() => false)
-
-  if (closedSecond) return
-
-  await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
-  await expect(dialog).toHaveCount(0)
-}
-
-async function isSidebarClosed(page: Page) {
-  const button = await waitSidebarButton(page, "isSidebarClosed")
-  return (await button.getAttribute("aria-expanded")) !== "true"
-}
-
-async function errorBoundaryText(page: Page) {
-  const title = page.getByRole("heading", { name: /something went wrong/i }).first()
-  if (!(await title.isVisible().catch(() => false))) return
-
-  const description = await page
-    .getByText(/an error occurred while loading the application\./i)
-    .first()
-    .textContent()
-    .catch(() => "")
-  const detail = await page
-    .getByRole("textbox", { name: /error details/i })
-    .first()
-    .inputValue()
-    .catch(async () =>
-      (
-        (await page
-          .getByRole("textbox", { name: /error details/i })
-          .first()
-          .textContent()
-          .catch(() => "")) ?? ""
-      ).trim(),
-    )
-
-  return [title ? "Error boundary" : "", description ?? "", detail ?? ""].filter(Boolean).join("\n")
-}
-
-async function assertHealthy(page: Page, context: string) {
-  const text = await errorBoundaryText(page)
-  if (!text) return
-  console.log(`[e2e:error-boundary][${context}]\n${text}`)
-  throw new Error(`Error boundary during ${context}\n${text}`)
-}
-
-async function waitSidebarButton(page: Page, context: string) {
-  const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
-  const boundary = page.getByRole("heading", { name: /something went wrong/i }).first()
-  await button.or(boundary).first().waitFor({ state: "visible", timeout: 10_000 })
-  await assertHealthy(page, context)
-  return button
-}
-
-export async function toggleSidebar(page: Page) {
-  await defocus(page)
-  await page.keyboard.press(`${modKey}+B`)
-}
-
-export async function openSidebar(page: Page) {
-  if (!(await isSidebarClosed(page))) return
-
-  const button = await waitSidebarButton(page, "openSidebar")
-  await button.click()
-
-  const opened = await expect(button)
-    .toHaveAttribute("aria-expanded", "true", { timeout: 1500 })
-    .then(() => true)
-    .catch(() => false)
-
-  if (opened) return
-
-  await toggleSidebar(page)
-  await expect(button).toHaveAttribute("aria-expanded", "true")
-}
-
-export async function closeSidebar(page: Page) {
-  if (await isSidebarClosed(page)) return
-
-  const button = await waitSidebarButton(page, "closeSidebar")
-  await button.click()
-
-  const closed = await expect(button)
-    .toHaveAttribute("aria-expanded", "false", { timeout: 1500 })
-    .then(() => true)
-    .catch(() => false)
-
-  if (closed) return
-
-  await toggleSidebar(page)
-  await expect(button).toHaveAttribute("aria-expanded", "false")
-}
-
-export async function openSettings(page: Page) {
-  await assertHealthy(page, "openSettings")
-  await defocus(page)
-
-  const dialog = page.getByRole("dialog")
-  await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
-
-  const opened = await dialog
-    .waitFor({ state: "visible", timeout: 3000 })
-    .then(() => true)
-    .catch(() => false)
-
-  if (opened) return dialog
-
-  await assertHealthy(page, "openSettings")
-
-  await page.getByRole("button", { name: "Settings" }).first().click()
-  await expect(dialog).toBeVisible()
-  return dialog
-}
-
-export async function createTestProject(input?: { serverUrl?: string }) {
-  const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
-  const id = `e2e-${path.basename(root)}`
-
-  await fs.writeFile(path.join(root, "README.md"), `# e2e\n\n${id}\n`)
-
-  execSync("git init", { cwd: root, stdio: "ignore" })
-  await fs.writeFile(path.join(root, ".git", "opencode"), id)
-  execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
-  execSync("git config commit.gpgsign false", { cwd: root, stdio: "ignore" })
-  execSync("git add -A", { cwd: root, stdio: "ignore" })
-  execSync('git -c user.name="e2e" -c user.email="[email protected]" commit -m "init" --allow-empty', {
-    cwd: root,
-    stdio: "ignore",
-  })
-
-  return resolveDirectory(root, input?.serverUrl)
-}
-
-export async function cleanupTestProject(directory: string) {
-  try {
-    execSync("git fsmonitor--daemon stop", { cwd: directory, stdio: "ignore" })
-  } catch {}
-  await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
-}
-
-export function slugFromUrl(url: string) {
-  return /\/([^/]+)\/session(?:[/?#]|$)/.exec(url)?.[1] ?? ""
-}
-
-async function probeSession(page: Page) {
-  return page
-    .evaluate(() => {
-      const win = window as E2EWindow
-      const current = win.__opencode_e2e?.model?.current
-      if (!current) return null
-      return { dir: current.dir, sessionID: current.sessionID }
-    })
-    .catch(() => null as { dir?: string; sessionID?: string } | null)
-}
-
-export async function waitSlug(page: Page, skip: string[] = []) {
-  let prev = ""
-  let next = ""
-  await expect
-    .poll(
-      async () => {
-        await assertHealthy(page, "waitSlug")
-        const slug = slugFromUrl(page.url())
-        if (!slug) return ""
-        if (skip.includes(slug)) return ""
-        if (slug !== prev) {
-          prev = slug
-          next = ""
-          return ""
-        }
-        next = slug
-        return slug
-      },
-      { timeout: 45_000 },
-    )
-    .not.toBe("")
-  return next
-}
-
-export async function resolveSlug(slug: string, input?: { serverUrl?: string }) {
-  const directory = base64Decode(slug)
-  if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
-  const resolved = await resolveDirectory(directory, input?.serverUrl)
-  return { directory: resolved, slug: base64Encode(resolved), raw: slug }
-}
-
-export async function waitDir(page: Page, directory: string, input?: { serverUrl?: string }) {
-  const target = await resolveDirectory(directory, input?.serverUrl)
-  await expect
-    .poll(
-      async () => {
-        await assertHealthy(page, "waitDir")
-        const slug = slugFromUrl(page.url())
-        if (!slug) return ""
-        return resolveSlug(slug, input)
-          .then((item) => item.directory)
-          .catch(() => "")
-      },
-      { timeout: 45_000 },
-    )
-    .toBe(target)
-  return { directory: target, slug: base64Encode(target) }
-}
-
-export async function waitSession(
-  page: Page,
-  input: {
-    directory: string
-    sessionID?: string
-    serverUrl?: string
-    allowAnySession?: boolean
-  },
-) {
-  const target = await resolveDirectory(input.directory, input.serverUrl)
-  await expect
-    .poll(
-      async () => {
-        await assertHealthy(page, "waitSession")
-        const slug = slugFromUrl(page.url())
-        if (!slug) return false
-        const resolved = await resolveSlug(slug, { serverUrl: input.serverUrl }).catch(() => undefined)
-        if (!resolved || resolved.directory !== target) return false
-        const current = sessionIDFromUrl(page.url())
-        if (input.sessionID && current !== input.sessionID) return false
-        if (!input.sessionID && !input.allowAnySession && current) return false
-
-        const state = await probeSession(page)
-        if (input.sessionID && (!state || state.sessionID !== input.sessionID)) return false
-        if (!input.sessionID && !input.allowAnySession && state?.sessionID) return false
-        if (state?.dir) {
-          const dir = await resolveDirectory(state.dir, input.serverUrl).catch(() => state.dir ?? "")
-          if (dir !== target) return false
-        }
-
-        return page
-          .locator(promptSelector)
-          .first()
-          .isVisible()
-          .catch(() => false)
-      },
-      { timeout: 45_000 },
-    )
-    .toBe(true)
-  return { directory: target, slug: base64Encode(target) }
-}
-
-export async function waitSessionSaved(directory: string, sessionID: string, timeout = 30_000, serverUrl?: string) {
-  const sdk = createSdk(directory, serverUrl)
-  const target = await resolveDirectory(directory, serverUrl)
-
-  await expect
-    .poll(
-      async () => {
-        const data = await sdk.session
-          .get({ sessionID })
-          .then((x) => x.data)
-          .catch(() => undefined)
-        if (!data?.directory) return ""
-        return resolveDirectory(data.directory, serverUrl).catch(() => data.directory)
-      },
-      { timeout },
-    )
-    .toBe(target)
-
-  await expect
-    .poll(
-      async () => {
-        const items = await sdk.session
-          .messages({ sessionID, limit: 20 })
-          .then((x) => x.data ?? [])
-          .catch(() => [])
-        return items.some((item) => item.info.role === "user")
-      },
-      { timeout },
-    )
-    .toBe(true)
-}
-
-export function sessionIDFromUrl(url: string) {
-  const match = /\/session\/([^/?#]+)/.exec(url)
-  return match?.[1]
-}
-
-export async function hoverSessionItem(page: Page, sessionID: string) {
-  const sessionEl = page.locator(`[data-session-id="${sessionID}"]`).last()
-  await expect(sessionEl).toBeVisible()
-  await sessionEl.hover()
-  return sessionEl
-}
-
-export async function openSessionMoreMenu(page: Page, sessionID: string) {
-  await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
-
-  const scroller = page.locator(".scroll-view__viewport").first()
-  await expect(scroller).toBeVisible()
-  await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
-
-  const menu = page
-    .locator(dropdownMenuContentSelector)
-    .filter({ has: page.getByRole("menuitem", { name: /rename/i }) })
-    .filter({ has: page.getByRole("menuitem", { name: /archive/i }) })
-    .filter({ has: page.getByRole("menuitem", { name: /delete/i }) })
-    .first()
-
-  const opened = await menu
-    .isVisible()
-    .then((x) => x)
-    .catch(() => false)
-
-  if (opened) return menu
-
-  const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
-  await expect(menuTrigger).toBeVisible()
-  await menuTrigger.click()
-
-  await expect(menu).toBeVisible()
-  return menu
-}
-
-export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) {
-  const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first()
-  await expect(item).toBeVisible()
-  await item.click({ force: options?.force })
-}
-
-export async function confirmDialog(page: Page, buttonName: string | RegExp) {
-  const dialog = page.getByRole("dialog").first()
-  await expect(dialog).toBeVisible()
-
-  const button = dialog.getByRole("button").filter({ hasText: buttonName }).first()
-  await expect(button).toBeVisible()
-  await button.click()
-}
-
-export async function openSharePopover(page: Page) {
-  const scroller = page.locator(".scroll-view__viewport").first()
-  await expect(scroller).toBeVisible()
-  await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
-
-  const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
-  await expect(menuTrigger).toBeVisible({ timeout: 30_000 })
-
-  const popoverBody = page
-    .locator('[data-component="popover-content"]')
-    .filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) })
-    .first()
-
-  const opened = await popoverBody
-    .isVisible()
-    .then((x) => x)
-    .catch(() => false)
-
-  if (!opened) {
-    const menu = page.locator(dropdownMenuContentSelector).first()
-    await menuTrigger.click()
-    await clickMenuItem(menu, /share/i)
-    await expect(menu).toHaveCount(0)
-    await expect(popoverBody).toBeVisible({ timeout: 30_000 })
-  }
-  return { rightSection: scroller, popoverBody }
-}
-
-export async function clickListItem(
-  container: Locator | Page,
-  filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string },
-): Promise<Locator> {
-  let item: Locator
-
-  if (typeof filter === "string" || filter instanceof RegExp) {
-    item = container.locator(listItemSelector).filter({ hasText: filter }).first()
-  } else if (filter.keyStartsWith) {
-    item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first()
-  } else if (filter.key) {
-    item = container.locator(listItemKeySelector(filter.key)).first()
-  } else if (filter.text) {
-    item = container.locator(listItemSelector).filter({ hasText: filter.text }).first()
-  } else {
-    throw new Error("Invalid filter provided to clickListItem")
-  }
-
-  await expect(item).toBeVisible()
-  await item.click()
-  return item
-}
-
-async function status(sdk: ReturnType<typeof createSdk>, sessionID: string) {
-  const data = await sdk.session
-    .status()
-    .then((x) => x.data ?? {})
-    .catch(() => undefined)
-  return data?.[sessionID]
-}
-
-async function stable(sdk: ReturnType<typeof createSdk>, sessionID: string, timeout = 10_000) {
-  let prev = ""
-  await expect
-    .poll(
-      async () => {
-        const info = await sdk.session
-          .get({ sessionID })
-          .then((x) => x.data)
-          .catch(() => undefined)
-        if (!info) return true
-        const next = `${info.title}:${info.time.updated ?? info.time.created}`
-        if (next !== prev) {
-          prev = next
-          return false
-        }
-        return true
-      },
-      { timeout },
-    )
-    .toBe(true)
-}
-
-export async function waitSessionIdle(sdk: ReturnType<typeof createSdk>, sessionID: string, timeout = 30_000) {
-  await expect.poll(() => status(sdk, sessionID).then((x) => !x || x.type === "idle"), { timeout }).toBe(true)
-}
-
-export async function cleanupSession(input: {
-  sessionID: string
-  directory?: string
-  sdk?: ReturnType<typeof createSdk>
-  serverUrl?: string
-}) {
-  const sdk = input.sdk ?? (input.directory ? createSdk(input.directory, input.serverUrl) : undefined)
-  if (!sdk) throw new Error("cleanupSession requires sdk or directory")
-  await waitSessionIdle(sdk, input.sessionID, 5_000).catch(() => undefined)
-  const current = await status(sdk, input.sessionID).catch(() => undefined)
-  if (current && current.type !== "idle") {
-    await sdk.session.abort({ sessionID: input.sessionID }).catch(() => undefined)
-    await waitSessionIdle(sdk, input.sessionID).catch(() => undefined)
-  }
-  await stable(sdk, input.sessionID).catch(() => undefined)
-  await sdk.session.delete({ sessionID: input.sessionID }).catch(() => undefined)
-}
-
-export async function withSession<T>(
-  sdk: ReturnType<typeof createSdk>,
-  title: string,
-  callback: (session: { id: string; title: string }) => Promise<T>,
-): Promise<T> {
-  const session = await sdk.session.create({ title }).then((r) => r.data)
-  if (!session?.id) throw new Error("Session create did not return an id")
-
-  try {
-    return await callback(session)
-  } finally {
-    await cleanupSession({ sdk, sessionID: session.id })
-  }
-}
-
-const seedSystem = [
-  "You are seeding deterministic e2e UI state.",
-  "Follow the user's instruction exactly.",
-  "When asked to call a tool, call exactly that tool exactly once with the exact JSON input.",
-  "Do not call any extra tools.",
-].join(" ")
-
-const wait = async <T>(input: { probe: () => Promise<T | undefined>; timeout?: number }) => {
-  const timeout = input.timeout ?? 30_000
-  const end = Date.now() + timeout
-  while (Date.now() < end) {
-    const value = await input.probe()
-    if (value !== undefined) return value
-    await new Promise((resolve) => setTimeout(resolve, 250))
-  }
-}
-
-const seed = async <T>(input: {
-  sessionID: string
-  prompt: string
-  sdk: ReturnType<typeof createSdk>
-  probe: () => Promise<T | undefined>
-  timeout?: number
-  attempts?: number
-}) => {
-  for (let i = 0; i < (input.attempts ?? 2); i++) {
-    await input.sdk.session.promptAsync({
-      sessionID: input.sessionID,
-      agent: "build",
-      system: seedSystem,
-      parts: [{ type: "text", text: input.prompt }],
-    })
-    const value = await wait({ probe: input.probe, timeout: input.timeout })
-    if (value !== undefined) return value
-  }
-}
-
-export async function seedSessionQuestion(
-  sdk: ReturnType<typeof createSdk>,
-  input: {
-    sessionID: string
-    questions: Array<{
-      header: string
-      question: string
-      options: Array<{ label: string; description: string }>
-      multiple?: boolean
-      custom?: boolean
-    }>
-  },
-) {
-  const first = input.questions[0]
-  if (!first) throw new Error("Question seed requires at least one question")
-
-  const text = [
-    "Your only valid response is one question tool call.",
-    `Use this JSON input: ${JSON.stringify({ questions: input.questions })}`,
-    "Do not output plain text.",
-    "After calling the tool, wait for the user response.",
-  ].join("\n")
-
-  const result = await seed({
-    sdk,
-    sessionID: input.sessionID,
-    prompt: text,
-    timeout: 30_000,
-    probe: async () => {
-      const list = await sdk.question.list().then((x) => x.data ?? [])
-      return list.find((item) => item.sessionID === input.sessionID && item.questions[0]?.header === first.header)
-    },
-  })
-
-  if (!result) throw new Error("Timed out seeding question request")
-  return { id: result.id }
-}
-
-export async function seedSessionTask(
-  sdk: ReturnType<typeof createSdk>,
-  input: {
-    sessionID: string
-    description: string
-    prompt: string
-    subagentType?: string
-  },
-) {
-  const text = [
-    "Your only valid response is one task tool call.",
-    `Use this JSON input: ${JSON.stringify({
-      description: input.description,
-      prompt: input.prompt,
-      subagent_type: input.subagentType ?? "general",
-    })}`,
-    "Do not output plain text.",
-    "Wait for the task to start and return the child session id.",
-  ].join("\n")
-
-  const result = await seed({
-    sdk,
-    sessionID: input.sessionID,
-    prompt: text,
-    timeout: 90_000,
-    probe: async () => {
-      const messages = await sdk.session.messages({ sessionID: input.sessionID, limit: 50 }).then((x) => x.data ?? [])
-      const part = messages
-        .flatMap((message) => message.parts)
-        .find((part) => {
-          if (part.type !== "tool" || part.tool !== "task") return false
-          if (!("state" in part) || !part.state || typeof part.state !== "object") return false
-          if (!("input" in part.state) || !part.state.input || typeof part.state.input !== "object") return false
-          if (!("description" in part.state.input) || part.state.input.description !== input.description) return false
-          if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object")
-            return false
-          if (!("sessionId" in part.state.metadata)) return false
-          return typeof part.state.metadata.sessionId === "string" && part.state.metadata.sessionId.length > 0
-        })
-
-      if (!part || !("state" in part) || !part.state || typeof part.state !== "object") return
-      if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
-      if (!("sessionId" in part.state.metadata)) return
-      const id = part.state.metadata.sessionId
-      if (typeof id !== "string" || !id) return
-      const child = await sdk.session
-        .get({ sessionID: id })
-        .then((x) => x.data)
-        .catch(() => undefined)
-      if (!child?.id) return
-      return { sessionID: id }
-    },
-  })
-
-  if (!result) throw new Error("Timed out seeding task tool")
-  return result
-}
-
-export async function clearSessionDockSeed(sdk: ReturnType<typeof createSdk>, sessionID: string) {
-  const [questions, permissions] = await Promise.all([
-    sdk.question.list().then((x) => x.data ?? []),
-    sdk.permission.list().then((x) => x.data ?? []),
-  ])
-
-  await Promise.all([
-    ...questions
-      .filter((item) => item.sessionID === sessionID)
-      .map((item) => sdk.question.reject({ requestID: item.id }).catch(() => undefined)),
-    ...permissions
-      .filter((item) => item.sessionID === sessionID)
-      .map((item) => sdk.permission.reply({ requestID: item.id, reply: "reject" }).catch(() => undefined)),
-  ])
-
-  return true
-}
-
-export async function openStatusPopover(page: Page) {
-  await defocus(page)
-
-  const rightSection = page.locator(titlebarRightSelector)
-  const trigger = rightSection.getByRole("button", { name: /status/i }).first()
-
-  const popoverBody = page.locator(popoverBodySelector).filter({ has: page.locator('[data-component="tabs"]') })
-
-  const opened = await popoverBody
-    .isVisible()
-    .then((x) => x)
-    .catch(() => false)
-
-  if (!opened) {
-    await expect(trigger).toBeVisible()
-    await trigger.click()
-    await expect(popoverBody).toBeVisible()
-  }
-
-  return { rightSection, popoverBody }
-}
-
-export async function openProjectMenu(page: Page, projectSlug: string) {
-  await openSidebar(page)
-  const item = page.locator(projectSwitchSelector(projectSlug)).first()
-  await expect(item).toBeVisible()
-  await item.hover()
-
-  const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
-  await expect(trigger).toHaveCount(1)
-  await expect(trigger).toBeVisible()
-
-  const menu = page
-    .locator(dropdownMenuContentSelector)
-    .filter({ has: page.locator(projectCloseMenuSelector(projectSlug)) })
-    .first()
-  const close = menu.locator(projectCloseMenuSelector(projectSlug)).first()
-
-  const clicked = await trigger
-    .click({ force: true, timeout: 1500 })
-    .then(() => true)
-    .catch(() => false)
-
-  if (clicked) {
-    const opened = await menu
-      .waitFor({ state: "visible", timeout: 1500 })
-      .then(() => true)
-      .catch(() => false)
-    if (opened) {
-      await expect(close).toBeVisible()
-      return menu
-    }
-  }
-
-  await trigger.focus()
-  await page.keyboard.press("Enter")
-
-  const opened = await menu
-    .waitFor({ state: "visible", timeout: 1500 })
-    .then(() => true)
-    .catch(() => false)
-
-  if (opened) {
-    await expect(close).toBeVisible()
-    return menu
-  }
-
-  throw new Error(`Failed to open project menu: ${projectSlug}`)
-}
-
-export async function setWorkspacesEnabled(page: Page, projectSlug: string, enabled: boolean) {
-  const current = () =>
-    page
-      .getByRole("button", { name: "New workspace" })
-      .first()
-      .isVisible()
-      .then((x) => x)
-      .catch(() => false)
-
-  if ((await current()) === enabled) return
-
-  if (enabled) {
-    await page.reload()
-    await openSidebar(page)
-    if ((await current()) === enabled) return
-  }
-
-  const flip = async (timeout?: number) => {
-    const menu = await openProjectMenu(page, projectSlug)
-    const toggle = menu.locator(projectWorkspacesToggleSelector(projectSlug)).first()
-    await expect(toggle).toBeVisible()
-    await expect(toggle).toBeEnabled({ timeout: 30_000 })
-    const clicked = await toggle
-      .click({ force: true, timeout })
-      .then(() => true)
-      .catch(() => false)
-    if (clicked) return
-    await toggle.focus()
-    await page.keyboard.press("Enter")
-  }
-
-  for (const timeout of [1500, undefined, undefined]) {
-    if ((await current()) === enabled) break
-    await flip(timeout)
-      .then(() => undefined)
-      .catch(() => undefined)
-    const matched = await expect
-      .poll(current, { timeout: 5_000 })
-      .toBe(enabled)
-      .then(() => true)
-      .catch(() => false)
-    if (matched) break
-  }
-
-  if ((await current()) !== enabled) {
-    await page.reload()
-    await openSidebar(page)
-  }
-
-  const expected = enabled ? "New workspace" : "New session"
-  await expect.poll(current, { timeout: 60_000 }).toBe(enabled)
-  await expect(page.getByRole("button", { name: expected }).first()).toBeVisible({ timeout: 30_000 })
-}
-
-export async function openWorkspaceMenu(page: Page, workspaceSlug: string) {
-  const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
-  await expect(item).toBeVisible()
-  await item.hover()
-
-  const trigger = page.locator(workspaceMenuTriggerSelector(workspaceSlug)).first()
-  await expect(trigger).toBeVisible()
-  await trigger.click({ force: true })
-
-  const menu = page.locator(dropdownMenuContentSelector).first()
-  await expect(menu).toBeVisible()
-  return menu
-}
-
-export async function assistantText(sdk: ReturnType<typeof createSdk>, sessionID: string) {
-  const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
-  return messages
-    .filter((m) => m.info.role === "assistant")
-    .flatMap((m) => m.parts)
-    .filter((p) => p.type === "text")
-    .map((p) => p.text)
-    .join("\n")
-}

+ 0 - 24
packages/app/e2e/app/home.spec.ts

@@ -1,24 +0,0 @@
-import { test, expect } from "../fixtures"
-import { serverNamePattern } from "../utils"
-
-test("home renders and shows core entrypoints", async ({ page }) => {
-  await page.goto("/")
-  const nav = page.locator('[data-component="sidebar-nav-desktop"]')
-
-  await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
-  await expect(nav.getByText("No projects open")).toBeVisible()
-  await expect(nav.getByText("Open a project to get started")).toBeVisible()
-  await expect(page.getByRole("button", { name: serverNamePattern })).toBeVisible()
-})
-
-test("server picker dialog opens from home", async ({ page }) => {
-  await page.goto("/")
-
-  const trigger = page.getByRole("button", { name: serverNamePattern })
-  await expect(trigger).toBeVisible()
-  await trigger.click()
-
-  const dialog = page.getByRole("dialog")
-  await expect(dialog).toBeVisible()
-  await expect(dialog.getByRole("textbox").first()).toBeVisible()
-})

+ 0 - 10
packages/app/e2e/app/navigation.spec.ts

@@ -1,10 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { dirPath } from "../utils"
-
-test("project route redirects to /session", async ({ page, directory, slug }) => {
-  await page.goto(dirPath(directory))
-
-  await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
-  await expect(page.locator(promptSelector)).toBeVisible()
-})

+ 0 - 20
packages/app/e2e/app/palette.spec.ts

@@ -1,20 +0,0 @@
-import { test, expect } from "../fixtures"
-import { closeDialog, openPalette } from "../actions"
-
-test("search palette opens and closes", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openPalette(page)
-
-  await page.keyboard.press("Escape")
-  await expect(dialog).toHaveCount(0)
-})
-
-test("search palette also opens with cmd+p", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openPalette(page, "P")
-
-  await closeDialog(page, dialog)
-  await expect(dialog).toHaveCount(0)
-})

+ 0 - 58
packages/app/e2e/app/server-default.spec.ts

@@ -1,58 +0,0 @@
-import { test, expect } from "../fixtures"
-import { serverNamePattern, serverUrls } from "../utils"
-import { closeDialog, clickMenuItem } from "../actions"
-
-const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
-
-test("can set a default server on web", async ({ page, gotoSession }) => {
-  await page.addInitScript((key: string) => {
-    try {
-      localStorage.removeItem(key)
-    } catch {
-      return
-    }
-  }, DEFAULT_SERVER_URL_KEY)
-
-  await gotoSession()
-
-  const status = page.getByRole("button", { name: "Status" })
-  await expect(status).toBeVisible()
-  const popover = page.locator('[data-component="popover-content"]').filter({ hasText: "Manage servers" })
-
-  const ensurePopoverOpen = async () => {
-    if (await popover.isVisible()) return
-    await status.click()
-    await expect(popover).toBeVisible()
-  }
-
-  await ensurePopoverOpen()
-  await popover.getByRole("button", { name: "Manage servers" }).click()
-
-  const dialog = page.getByRole("dialog")
-  await expect(dialog).toBeVisible()
-
-  await expect(dialog.getByText(serverNamePattern).first()).toBeVisible()
-
-  const menuTrigger = dialog.locator('[data-slot="dropdown-menu-trigger"]').first()
-  await expect(menuTrigger).toBeVisible()
-  await menuTrigger.click({ force: true })
-
-  const menu = page.locator('[data-component="dropdown-menu-content"]').first()
-  await expect(menu).toBeVisible()
-  await clickMenuItem(menu, /set as default/i)
-
-  await expect
-    .poll(async () =>
-      serverUrls.includes((await page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)) ?? ""),
-    )
-    .toBe(true)
-  await expect(dialog.getByText("Default", { exact: true })).toBeVisible()
-
-  await closeDialog(page, dialog)
-
-  await ensurePopoverOpen()
-
-  const serverRow = popover.locator("button").filter({ hasText: serverNamePattern }).first()
-  await expect(serverRow).toBeVisible()
-  await expect(serverRow.getByText("Default", { exact: true })).toBeVisible()
-})

+ 0 - 16
packages/app/e2e/app/session.spec.ts

@@ -1,16 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { withSession } from "../actions"
-
-test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => {
-  const title = `e2e smoke ${Date.now()}`
-
-  await withSession(sdk, title, async (session) => {
-    await gotoSession(session.id)
-
-    const prompt = page.locator(promptSelector)
-    await prompt.click()
-    await page.keyboard.type("hello from e2e")
-    await expect(prompt).toContainText("hello from e2e")
-  })
-})

+ 0 - 120
packages/app/e2e/app/titlebar-history.spec.ts

@@ -1,120 +0,0 @@
-import { test, expect } from "../fixtures"
-import { defocus, openSidebar, withSession } from "../actions"
-import { promptSelector } from "../selectors"
-import { modKey } from "../utils"
-
-test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const stamp = Date.now()
-
-  await withSession(sdk, `e2e titlebar history 1 ${stamp}`, async (one) => {
-    await withSession(sdk, `e2e titlebar history 2 ${stamp}`, async (two) => {
-      await gotoSession(one.id)
-
-      await openSidebar(page)
-
-      const link = page.locator(`[data-session-id="${two.id}"] a`).first()
-      await expect(link).toBeVisible()
-      await link.click()
-
-      await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
-      await expect(page.locator(promptSelector)).toBeVisible()
-
-      const back = page.getByRole("button", { name: "Back" })
-      const forward = page.getByRole("button", { name: "Forward" })
-
-      await expect(back).toBeVisible()
-      await expect(back).toBeEnabled()
-      await back.click()
-
-      await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
-      await expect(page.locator(promptSelector)).toBeVisible()
-
-      await expect(forward).toBeVisible()
-      await expect(forward).toBeEnabled()
-      await forward.click()
-
-      await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
-      await expect(page.locator(promptSelector)).toBeVisible()
-    })
-  })
-})
-
-test("titlebar forward is cleared after branching history from sidebar", async ({ page, slug, sdk, gotoSession }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const stamp = Date.now()
-
-  await withSession(sdk, `e2e titlebar history a ${stamp}`, async (a) => {
-    await withSession(sdk, `e2e titlebar history b ${stamp}`, async (b) => {
-      await withSession(sdk, `e2e titlebar history c ${stamp}`, async (c) => {
-        await gotoSession(a.id)
-
-        await openSidebar(page)
-
-        const second = page.locator(`[data-session-id="${b.id}"] a`).first()
-        await expect(second).toBeVisible()
-        await second.click()
-
-        await expect(page).toHaveURL(new RegExp(`/${slug}/session/${b.id}(?:\\?|#|$)`))
-        await expect(page.locator(promptSelector)).toBeVisible()
-
-        const back = page.getByRole("button", { name: "Back" })
-        const forward = page.getByRole("button", { name: "Forward" })
-
-        await expect(back).toBeVisible()
-        await expect(back).toBeEnabled()
-        await back.click()
-
-        await expect(page).toHaveURL(new RegExp(`/${slug}/session/${a.id}(?:\\?|#|$)`))
-        await expect(page.locator(promptSelector)).toBeVisible()
-
-        await openSidebar(page)
-
-        const third = page.locator(`[data-session-id="${c.id}"] a`).first()
-        await expect(third).toBeVisible()
-        await third.click()
-
-        await expect(page).toHaveURL(new RegExp(`/${slug}/session/${c.id}(?:\\?|#|$)`))
-        await expect(page.locator(promptSelector)).toBeVisible()
-
-        await expect(forward).toBeVisible()
-        await expect(forward).toBeDisabled()
-      })
-    })
-  })
-})
-
-test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, gotoSession }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const stamp = Date.now()
-
-  await withSession(sdk, `e2e titlebar shortcuts 1 ${stamp}`, async (one) => {
-    await withSession(sdk, `e2e titlebar shortcuts 2 ${stamp}`, async (two) => {
-      await gotoSession(one.id)
-
-      await openSidebar(page)
-
-      const link = page.locator(`[data-session-id="${two.id}"] a`).first()
-      await expect(link).toBeVisible()
-      await link.click()
-
-      await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
-      await expect(page.locator(promptSelector)).toBeVisible()
-
-      await defocus(page)
-      await page.keyboard.press(`${modKey}+[`)
-
-      await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
-      await expect(page.locator(promptSelector)).toBeVisible()
-
-      await defocus(page)
-      await page.keyboard.press(`${modKey}+]`)
-
-      await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
-      await expect(page.locator(promptSelector)).toBeVisible()
-    })
-  })
-})

+ 0 - 141
packages/app/e2e/backend.ts

@@ -1,141 +0,0 @@
-import { spawn } from "node:child_process"
-import fs from "node:fs/promises"
-import net from "node:net"
-import os from "node:os"
-import path from "node:path"
-import { fileURLToPath } from "node:url"
-
-type Handle = {
-  url: string
-  stop: () => Promise<void>
-}
-
-function freePort() {
-  return new Promise<number>((resolve, reject) => {
-    const server = net.createServer()
-    server.once("error", reject)
-    server.listen(0, () => {
-      const address = server.address()
-      if (!address || typeof address === "string") {
-        server.close(() => reject(new Error("Failed to acquire a free port")))
-        return
-      }
-      server.close((err) => {
-        if (err) reject(err)
-        else resolve(address.port)
-      })
-    })
-  })
-}
-
-async function waitForHealth(url: string, probe = "/global/health") {
-  const end = Date.now() + 120_000
-  let last = ""
-  while (Date.now() < end) {
-    try {
-      const res = await fetch(`${url}${probe}`)
-      if (res.ok) return
-      last = `status ${res.status}`
-    } catch (err) {
-      last = err instanceof Error ? err.message : String(err)
-    }
-    await new Promise((resolve) => setTimeout(resolve, 250))
-  }
-  throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
-}
-
-function done(proc: ReturnType<typeof spawn>) {
-  return proc.exitCode !== null || proc.signalCode !== null
-}
-
-async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
-  if (done(proc)) return
-  await Promise.race([
-    new Promise<void>((resolve) => proc.once("exit", () => resolve())),
-    new Promise<void>((resolve) => setTimeout(resolve, timeout)),
-  ])
-}
-
-const LOG_CAP = 100
-
-function cap(input: string[]) {
-  if (input.length > LOG_CAP) input.splice(0, input.length - LOG_CAP)
-}
-
-function tail(input: string[]) {
-  return input.slice(-40).join("")
-}
-
-export async function startBackend(label: string, input?: { llmUrl?: string }): Promise<Handle> {
-  const port = await freePort()
-  const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), `opencode-e2e-${label}-`))
-  const appDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
-  const repoDir = path.resolve(appDir, "../..")
-  const opencodeDir = path.join(repoDir, "packages", "opencode")
-  const env = {
-    ...process.env,
-    OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
-    OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
-    OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
-    OPENCODE_TEST_HOME: path.join(sandbox, "home"),
-    XDG_DATA_HOME: path.join(sandbox, "share"),
-    XDG_CACHE_HOME: path.join(sandbox, "cache"),
-    XDG_CONFIG_HOME: path.join(sandbox, "config"),
-    XDG_STATE_HOME: path.join(sandbox, "state"),
-    OPENCODE_CLIENT: "app",
-    OPENCODE_STRICT_CONFIG_DEPS: "true",
-    OPENCODE_E2E_LLM_URL: input?.llmUrl,
-  } satisfies Record<string, string | undefined>
-  const out: string[] = []
-  const err: string[] = []
-  const proc = spawn(
-    "bun",
-    ["run", "--conditions=browser", "./src/index.ts", "serve", "--port", String(port), "--hostname", "127.0.0.1"],
-    {
-      cwd: opencodeDir,
-      env,
-      stdio: ["ignore", "pipe", "pipe"],
-    },
-  )
-  proc.stdout?.on("data", (chunk) => {
-    out.push(String(chunk))
-    cap(out)
-  })
-  proc.stderr?.on("data", (chunk) => {
-    err.push(String(chunk))
-    cap(err)
-  })
-
-  const url = `http://127.0.0.1:${port}`
-  try {
-    await waitForHealth(url)
-  } catch (error) {
-    proc.kill("SIGTERM")
-    await fs.rm(sandbox, { recursive: true, force: true }).catch(() => undefined)
-    throw new Error(
-      [
-        `Failed to start isolated e2e backend for ${label}`,
-        error instanceof Error ? error.message : String(error),
-        tail(out),
-        tail(err),
-      ]
-        .filter(Boolean)
-        .join("\n"),
-    )
-  }
-
-  return {
-    url,
-    async stop() {
-      if (!done(proc)) {
-        proc.kill("SIGTERM")
-        await waitExit(proc)
-      }
-      if (!done(proc)) {
-        proc.kill("SIGKILL")
-        await waitExit(proc)
-      }
-      await fs.rm(sandbox, { recursive: true, force: true }).catch(() => undefined)
-    },
-  }
-}

+ 0 - 15
packages/app/e2e/commands/input-focus.spec.ts

@@ -1,15 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-
-test("ctrl+l focuses the prompt", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const prompt = page.locator(promptSelector)
-  await expect(prompt).toBeVisible()
-
-  await page.locator("main").click({ position: { x: 5, y: 5 } })
-  await expect(prompt).not.toBeFocused()
-
-  await page.keyboard.press("Control+L")
-  await expect(prompt).toBeFocused()
-})

+ 0 - 33
packages/app/e2e/commands/panels.spec.ts

@@ -1,33 +0,0 @@
-import { test, expect } from "../fixtures"
-import { modKey } from "../utils"
-
-const expanded = async (el: { getAttribute: (name: string) => Promise<string | null> }) => {
-  const value = await el.getAttribute("aria-expanded")
-  if (value !== "true" && value !== "false") throw new Error(`Expected aria-expanded to be true|false, got: ${value}`)
-  return value === "true"
-}
-
-test("review panel can be toggled via keybind", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const reviewPanel = page.locator("#review-panel")
-
-  const treeToggle = page.getByRole("button", { name: "Toggle file tree" }).first()
-  await expect(treeToggle).toBeVisible()
-  if (await expanded(treeToggle)) await treeToggle.click()
-  await expect(treeToggle).toHaveAttribute("aria-expanded", "false")
-
-  const reviewToggle = page.getByRole("button", { name: "Toggle review" }).first()
-  await expect(reviewToggle).toBeVisible()
-  if (await expanded(reviewToggle)) await reviewToggle.click()
-  await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
-  await expect(reviewPanel).toHaveAttribute("aria-hidden", "true")
-
-  await page.keyboard.press(`${modKey}+Shift+R`)
-  await expect(reviewToggle).toHaveAttribute("aria-expanded", "true")
-  await expect(reviewPanel).toHaveAttribute("aria-hidden", "false")
-
-  await page.keyboard.press(`${modKey}+Shift+R`)
-  await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
-  await expect(reviewPanel).toHaveAttribute("aria-hidden", "true")
-})

+ 0 - 32
packages/app/e2e/commands/tab-close.spec.ts

@@ -1,32 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { modKey } from "../utils"
-
-test("mod+w closes the active file tab", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/open")
-  await expect(page.locator('[data-slash-id="file.open"]').first()).toBeVisible()
-  await page.keyboard.press("Enter")
-
-  const dialog = page
-    .getByRole("dialog")
-    .filter({ has: page.getByPlaceholder(/search files/i) })
-    .first()
-  await expect(dialog).toBeVisible()
-
-  await dialog.getByRole("textbox").first().fill("package.json")
-  const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
-  await expect(item).toBeVisible({ timeout: 30_000 })
-  await item.click()
-  await expect(dialog).toHaveCount(0)
-
-  const tab = page.getByRole("tab", { name: "package.json" }).first()
-  await expect(tab).toBeVisible()
-  await tab.click()
-  await expect(tab).toHaveAttribute("aria-selected", "true")
-
-  await page.keyboard.press(`${modKey}+W`)
-  await expect(page.getByRole("tab", { name: "package.json" })).toHaveCount(0)
-})

+ 0 - 31
packages/app/e2e/files/file-open.spec.ts

@@ -1,31 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-
-test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/open")
-
-  const command = page.locator('[data-slash-id="file.open"]').first()
-  await expect(command).toBeVisible()
-  await page.keyboard.press("Enter")
-
-  const dialog = page
-    .getByRole("dialog")
-    .filter({ has: page.getByPlaceholder(/search files/i) })
-    .first()
-  await expect(dialog).toBeVisible()
-
-  const input = dialog.getByRole("textbox").first()
-  await input.fill("package.json")
-
-  const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
-  await expect(item).toBeVisible({ timeout: 30_000 })
-  await item.click()
-
-  await expect(dialog).toHaveCount(0)
-
-  const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
-  await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible()
-})

+ 0 - 56
packages/app/e2e/files/file-tree.spec.ts

@@ -1,56 +0,0 @@
-import { test, expect } from "../fixtures"
-
-test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const toggle = page.getByRole("button", { name: "Toggle file tree" })
-  const panel = page.locator("#file-tree-panel")
-  const treeTabs = panel.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
-
-  await expect(toggle).toBeVisible()
-  if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
-  await expect(toggle).toHaveAttribute("aria-expanded", "true")
-  await expect(panel).toBeVisible()
-  await expect(treeTabs).toBeVisible()
-
-  const allTab = treeTabs.getByRole("tab", { name: /^all files$/i })
-  await expect(allTab).toBeVisible()
-  await allTab.click()
-  await expect(allTab).toHaveAttribute("aria-selected", "true")
-
-  const tree = treeTabs.locator('[data-slot="tabs-content"]:not([hidden])')
-  await expect(tree).toBeVisible()
-
-  const expand = async (name: string) => {
-    const folder = tree.getByRole("button", { name, exact: true }).first()
-    await expect(folder).toBeVisible()
-    await expect(folder).toHaveAttribute("aria-expanded", /true|false/)
-    if ((await folder.getAttribute("aria-expanded")) === "false") await folder.click()
-    await expect(folder).toHaveAttribute("aria-expanded", "true")
-  }
-
-  await expand("packages")
-  await expand("app")
-  await expand("src")
-  await expand("components")
-
-  const file = tree.getByRole("button", { name: "file-tree.tsx", exact: true }).first()
-  await expect(file).toBeVisible()
-  await file.click()
-
-  const tab = page.getByRole("tab", { name: "file-tree.tsx" })
-  await expect(tab).toBeVisible()
-  await tab.click()
-  await expect(tab).toHaveAttribute("aria-selected", "true")
-
-  await toggle.click()
-  await expect(toggle).toHaveAttribute("aria-expanded", "false")
-
-  await toggle.click()
-  await expect(toggle).toHaveAttribute("aria-expanded", "true")
-  await expect(allTab).toHaveAttribute("aria-selected", "true")
-
-  const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
-  await expect(viewer).toBeVisible()
-  await expect(viewer).toContainText("export default function FileTree")
-})

+ 0 - 156
packages/app/e2e/files/file-viewer.spec.ts

@@ -1,156 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { modKey } from "../utils"
-
-test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/open")
-
-  const command = page.locator('[data-slash-id="file.open"]').first()
-  await expect(command).toBeVisible()
-  await page.keyboard.press("Enter")
-
-  const dialog = page
-    .getByRole("dialog")
-    .filter({ has: page.getByPlaceholder(/search files/i) })
-    .first()
-  await expect(dialog).toBeVisible()
-
-  const input = dialog.getByRole("textbox").first()
-  await input.fill("package.json")
-
-  const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
-  let index = -1
-  await expect
-    .poll(
-      async () => {
-        const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
-        index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
-        return index >= 0
-      },
-      { timeout: 30_000 },
-    )
-    .toBe(true)
-
-  const item = items.nth(index)
-  await expect(item).toBeVisible()
-  await item.click()
-
-  await expect(dialog).toHaveCount(0)
-
-  const tab = page.getByRole("tab", { name: "package.json" })
-  await expect(tab).toBeVisible()
-  await tab.click()
-
-  const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
-  await expect(viewer).toBeVisible()
-  await expect(viewer.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
-})
-
-test("cmd+f opens text viewer search while prompt is focused", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/open")
-
-  const command = page.locator('[data-slash-id="file.open"]').first()
-  await expect(command).toBeVisible()
-  await page.keyboard.press("Enter")
-
-  const dialog = page
-    .getByRole("dialog")
-    .filter({ has: page.getByPlaceholder(/search files/i) })
-    .first()
-  await expect(dialog).toBeVisible()
-
-  const input = dialog.getByRole("textbox").first()
-  await input.fill("package.json")
-
-  const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
-  let index = -1
-  await expect
-    .poll(
-      async () => {
-        const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
-        index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
-        return index >= 0
-      },
-      { timeout: 30_000 },
-    )
-    .toBe(true)
-
-  const item = items.nth(index)
-  await expect(item).toBeVisible()
-  await item.click()
-
-  await expect(dialog).toHaveCount(0)
-
-  const tab = page.getByRole("tab", { name: "package.json" })
-  await expect(tab).toBeVisible()
-  await tab.click()
-
-  const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
-  await expect(viewer).toBeVisible()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.press(`${modKey}+f`)
-
-  const findInput = page.getByPlaceholder("Find")
-  await expect(findInput).toBeVisible()
-  await expect(findInput).toBeFocused()
-})
-
-test("cmd+f opens text viewer search while prompt is not focused", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/open")
-
-  const command = page.locator('[data-slash-id="file.open"]').first()
-  await expect(command).toBeVisible()
-  await page.keyboard.press("Enter")
-
-  const dialog = page
-    .getByRole("dialog")
-    .filter({ has: page.getByPlaceholder(/search files/i) })
-    .first()
-  await expect(dialog).toBeVisible()
-
-  const input = dialog.getByRole("textbox").first()
-  await input.fill("package.json")
-
-  const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
-  let index = -1
-  await expect
-    .poll(
-      async () => {
-        const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
-        index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
-        return index >= 0
-      },
-      { timeout: 30_000 },
-    )
-    .toBe(true)
-
-  const item = items.nth(index)
-  await expect(item).toBeVisible()
-  await item.click()
-
-  await expect(dialog).toHaveCount(0)
-
-  const tab = page.getByRole("tab", { name: "package.json" })
-  await expect(tab).toBeVisible()
-  await tab.click()
-
-  const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
-  await expect(viewer).toBeVisible()
-
-  await viewer.click()
-  await page.keyboard.press(`${modKey}+f`)
-
-  const findInput = page.getByPlaceholder("Find")
-  await expect(findInput).toBeVisible()
-  await expect(findInput).toBeFocused()
-})

+ 0 - 604
packages/app/e2e/fixtures.ts

@@ -1,604 +0,0 @@
-import { test as base, expect, type Page } from "@playwright/test"
-import { ManagedRuntime } from "effect"
-import type { E2EWindow } from "../src/testing/terminal"
-import type { Item, Reply, Usage } from "../../opencode/test/lib/llm-server"
-import { TestLLMServer } from "../../opencode/test/lib/llm-server"
-import { startBackend } from "./backend"
-import {
-  healthPhase,
-  cleanupSession,
-  cleanupTestProject,
-  createTestProject,
-  setHealthPhase,
-  sessionIDFromUrl,
-  waitSession,
-  waitSessionIdle,
-  waitSessionSaved,
-  waitSlug,
-} from "./actions"
-import { promptSelector } from "./selectors"
-import { createSdk, dirSlug, getWorktree, serverUrl, sessionPath } from "./utils"
-
-type LLMFixture = {
-  url: string
-  push: (...input: (Item | Reply)[]) => Promise<void>
-  pushMatch: (
-    match: (hit: { url: URL; body: Record<string, unknown> }) => boolean,
-    ...input: (Item | Reply)[]
-  ) => Promise<void>
-  textMatch: (
-    match: (hit: { url: URL; body: Record<string, unknown> }) => boolean,
-    value: string,
-    opts?: { usage?: Usage },
-  ) => Promise<void>
-  toolMatch: (
-    match: (hit: { url: URL; body: Record<string, unknown> }) => boolean,
-    name: string,
-    input: unknown,
-  ) => Promise<void>
-  text: (value: string, opts?: { usage?: Usage }) => Promise<void>
-  tool: (name: string, input: unknown) => Promise<void>
-  toolHang: (name: string, input: unknown) => Promise<void>
-  reason: (value: string, opts?: { text?: string; usage?: Usage }) => Promise<void>
-  fail: (message?: unknown) => Promise<void>
-  error: (status: number, body: unknown) => Promise<void>
-  hang: () => Promise<void>
-  hold: (value: string, wait: PromiseLike<unknown>) => Promise<void>
-  hits: () => Promise<Array<{ url: URL; body: Record<string, unknown> }>>
-  calls: () => Promise<number>
-  wait: (count: number) => Promise<void>
-  inputs: () => Promise<Record<string, unknown>[]>
-  pending: () => Promise<number>
-  misses: () => Promise<Array<{ url: URL; body: Record<string, unknown> }>>
-}
-
-type LLMWorker = LLMFixture & {
-  reset: () => Promise<void>
-}
-
-type AssistantFixture = {
-  reply: LLMFixture["text"]
-  tool: LLMFixture["tool"]
-  toolHang: LLMFixture["toolHang"]
-  reason: LLMFixture["reason"]
-  fail: LLMFixture["fail"]
-  error: LLMFixture["error"]
-  hang: LLMFixture["hang"]
-  hold: LLMFixture["hold"]
-  calls: LLMFixture["calls"]
-  pending: LLMFixture["pending"]
-}
-
-export const settingsKey = "settings.v3"
-
-const seedModel = (() => {
-  const [providerID = "opencode", modelID = "big-pickle"] = (
-    process.env.OPENCODE_E2E_MODEL ?? "opencode/big-pickle"
-  ).split("/")
-  return {
-    providerID: providerID || "opencode",
-    modelID: modelID || "big-pickle",
-  }
-})()
-
-function clean(value: string | null) {
-  return (value ?? "").replace(/\u200B/g, "").trim()
-}
-
-async function visit(page: Page, url: string) {
-  let err: unknown
-  for (const _ of [0, 1, 2]) {
-    try {
-      await page.goto(url)
-      return
-    } catch (cause) {
-      err = cause
-      if (!String(cause).includes("ERR_CONNECTION_REFUSED")) throw cause
-      await new Promise((resolve) => setTimeout(resolve, 300))
-    }
-  }
-  throw err
-}
-
-async function promptSend(page: Page) {
-  return page
-    .evaluate(() => {
-      const win = window as E2EWindow
-      const sent = win.__opencode_e2e?.prompt?.sent
-      return {
-        started: sent?.started ?? 0,
-        count: sent?.count ?? 0,
-        sessionID: sent?.sessionID,
-        directory: sent?.directory,
-      }
-    })
-    .catch(() => ({ started: 0, count: 0, sessionID: undefined, directory: undefined }))
-}
-
-type ProjectHandle = {
-  directory: string
-  slug: string
-  gotoSession: (sessionID?: string) => Promise<void>
-  trackSession: (sessionID: string, directory?: string) => void
-  trackDirectory: (directory: string) => void
-  sdk: ReturnType<typeof createSdk>
-}
-
-type ProjectOptions = {
-  extra?: string[]
-  model?: { providerID: string; modelID: string }
-  setup?: (directory: string) => Promise<void>
-  beforeGoto?: (project: { directory: string; sdk: ReturnType<typeof createSdk> }) => Promise<void>
-}
-
-type ProjectFixture = ProjectHandle & {
-  open: (options?: ProjectOptions) => Promise<void>
-  prompt: (text: string) => Promise<string>
-  user: (text: string) => Promise<string>
-  shell: (cmd: string) => Promise<string>
-}
-
-type TestFixtures = {
-  llm: LLMFixture
-  assistant: AssistantFixture
-  project: ProjectFixture
-  sdk: ReturnType<typeof createSdk>
-  gotoSession: (sessionID?: string) => Promise<void>
-}
-
-type WorkerFixtures = {
-  _llm: LLMWorker
-  backend: {
-    url: string
-    sdk: (directory?: string) => ReturnType<typeof createSdk>
-  }
-  directory: string
-  slug: string
-}
-
-export const test = base.extend<TestFixtures, WorkerFixtures>({
-  _llm: [
-    async ({}, use) => {
-      const rt = ManagedRuntime.make(TestLLMServer.layer)
-      try {
-        const svc = await rt.runPromise(TestLLMServer.asEffect())
-        await use({
-          url: svc.url,
-          push: (...input) => rt.runPromise(svc.push(...input)),
-          pushMatch: (match, ...input) => rt.runPromise(svc.pushMatch(match, ...input)),
-          textMatch: (match, value, opts) => rt.runPromise(svc.textMatch(match, value, opts)),
-          toolMatch: (match, name, input) => rt.runPromise(svc.toolMatch(match, name, input)),
-          text: (value, opts) => rt.runPromise(svc.text(value, opts)),
-          tool: (name, input) => rt.runPromise(svc.tool(name, input)),
-          toolHang: (name, input) => rt.runPromise(svc.toolHang(name, input)),
-          reason: (value, opts) => rt.runPromise(svc.reason(value, opts)),
-          fail: (message) => rt.runPromise(svc.fail(message)),
-          error: (status, body) => rt.runPromise(svc.error(status, body)),
-          hang: () => rt.runPromise(svc.hang),
-          hold: (value, wait) => rt.runPromise(svc.hold(value, wait)),
-          reset: () => rt.runPromise(svc.reset),
-          hits: () => rt.runPromise(svc.hits),
-          calls: () => rt.runPromise(svc.calls),
-          wait: (count) => rt.runPromise(svc.wait(count)),
-          inputs: () => rt.runPromise(svc.inputs),
-          pending: () => rt.runPromise(svc.pending),
-          misses: () => rt.runPromise(svc.misses),
-        })
-      } finally {
-        await rt.dispose()
-      }
-    },
-    { scope: "worker" },
-  ],
-  backend: [
-    async ({ _llm }, use, workerInfo) => {
-      const handle = await startBackend(`w${workerInfo.workerIndex}`, { llmUrl: _llm.url })
-      try {
-        await use({
-          url: handle.url,
-          sdk: (directory?: string) => createSdk(directory, handle.url),
-        })
-      } finally {
-        await handle.stop()
-      }
-    },
-    { scope: "worker" },
-  ],
-  llm: async ({ _llm }, use) => {
-    await _llm.reset()
-    await use({
-      url: _llm.url,
-      push: _llm.push,
-      pushMatch: _llm.pushMatch,
-      textMatch: _llm.textMatch,
-      toolMatch: _llm.toolMatch,
-      text: _llm.text,
-      tool: _llm.tool,
-      toolHang: _llm.toolHang,
-      reason: _llm.reason,
-      fail: _llm.fail,
-      error: _llm.error,
-      hang: _llm.hang,
-      hold: _llm.hold,
-      hits: _llm.hits,
-      calls: _llm.calls,
-      wait: _llm.wait,
-      inputs: _llm.inputs,
-      pending: _llm.pending,
-      misses: _llm.misses,
-    })
-    const pending = await _llm.pending()
-    if (pending > 0) {
-      throw new Error(`TestLLMServer still has ${pending} queued response(s) after the test finished`)
-    }
-  },
-  assistant: async ({ llm }, use) => {
-    await use({
-      reply: llm.text,
-      tool: llm.tool,
-      toolHang: llm.toolHang,
-      reason: llm.reason,
-      fail: llm.fail,
-      error: llm.error,
-      hang: llm.hang,
-      hold: llm.hold,
-      calls: llm.calls,
-      pending: llm.pending,
-    })
-  },
-  page: async ({ page }, use) => {
-    let boundary: string | undefined
-    setHealthPhase(page, "test")
-    const consoleHandler = (msg: { text(): string }) => {
-      const text = msg.text()
-      if (!text.includes("[e2e:error-boundary]")) return
-      if (healthPhase(page) === "cleanup") {
-        console.warn(`[e2e:error-boundary][cleanup-warning]\n${text}`)
-        return
-      }
-      boundary ||= text
-      console.log(text)
-    }
-    const pageErrorHandler = (err: Error) => {
-      console.log(`[e2e:pageerror] ${err.stack || err.message}`)
-    }
-    page.on("console", consoleHandler)
-    page.on("pageerror", pageErrorHandler)
-    await use(page)
-    page.off("console", consoleHandler)
-    page.off("pageerror", pageErrorHandler)
-    if (boundary) throw new Error(boundary)
-  },
-  directory: [
-    async ({ backend }, use) => {
-      await use(await getWorktree(backend.url))
-    },
-    { scope: "worker" },
-  ],
-  slug: [
-    async ({ directory }, use) => {
-      await use(dirSlug(directory))
-    },
-    { scope: "worker" },
-  ],
-  sdk: async ({ directory, backend }, use) => {
-    await use(backend.sdk(directory))
-  },
-  gotoSession: async ({ page, directory, backend }, use) => {
-    await seedStorage(page, { directory, serverUrl: backend.url })
-
-    const gotoSession = async (sessionID?: string) => {
-      await visit(page, sessionPath(directory, sessionID))
-      await waitSession(page, {
-        directory,
-        sessionID,
-        serverUrl: backend.url,
-        allowAnySession: !sessionID,
-      })
-    }
-    await use(gotoSession)
-  },
-  project: async ({ page, llm, backend }, use) => {
-    const item = makeProject(page, llm, backend)
-    try {
-      await use(item.project)
-    } finally {
-      await item.cleanup()
-    }
-  },
-})
-
-function makeProject(
-  page: Page,
-  llm: LLMFixture,
-  backend: { url: string; sdk: (directory?: string) => ReturnType<typeof createSdk> },
-) {
-  let state:
-    | {
-        directory: string
-        slug: string
-        sdk: ReturnType<typeof createSdk>
-        sessions: Map<string, string>
-        dirs: Set<string>
-      }
-    | undefined
-
-  const need = () => {
-    if (state) return state
-    throw new Error("project.open() must be called first")
-  }
-
-  const trackSession = (sessionID: string, directory?: string) => {
-    const cur = need()
-    cur.sessions.set(sessionID, directory ?? cur.directory)
-  }
-
-  const trackDirectory = (directory: string) => {
-    const cur = need()
-    if (directory !== cur.directory) cur.dirs.add(directory)
-  }
-
-  const gotoSession = async (sessionID?: string) => {
-    const cur = need()
-    await visit(page, sessionPath(cur.directory, sessionID))
-    await waitSession(page, {
-      directory: cur.directory,
-      sessionID,
-      serverUrl: backend.url,
-      allowAnySession: !sessionID,
-    })
-    const current = sessionIDFromUrl(page.url())
-    if (current) trackSession(current)
-  }
-
-  const open = async (options?: ProjectOptions) => {
-    if (state) return
-    const directory = await createTestProject({ serverUrl: backend.url })
-    const sdk = backend.sdk(directory)
-    await options?.setup?.(directory)
-    await seedStorage(page, {
-      directory,
-      extra: options?.extra,
-      model: options?.model,
-      serverUrl: backend.url,
-    })
-    state = {
-      directory,
-      slug: "",
-      sdk,
-      sessions: new Map(),
-      dirs: new Set(),
-    }
-    await options?.beforeGoto?.({ directory, sdk })
-    await gotoSession()
-    need().slug = await waitSlug(page)
-  }
-
-  const send = async (text: string, input: { noReply: boolean; shell: boolean }) => {
-    if (input.noReply) {
-      const cur = need()
-      const state = await page.evaluate(() => {
-        const model = (window as E2EWindow).__opencode_e2e?.model?.current
-        if (!model) return null
-        return {
-          dir: model.dir,
-          sessionID: model.sessionID,
-          agent: model.agent,
-          model: model.model ? { providerID: model.model.providerID, modelID: model.model.modelID } : undefined,
-          variant: model.variant ?? undefined,
-        }
-      })
-      const dir = state?.dir ?? cur.directory
-      const sdk = backend.sdk(dir)
-      const sessionID = state?.sessionID
-        ? state.sessionID
-        : await sdk.session.create({ directory: dir, title: "E2E Session" }).then((res) => {
-            if (!res.data?.id) throw new Error("Failed to create no-reply session")
-            return res.data.id
-          })
-      await sdk.session.prompt({
-        sessionID,
-        agent: state?.agent,
-        model: state?.model,
-        variant: state?.variant,
-        noReply: true,
-        parts: [{ type: "text", text }],
-      })
-      await visit(page, sessionPath(dir, sessionID))
-      const active = await waitSession(page, {
-        directory: dir,
-        sessionID,
-        serverUrl: backend.url,
-      })
-      trackSession(sessionID, active.directory)
-      await waitSessionSaved(active.directory, sessionID, 90_000, backend.url)
-      return sessionID
-    }
-
-    const prev = await promptSend(page)
-    if (!input.noReply && !input.shell && (await llm.pending()) === 0) {
-      await llm.text("ok")
-    }
-
-    const prompt = page.locator(promptSelector).first()
-    const submit = async () => {
-      await expect(prompt).toBeVisible()
-      await prompt.click()
-      if (input.shell) {
-        await page.keyboard.type("!")
-        await expect(prompt).toHaveAttribute("aria-label", /enter shell command/i)
-      }
-      await page.keyboard.type(text)
-      await expect.poll(async () => clean(await prompt.textContent())).toBe(text)
-      await page.keyboard.press("Enter")
-      const started = await expect
-        .poll(async () => (await promptSend(page)).started, { timeout: 5_000 })
-        .toBeGreaterThan(prev.started)
-        .then(() => true)
-        .catch(() => false)
-      if (started) return
-      const send = page.getByRole("button", { name: "Send" }).first()
-      const enabled = await send
-        .isEnabled()
-        .then((x) => x)
-        .catch(() => false)
-      if (enabled) {
-        await send.click()
-      } else {
-        await prompt.click()
-        await page.keyboard.press("Enter")
-      }
-      await expect.poll(async () => (await promptSend(page)).started, { timeout: 5_000 }).toBeGreaterThan(prev.started)
-    }
-
-    await submit()
-
-    let next: { sessionID: string; directory: string } | undefined
-    await expect
-      .poll(
-        async () => {
-          const sent = await promptSend(page)
-          if (sent.count <= prev.count) return ""
-          if (!sent.sessionID || !sent.directory) return ""
-          next = { sessionID: sent.sessionID, directory: sent.directory }
-          return sent.sessionID
-        },
-        { timeout: 90_000 },
-      )
-      .not.toBe("")
-
-    if (!next) throw new Error("Failed to observe prompt submission in e2e prompt probe")
-    const active = await waitSession(page, {
-      directory: next.directory,
-      sessionID: next.sessionID,
-      serverUrl: backend.url,
-    })
-    trackSession(next.sessionID, active.directory)
-    if (!input.shell) {
-      await waitSessionSaved(active.directory, next.sessionID, 90_000, backend.url)
-    }
-    await waitSessionIdle(backend.sdk(active.directory), next.sessionID, 90_000).catch(() => undefined)
-    return next.sessionID
-  }
-
-  const prompt = async (text: string) => {
-    return send(text, { noReply: false, shell: false })
-  }
-
-  const user = async (text: string) => {
-    return send(text, { noReply: true, shell: false })
-  }
-
-  const shell = async (cmd: string) => {
-    return send(cmd, { noReply: false, shell: true })
-  }
-
-  const cleanup = async () => {
-    const cur = state
-    if (!cur) return
-    setHealthPhase(page, "cleanup")
-    await Promise.allSettled(
-      Array.from(cur.sessions, ([sessionID, directory]) =>
-        cleanupSession({ sessionID, directory, serverUrl: backend.url }),
-      ),
-    )
-    await Promise.allSettled(Array.from(cur.dirs, (directory) => cleanupTestProject(directory)))
-    await cleanupTestProject(cur.directory)
-    state = undefined
-    setHealthPhase(page, "test")
-  }
-
-  return {
-    project: {
-      open,
-      prompt,
-      user,
-      shell,
-      gotoSession,
-      trackSession,
-      trackDirectory,
-      get directory() {
-        return need().directory
-      },
-      get slug() {
-        return need().slug
-      },
-      get sdk() {
-        return need().sdk
-      },
-    },
-    cleanup,
-  }
-}
-
-async function seedStorage(
-  page: Page,
-  input: {
-    directory: string
-    extra?: string[]
-    model?: { providerID: string; modelID: string }
-    serverUrl?: string
-  },
-) {
-  const origin = input.serverUrl ?? serverUrl
-  await page.addInitScript(
-    (args: {
-      directory: string
-      serverUrl: string
-      extra: string[]
-      model: { providerID: string; modelID: string }
-    }) => {
-      const key = "opencode.global.dat:server"
-      const raw = localStorage.getItem(key)
-      const parsed = (() => {
-        if (!raw) return undefined
-        try {
-          return JSON.parse(raw) as unknown
-        } catch {
-          return undefined
-        }
-      })()
-
-      const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
-      const list = Array.isArray(store.list) ? store.list : []
-      const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
-      const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
-      const next = { ...(projects as Record<string, unknown>) }
-      const nextList = list.includes(args.serverUrl) ? list : [args.serverUrl, ...list]
-
-      const add = (origin: string, directory: string) => {
-        const current = next[origin]
-        const items = Array.isArray(current) ? current : []
-        const existing = items.filter(
-          (p): p is { worktree: string; expanded?: boolean } =>
-            !!p &&
-            typeof p === "object" &&
-            "worktree" in p &&
-            typeof (p as { worktree?: unknown }).worktree === "string",
-        )
-        if (existing.some((p) => p.worktree === directory)) return
-        next[origin] = [{ worktree: directory, expanded: true }, ...existing]
-      }
-
-      for (const directory of [args.directory, ...args.extra]) {
-        add("local", directory)
-        add(args.serverUrl, directory)
-      }
-
-      localStorage.setItem(key, JSON.stringify({ list: nextList, projects: next, lastProject }))
-      localStorage.setItem("opencode.settings.dat:defaultServerUrl", args.serverUrl)
-
-      const win = window as E2EWindow
-      win.__opencode_e2e = {
-        ...win.__opencode_e2e,
-        model: { enabled: true },
-        prompt: { enabled: true },
-        terminal: { enabled: true, terminals: {} },
-      }
-      localStorage.setItem("opencode.global.dat:model", JSON.stringify({ recent: [args.model], user: [], variant: {} }))
-    },
-    { directory: input.directory, serverUrl: origin, extra: input.extra ?? [], model: input.model ?? seedModel },
-  )
-}
-
-export { expect }

+ 0 - 48
packages/app/e2e/models/model-picker.spec.ts

@@ -1,48 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { clickListItem } from "../actions"
-
-test.fixme("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-
-  const command = page.locator('[data-slash-id="model.choose"]')
-  await expect(command).toBeVisible()
-  await command.hover()
-
-  await page.keyboard.press("Enter")
-
-  const dialog = page.getByRole("dialog")
-  await expect(dialog).toBeVisible()
-
-  const input = dialog.getByRole("textbox").first()
-
-  const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first()
-  await expect(selected).toBeVisible()
-
-  const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first()
-  const target = (await other.count()) > 0 ? other : selected
-
-  const key = await target.getAttribute("data-key")
-  if (!key) throw new Error("Failed to resolve model key from list item")
-
-  const model = key.split(":").slice(1).join(":")
-
-  await input.fill(model)
-
-  await clickListItem(dialog, { key })
-
-  await expect(dialog).toHaveCount(0)
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-  await expect(command).toBeVisible()
-  await command.hover()
-  await page.keyboard.press("Enter")
-
-  const dialogAgain = page.getByRole("dialog")
-  await expect(dialogAgain).toBeVisible()
-  await expect(dialogAgain.locator(`[data-slot="list-item"][data-key="${key}"][data-selected="true"]`)).toBeVisible()
-})

+ 0 - 61
packages/app/e2e/models/models-visibility.spec.ts

@@ -1,61 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { closeDialog, openSettings, clickListItem } from "../actions"
-
-test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-
-  const command = page.locator('[data-slash-id="model.choose"]')
-  await expect(command).toBeVisible()
-  await command.hover()
-  await page.keyboard.press("Enter")
-
-  const picker = page.getByRole("dialog")
-  await expect(picker).toBeVisible()
-
-  const target = picker.locator('[data-slot="list-item"]').first()
-  await expect(target).toBeVisible()
-
-  const key = await target.getAttribute("data-key")
-  if (!key) throw new Error("Failed to resolve model key from list item")
-
-  const name = (await target.locator("span").first().innerText()).trim()
-  if (!name) throw new Error("Failed to resolve model name from list item")
-
-  await page.keyboard.press("Escape")
-  await expect(picker).toHaveCount(0)
-
-  const settings = await openSettings(page)
-
-  await settings.getByRole("tab", { name: "Models" }).click()
-  const search = settings.getByPlaceholder("Search models")
-  await expect(search).toBeVisible()
-  await search.fill(name)
-
-  const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
-  const input = toggle.locator('[data-slot="switch-input"]')
-  await expect(toggle).toBeVisible()
-  await expect(input).toHaveAttribute("aria-checked", "true")
-  await toggle.locator('[data-slot="switch-control"]').click()
-  await expect(input).toHaveAttribute("aria-checked", "false")
-
-  await closeDialog(page, settings)
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-  await expect(command).toBeVisible()
-  await command.hover()
-  await page.keyboard.press("Enter")
-
-  const pickerAgain = page.getByRole("dialog")
-  await expect(pickerAgain).toBeVisible()
-  await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
-
-  await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
-
-  await page.keyboard.press("Escape")
-  await expect(pickerAgain).toHaveCount(0)
-})

+ 0 - 49
packages/app/e2e/projects/project-edit.spec.ts

@@ -1,49 +0,0 @@
-import { test, expect } from "../fixtures"
-import { clickMenuItem, openProjectMenu, openSidebar } from "../actions"
-
-test("dialog edit project updates name and startup script", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  await project.open()
-  await openSidebar(page)
-
-  const open = async () => {
-    const menu = await openProjectMenu(page, project.slug)
-    await clickMenuItem(menu, /^Edit$/i, { force: true })
-
-    const dialog = page.getByRole("dialog")
-    await expect(dialog).toBeVisible()
-    await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
-    return dialog
-  }
-
-  const name = `e2e project ${Date.now()}`
-  const startup = `echo e2e_${Date.now()}`
-
-  const dialog = await open()
-
-  const nameInput = dialog.getByLabel("Name")
-  await nameInput.fill(name)
-
-  const startupInput = dialog.getByLabel("Workspace startup script")
-  await startupInput.fill(startup)
-
-  await dialog.getByRole("button", { name: "Save" }).click()
-  await expect(dialog).toHaveCount(0)
-
-  await expect
-    .poll(
-      async () => {
-        await page.reload()
-        await openSidebar(page)
-        const reopened = await open()
-        const value = await reopened.getByLabel("Name").inputValue()
-        const next = await reopened.getByLabel("Workspace startup script").inputValue()
-        await reopened.getByRole("button", { name: "Cancel" }).click()
-        await expect(reopened).toHaveCount(0)
-        return `${value}\n${next}`
-      },
-      { timeout: 30_000 },
-    )
-    .toBe(`${name}\n${startup}`)
-})

+ 0 - 49
packages/app/e2e/projects/projects-close.spec.ts

@@ -1,49 +0,0 @@
-import { test, expect } from "../fixtures"
-import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions"
-import { projectSwitchSelector } from "../selectors"
-import { dirSlug } from "../utils"
-
-test("closing active project navigates to another open project", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const other = await createTestProject()
-  const otherSlug = dirSlug(other)
-
-  try {
-    await project.open({ extra: [other] })
-    await openSidebar(page)
-
-    const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
-    await expect(otherButton).toBeVisible()
-    await otherButton.click()
-
-    await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
-
-    const menu = await openProjectMenu(page, otherSlug)
-    await clickMenuItem(menu, /^Close$/i, { force: true })
-
-    await expect
-      .poll(
-        () => {
-          const pathname = new URL(page.url()).pathname
-          if (new RegExp(`^/${project.slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
-          if (pathname === "/") return "home"
-          return ""
-        },
-        { timeout: 15_000 },
-      )
-      .toMatch(/^(project|home)$/)
-
-    await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`))
-    await expect
-      .poll(
-        async () => {
-          return await page.locator(projectSwitchSelector(otherSlug)).count()
-        },
-        { timeout: 15_000 },
-      )
-      .toBe(0)
-  } finally {
-    await cleanupTestProject(other)
-  }
-})

+ 0 - 94
packages/app/e2e/projects/projects-switch.spec.ts

@@ -1,94 +0,0 @@
-import { base64Decode } from "@opencode-ai/util/encode"
-import { test, expect } from "../fixtures"
-import {
-  defocus,
-  createTestProject,
-  cleanupTestProject,
-  openSidebar,
-  setWorkspacesEnabled,
-  waitSession,
-  waitSlug,
-} from "../actions"
-import { projectSwitchSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
-import { dirSlug, resolveDirectory } from "../utils"
-
-test("can switch between projects from sidebar", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const other = await createTestProject()
-  const otherSlug = dirSlug(other)
-
-  try {
-    await project.open({ extra: [other] })
-    await defocus(page)
-
-    const currentSlug = dirSlug(project.directory)
-    const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
-    await expect(otherButton).toBeVisible()
-    await otherButton.click()
-
-    await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
-
-    const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
-    await expect(currentButton).toBeVisible()
-    await currentButton.click()
-
-    await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
-  } finally {
-    await cleanupTestProject(other)
-  }
-})
-
-test("switching back to a project opens the latest workspace session", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const other = await createTestProject()
-  const otherSlug = dirSlug(other)
-  try {
-    await project.open({ extra: [other] })
-    await defocus(page)
-    await setWorkspacesEnabled(page, project.slug, true)
-    await openSidebar(page)
-    await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
-
-    await page.getByRole("button", { name: "New workspace" }).first().click()
-
-    const raw = await waitSlug(page, [project.slug])
-    const dir = base64Decode(raw)
-    if (!dir) throw new Error(`Failed to decode workspace slug: ${raw}`)
-    const space = await resolveDirectory(dir)
-    const next = dirSlug(space)
-    project.trackDirectory(space)
-    await openSidebar(page)
-
-    const item = page.locator(`${workspaceItemSelector(next)}, ${workspaceItemSelector(raw)}`).first()
-    await expect(item).toBeVisible()
-    await item.hover()
-
-    const btn = page.locator(`${workspaceNewSessionSelector(next)}, ${workspaceNewSessionSelector(raw)}`).first()
-    await expect(btn).toBeVisible()
-    await btn.click({ force: true })
-
-    await waitSession(page, { directory: space })
-
-    const created = await project.user("test")
-
-    await expect(page).toHaveURL(new RegExp(`/${next}/session/${created}(?:[/?#]|$)`))
-
-    await openSidebar(page)
-
-    const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
-    await expect(otherButton).toBeVisible()
-    await otherButton.click({ force: true })
-    await waitSession(page, { directory: other })
-
-    const rootButton = page.locator(projectSwitchSelector(project.slug)).first()
-    await expect(rootButton).toBeVisible()
-    await rootButton.click({ force: true })
-
-    await waitSession(page, { directory: space, sessionID: created })
-    await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
-  } finally {
-    await cleanupTestProject(other)
-  }
-})

+ 0 - 78
packages/app/e2e/projects/workspace-new-session.spec.ts

@@ -1,78 +0,0 @@
-import type { Page } from "@playwright/test"
-import { test, expect } from "../fixtures"
-import {
-  openSidebar,
-  resolveSlug,
-  sessionIDFromUrl,
-  setWorkspacesEnabled,
-  waitDir,
-  waitSession,
-  waitSlug,
-} from "../actions"
-import { workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
-
-function item(space: { slug: string; raw: string }) {
-  return `${workspaceItemSelector(space.slug)}, ${workspaceItemSelector(space.raw)}`
-}
-
-function button(space: { slug: string; raw: string }) {
-  return `${workspaceNewSessionSelector(space.slug)}, ${workspaceNewSessionSelector(space.raw)}`
-}
-
-async function waitWorkspaceReady(page: Page, space: { slug: string; raw: string }) {
-  await openSidebar(page)
-  await expect(page.locator(item(space)).first()).toBeVisible({ timeout: 60_000 })
-}
-
-async function createWorkspace(page: Page, root: string, seen: string[]) {
-  await openSidebar(page)
-  await page.getByRole("button", { name: "New workspace" }).first().click()
-
-  const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
-  await waitDir(page, next.directory)
-  return next
-}
-
-async function openWorkspaceNewSession(page: Page, space: { slug: string; raw: string; directory: string }) {
-  await waitWorkspaceReady(page, space)
-
-  const row = page.locator(item(space)).first()
-  await row.hover()
-
-  const next = page.locator(button(space)).first()
-  await expect(next).toBeVisible()
-  await next.click({ force: true })
-
-  await waitSession(page, { directory: space.directory })
-  await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe("")
-}
-
-async function createSessionFromWorkspace(
-  project: Parameters<typeof test>[0]["project"],
-  page: Page,
-  space: { slug: string; raw: string; directory: string },
-  text: string,
-) {
-  await openWorkspaceNewSession(page, space)
-  return project.user(text)
-}
-
-test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  await project.open()
-  await openSidebar(page)
-  await setWorkspacesEnabled(page, project.slug, true)
-
-  const first = await createWorkspace(page, project.slug, [])
-  project.trackDirectory(first.directory)
-  await waitWorkspaceReady(page, first)
-
-  const second = await createWorkspace(page, project.slug, [first.slug])
-  project.trackDirectory(second.directory)
-  await waitWorkspaceReady(page, second)
-
-  await createSessionFromWorkspace(project, page, first, `workspace one ${Date.now()}`)
-  await createSessionFromWorkspace(project, page, second, `workspace two ${Date.now()}`)
-  await createSessionFromWorkspace(project, page, first, `workspace one again ${Date.now()}`)
-})

+ 0 - 368
packages/app/e2e/projects/workspaces.spec.ts

@@ -1,368 +0,0 @@
-import fs from "node:fs/promises"
-import os from "node:os"
-import path from "node:path"
-import { base64Decode } from "@opencode-ai/util/encode"
-import type { Page } from "@playwright/test"
-
-import { test, expect } from "../fixtures"
-
-test.describe.configure({ mode: "serial" })
-import {
-  cleanupTestProject,
-  clickMenuItem,
-  confirmDialog,
-  openSidebar,
-  openWorkspaceMenu,
-  resolveSlug,
-  setWorkspacesEnabled,
-  slugFromUrl,
-  waitDir,
-  waitSlug,
-} from "../actions"
-import { inlineInputSelector, workspaceItemSelector } from "../selectors"
-import { dirSlug } from "../utils"
-
-async function setupWorkspaceTest(page: Page, project: { slug: string; trackDirectory: (directory: string) => void }) {
-  const rootSlug = project.slug
-  await openSidebar(page)
-
-  await setWorkspacesEnabled(page, rootSlug, true)
-
-  await page.getByRole("button", { name: "New workspace" }).first().click()
-  const next = await resolveSlug(await waitSlug(page, [rootSlug]))
-  await waitDir(page, next.directory)
-  project.trackDirectory(next.directory)
-
-  await openSidebar(page)
-
-  await expect
-    .poll(
-      async () => {
-        const item = page.locator(workspaceItemSelector(next.slug)).first()
-        try {
-          await item.hover({ timeout: 500 })
-          return true
-        } catch {
-          return false
-        }
-      },
-      { timeout: 60_000 },
-    )
-    .toBe(true)
-
-  return { rootSlug, slug: next.slug, directory: next.directory }
-}
-
-test("can enable and disable workspaces from project menu", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-  await project.open()
-
-  await openSidebar(page)
-
-  await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
-  await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
-
-  await setWorkspacesEnabled(page, project.slug, true)
-  await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
-  await expect(page.locator(workspaceItemSelector(project.slug)).first()).toBeVisible()
-
-  await setWorkspacesEnabled(page, project.slug, false)
-  await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
-  await expect(page.locator(workspaceItemSelector(project.slug))).toHaveCount(0)
-})
-
-test("can create a workspace", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-  await project.open()
-
-  await openSidebar(page)
-  await setWorkspacesEnabled(page, project.slug, true)
-
-  await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
-
-  await page.getByRole("button", { name: "New workspace" }).first().click()
-  const next = await resolveSlug(await waitSlug(page, [project.slug]))
-  await waitDir(page, next.directory)
-  project.trackDirectory(next.directory)
-
-  await openSidebar(page)
-
-  await expect
-    .poll(
-      async () => {
-        const item = page.locator(workspaceItemSelector(next.slug)).first()
-        try {
-          await item.hover({ timeout: 500 })
-          return true
-        } catch {
-          return false
-        }
-      },
-      { timeout: 60_000 },
-    )
-    .toBe(true)
-
-  await expect(page.locator(workspaceItemSelector(next.slug)).first()).toBeVisible()
-})
-
-test("non-git projects keep workspace mode disabled", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-"))
-  const nonGitSlug = dirSlug(nonGit)
-
-  await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
-
-  try {
-    await project.open({ extra: [nonGit] })
-    await page.goto(`/${nonGitSlug}/session`)
-
-    await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
-
-    const activeDir = await resolveSlug(slugFromUrl(page.url())).then((item) => item.directory)
-    expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
-
-    await openSidebar(page)
-    await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
-    await expect(page.getByRole("button", { name: "Create Git repository" })).toBeVisible()
-  } finally {
-    await cleanupTestProject(nonGit)
-  }
-})
-
-test("can rename a workspace", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-  await project.open()
-
-  const { slug } = await setupWorkspaceTest(page, project)
-
-  const rename = `e2e workspace ${Date.now()}`
-  const menu = await openWorkspaceMenu(page, slug)
-  await clickMenuItem(menu, /^Rename$/i, { force: true })
-
-  await expect(menu).toHaveCount(0)
-
-  const item = page.locator(workspaceItemSelector(slug)).first()
-  await expect(item).toBeVisible()
-  const input = item.locator(inlineInputSelector).first()
-  const shown = await input
-    .isVisible()
-    .then((x) => x)
-    .catch(() => false)
-  if (!shown) {
-    const retry = await openWorkspaceMenu(page, slug)
-    await clickMenuItem(retry, /^Rename$/i, { force: true })
-    await expect(retry).toHaveCount(0)
-  }
-  await expect(input).toBeVisible()
-  await input.fill(rename)
-  await input.press("Enter")
-  await expect(item).toContainText(rename)
-})
-
-test("can reset a workspace", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-  await project.open()
-
-  const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
-
-  const readme = path.join(createdDir, "README.md")
-  const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
-  const original = await fs.readFile(readme, "utf8")
-  const dirty = `${original.trimEnd()}\n\nchange_${Date.now()}\n`
-  await fs.writeFile(readme, dirty, "utf8")
-  await fs.writeFile(extra, `created_${Date.now()}\n`, "utf8")
-
-  await expect
-    .poll(async () => {
-      return await fs
-        .stat(extra)
-        .then(() => true)
-        .catch(() => false)
-    })
-    .toBe(true)
-
-  await expect
-    .poll(async () => {
-      const files = await project.sdk.file
-        .status({ directory: createdDir })
-        .then((r) => r.data ?? [])
-        .catch(() => [])
-      return files.length
-    })
-    .toBeGreaterThan(0)
-
-  const menu = await openWorkspaceMenu(page, slug)
-  await clickMenuItem(menu, /^Reset$/i, { force: true })
-  await confirmDialog(page, /^Reset workspace$/i)
-
-  await expect
-    .poll(
-      async () => {
-        const files = await project.sdk.file
-          .status({ directory: createdDir })
-          .then((r) => r.data ?? [])
-          .catch(() => [])
-        return files.length
-      },
-      { timeout: 120_000 },
-    )
-    .toBe(0)
-
-  await expect.poll(() => fs.readFile(readme, "utf8"), { timeout: 120_000 }).toBe(original)
-
-  await expect
-    .poll(async () => {
-      return await fs
-        .stat(extra)
-        .then(() => true)
-        .catch(() => false)
-    })
-    .toBe(false)
-})
-
-test("can reorder workspaces by drag and drop", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-  await project.open()
-  const rootSlug = project.slug
-
-  const listSlugs = async () => {
-    const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
-    const slugs = await nodes.evaluateAll((els) => {
-      return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
-    })
-    return slugs
-  }
-
-  const waitReady = async (slug: string) => {
-    await expect
-      .poll(
-        async () => {
-          const item = page.locator(workspaceItemSelector(slug)).first()
-          try {
-            await item.hover({ timeout: 500 })
-            return true
-          } catch {
-            return false
-          }
-        },
-        { timeout: 60_000 },
-      )
-      .toBe(true)
-  }
-
-  const drag = async (from: string, to: string) => {
-    const src = page.locator(workspaceItemSelector(from)).first()
-    const dst = page.locator(workspaceItemSelector(to)).first()
-
-    const a = await src.boundingBox()
-    const b = await dst.boundingBox()
-    if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
-
-    await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
-    await page.mouse.down()
-    await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
-    await page.mouse.up()
-  }
-
-  await openSidebar(page)
-
-  await setWorkspacesEnabled(page, rootSlug, true)
-
-  const workspaces = [] as { directory: string; slug: string }[]
-  for (const _ of [0, 1]) {
-    const prev = slugFromUrl(page.url())
-    await page.getByRole("button", { name: "New workspace" }).first().click()
-    const next = await resolveSlug(await waitSlug(page, [rootSlug, prev]))
-    await waitDir(page, next.directory)
-    project.trackDirectory(next.directory)
-    workspaces.push(next)
-
-    await openSidebar(page)
-  }
-
-  if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
-
-  const a = workspaces[0].slug
-  const b = workspaces[1].slug
-
-  await waitReady(a)
-  await waitReady(b)
-
-  const list = async () => {
-    const slugs = await listSlugs()
-    return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
-  }
-
-  await expect
-    .poll(async () => {
-      const slugs = await list()
-      return slugs.length === 2
-    })
-    .toBe(true)
-
-  const before = await list()
-  const from = before[1]
-  const to = before[0]
-  if (!from || !to) throw new Error("Failed to resolve initial workspace order")
-
-  await drag(from, to)
-
-  await expect.poll(async () => await list()).toEqual([from, to])
-})
-
-test("can delete a workspace", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-  await project.open()
-
-  const rootSlug = project.slug
-  await openSidebar(page)
-  await setWorkspacesEnabled(page, rootSlug, true)
-
-  const created = await project.sdk.worktree.create({ directory: project.directory }).then((res) => res.data)
-  if (!created?.directory) throw new Error("Failed to create workspace for delete test")
-
-  const directory = created.directory
-  const slug = dirSlug(directory)
-  project.trackDirectory(directory)
-
-  await page.reload()
-  await openSidebar(page)
-  await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible({ timeout: 60_000 })
-
-  await expect
-    .poll(
-      async () => {
-        const worktrees = await project.sdk.worktree
-          .list()
-          .then((r) => r.data ?? [])
-          .catch(() => [] as string[])
-        return worktrees.includes(directory)
-      },
-      { timeout: 30_000 },
-    )
-    .toBe(true)
-
-  const menu = await openWorkspaceMenu(page, slug)
-  await clickMenuItem(menu, /^Delete$/i, { force: true })
-  await confirmDialog(page, /^Delete workspace$/i)
-
-  await expect.poll(() => base64Decode(slugFromUrl(page.url()))).toBe(project.directory)
-
-  await expect
-    .poll(
-      async () => {
-        const worktrees = await project.sdk.worktree
-          .list()
-          .then((r) => r.data ?? [])
-          .catch(() => [] as string[])
-        return worktrees.includes(directory)
-      },
-      { timeout: 60_000 },
-    )
-    .toBe(false)
-
-  await openSidebar(page)
-  await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
-  await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
-})

+ 0 - 95
packages/app/e2e/prompt/context.spec.ts

@@ -1,95 +0,0 @@
-import { test, expect } from "../fixtures"
-import type { Page } from "@playwright/test"
-import { promptSelector } from "../selectors"
-import { withSession } from "../actions"
-
-function contextButton(page: Page) {
-  return page
-    .locator('[data-component="button"]')
-    .filter({ has: page.locator('[data-component="progress-circle"]').first() })
-    .first()
-}
-
-async function seedContextSession(input: { sessionID: string; sdk: Parameters<typeof withSession>[0] }) {
-  await input.sdk.session.promptAsync({
-    sessionID: input.sessionID,
-    noReply: true,
-    parts: [
-      {
-        type: "text",
-        text: "seed context",
-      },
-    ],
-  })
-
-  await expect
-    .poll(async () => {
-      const messages = await input.sdk.session
-        .messages({ sessionID: input.sessionID, limit: 1 })
-        .then((r) => r.data ?? [])
-      return messages.length
-    })
-    .toBeGreaterThan(0)
-}
-
-test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
-  const title = `e2e smoke context ${Date.now()}`
-
-  await withSession(sdk, title, async (session) => {
-    await seedContextSession({ sessionID: session.id, sdk })
-
-    await gotoSession(session.id)
-
-    const trigger = contextButton(page)
-    await expect(trigger).toBeVisible()
-    await trigger.click()
-
-    const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
-    await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
-  })
-})
-
-test("context panel can be closed from the context tab close action", async ({ page, sdk, gotoSession }) => {
-  await withSession(sdk, `e2e context toggle ${Date.now()}`, async (session) => {
-    await seedContextSession({ sessionID: session.id, sdk })
-    await gotoSession(session.id)
-
-    await page.locator(promptSelector).click()
-
-    const trigger = contextButton(page)
-    await expect(trigger).toBeVisible()
-    await trigger.click()
-
-    const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
-    const context = tabs.getByRole("tab", { name: "Context" })
-    await expect(context).toBeVisible()
-
-    await page.getByRole("button", { name: "Close tab" }).first().click()
-    await expect(context).toHaveCount(0)
-  })
-})
-
-test("context panel can open file picker from context actions", async ({ page, sdk, gotoSession }) => {
-  await withSession(sdk, `e2e context tabs ${Date.now()}`, async (session) => {
-    await seedContextSession({ sessionID: session.id, sdk })
-    await gotoSession(session.id)
-
-    await page.locator(promptSelector).click()
-
-    const trigger = contextButton(page)
-    await expect(trigger).toBeVisible()
-    await trigger.click()
-
-    await expect(page.getByRole("tab", { name: "Context" })).toBeVisible()
-    await page.getByRole("button", { name: "Open file" }).first().click()
-
-    const dialog = page
-      .getByRole("dialog")
-      .filter({ has: page.getByPlaceholder(/search files/i) })
-      .first()
-    await expect(dialog).toBeVisible()
-
-    await page.keyboard.press("Escape")
-    await expect(dialog).toHaveCount(0)
-  })
-})

+ 0 - 15
packages/app/e2e/prompt/mock.ts

@@ -1,15 +0,0 @@
-type Hit = { body: Record<string, unknown> }
-
-export function bodyText(hit: Hit) {
-  return JSON.stringify(hit.body)
-}
-
-/**
- * Match requests whose body contains the exact serialized tool input.
- * The seed prompts embed JSON.stringify(input) in the prompt text, which
- * gets escaped again inside the JSON body — so we double-escape to match.
- */
-export function inputMatch(input: unknown) {
-  const escaped = JSON.stringify(JSON.stringify(input)).slice(1, -1)
-  return (hit: Hit) => bodyText(hit).includes(escaped)
-}

+ 0 - 54
packages/app/e2e/prompt/prompt-async.spec.ts

@@ -1,54 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { assistantText, withSession } from "../actions"
-
-const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
-
-// Regression test for Issue #12453: the synchronous POST /message endpoint holds
-// the connection open while the agent works, causing "Failed to fetch" over
-// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately.
-test("prompt succeeds when sync message endpoint is unreachable", async ({ page, project, assistant }) => {
-  test.setTimeout(120_000)
-
-  // Simulate Tailscale/VPN killing the long-lived sync connection
-  await page.route("**/session/*/message", (route) => route.abort("connectionfailed"))
-
-  const token = `E2E_ASYNC_${Date.now()}`
-  await project.open()
-  await assistant.reply(token)
-  const sessionID = await project.prompt(`Reply with exactly: ${token}`)
-
-  await expect.poll(() => assistant.calls()).toBeGreaterThanOrEqual(1)
-  await expect.poll(() => assistantText(project.sdk, sessionID), { timeout: 90_000 }).toContain(token)
-})
-
-test("failed prompt send restores the composer input", async ({ page, sdk, gotoSession }) => {
-  await withSession(sdk, `e2e prompt failure ${Date.now()}`, async (session) => {
-    const prompt = page.locator(promptSelector)
-    const value = `restore ${Date.now()}`
-
-    await page.route(`**/session/${session.id}/prompt_async`, (route) =>
-      route.fulfill({
-        status: 500,
-        contentType: "application/json",
-        body: JSON.stringify({ message: "e2e prompt failure" }),
-      }),
-    )
-
-    await gotoSession(session.id)
-    await prompt.click()
-    await page.keyboard.type(value)
-    await page.keyboard.press("Enter")
-
-    await expect.poll(async () => text(await prompt.textContent())).toBe(value)
-    await expect
-      .poll(
-        async () => {
-          const messages = await sdk.session.messages({ sessionID: session.id, limit: 50 }).then((r) => r.data ?? [])
-          return messages.length
-        },
-        { timeout: 15_000 },
-      )
-      .toBe(0)
-  })
-})

+ 0 - 22
packages/app/e2e/prompt/prompt-drop-file-uri.spec.ts

@@ -1,22 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-
-test("dropping text/plain file: uri inserts a file pill", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const prompt = page.locator(promptSelector)
-  await prompt.click()
-
-  const path = process.platform === "win32" ? "C:\\opencode-e2e-drop.txt" : "/tmp/opencode-e2e-drop.txt"
-  const dt = await page.evaluateHandle((text) => {
-    const dt = new DataTransfer()
-    dt.setData("text/plain", text)
-    return dt
-  }, `file:${path}`)
-
-  await page.dispatchEvent("body", "drop", { dataTransfer: dt })
-
-  const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
-  await expect(pill).toBeVisible()
-  await expect(pill).toHaveAttribute("data-path", path)
-})

+ 0 - 30
packages/app/e2e/prompt/prompt-drop-file.spec.ts

@@ -1,30 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-
-test("dropping an image file adds an attachment", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const prompt = page.locator(promptSelector)
-  await prompt.click()
-
-  const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3+4uQAAAAASUVORK5CYII="
-  const dt = await page.evaluateHandle((b64) => {
-    const dt = new DataTransfer()
-    const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))
-    const file = new File([bytes], "drop.png", { type: "image/png" })
-    dt.items.add(file)
-    return dt
-  }, png)
-
-  await page.dispatchEvent("body", "drop", { dataTransfer: dt })
-
-  const img = page.locator('img[alt="drop.png"]').first()
-  await expect(img).toBeVisible()
-
-  const remove = page.getByRole("button", { name: "Remove attachment" }).first()
-  await expect(remove).toBeVisible()
-
-  await img.hover()
-  await remove.click()
-  await expect(page.locator('img[alt="drop.png"]')).toHaveCount(0)
-})

+ 0 - 88
packages/app/e2e/prompt/prompt-footer-focus.spec.ts

@@ -1,88 +0,0 @@
-import type { Locator, Page } from "@playwright/test"
-import { test, expect } from "../fixtures"
-import { promptAgentSelector, promptModelSelector, promptSelector } from "../selectors"
-
-type Probe = {
-  agent?: string
-  model?: { providerID: string; modelID: string; name?: string }
-  models?: Array<{ providerID: string; modelID: string; name: string }>
-  agents?: Array<{ name: string }>
-}
-
-async function probe(page: Page): Promise<Probe | null> {
-  return page.evaluate(() => {
-    const win = window as Window & {
-      __opencode_e2e?: {
-        model?: {
-          current?: Probe
-        }
-      }
-    }
-    return win.__opencode_e2e?.model?.current ?? null
-  })
-}
-
-async function state(page: Page) {
-  const value = await probe(page)
-  if (!value) throw new Error("Failed to resolve model selection probe")
-  return value
-}
-
-async function ready(page: Page) {
-  const prompt = page.locator(promptSelector)
-  await prompt.click()
-  await expect(prompt).toBeFocused()
-  await prompt.pressSequentially("focus")
-  return prompt
-}
-
-async function body(prompt: Locator) {
-  return prompt.evaluate((el) => (el as HTMLElement).innerText)
-}
-
-test("agent select returns focus to the prompt", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const prompt = await ready(page)
-
-  const info = await state(page)
-  const next = info.agents?.map((item) => item.name).find((name) => name !== info.agent)
-  test.skip(!next, "only one agent available")
-  if (!next) return
-
-  await page.locator(`${promptAgentSelector} [data-slot="select-select-trigger"]`).first().click()
-
-  const item = page.locator('[data-slot="select-select-item"]').filter({ hasText: next }).first()
-  await expect(item).toBeVisible()
-  await item.click({ force: true })
-
-  await expect(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()).toHaveText(
-    next,
-  )
-  await expect(prompt).toBeFocused()
-  await prompt.pressSequentially(" agent")
-  await expect.poll(() => body(prompt)).toContain("focus agent")
-})
-
-test("model select returns focus to the prompt", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const prompt = await ready(page)
-
-  const info = await state(page)
-  const key = info.model ? `${info.model.providerID}:${info.model.modelID}` : null
-  const next = info.models?.find((item) => `${item.providerID}:${item.modelID}` !== key)
-  test.skip(!next, "only one model available")
-  if (!next) return
-
-  await page.locator(`${promptModelSelector} [data-action="prompt-model"]`).first().click()
-
-  const item = page.locator(`[data-slot="list-item"][data-key="${next.providerID}:${next.modelID}"]`).first()
-  await expect(item).toBeVisible()
-  await item.click({ force: true })
-
-  await expect(page.locator(`${promptModelSelector} [data-action="prompt-model"] span`).first()).toHaveText(next.name)
-  await expect(prompt).toBeFocused()
-  await prompt.pressSequentially(" model")
-  await expect.poll(() => body(prompt)).toContain("focus model")
-})

+ 0 - 146
packages/app/e2e/prompt/prompt-history.spec.ts

@@ -1,146 +0,0 @@
-import type { ToolPart } from "@opencode-ai/sdk/v2/client"
-import type { Page } from "@playwright/test"
-import { test, expect } from "../fixtures"
-import { assistantText } from "../actions"
-import { promptSelector } from "../selectors"
-import { createSdk } from "../utils"
-
-const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
-type Sdk = ReturnType<typeof createSdk>
-
-const isBash = (part: unknown): part is ToolPart => {
-  if (!part || typeof part !== "object") return false
-  if (!("type" in part) || part.type !== "tool") return false
-  if (!("tool" in part) || part.tool !== "bash") return false
-  return "state" in part
-}
-
-async function wait(page: Page, value: string) {
-  await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value)
-}
-
-async function reply(sdk: Sdk, sessionID: string, token: string) {
-  await expect.poll(() => assistantText(sdk, sessionID), { timeout: 90_000 }).toContain(token)
-}
-
-async function shell(sdk: Sdk, sessionID: string, cmd: string, token: string) {
-  await expect
-    .poll(
-      async () => {
-        const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
-        const part = messages
-          .filter((item) => item.info.role === "assistant")
-          .flatMap((item) => item.parts)
-          .filter(isBash)
-          .find((item) => item.state.input?.command === cmd && item.state.status === "completed")
-
-        if (!part || part.state.status !== "completed") return
-        return typeof part.state.metadata?.output === "string" ? part.state.metadata.output : part.state.output
-      },
-      { timeout: 90_000 },
-    )
-    .toContain(token)
-}
-
-test("prompt history restores unsent draft with arrow navigation", async ({ page, project, assistant }) => {
-  test.setTimeout(120_000)
-
-  const firstToken = `E2E_HISTORY_ONE_${Date.now()}`
-  const secondToken = `E2E_HISTORY_TWO_${Date.now()}`
-  const first = `Reply with exactly: ${firstToken}`
-  const second = `Reply with exactly: ${secondToken}`
-  const draft = `draft ${Date.now()}`
-
-  await project.open()
-  await assistant.reply(firstToken)
-  const sessionID = await project.prompt(first)
-  await wait(page, "")
-  await reply(project.sdk, sessionID, firstToken)
-
-  await assistant.reply(secondToken)
-  await project.prompt(second)
-  await wait(page, "")
-  await reply(project.sdk, sessionID, secondToken)
-
-  const prompt = page.locator(promptSelector)
-  await prompt.click()
-  await page.keyboard.type(draft)
-  await wait(page, draft)
-
-  await prompt.fill("")
-  await wait(page, "")
-
-  await page.keyboard.press("ArrowUp")
-  await wait(page, second)
-
-  await page.keyboard.press("ArrowUp")
-  await wait(page, first)
-
-  await page.keyboard.press("ArrowDown")
-  await wait(page, second)
-
-  await page.keyboard.press("ArrowDown")
-  await wait(page, "")
-})
-
-test.fixme("shell history stays separate from normal prompt history", async ({ page, sdk, gotoSession }) => {
-  test.setTimeout(120_000)
-
-  const firstToken = `E2E_SHELL_ONE_${Date.now()}`
-  const secondToken = `E2E_SHELL_TWO_${Date.now()}`
-  const normalToken = `E2E_NORMAL_${Date.now()}`
-  const first = `echo ${firstToken}`
-  const second = `echo ${secondToken}`
-  const normal = `Reply with exactly: ${normalToken}`
-
-  await gotoSession()
-
-  const prompt = page.locator(promptSelector)
-
-  await prompt.click()
-  await page.keyboard.type("!")
-  await page.keyboard.type(first)
-  await page.keyboard.press("Enter")
-  await wait(page, "")
-
-  await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
-  const sessionID = sessionIDFromUrl(page.url())!
-  await shell(sdk, sessionID, first, firstToken)
-
-  await prompt.click()
-  await page.keyboard.type("!")
-  await page.keyboard.type(second)
-  await page.keyboard.press("Enter")
-  await wait(page, "")
-  await shell(sdk, sessionID, second, secondToken)
-
-  await page.keyboard.press("Escape")
-  await wait(page, "")
-
-  await prompt.click()
-  await page.keyboard.type("!")
-  await page.keyboard.press("ArrowUp")
-  await wait(page, second)
-
-  await page.keyboard.press("ArrowUp")
-  await wait(page, first)
-
-  await page.keyboard.press("ArrowDown")
-  await wait(page, second)
-
-  await page.keyboard.press("ArrowDown")
-  await wait(page, "")
-
-  await page.keyboard.press("Escape")
-  await wait(page, "")
-
-  await prompt.click()
-  await page.keyboard.type(normal)
-  await page.keyboard.press("Enter")
-  await wait(page, "")
-  await reply(sdk, sessionID, normalToken)
-
-  await prompt.click()
-  await page.keyboard.press("ArrowUp")
-  await wait(page, normal)
-})

+ 0 - 26
packages/app/e2e/prompt/prompt-mention.spec.ts

@@ -1,26 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-
-test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  const sep = process.platform === "win32" ? "\\" : "/"
-  const file = ["packages", "app", "package.json"].join(sep)
-  const filePattern = /packages[\\/]+app[\\/]+\s*package\.json/
-
-  await page.keyboard.type(`@${file}`)
-
-  const suggestion = page.getByRole("button", { name: filePattern }).first()
-  await expect(suggestion).toBeVisible()
-  await suggestion.hover()
-
-  await page.keyboard.press("Tab")
-
-  const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
-  await expect(pill).toBeVisible()
-  await expect(pill).toHaveAttribute("data-path", filePattern)
-
-  await page.keyboard.type(" ok")
-  await expect(page.locator(promptSelector)).toContainText("ok")
-})

+ 0 - 24
packages/app/e2e/prompt/prompt-multiline.spec.ts

@@ -1,24 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-
-test("shift+enter inserts a newline without submitting", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await expect(page).toHaveURL(/\/session\/?$/)
-
-  const prompt = page.locator(promptSelector)
-  await prompt.focus()
-  await expect(prompt).toBeFocused()
-
-  await prompt.pressSequentially("line one")
-  await expect(prompt).toBeFocused()
-
-  await prompt.press("Shift+Enter")
-  await expect(page).toHaveURL(/\/session\/?$/)
-  await expect(prompt).toBeFocused()
-
-  await prompt.pressSequentially("line two")
-
-  await expect(page).toHaveURL(/\/session\/?$/)
-  await expect.poll(() => prompt.evaluate((el) => el.innerText)).toBe("line one\nline two")
-})

+ 0 - 74
packages/app/e2e/prompt/prompt-shell.spec.ts

@@ -1,74 +0,0 @@
-import type { ToolPart } from "@opencode-ai/sdk/v2/client"
-import { test, expect } from "../fixtures"
-import { closeDialog, openSettings, withSession } from "../actions"
-import { promptModelSelector, promptSelector, promptVariantSelector } from "../selectors"
-
-const isBash = (part: unknown): part is ToolPart => {
-  if (!part || typeof part !== "object") return false
-  if (!("type" in part) || part.type !== "tool") return false
-  if (!("tool" in part) || part.tool !== "bash") return false
-  return "state" in part
-}
-
-test("shell mode runs a command in the project directory", async ({ page, project }) => {
-  test.setTimeout(120_000)
-
-  await project.open()
-  const cmd = process.platform === "win32" ? "dir" : "command ls"
-
-  await withSession(project.sdk, `e2e shell ${Date.now()}`, async (session) => {
-    project.trackSession(session.id)
-    await project.gotoSession(session.id)
-    const dialog = await openSettings(page)
-    const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first()
-    const input = toggle.locator('[data-slot="switch-input"]').first()
-    await expect(toggle).toBeVisible()
-    if ((await input.getAttribute("aria-checked")) !== "true") {
-      await toggle.locator('[data-slot="switch-control"]').click()
-      await expect(input).toHaveAttribute("aria-checked", "true")
-    }
-    await closeDialog(page, dialog)
-    await project.shell(cmd)
-
-    await expect
-      .poll(
-        async () => {
-          const list = await project.sdk.session
-            .messages({ sessionID: session.id, limit: 50 })
-            .then((x) => x.data ?? [])
-          const msg = list.findLast(
-            (item) => item.info.role === "assistant" && "path" in item.info && item.info.path.cwd === project.directory,
-          )
-          if (!msg) return
-
-          const part = msg.parts
-            .filter(isBash)
-            .find((item) => item.state.input?.command === cmd && item.state.status === "completed")
-
-          if (!part || part.state.status !== "completed") return
-          const output =
-            typeof part.state.metadata?.output === "string" ? part.state.metadata.output : part.state.output
-          if (!output.includes("README.md")) return
-
-          return { cwd: project.directory, output }
-        },
-        { timeout: 90_000 },
-      )
-      .toEqual(expect.objectContaining({ cwd: project.directory, output: expect.stringContaining("README.md") }))
-  })
-})
-
-test("shell mode unmounts model and variant controls", async ({ page, project }) => {
-  await project.open()
-
-  const prompt = page.locator(promptSelector).first()
-  await expect(page.locator(promptModelSelector)).toHaveCount(1)
-  await expect(page.locator(promptVariantSelector)).toHaveCount(1)
-
-  await prompt.click()
-  await page.keyboard.type("!")
-
-  await expect(prompt).toHaveAttribute("aria-label", /enter shell command/i)
-  await expect(page.locator(promptModelSelector)).toHaveCount(0)
-  await expect(page.locator(promptVariantSelector)).toHaveCount(0)
-})

+ 0 - 22
packages/app/e2e/prompt/prompt-slash-open.spec.ts

@@ -1,22 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-
-test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/open")
-
-  const command = page.locator('[data-slash-id="file.open"]')
-  await expect(command).toBeVisible()
-  await command.hover()
-
-  await page.keyboard.press("Enter")
-
-  const dialog = page.getByRole("dialog")
-  await expect(dialog).toBeVisible()
-  await expect(dialog.getByRole("textbox").first()).toBeVisible()
-
-  await page.keyboard.press("Escape")
-  await expect(dialog).toHaveCount(0)
-})

+ 0 - 66
packages/app/e2e/prompt/prompt-slash-share.spec.ts

@@ -1,66 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { withSession } from "../actions"
-
-const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
-
-async function seed(sdk: Parameters<typeof withSession>[0], sessionID: string) {
-  await sdk.session.promptAsync({
-    sessionID,
-    noReply: true,
-    parts: [{ type: "text", text: "e2e share seed" }],
-  })
-
-  await expect
-    .poll(
-      async () => {
-        const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
-        return messages.length
-      },
-      { timeout: 30_000 },
-    )
-    .toBeGreaterThan(0)
-}
-
-test("/share and /unshare update session share state", async ({ page, project }) => {
-  test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
-
-  await project.open()
-  await withSession(project.sdk, `e2e slash share ${Date.now()}`, async (session) => {
-    project.trackSession(session.id)
-    const prompt = page.locator(promptSelector)
-
-    await seed(project.sdk, session.id)
-    await project.gotoSession(session.id)
-
-    await prompt.click()
-    await page.keyboard.type("/share")
-    await expect(page.locator('[data-slash-id="session.share"]').first()).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(
-        async () => {
-          const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
-          return data?.share?.url || undefined
-        },
-        { timeout: 30_000 },
-      )
-      .not.toBeUndefined()
-
-    await prompt.click()
-    await page.keyboard.type("/unshare")
-    await expect(page.locator('[data-slash-id="session.unshare"]').first()).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(
-        async () => {
-          const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
-          return data?.share?.url || undefined
-        },
-        { timeout: 30_000 },
-      )
-      .toBeUndefined()
-  })
-})

+ 0 - 18
packages/app/e2e/prompt/prompt-slash-terminal.spec.ts

@@ -1,18 +0,0 @@
-import { test, expect } from "../fixtures"
-import { runPromptSlash, waitTerminalFocusIdle } from "../actions"
-import { promptSelector, terminalSelector } from "../selectors"
-
-test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const prompt = page.locator(promptSelector)
-  const terminal = page.locator(terminalSelector)
-
-  await expect(terminal).not.toBeVisible()
-
-  await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" })
-  await waitTerminalFocusIdle(page, { term: terminal })
-
-  await runPromptSlash(page, { prompt, text: "/terminal", id: "terminal.toggle" })
-  await expect(terminal).not.toBeVisible()
-})

+ 0 - 28
packages/app/e2e/prompt/prompt.spec.ts

@@ -1,28 +0,0 @@
-import { test, expect } from "../fixtures"
-import { assistantText } from "../actions"
-
-test("can send a prompt and receive a reply", async ({ page, project, assistant }) => {
-  test.setTimeout(120_000)
-
-  const pageErrors: string[] = []
-  const onPageError = (err: Error) => {
-    pageErrors.push(err.message)
-  }
-  page.on("pageerror", onPageError)
-
-  try {
-    const token = `E2E_OK_${Date.now()}`
-    await project.open()
-    await assistant.reply(token)
-    const sessionID = await project.prompt(`Reply with exactly: ${token}`)
-
-    await expect.poll(() => assistant.calls()).toBeGreaterThanOrEqual(1)
-    await expect.poll(() => assistantText(project.sdk, sessionID), { timeout: 30_000 }).toContain(token)
-  } finally {
-    page.off("pageerror", onPageError)
-  }
-
-  if (pageErrors.length > 0) {
-    throw new Error(`Page error(s):\n${pageErrors.join("\n")}`)
-  }
-})

+ 0 - 65
packages/app/e2e/selectors.ts

@@ -1,65 +0,0 @@
-export const promptSelector = '[data-component="prompt-input"]'
-const terminalPanelSelector = '#terminal-panel[aria-hidden="false"]'
-export const terminalSelector = `${terminalPanelSelector} [data-component="terminal"]`
-export const sessionComposerDockSelector = '[data-component="session-prompt-dock"]'
-export const questionDockSelector = '[data-component="dock-prompt"][data-kind="question"]'
-export const permissionDockSelector = '[data-component="dock-prompt"][data-kind="permission"]'
-export const sessionTodoToggleButtonSelector = '[data-action="session-todo-toggle-button"]'
-
-export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
-export const promptAgentSelector = '[data-component="prompt-agent-control"]'
-export const promptModelSelector = '[data-component="prompt-model-control"]'
-export const promptVariantSelector = '[data-component="prompt-variant-control"]'
-export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
-export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
-export const settingsThemeSelector = '[data-action="settings-theme"]'
-export const settingsCodeFontSelector = '[data-action="settings-code-font"]'
-export const settingsUIFontSelector = '[data-action="settings-ui-font"]'
-export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]'
-export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
-export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
-export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]'
-export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
-export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
-export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]'
-export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]'
-
-const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]'
-
-export const projectSwitchSelector = (slug: string) =>
-  `${sidebarNavSelector} [data-action="project-switch"][data-project="${slug}"]`
-
-export const projectMenuTriggerSelector = (slug: string) =>
-  `${sidebarNavSelector} [data-action="project-menu"][data-project="${slug}"]`
-
-export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
-
-export const projectWorkspacesToggleSelector = (slug: string) =>
-  `[data-action="project-workspaces-toggle"][data-project="${slug}"]`
-
-export const titlebarRightSelector = "#opencode-titlebar-right"
-
-export const popoverBodySelector = '[data-slot="popover-body"]'
-
-export const dropdownMenuContentSelector = '[data-component="dropdown-menu-content"]'
-
-export const inlineInputSelector = '[data-component="inline-input"]'
-
-export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`
-
-export const workspaceItemSelector = (slug: string) =>
-  `${sidebarNavSelector} [data-component="workspace-item"][data-workspace="${slug}"]`
-
-export const workspaceMenuTriggerSelector = (slug: string) =>
-  `${sidebarNavSelector} [data-action="workspace-menu"][data-workspace="${slug}"]`
-
-export const workspaceNewSessionSelector = (slug: string) =>
-  `${sidebarNavSelector} [data-action="workspace-new-session"][data-workspace="${slug}"]`
-
-export const listItemSelector = '[data-slot="list-item"]'
-
-export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
-
-export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]`
-
-export const keybindButtonSelector = (id: string) => `[data-keybind-id="${id}"]`

+ 0 - 64
packages/app/e2e/session/session-child-navigation.spec.ts

@@ -1,64 +0,0 @@
-import { seedSessionTask, withSession } from "../actions"
-import { test, expect } from "../fixtures"
-import { inputMatch } from "../prompt/mock"
-
-test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => {
-  test.setTimeout(120_000)
-
-  const errs: string[] = []
-  const onError = (err: Error) => {
-    errs.push(err.message)
-  }
-  page.on("pageerror", onError)
-
-  try {
-    await project.open()
-    await withSession(project.sdk, `e2e child nav ${Date.now()}`, async (session) => {
-      const taskInput = {
-        description: "Open child session",
-        prompt: "Search the repository for AssistantParts and then reply with exactly CHILD_OK.",
-        subagent_type: "general",
-      }
-      await llm.toolMatch(inputMatch(taskInput), "task", taskInput)
-      const child = await seedSessionTask(project.sdk, {
-        sessionID: session.id,
-        description: taskInput.description,
-        prompt: taskInput.prompt,
-      })
-      project.trackSession(child.sessionID)
-
-      await project.gotoSession(session.id)
-
-      const header = page.locator("[data-session-title]")
-      await expect(header.getByRole("button", { name: "More options" })).toBeVisible({ timeout: 30_000 })
-
-      const card = page
-        .locator('[data-component="task-tool-card"]')
-        .filter({ hasText: /open child session/i })
-        .first()
-      await expect(card).toBeVisible({ timeout: 30_000 })
-      await card.click()
-
-      await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
-      await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(session.title)
-      await expect(header.locator('[data-slot="session-title-child"]')).toHaveText(taskInput.description)
-      await expect(header.locator('[data-slot="session-title-separator"]')).toHaveText("/")
-      await expect
-        .poll(
-          () =>
-            header.locator('[data-slot="session-title-separator"]').evaluate((el) => ({
-              left: getComputedStyle(el).paddingLeft,
-              right: getComputedStyle(el).paddingRight,
-            })),
-          { timeout: 30_000 },
-        )
-        .toEqual({ left: "8px", right: "8px" })
-      await expect(header.getByRole("button", { name: "More options" })).toHaveCount(0)
-      await expect(page.getByText("Subagent sessions cannot be prompted.")).toBeVisible({ timeout: 30_000 })
-      await expect(page.getByRole("button", { name: "Back to main session." })).toBeVisible({ timeout: 30_000 })
-      await expect.poll(() => errs, { timeout: 5_000 }).toEqual([])
-    })
-  } finally {
-    page.off("pageerror", onError)
-  }
-})

+ 0 - 655
packages/app/e2e/session/session-composer-dock.spec.ts

@@ -1,655 +0,0 @@
-import { test, expect } from "../fixtures"
-import {
-  composerEvent,
-  type ComposerDriverState,
-  type ComposerProbeState,
-  type ComposerWindow,
-} from "../../src/testing/session-composer"
-import { cleanupSession, clearSessionDockSeed, closeDialog, openSettings, seedSessionQuestion } from "../actions"
-import {
-  permissionDockSelector,
-  promptSelector,
-  questionDockSelector,
-  sessionComposerDockSelector,
-  sessionTodoToggleButtonSelector,
-} from "../selectors"
-import { modKey } from "../utils"
-import { inputMatch } from "../prompt/mock"
-
-type Sdk = Parameters<typeof clearSessionDockSeed>[0]
-type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" }
-
-async function withDockSession<T>(
-  sdk: Sdk,
-  title: string,
-  fn: (session: { id: string; title: string }) => Promise<T>,
-  opts?: { permission?: PermissionRule[]; trackSession?: (sessionID: string) => void },
-) {
-  const session = await sdk.session
-    .create(opts?.permission ? { title, permission: opts.permission } : { title })
-    .then((r) => r.data)
-  if (!session?.id) throw new Error("Session create did not return an id")
-  opts?.trackSession?.(session.id)
-  try {
-    return await fn(session)
-  } finally {
-    await cleanupSession({ sdk, sessionID: session.id })
-  }
-}
-
-const defaultQuestions = [
-  {
-    header: "Need input",
-    question: "Pick one option",
-    options: [
-      { label: "Continue", description: "Continue now" },
-      { label: "Stop", description: "Stop here" },
-    ],
-  },
-]
-
-test.setTimeout(120_000)
-
-async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>) {
-  try {
-    return await fn()
-  } finally {
-    await clearSessionDockSeed(sdk, sessionID).catch(() => undefined)
-  }
-}
-
-async function clearPermissionDock(page: any, label: RegExp) {
-  const dock = page.locator(permissionDockSelector)
-  await expect(dock).toBeVisible()
-  await dock.getByRole("button", { name: label }).click()
-}
-
-async function setAutoAccept(page: any, enabled: boolean) {
-  const dialog = await openSettings(page)
-  const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first()
-  const input = toggle.locator('[data-slot="switch-input"]').first()
-  await expect(toggle).toBeVisible()
-  const checked = (await input.getAttribute("aria-checked")) === "true"
-  if (checked !== enabled) await toggle.locator('[data-slot="switch-control"]').click()
-  await expect(input).toHaveAttribute("aria-checked", enabled ? "true" : "false")
-  await closeDialog(page, dialog)
-}
-
-async function expectQuestionBlocked(page: any) {
-  await expect(page.locator(questionDockSelector)).toBeVisible()
-  await expect(page.locator(promptSelector)).toHaveCount(0)
-}
-
-async function expectQuestionOpen(page: any) {
-  await expect(page.locator(questionDockSelector)).toHaveCount(0)
-  await expect(page.locator(promptSelector)).toBeVisible()
-}
-
-async function expectPermissionBlocked(page: any) {
-  await expect(page.locator(permissionDockSelector)).toBeVisible()
-  await expect(page.locator(promptSelector)).toHaveCount(0)
-}
-
-async function expectPermissionOpen(page: any) {
-  await expect(page.locator(permissionDockSelector)).toHaveCount(0)
-  await expect(page.locator(promptSelector)).toBeVisible()
-}
-
-async function todoDock(page: any, sessionID: string) {
-  await page.addInitScript(() => {
-    const win = window as ComposerWindow
-    win.__opencode_e2e = {
-      ...win.__opencode_e2e,
-      composer: {
-        enabled: true,
-        sessions: {},
-      },
-    }
-  })
-
-  const write = async (driver: ComposerDriverState | undefined) => {
-    await page.evaluate(
-      (input: { event: string; sessionID: string; driver: ComposerDriverState | undefined }) => {
-        const win = window as ComposerWindow
-        const composer = win.__opencode_e2e?.composer
-        if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled")
-        composer.sessions ??= {}
-        const prev = composer.sessions[input.sessionID] ?? {}
-        if (!input.driver) {
-          if (!prev.probe) {
-            delete composer.sessions[input.sessionID]
-          } else {
-            composer.sessions[input.sessionID] = { probe: prev.probe }
-          }
-        } else {
-          composer.sessions[input.sessionID] = {
-            ...prev,
-            driver: input.driver,
-          }
-        }
-        window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } }))
-      },
-      { event: composerEvent, sessionID, driver },
-    )
-  }
-
-  const read = () =>
-    page.evaluate((sessionID: string) => {
-      const win = window as ComposerWindow
-      return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null
-    }, sessionID) as Promise<ComposerProbeState | null>
-
-  const api = {
-    async clear() {
-      await write(undefined)
-      return api
-    },
-    async open(todos: NonNullable<ComposerDriverState["todos"]>) {
-      await write({ live: true, todos })
-      return api
-    },
-    async finish(todos: NonNullable<ComposerDriverState["todos"]>) {
-      await write({ live: false, todos })
-      return api
-    },
-    async expectOpen(states: ComposerProbeState["states"]) {
-      await expect.poll(read, { timeout: 10_000 }).toMatchObject({
-        mounted: true,
-        collapsed: false,
-        hidden: false,
-        count: states.length,
-        states,
-      })
-      return api
-    },
-    async expectCollapsed(states: ComposerProbeState["states"]) {
-      await expect.poll(read, { timeout: 10_000 }).toMatchObject({
-        mounted: true,
-        collapsed: true,
-        hidden: true,
-        count: states.length,
-        states,
-      })
-      return api
-    },
-    async expectClosed() {
-      await expect.poll(read, { timeout: 10_000 }).toMatchObject({ mounted: false })
-      return api
-    },
-    async collapse() {
-      await page.locator(sessionTodoToggleButtonSelector).click()
-      return api
-    },
-    async expand() {
-      await page.locator(sessionTodoToggleButtonSelector).click()
-      return api
-    },
-  }
-
-  return api
-}
-
-async function withMockPermission<T>(
-  page: any,
-  request: {
-    id: string
-    sessionID: string
-    permission: string
-    patterns: string[]
-    metadata?: Record<string, unknown>
-    always?: string[]
-  },
-  opts: { child?: any } | undefined,
-  fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
-) {
-  const listUrl = /\/permission(?:\?.*)?$/
-  const replyUrls = [/\/session\/[^/]+\/permissions\/[^/?]+(?:\?.*)?$/, /\/permission\/[^/]+\/reply(?:\?.*)?$/]
-  let pending = [
-    {
-      ...request,
-      always: request.always ?? ["*"],
-      metadata: request.metadata ?? {},
-    },
-  ]
-
-  const list = async (route: any) => {
-    await route.fulfill({
-      status: 200,
-      contentType: "application/json",
-      body: JSON.stringify(pending),
-    })
-  }
-
-  const reply = async (route: any) => {
-    const url = new URL(route.request().url())
-    const parts = url.pathname.split("/").filter(Boolean)
-    const id = parts.at(-1) === "reply" ? parts.at(-2) : parts.at(-1)
-    pending = pending.filter((item) => item.id !== id)
-    await route.fulfill({
-      status: 200,
-      contentType: "application/json",
-      body: JSON.stringify(true),
-    })
-  }
-
-  await page.route(listUrl, list)
-  for (const item of replyUrls) {
-    await page.route(item, reply)
-  }
-
-  const sessionList = opts?.child
-    ? async (route: any) => {
-        const res = await route.fetch()
-        const json = await res.json()
-        const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
-        if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child)
-        await route.fulfill({
-          response: res,
-          body: JSON.stringify(json),
-        })
-      }
-    : undefined
-
-  if (sessionList) await page.route("**/session?*", sessionList)
-
-  const state = {
-    async resolved() {
-      await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0)
-    },
-  }
-
-  try {
-    return await fn(state)
-  } finally {
-    await page.unroute(listUrl, list)
-    for (const item of replyUrls) {
-      await page.unroute(item, reply)
-    }
-    if (sessionList) await page.unroute("**/session?*", sessionList)
-  }
-}
-
-test("default dock shows prompt input", async ({ page, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock default",
-    async (session) => {
-      await project.gotoSession(session.id)
-
-      await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
-      await expect(page.locator(promptSelector)).toBeVisible()
-      await expect(page.locator('[data-action="prompt-permissions"]')).toHaveCount(0)
-      await expect(page.locator(questionDockSelector)).toHaveCount(0)
-      await expect(page.locator(permissionDockSelector)).toHaveCount(0)
-
-      await page.locator(promptSelector).click()
-      await expect(page.locator(promptSelector)).toBeFocused()
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("auto-accept toggle works before first submit", async ({ page, project }) => {
-  await project.open()
-
-  await setAutoAccept(page, true)
-  await setAutoAccept(page, false)
-})
-
-test("blocked question flow unblocks after submit", async ({ page, llm, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock question",
-    async (session) => {
-      await withDockSeed(project.sdk, session.id, async () => {
-        await project.gotoSession(session.id)
-
-        await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions })
-        await seedSessionQuestion(project.sdk, {
-          sessionID: session.id,
-          questions: defaultQuestions,
-        })
-
-        const dock = page.locator(questionDockSelector)
-        await expectQuestionBlocked(page)
-
-        await dock.locator('[data-slot="question-option"]').first().click()
-        await dock.getByRole("button", { name: /submit/i }).click()
-
-        await expectQuestionOpen(page)
-      })
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("blocked question flow supports keyboard shortcuts", async ({ page, llm, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock question keyboard",
-    async (session) => {
-      await withDockSeed(project.sdk, session.id, async () => {
-        await project.gotoSession(session.id)
-
-        await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions })
-        await seedSessionQuestion(project.sdk, {
-          sessionID: session.id,
-          questions: defaultQuestions,
-        })
-
-        const dock = page.locator(questionDockSelector)
-        const first = dock.locator('[data-slot="question-option"]').first()
-        const second = dock.locator('[data-slot="question-option"]').nth(1)
-
-        await expectQuestionBlocked(page)
-        await expect(first).toBeFocused()
-
-        await page.keyboard.press("ArrowDown")
-        await expect(second).toBeFocused()
-
-        await page.keyboard.press("Space")
-        await page.keyboard.press(`${modKey}+Enter`)
-        await expectQuestionOpen(page)
-      })
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("blocked question flow supports escape dismiss", async ({ page, llm, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock question escape",
-    async (session) => {
-      await withDockSeed(project.sdk, session.id, async () => {
-        await project.gotoSession(session.id)
-
-        await llm.toolMatch(inputMatch({ questions: defaultQuestions }), "question", { questions: defaultQuestions })
-        await seedSessionQuestion(project.sdk, {
-          sessionID: session.id,
-          questions: defaultQuestions,
-        })
-
-        const dock = page.locator(questionDockSelector)
-        const first = dock.locator('[data-slot="question-option"]').first()
-
-        await expectQuestionBlocked(page)
-        await expect(first).toBeFocused()
-
-        await page.keyboard.press("Escape")
-        await expectQuestionOpen(page)
-      })
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("blocked permission flow supports allow once", async ({ page, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock permission once",
-    async (session) => {
-      await project.gotoSession(session.id)
-      await setAutoAccept(page, false)
-      await withMockPermission(
-        page,
-        {
-          id: "per_e2e_once",
-          sessionID: session.id,
-          permission: "bash",
-          patterns: ["/tmp/opencode-e2e-perm-once"],
-          metadata: { description: "Need permission for command" },
-        },
-        undefined,
-        async (state) => {
-          await page.goto(page.url())
-          await expectPermissionBlocked(page)
-
-          await clearPermissionDock(page, /allow once/i)
-          await state.resolved()
-          await page.goto(page.url())
-          await expectPermissionOpen(page)
-        },
-      )
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("blocked permission flow supports reject", async ({ page, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock permission reject",
-    async (session) => {
-      await project.gotoSession(session.id)
-      await setAutoAccept(page, false)
-      await withMockPermission(
-        page,
-        {
-          id: "per_e2e_reject",
-          sessionID: session.id,
-          permission: "bash",
-          patterns: ["/tmp/opencode-e2e-perm-reject"],
-        },
-        undefined,
-        async (state) => {
-          await page.goto(page.url())
-          await expectPermissionBlocked(page)
-
-          await clearPermissionDock(page, /deny/i)
-          await state.resolved()
-          await page.goto(page.url())
-          await expectPermissionOpen(page)
-        },
-      )
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("blocked permission flow supports allow always", async ({ page, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock permission always",
-    async (session) => {
-      await project.gotoSession(session.id)
-      await setAutoAccept(page, false)
-      await withMockPermission(
-        page,
-        {
-          id: "per_e2e_always",
-          sessionID: session.id,
-          permission: "bash",
-          patterns: ["/tmp/opencode-e2e-perm-always"],
-          metadata: { description: "Need permission for command" },
-        },
-        undefined,
-        async (state) => {
-          await page.goto(page.url())
-          await expectPermissionBlocked(page)
-
-          await clearPermissionDock(page, /allow always/i)
-          await state.resolved()
-          await page.goto(page.url())
-          await expectPermissionOpen(page)
-        },
-      )
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("child session question request blocks parent dock and unblocks after submit", async ({ page, llm, project }) => {
-  const questions = [
-    {
-      header: "Child input",
-      question: "Pick one child option",
-      options: [
-        { label: "Continue", description: "Continue child" },
-        { label: "Stop", description: "Stop child" },
-      ],
-    },
-  ]
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock child question parent",
-    async (session) => {
-      await project.gotoSession(session.id)
-
-      const child = await project.sdk.session
-        .create({
-          title: "e2e composer dock child question",
-          parentID: session.id,
-        })
-        .then((r) => r.data)
-      if (!child?.id) throw new Error("Child session create did not return an id")
-      project.trackSession(child.id)
-
-      try {
-        await withDockSeed(project.sdk, child.id, async () => {
-          await llm.toolMatch(inputMatch({ questions }), "question", { questions })
-          await seedSessionQuestion(project.sdk, {
-            sessionID: child.id,
-            questions,
-          })
-
-          const dock = page.locator(questionDockSelector)
-          await expectQuestionBlocked(page)
-
-          await dock.locator('[data-slot="question-option"]').first().click()
-          await dock.getByRole("button", { name: /submit/i }).click()
-
-          await expectQuestionOpen(page)
-        })
-      } finally {
-        await cleanupSession({ sdk: project.sdk, sessionID: child.id })
-      }
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("child session permission request blocks parent dock and supports allow once", async ({ page, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock child permission parent",
-    async (session) => {
-      await project.gotoSession(session.id)
-      await setAutoAccept(page, false)
-
-      const child = await project.sdk.session
-        .create({
-          title: "e2e composer dock child permission",
-          parentID: session.id,
-        })
-        .then((r) => r.data)
-      if (!child?.id) throw new Error("Child session create did not return an id")
-      project.trackSession(child.id)
-
-      try {
-        await withMockPermission(
-          page,
-          {
-            id: "per_e2e_child",
-            sessionID: child.id,
-            permission: "bash",
-            patterns: ["/tmp/opencode-e2e-perm-child"],
-            metadata: { description: "Need child permission" },
-          },
-          { child },
-          async (state) => {
-            await page.goto(page.url())
-            await expectPermissionBlocked(page)
-
-            await clearPermissionDock(page, /allow once/i)
-            await state.resolved()
-            await page.goto(page.url())
-
-            await expectPermissionOpen(page)
-          },
-        )
-      } finally {
-        await cleanupSession({ sdk: project.sdk, sessionID: child.id })
-      }
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("todo dock transitions and collapse behavior", async ({ page, project }) => {
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock todo",
-    async (session) => {
-      const dock = await todoDock(page, session.id)
-      await project.gotoSession(session.id)
-      await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
-
-      try {
-        await dock.open([
-          { content: "first task", status: "pending", priority: "high" },
-          { content: "second task", status: "in_progress", priority: "medium" },
-        ])
-        await dock.expectOpen(["pending", "in_progress"])
-
-        await dock.collapse()
-        await dock.expectCollapsed(["pending", "in_progress"])
-
-        await dock.expand()
-        await dock.expectOpen(["pending", "in_progress"])
-
-        await dock.finish([
-          { content: "first task", status: "completed", priority: "high" },
-          { content: "second task", status: "cancelled", priority: "medium" },
-        ])
-        await dock.expectClosed()
-      } finally {
-        await dock.clear()
-      }
-    },
-    { trackSession: project.trackSession },
-  )
-})
-
-test("keyboard focus stays off prompt while blocked", async ({ page, llm, project }) => {
-  const questions = [
-    {
-      header: "Need input",
-      question: "Pick one option",
-      options: [{ label: "Continue", description: "Continue now" }],
-    },
-  ]
-  await project.open()
-  await withDockSession(
-    project.sdk,
-    "e2e composer dock keyboard",
-    async (session) => {
-      await withDockSeed(project.sdk, session.id, async () => {
-        await project.gotoSession(session.id)
-
-        await llm.toolMatch(inputMatch({ questions }), "question", { questions })
-        await seedSessionQuestion(project.sdk, {
-          sessionID: session.id,
-          questions,
-        })
-
-        await expectQuestionBlocked(page)
-
-        await page.locator("main").click({ position: { x: 5, y: 5 } })
-        await page.keyboard.type("abc")
-        await expect(page.locator(promptSelector)).toHaveCount(0)
-      })
-    },
-    { trackSession: project.trackSession },
-  )
-})

+ 0 - 362
packages/app/e2e/session/session-model-persistence.spec.ts

@@ -1,362 +0,0 @@
-import type { Locator, Page } from "@playwright/test"
-import { test, expect } from "../fixtures"
-import { openSidebar, resolveSlug, setWorkspacesEnabled, waitSession, waitSlug } from "../actions"
-import {
-  promptAgentSelector,
-  promptModelSelector,
-  promptVariantSelector,
-  workspaceItemSelector,
-  workspaceNewSessionSelector,
-} from "../selectors"
-import { createSdk, sessionPath } from "../utils"
-
-type Footer = {
-  agent: string
-  model: string
-  variant: string
-}
-
-type Probe = {
-  dir?: string
-  sessionID?: string
-  agent?: string
-  model?: { providerID: string; modelID: string; name?: string }
-  variant?: string | null
-  pick?: {
-    agent?: string
-    model?: { providerID: string; modelID: string }
-    variant?: string | null
-  }
-  variants?: string[]
-  models?: Array<{ providerID: string; modelID: string; name: string }>
-  agents?: Array<{ name: string }>
-}
-
-const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
-
-const text = async (locator: Locator) => ((await locator.textContent()) ?? "").trim()
-
-const modelKey = (state: Probe | null) => (state?.model ? `${state.model.providerID}:${state.model.modelID}` : null)
-
-async function probe(page: Page): Promise<Probe | null> {
-  return page.evaluate(() => {
-    const win = window as Window & {
-      __opencode_e2e?: {
-        model?: {
-          current?: Probe
-        }
-      }
-    }
-    return win.__opencode_e2e?.model?.current ?? null
-  })
-}
-
-async function currentModel(page: Page) {
-  await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).not.toBe(null)
-  const value = await probe(page).then(modelKey)
-  if (!value) throw new Error("Failed to resolve current model key")
-  return value
-}
-
-async function waitControl(page: Page, key: "setAgent" | "setModel" | "setVariant") {
-  await expect
-    .poll(
-      () =>
-        page.evaluate((key) => {
-          const win = window as Window & {
-            __opencode_e2e?: {
-              model?: {
-                controls?: Record<string, unknown>
-              }
-            }
-          }
-          return !!win.__opencode_e2e?.model?.controls?.[key]
-        }, key),
-      { timeout: 30_000 },
-    )
-    .toBe(true)
-}
-
-async function pickAgent(page: Page, value: string) {
-  await waitControl(page, "setAgent")
-  await page.evaluate((value) => {
-    const win = window as Window & {
-      __opencode_e2e?: {
-        model?: {
-          controls?: {
-            setAgent?: (value: string | undefined) => void
-          }
-        }
-      }
-    }
-    const fn = win.__opencode_e2e?.model?.controls?.setAgent
-    if (!fn) throw new Error("Model e2e agent control is not enabled")
-    fn(value)
-  }, value)
-}
-
-async function pickModel(page: Page, value: { providerID: string; modelID: string }) {
-  await waitControl(page, "setModel")
-  await page.evaluate((value) => {
-    const win = window as Window & {
-      __opencode_e2e?: {
-        model?: {
-          controls?: {
-            setModel?: (value: { providerID: string; modelID: string } | undefined) => void
-          }
-        }
-      }
-    }
-    const fn = win.__opencode_e2e?.model?.controls?.setModel
-    if (!fn) throw new Error("Model e2e model control is not enabled")
-    fn(value)
-  }, value)
-}
-
-async function pickVariant(page: Page, value: string) {
-  await waitControl(page, "setVariant")
-  await page.evaluate((value) => {
-    const win = window as Window & {
-      __opencode_e2e?: {
-        model?: {
-          controls?: {
-            setVariant?: (value: string | undefined) => void
-          }
-        }
-      }
-    }
-    const fn = win.__opencode_e2e?.model?.controls?.setVariant
-    if (!fn) throw new Error("Model e2e variant control is not enabled")
-    fn(value)
-  }, value)
-}
-
-async function read(page: Page): Promise<Footer> {
-  return {
-    agent: await text(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()),
-    model: await text(page.locator(`${promptModelSelector} [data-action="prompt-model"] span`).first()),
-    variant: await text(page.locator(`${promptVariantSelector} [data-slot="select-select-trigger-value"]`).first()),
-  }
-}
-
-async function waitFooter(page: Page, expected: Partial<Footer>) {
-  let hit: Footer | null = null
-  await expect
-    .poll(
-      async () => {
-        const state = await read(page)
-        const ok = Object.entries(expected).every(([key, value]) => state[key as keyof Footer] === value)
-        if (ok) hit = state
-        return ok
-      },
-      { timeout: 30_000 },
-    )
-    .toBe(true)
-  if (!hit) throw new Error("Failed to resolve prompt footer state")
-  return hit
-}
-
-async function waitModel(page: Page, value: string) {
-  await expect.poll(() => probe(page).then(modelKey), { timeout: 30_000 }).toBe(value)
-}
-
-async function choose(page: Page, root: string, value: string) {
-  const select = page.locator(root)
-  await expect(select).toBeVisible()
-  await pickAgent(page, value)
-}
-
-async function variantCount(page: Page) {
-  return (await probe(page))?.variants?.length ?? 0
-}
-
-async function agents(page: Page) {
-  return ((await probe(page))?.agents ?? []).map((item) => item.name).filter(Boolean)
-}
-
-async function ensureVariant(page: Page, directory: string): Promise<Footer> {
-  const current = await read(page)
-  if ((await variantCount(page)) >= 2) return current
-
-  const cfg = await createSdk(directory)
-    .config.get()
-    .then((x) => x.data)
-  const visible = new Set(await agents(page))
-  const entry = Object.entries(cfg?.agent ?? {}).find((item) => {
-    const value = item[1]
-    return !!value && typeof value === "object" && "variant" in value && "model" in value && visible.has(item[0])
-  })
-  const name = entry?.[0]
-  test.skip(!name, "no agent with alternate variants available")
-  if (!name) return current
-
-  await choose(page, promptAgentSelector, name)
-  await expect.poll(() => variantCount(page), { timeout: 30_000 }).toBeGreaterThanOrEqual(2)
-  return waitFooter(page, { agent: name })
-}
-
-async function chooseDifferentVariant(page: Page): Promise<Footer> {
-  const current = await read(page)
-  const next = (await probe(page))?.variants?.find((item) => item !== current.variant)
-  if (!next) throw new Error("Current model has no alternate variant to select")
-
-  await pickVariant(page, next)
-  return waitFooter(page, { agent: current.agent, model: current.model, variant: next })
-}
-
-async function chooseOtherModel(page: Page, skip: string[] = []): Promise<Footer> {
-  const current = await currentModel(page)
-  const next = (await probe(page))?.models?.find((item) => {
-    const key = `${item.providerID}:${item.modelID}`
-    return key !== current && !skip.includes(key)
-  })
-  if (!next) throw new Error("Failed to choose a different model")
-  await pickModel(page, { providerID: next.providerID, modelID: next.modelID })
-  await expect.poll(async () => (await read(page)).model, { timeout: 30_000 }).toBe(next.name)
-  return read(page)
-}
-
-async function goto(page: Page, directory: string, sessionID?: string) {
-  await page.goto(sessionPath(directory, sessionID))
-  await waitSession(page, { directory, sessionID })
-}
-
-async function submit(project: Parameters<typeof test>[0]["project"], value: string) {
-  return project.prompt(value)
-}
-
-async function createWorkspace(page: Page, root: string, seen: string[]) {
-  await openSidebar(page)
-  await page.getByRole("button", { name: "New workspace" }).first().click()
-
-  const next = await resolveSlug(await waitSlug(page, [root, ...seen]))
-  await waitSession(page, { directory: next.directory })
-  return next
-}
-
-async function waitWorkspace(page: Page, slug: string) {
-  await openSidebar(page)
-  await expect
-    .poll(
-      async () => {
-        const item = page.locator(workspaceItemSelector(slug)).first()
-        try {
-          await item.hover({ timeout: 500 })
-          return true
-        } catch {
-          return false
-        }
-      },
-      { timeout: 60_000 },
-    )
-    .toBe(true)
-}
-
-async function newWorkspaceSession(page: Page, slug: string) {
-  await waitWorkspace(page, slug)
-  const item = page.locator(workspaceItemSelector(slug)).first()
-  await item.hover()
-
-  const button = page.locator(workspaceNewSessionSelector(slug)).first()
-  await expect(button).toBeVisible()
-  await button.click({ force: true })
-
-  const next = await resolveSlug(await waitSlug(page))
-  return waitSession(page, { directory: next.directory }).then((item) => item.directory)
-}
-
-test("session model restore per session without leaking into new sessions", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1440, height: 900 })
-
-  await project.open()
-  await project.gotoSession()
-
-  const firstState = await chooseOtherModel(page)
-  const firstKey = await currentModel(page)
-  const first = await submit(project, `session variant ${Date.now()}`)
-
-  await page.reload()
-  await waitSession(page, { directory: project.directory, sessionID: first })
-  await waitFooter(page, firstState)
-
-  await project.gotoSession()
-  const fresh = await read(page)
-  expect(fresh.model).not.toBe(firstState.model)
-
-  const secondState = await chooseOtherModel(page, [firstKey])
-  const second = await submit(project, `session model ${Date.now()}`)
-
-  await goto(page, project.directory, first)
-  await waitFooter(page, firstState)
-
-  await goto(page, project.directory, second)
-  await waitFooter(page, secondState)
-
-  await project.gotoSession()
-  await page.reload()
-  await waitSession(page, { directory: project.directory })
-  await waitFooter(page, fresh)
-})
-
-test("session model restore across workspaces", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1440, height: 900 })
-
-  await project.open()
-  const root = project.directory
-  await project.gotoSession()
-
-  const firstState = await chooseOtherModel(page)
-  const firstKey = await currentModel(page)
-  const first = await submit(project, `root session ${Date.now()}`)
-
-  await openSidebar(page)
-  await setWorkspacesEnabled(page, project.slug, true)
-
-  const one = await createWorkspace(page, project.slug, [])
-  const oneDir = await newWorkspaceSession(page, one.slug)
-  project.trackDirectory(oneDir)
-
-  const secondState = await chooseOtherModel(page, [firstKey])
-  const secondKey = await currentModel(page)
-  const second = await submit(project, `workspace one ${Date.now()}`)
-
-  const two = await createWorkspace(page, project.slug, [one.slug])
-  const twoDir = await newWorkspaceSession(page, two.slug)
-  project.trackDirectory(twoDir)
-
-  const thirdState = await chooseOtherModel(page, [firstKey, secondKey])
-  const third = await submit(project, `workspace two ${Date.now()}`)
-
-  await goto(page, root, first)
-  await waitFooter(page, firstState)
-
-  await goto(page, oneDir, second)
-  await waitFooter(page, secondState)
-
-  await goto(page, twoDir, third)
-  await waitFooter(page, thirdState)
-
-  await goto(page, root, first)
-  await waitFooter(page, firstState)
-})
-
-test("variant preserved when switching agent modes", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1440, height: 900 })
-
-  await project.open()
-  await project.gotoSession()
-
-  await ensureVariant(page, project.directory)
-  const updated = await chooseDifferentVariant(page)
-
-  const available = await agents(page)
-  const other = available.find((name) => name !== updated.agent)
-  test.skip(!other, "only one agent available")
-  if (!other) return
-
-  await choose(page, promptAgentSelector, other)
-  await waitFooter(page, { agent: other, variant: updated.variant })
-
-  await choose(page, promptAgentSelector, updated.agent)
-  await waitFooter(page, { agent: updated.agent, variant: updated.variant })
-})

+ 0 - 440
packages/app/e2e/session/session-review.spec.ts

@@ -1,440 +0,0 @@
-import { waitSessionIdle, withSession } from "../actions"
-import { test, expect } from "../fixtures"
-import { bodyText } from "../prompt/mock"
-
-const count = 14
-
-function body(mark: string) {
-  return [
-    `title ${mark}`,
-    `mark ${mark}`,
-    ...Array.from({ length: 32 }, (_, i) => `line ${String(i + 1).padStart(2, "0")} ${mark}`),
-  ]
-}
-
-function files(tag: string) {
-  return Array.from({ length: count }, (_, i) => {
-    const id = String(i).padStart(2, "0")
-    return {
-      file: `review-scroll-${id}.txt`,
-      mark: `${tag}-${id}`,
-    }
-  })
-}
-
-function seed(list: ReturnType<typeof files>) {
-  const out = ["*** Begin Patch"]
-
-  for (const item of list) {
-    out.push(`*** Add File: ${item.file}`)
-    for (const line of body(item.mark)) out.push(`+${line}`)
-  }
-
-  out.push("*** End Patch")
-  return out.join("\n")
-}
-
-function edit(file: string, prev: string, next: string) {
-  return ["*** Begin Patch", `*** Update File: ${file}`, "@@", `-mark ${prev}`, `+mark ${next}`, "*** End Patch"].join(
-    "\n",
-  )
-}
-
-async function patchWithMock(
-  llm: Parameters<typeof test>[0]["llm"],
-  sdk: Parameters<typeof withSession>[0],
-  sessionID: string,
-  patchText: string,
-) {
-  const callsBefore = await llm.calls()
-  await llm.toolMatch(
-    (hit) => bodyText(hit).includes("Your only valid response is one apply_patch tool call."),
-    "apply_patch",
-    { patchText },
-  )
-  await sdk.session.prompt({
-    sessionID,
-    agent: "build",
-    system: [
-      "You are seeding deterministic e2e UI state.",
-      "Your only valid response is one apply_patch tool call.",
-      `Use this JSON input: ${JSON.stringify({ patchText })}`,
-      "Do not call any other tools.",
-      "Do not output plain text.",
-    ].join("\n"),
-    parts: [{ type: "text", text: "Apply the provided patch exactly once." }],
-  })
-
-  await expect.poll(() => llm.calls().then((c) => c > callsBefore), { timeout: 30_000 }).toBe(true)
-  await expect
-    .poll(
-      async () => {
-        const diff = await sdk.session.diff({ sessionID }).then((res) => res.data ?? [])
-        return diff.length
-      },
-      { timeout: 120_000 },
-    )
-    .toBeGreaterThan(0)
-}
-
-async function show(page: Parameters<typeof test>[0]["page"]) {
-  const btn = page.getByRole("button", { name: "Toggle review" }).first()
-  await expect(btn).toBeVisible()
-  if ((await btn.getAttribute("aria-expanded")) !== "true") await btn.click()
-  await expect(btn).toHaveAttribute("aria-expanded", "true")
-}
-
-async function expand(page: Parameters<typeof test>[0]["page"]) {
-  const close = page.getByRole("button", { name: /^Collapse all$/i }).first()
-  const open = await close
-    .isVisible()
-    .then((value) => value)
-    .catch(() => false)
-
-  const btn = page.getByRole("button", { name: /^Expand all$/i }).first()
-  if (open) {
-    await close.click()
-    await expect(btn).toBeVisible()
-  }
-
-  await expect(btn).toBeVisible()
-  await btn.click()
-  await expect(close).toBeVisible()
-}
-
-async function waitMark(page: Parameters<typeof test>[0]["page"], file: string, mark: string) {
-  await page.waitForFunction(
-    ({ file, mark }) => {
-      const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
-      if (!(view instanceof HTMLElement)) return false
-
-      const head = Array.from(view.querySelectorAll("h3")).find(
-        (node) => node instanceof HTMLElement && node.textContent?.includes(file),
-      )
-      if (!(head instanceof HTMLElement)) return false
-
-      return Array.from(head.parentElement?.querySelectorAll("diffs-container") ?? []).some((host) => {
-        if (!(host instanceof HTMLElement)) return false
-        const root = host.shadowRoot
-        return root?.textContent?.includes(`mark ${mark}`) ?? false
-      })
-    },
-    { file, mark },
-    { timeout: 60_000 },
-  )
-}
-
-async function spot(page: Parameters<typeof test>[0]["page"], file: string) {
-  return page.evaluate((file) => {
-    const view = document.querySelector('[data-slot="session-review-scroll"] .scroll-view__viewport')
-    if (!(view instanceof HTMLElement)) return null
-
-    const row = Array.from(view.querySelectorAll("h3")).find(
-      (node) => node instanceof HTMLElement && node.textContent?.includes(file),
-    )
-    if (!(row instanceof HTMLElement)) return null
-
-    const a = row.getBoundingClientRect()
-    const b = view.getBoundingClientRect()
-    return {
-      top: a.top - b.top,
-      y: view.scrollTop,
-    }
-  }, file)
-}
-
-async function comment(page: Parameters<typeof test>[0]["page"], file: string, note: string) {
-  const row = page.locator(`[data-file="${file}"]`).first()
-  await expect(row).toBeVisible()
-
-  const line = row.locator('diffs-container [data-line="2"]').first()
-  await expect(line).toBeVisible()
-  await line.hover()
-
-  const add = row.getByRole("button", { name: /^Comment$/ }).first()
-  await expect(add).toBeVisible()
-  await add.click()
-
-  const area = row.locator('[data-slot="line-comment-textarea"]').first()
-  await expect(area).toBeVisible()
-  await area.fill(note)
-
-  const submit = row.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
-  await expect(submit).toBeEnabled()
-  await submit.click()
-
-  await expect(row.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
-  await expect(row.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
-}
-
-async function overflow(page: Parameters<typeof test>[0]["page"], file: string) {
-  const row = page.locator(`[data-file="${file}"]`).first()
-  const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
-  const pop = row.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
-  const tools = row.locator('[data-slot="line-comment-tools"]').first()
-
-  const [width, viewBox, popBox, toolsBox] = await Promise.all([
-    view.evaluate((el) => el.scrollWidth - el.clientWidth),
-    view.boundingBox(),
-    pop.boundingBox(),
-    tools.boundingBox(),
-  ])
-
-  if (!viewBox || !popBox || !toolsBox) return null
-
-  return {
-    width,
-    pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
-    tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
-  }
-}
-
-async function openReviewFile(page: Parameters<typeof test>[0]["page"], file: string) {
-  const row = page.locator(`[data-file="${file}"]`).first()
-  await expect(row).toBeVisible()
-  await row.hover()
-
-  const open = row.getByRole("button", { name: /^Open file$/i }).first()
-  await expect(open).toBeVisible()
-  await open.click()
-
-  const tab = page.getByRole("tab", { name: file }).first()
-  await expect(tab).toBeVisible()
-  await tab.click()
-
-  const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
-  await expect(viewer).toBeVisible()
-  return viewer
-}
-
-async function fileComment(page: Parameters<typeof test>[0]["page"], note: string) {
-  const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
-  await expect(viewer).toBeVisible()
-
-  const line = viewer.locator('diffs-container [data-line="2"]').first()
-  await expect(line).toBeVisible()
-  await line.hover()
-
-  const add = viewer.getByRole("button", { name: /^Comment$/ }).first()
-  await expect(add).toBeVisible()
-  await add.click()
-
-  const area = viewer.locator('[data-slot="line-comment-textarea"]').first()
-  await expect(area).toBeVisible()
-  await area.fill(note)
-
-  const submit = viewer.locator('[data-slot="line-comment-action"][data-variant="primary"]').first()
-  await expect(submit).toBeEnabled()
-  await submit.click()
-
-  await expect(viewer.locator('[data-slot="line-comment-content"]').filter({ hasText: note }).first()).toBeVisible()
-  await expect(viewer.locator('[data-slot="line-comment-tools"]').first()).toBeVisible()
-}
-
-async function fileOverflow(page: Parameters<typeof test>[0]["page"]) {
-  const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
-  const view = page.locator('[role="tabpanel"] .scroll-view__viewport').first()
-  const pop = viewer.locator('[data-slot="line-comment-popover"][data-inline-body]').first()
-  const tools = viewer.locator('[data-slot="line-comment-tools"]').first()
-
-  const [width, viewBox, popBox, toolsBox] = await Promise.all([
-    view.evaluate((el) => el.scrollWidth - el.clientWidth),
-    view.boundingBox(),
-    pop.boundingBox(),
-    tools.boundingBox(),
-  ])
-
-  if (!viewBox || !popBox || !toolsBox) return null
-
-  return {
-    width,
-    pop: popBox.x + popBox.width - (viewBox.x + viewBox.width),
-    tools: toolsBox.x + toolsBox.width - (viewBox.x + viewBox.width),
-  }
-}
-
-test("review applies inline comment clicks without horizontal overflow", async ({ page, llm, project }) => {
-  test.setTimeout(180_000)
-
-  const tag = `review-comment-${Date.now()}`
-  const file = `review-comment-${tag}.txt`
-  const note = `comment ${tag}`
-
-  await page.setViewportSize({ width: 1280, height: 900 })
-
-  await project.open()
-  await withSession(project.sdk, `e2e review comment ${tag}`, async (session) => {
-    project.trackSession(session.id)
-    await patchWithMock(llm, project.sdk, session.id, seed([{ file, mark: tag }]))
-
-    await expect
-      .poll(
-        async () => {
-          const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
-          return diff.length
-        },
-        { timeout: 60_000 },
-      )
-      .toBe(1)
-
-    await project.gotoSession(session.id)
-    await show(page)
-
-    const tab = page.getByRole("tab", { name: /Review/i }).first()
-    await expect(tab).toBeVisible()
-    await tab.click()
-
-    await expand(page)
-    await waitMark(page, file, tag)
-    await comment(page, file, note)
-
-    await expect
-      .poll(async () => (await overflow(page, file))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
-      .toBeLessThanOrEqual(1)
-    await expect
-      .poll(async () => (await overflow(page, file))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
-      .toBeLessThanOrEqual(1)
-    await expect
-      .poll(async () => (await overflow(page, file))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
-      .toBeLessThanOrEqual(1)
-  })
-})
-
-test("review file comments submit on click without clipping actions", async ({ page, llm, project }) => {
-  test.setTimeout(180_000)
-
-  const tag = `review-file-comment-${Date.now()}`
-  const file = `review-file-comment-${tag}.txt`
-  const note = `comment ${tag}`
-
-  await page.setViewportSize({ width: 1280, height: 900 })
-
-  await project.open()
-  await withSession(project.sdk, `e2e review file comment ${tag}`, async (session) => {
-    project.trackSession(session.id)
-    await patchWithMock(llm, project.sdk, session.id, seed([{ file, mark: tag }]))
-
-    await expect
-      .poll(
-        async () => {
-          const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
-          return diff.length
-        },
-        { timeout: 60_000 },
-      )
-      .toBe(1)
-
-    await project.gotoSession(session.id)
-    await show(page)
-
-    const tab = page.getByRole("tab", { name: /Review/i }).first()
-    await expect(tab).toBeVisible()
-    await tab.click()
-
-    await expand(page)
-    await waitMark(page, file, tag)
-    await openReviewFile(page, file)
-    await fileComment(page, note)
-
-    await expect
-      .poll(async () => (await fileOverflow(page))?.width ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
-      .toBeLessThanOrEqual(1)
-    await expect
-      .poll(async () => (await fileOverflow(page))?.pop ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
-      .toBeLessThanOrEqual(1)
-    await expect
-      .poll(async () => (await fileOverflow(page))?.tools ?? Number.POSITIVE_INFINITY, { timeout: 10_000 })
-      .toBeLessThanOrEqual(1)
-  })
-})
-
-test.fixme("review keeps scroll position after a live diff update", async ({ page, llm, project }) => {
-  test.setTimeout(180_000)
-
-  const tag = `review-${Date.now()}`
-  const list = files(tag)
-  const hit = list[list.length - 4]!
-  const next = `${tag}-live`
-
-  await page.setViewportSize({ width: 1600, height: 1000 })
-
-  await project.open()
-  await withSession(project.sdk, `e2e review ${tag}`, async (session) => {
-    project.trackSession(session.id)
-    await patchWithMock(llm, project.sdk, session.id, seed(list))
-
-    await expect
-      .poll(
-        async () => {
-          const info = await project.sdk.session.get({ sessionID: session.id }).then((res) => res.data)
-          return info?.summary?.files ?? 0
-        },
-        { timeout: 60_000 },
-      )
-      .toBe(list.length)
-
-    await expect
-      .poll(
-        async () => {
-          const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
-          return diff.length
-        },
-        { timeout: 60_000 },
-      )
-      .toBe(list.length)
-
-    await project.gotoSession(session.id)
-    await show(page)
-
-    const tab = page.getByRole("tab", { name: /Review/i }).first()
-    await expect(tab).toBeVisible()
-    await tab.click()
-
-    const view = page.locator('[data-slot="session-review-scroll"] .scroll-view__viewport').first()
-    await expect(view).toBeVisible()
-    const heads = page.getByRole("heading", { level: 3 }).filter({ hasText: /^review-scroll-/ })
-    await expect(heads).toHaveCount(list.length, { timeout: 60_000 })
-
-    await expand(page)
-    await waitMark(page, hit.file, hit.mark)
-
-    const row = page
-      .getByRole("heading", {
-        level: 3,
-        name: new RegExp(hit.file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
-      })
-      .first()
-    await expect(row).toBeVisible()
-    await row.evaluate((el) => el.scrollIntoView({ block: "center" }))
-
-    await expect.poll(async () => (await spot(page, hit.file))?.y ?? 0).toBeGreaterThan(200)
-    const prev = await spot(page, hit.file)
-    if (!prev) throw new Error(`missing review row for ${hit.file}`)
-
-    await patchWithMock(llm, project.sdk, session.id, edit(hit.file, hit.mark, next))
-
-    await expect
-      .poll(
-        async () => {
-          const diff = await project.sdk.session.diff({ sessionID: session.id }).then((res) => res.data ?? [])
-          const item = diff.find((item) => item.file === hit.file)
-          return typeof item?.after === "string" ? item.after : ""
-        },
-        { timeout: 60_000 },
-      )
-      .toContain(`mark ${next}`)
-
-    await waitMark(page, hit.file, next)
-
-    await expect
-      .poll(
-        async () => {
-          const next = await spot(page, hit.file)
-          if (!next) return Number.POSITIVE_INFINITY
-          return Math.max(Math.abs(next.top - prev.top), Math.abs(next.y - prev.y))
-        },
-        { timeout: 60_000 },
-      )
-      .toBeLessThanOrEqual(32)
-  })
-})

+ 0 - 233
packages/app/e2e/session/session-undo-redo.spec.ts

@@ -1,233 +0,0 @@
-import type { Page } from "@playwright/test"
-import { test, expect } from "../fixtures"
-import { withSession } from "../actions"
-import { createSdk, modKey } from "../utils"
-import { promptSelector } from "../selectors"
-
-async function seedConversation(input: {
-  page: Page
-  sdk: ReturnType<typeof createSdk>
-  sessionID: string
-  token: string
-}) {
-  const messages = async () =>
-    await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
-  const seeded = await messages()
-  const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
-
-  const prompt = input.page.locator(promptSelector)
-  await expect(prompt).toBeVisible()
-  await input.sdk.session.promptAsync({
-    sessionID: input.sessionID,
-    noReply: true,
-    parts: [{ type: "text", text: input.token }],
-  })
-
-  let userMessageID: string | undefined
-  await expect
-    .poll(
-      async () => {
-        const users = (await messages()).filter(
-          (m) =>
-            !userIDs.has(m.info.id) &&
-            m.info.role === "user" &&
-            m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
-        )
-        if (users.length === 0) return false
-
-        const user = users[users.length - 1]
-        if (!user) return false
-        userMessageID = user.info.id
-        return true
-      },
-      { timeout: 90_000, intervals: [250, 500, 1_000] },
-    )
-    .toBe(true)
-
-  if (!userMessageID) throw new Error("Expected a user message id")
-  await expect(input.page.locator(`[data-message-id="${userMessageID}"]`)).toHaveCount(1, { timeout: 30_000 })
-  return { prompt, userMessageID }
-}
-
-test("slash undo sets revert and restores prior prompt", async ({ page, project }) => {
-  test.setTimeout(120_000)
-
-  const token = `undo_${Date.now()}`
-
-  await project.open()
-  const sdk = project.sdk
-
-  await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
-    project.trackSession(session.id)
-    await project.gotoSession(session.id)
-
-    const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
-
-    await seeded.prompt.click()
-    await page.keyboard.type("/undo")
-
-    const undo = page.locator('[data-slash-id="session.undo"]').first()
-    await expect(undo).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
-        timeout: 30_000,
-      })
-      .toBe(seeded.userMessageID)
-
-    await expect(seeded.prompt).toContainText(token)
-    await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
-  })
-})
-
-test("slash redo clears revert and restores latest state", async ({ page, project }) => {
-  test.setTimeout(120_000)
-
-  const token = `redo_${Date.now()}`
-
-  await project.open()
-  const sdk = project.sdk
-
-  await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
-    project.trackSession(session.id)
-    await project.gotoSession(session.id)
-
-    const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
-
-    await seeded.prompt.click()
-    await page.keyboard.type("/undo")
-
-    const undo = page.locator('[data-slash-id="session.undo"]').first()
-    await expect(undo).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
-        timeout: 30_000,
-      })
-      .toBe(seeded.userMessageID)
-
-    await seeded.prompt.click()
-    await page.keyboard.press(`${modKey}+A`)
-    await page.keyboard.press("Backspace")
-    await page.keyboard.type("/redo")
-
-    const redo = page.locator('[data-slash-id="session.redo"]').first()
-    await expect(redo).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
-        timeout: 30_000,
-      })
-      .toBeUndefined()
-
-    await expect(seeded.prompt).not.toContainText(token)
-    await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(1)
-  })
-})
-
-test("slash undo/redo traverses multi-step revert stack", async ({ page, project }) => {
-  test.setTimeout(120_000)
-
-  const firstToken = `undo_redo_first_${Date.now()}`
-  const secondToken = `undo_redo_second_${Date.now()}`
-
-  await project.open()
-  const sdk = project.sdk
-
-  await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
-    project.trackSession(session.id)
-    await project.gotoSession(session.id)
-
-    const first = await seedConversation({
-      page,
-      sdk,
-      sessionID: session.id,
-      token: firstToken,
-    })
-    const second = await seedConversation({
-      page,
-      sdk,
-      sessionID: session.id,
-      token: secondToken,
-    })
-
-    expect(first.userMessageID).not.toBe(second.userMessageID)
-
-    const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
-    const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
-
-    await expect(firstMessage).toHaveCount(1)
-    await expect(secondMessage).toHaveCount(1)
-
-    await second.prompt.click()
-    await page.keyboard.press(`${modKey}+A`)
-    await page.keyboard.press("Backspace")
-    await page.keyboard.type("/undo")
-
-    const undo = page.locator('[data-slash-id="session.undo"]').first()
-    await expect(undo).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
-        timeout: 30_000,
-      })
-      .toBe(second.userMessageID)
-
-    await expect(firstMessage).toHaveCount(1)
-    await expect(secondMessage).toHaveCount(0)
-
-    await second.prompt.click()
-    await page.keyboard.press(`${modKey}+A`)
-    await page.keyboard.press("Backspace")
-    await page.keyboard.type("/undo")
-    await expect(undo).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
-        timeout: 30_000,
-      })
-      .toBe(first.userMessageID)
-
-    await expect(firstMessage).toHaveCount(0)
-    await expect(secondMessage).toHaveCount(0)
-
-    await second.prompt.click()
-    await page.keyboard.press(`${modKey}+A`)
-    await page.keyboard.press("Backspace")
-    await page.keyboard.type("/redo")
-
-    const redo = page.locator('[data-slash-id="session.redo"]').first()
-    await expect(redo).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
-        timeout: 30_000,
-      })
-      .toBe(second.userMessageID)
-
-    await expect(firstMessage).toHaveCount(1)
-    await expect(secondMessage).toHaveCount(0)
-
-    await second.prompt.click()
-    await page.keyboard.press(`${modKey}+A`)
-    await page.keyboard.press("Backspace")
-    await page.keyboard.type("/redo")
-    await expect(redo).toBeVisible()
-    await page.keyboard.press("Enter")
-
-    await expect
-      .poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
-        timeout: 30_000,
-      })
-      .toBeUndefined()
-
-    await expect(firstMessage).toHaveCount(1)
-    await expect(secondMessage).toHaveCount(1)
-  })
-})

+ 0 - 182
packages/app/e2e/session/session.spec.ts

@@ -1,182 +0,0 @@
-import { test, expect } from "../fixtures"
-import {
-  openSidebar,
-  openSessionMoreMenu,
-  clickMenuItem,
-  confirmDialog,
-  openSharePopover,
-  withSession,
-} from "../actions"
-import { sessionItemSelector, inlineInputSelector } from "../selectors"
-
-const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
-
-type Sdk = Parameters<typeof withSession>[0]
-
-async function seedMessage(sdk: Sdk, sessionID: string) {
-  await sdk.session.promptAsync({
-    sessionID,
-    noReply: true,
-    parts: [{ type: "text", text: "e2e seed" }],
-  })
-
-  await expect
-    .poll(
-      async () => {
-        const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
-        return messages.length
-      },
-      { timeout: 30_000 },
-    )
-    .toBeGreaterThan(0)
-}
-
-test("session can be renamed via header menu", async ({ page, project }) => {
-  const stamp = Date.now()
-  const originalTitle = `e2e rename test ${stamp}`
-  const renamedTitle = `e2e renamed ${stamp}`
-
-  await project.open()
-  await withSession(project.sdk, originalTitle, async (session) => {
-    project.trackSession(session.id)
-    await seedMessage(project.sdk, session.id)
-    await project.gotoSession(session.id)
-    await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle)
-
-    const menu = await openSessionMoreMenu(page, session.id)
-    await clickMenuItem(menu, /rename/i)
-
-    const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
-    await expect(input).toBeVisible()
-    await expect(input).toBeFocused()
-    await input.fill(renamedTitle)
-    await expect(input).toHaveValue(renamedTitle)
-    await input.press("Enter")
-
-    await expect
-      .poll(
-        async () => {
-          const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
-          return data?.title
-        },
-        { timeout: 30_000 },
-      )
-      .toBe(renamedTitle)
-
-    await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle)
-  })
-})
-
-test("session can be archived via header menu", async ({ page, project }) => {
-  const stamp = Date.now()
-  const title = `e2e archive test ${stamp}`
-
-  await project.open()
-  await withSession(project.sdk, title, async (session) => {
-    project.trackSession(session.id)
-    await seedMessage(project.sdk, session.id)
-    await project.gotoSession(session.id)
-    const menu = await openSessionMoreMenu(page, session.id)
-    await clickMenuItem(menu, /archive/i)
-
-    await expect
-      .poll(
-        async () => {
-          const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
-          return data?.time?.archived
-        },
-        { timeout: 30_000 },
-      )
-      .not.toBeUndefined()
-
-    await openSidebar(page)
-    await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
-  })
-})
-
-test("session can be deleted via header menu", async ({ page, project }) => {
-  const stamp = Date.now()
-  const title = `e2e delete test ${stamp}`
-
-  await project.open()
-  await withSession(project.sdk, title, async (session) => {
-    project.trackSession(session.id)
-    await seedMessage(project.sdk, session.id)
-    await project.gotoSession(session.id)
-    const menu = await openSessionMoreMenu(page, session.id)
-    await clickMenuItem(menu, /delete/i)
-    await confirmDialog(page, /delete/i)
-
-    await expect
-      .poll(
-        async () => {
-          const data = await project.sdk.session
-            .get({ sessionID: session.id })
-            .then((r) => r.data)
-            .catch(() => undefined)
-          return data?.id
-        },
-        { timeout: 30_000 },
-      )
-      .toBeUndefined()
-
-    await openSidebar(page)
-    await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
-  })
-})
-
-test("session can be shared and unshared via header button", async ({ page, project }) => {
-  test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
-
-  const stamp = Date.now()
-  const title = `e2e share test ${stamp}`
-
-  await project.open()
-  await withSession(project.sdk, title, async (session) => {
-    project.trackSession(session.id)
-    await project.gotoSession(session.id)
-    await project.prompt(`share seed ${stamp}`)
-
-    const shared = await openSharePopover(page)
-    const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first()
-    await expect(publish).toBeVisible({ timeout: 30_000 })
-    await publish.click()
-
-    await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({
-      timeout: 30_000,
-    })
-
-    await expect
-      .poll(
-        async () => {
-          const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
-          return data?.share?.url || undefined
-        },
-        { timeout: 30_000 },
-      )
-      .not.toBeUndefined()
-
-    const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()
-    await expect(unpublish).toBeVisible({ timeout: 30_000 })
-    await unpublish.click()
-
-    await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
-      timeout: 30_000,
-    })
-
-    await expect
-      .poll(
-        async () => {
-          const data = await project.sdk.session.get({ sessionID: session.id }).then((r) => r.data)
-          return data?.share?.url || undefined
-        },
-        { timeout: 30_000 },
-      )
-      .toBeUndefined()
-
-    const unshared = await openSharePopover(page)
-    await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
-      timeout: 30_000,
-    })
-  })
-})

+ 0 - 389
packages/app/e2e/settings/settings-keybinds.spec.ts

@@ -1,389 +0,0 @@
-import { test, expect } from "../fixtures"
-import { openSettings, closeDialog, waitTerminalFocusIdle, withSession } from "../actions"
-import { keybindButtonSelector, terminalSelector } from "../selectors"
-import { modKey } from "../utils"
-
-test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")).first()
-  await expect(keybindButton).toBeVisible()
-
-  const initialKeybind = await keybindButton.textContent()
-  expect(initialKeybind).toContain("B")
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press(`${modKey}+Shift+KeyH`)
-  await page.waitForTimeout(100)
-
-  const newKeybind = await keybindButton.textContent()
-  expect(newKeybind).toContain("H")
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["sidebar.toggle"]).toBe("mod+shift+h")
-
-  await closeDialog(page, dialog)
-
-  const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
-  const initiallyClosed = (await button.getAttribute("aria-expanded")) !== "true"
-
-  await page.keyboard.press(`${modKey}+Shift+H`)
-  await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "true" : "false")
-
-  const afterToggleClosed = (await button.getAttribute("aria-expanded")) !== "true"
-  expect(afterToggleClosed).toBe(!initiallyClosed)
-
-  await page.keyboard.press(`${modKey}+Shift+H`)
-  await expect(button).toHaveAttribute("aria-expanded", initiallyClosed ? "false" : "true")
-
-  const finalClosed = (await button.getAttribute("aria-expanded")) !== "true"
-  expect(finalClosed).toBe(initiallyClosed)
-})
-
-test("sidebar toggle keybind guards against shortcut conflicts", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
-  await expect(keybindButton).toBeVisible()
-
-  const initialKeybind = await keybindButton.textContent()
-  expect(initialKeybind).toContain("B")
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press(`${modKey}+Shift+KeyP`)
-  await page.waitForTimeout(100)
-
-  const toast = page.locator('[data-component="toast"]').last()
-  await expect(toast).toBeVisible()
-  await expect(toast).toContainText(/already/i)
-
-  await keybindButton.click()
-  await expect(keybindButton).toContainText("B")
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined()
-
-  await closeDialog(page, dialog)
-})
-
-test("resetting all keybinds to defaults works", async ({ page, gotoSession }) => {
-  await page.addInitScript(() => {
-    localStorage.setItem("settings.v3", JSON.stringify({ keybinds: { "sidebar.toggle": "mod+shift+x" } }))
-  })
-
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
-  await expect(keybindButton).toBeVisible()
-
-  const customKeybind = await keybindButton.textContent()
-  expect(customKeybind).toContain("X")
-
-  const resetButton = dialog.getByRole("button", { name: "Reset to defaults" })
-  await expect(resetButton).toBeVisible()
-  await expect(resetButton).toBeEnabled()
-  await resetButton.click()
-  await page.waitForTimeout(100)
-
-  const restoredKeybind = await keybindButton.textContent()
-  expect(restoredKeybind).toContain("B")
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined()
-
-  await closeDialog(page, dialog)
-})
-
-test("clearing a keybind works", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
-  await expect(keybindButton).toBeVisible()
-
-  const initialKeybind = await keybindButton.textContent()
-  expect(initialKeybind).toContain("B")
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press("Delete")
-  await page.waitForTimeout(100)
-
-  const clearedKeybind = await keybindButton.textContent()
-  expect(clearedKeybind).toMatch(/unassigned|press/i)
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["sidebar.toggle"]).toBe("none")
-
-  await closeDialog(page, dialog)
-
-  await page.keyboard.press(`${modKey}+B`)
-  await page.waitForTimeout(100)
-
-  const stillOnSession = page.url().includes("/session")
-  expect(stillOnSession).toBe(true)
-})
-
-test("changing settings open keybind works", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("settings.open"))
-  await expect(keybindButton).toBeVisible()
-
-  const initialKeybind = await keybindButton.textContent()
-  expect(initialKeybind).toContain(",")
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press(`${modKey}+Slash`)
-  await page.waitForTimeout(100)
-
-  const newKeybind = await keybindButton.textContent()
-  expect(newKeybind).toContain("/")
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["settings.open"]).toBe("mod+/")
-
-  await closeDialog(page, dialog)
-
-  const settingsDialog = page.getByRole("dialog")
-  await expect(settingsDialog).toHaveCount(0)
-
-  await page.keyboard.press(`${modKey}+Slash`)
-  await page.waitForTimeout(100)
-
-  await expect(settingsDialog).toBeVisible()
-
-  await closeDialog(page, settingsDialog)
-})
-
-test("changing new session keybind works", async ({ page, sdk, gotoSession }) => {
-  await withSession(sdk, "test session for keybind", async (session) => {
-    await gotoSession(session.id)
-
-    const initialUrl = page.url()
-    expect(initialUrl).toContain(`/session/${session.id}`)
-
-    const dialog = await openSettings(page)
-    await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-    const keybindButton = dialog.locator(keybindButtonSelector("session.new"))
-    await expect(keybindButton).toBeVisible()
-
-    await keybindButton.click()
-    await expect(keybindButton).toHaveText(/press/i)
-
-    await page.keyboard.press(`${modKey}+Shift+KeyN`)
-    await page.waitForTimeout(100)
-
-    const newKeybind = await keybindButton.textContent()
-    expect(newKeybind).toContain("N")
-
-    const stored = await page.evaluate(() => {
-      const raw = localStorage.getItem("settings.v3")
-      return raw ? JSON.parse(raw) : null
-    })
-    expect(stored?.keybinds?.["session.new"]).toBe("mod+shift+n")
-
-    await closeDialog(page, dialog)
-
-    await page.keyboard.press(`${modKey}+Shift+N`)
-    await page.waitForTimeout(200)
-
-    const newUrl = page.url()
-    expect(newUrl).toMatch(/\/session\/?$/)
-    expect(newUrl).not.toContain(session.id)
-  })
-})
-
-test("changing file open keybind works", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("file.open"))
-  await expect(keybindButton).toBeVisible()
-
-  const initialKeybind = await keybindButton.textContent()
-  expect(initialKeybind).toContain("K")
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press(`${modKey}+Shift+KeyF`)
-  await page.waitForTimeout(100)
-
-  const newKeybind = await keybindButton.textContent()
-  expect(newKeybind).toContain("F")
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["file.open"]).toBe("mod+shift+f")
-
-  await closeDialog(page, dialog)
-
-  const filePickerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder(/search files/i) })
-  await expect(filePickerDialog).toHaveCount(0)
-
-  await page.keyboard.press(`${modKey}+Shift+F`)
-  await page.waitForTimeout(100)
-
-  await expect(filePickerDialog).toBeVisible()
-
-  await page.keyboard.press("Escape")
-  await expect(filePickerDialog).toHaveCount(0)
-})
-
-test("changing terminal toggle keybind works", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle"))
-  await expect(keybindButton).toBeVisible()
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press(`${modKey}+KeyY`)
-  await page.waitForTimeout(100)
-
-  const newKeybind = await keybindButton.textContent()
-  expect(newKeybind).toContain("Y")
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["terminal.toggle"]).toBe("mod+y")
-
-  await closeDialog(page, dialog)
-
-  const terminal = page.locator(terminalSelector)
-  await expect(terminal).not.toBeVisible()
-
-  await page.keyboard.press(`${modKey}+Y`)
-  await waitTerminalFocusIdle(page, { term: terminal })
-
-  await page.keyboard.press(`${modKey}+Y`)
-  await expect(terminal).not.toBeVisible()
-})
-
-test("terminal toggle keybind persists after reload", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle"))
-  await expect(keybindButton).toBeVisible()
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press(`${modKey}+Shift+KeyY`)
-  await page.waitForTimeout(100)
-
-  await expect(keybindButton).toContainText("Y")
-  await closeDialog(page, dialog)
-
-  await page.reload()
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate(() => {
-        const raw = localStorage.getItem("settings.v3")
-        if (!raw) return
-        const parsed = JSON.parse(raw)
-        return parsed?.keybinds?.["terminal.toggle"]
-      })
-    })
-    .toBe("mod+shift+y")
-
-  const reloaded = await openSettings(page)
-  await reloaded.getByRole("tab", { name: "Shortcuts" }).click()
-  const reloadedKeybind = reloaded.locator(keybindButtonSelector("terminal.toggle")).first()
-  await expect(reloadedKeybind).toContainText("Y")
-  await closeDialog(page, reloaded)
-})
-
-test("changing command palette keybind works", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-
-  const keybindButton = dialog.locator(keybindButtonSelector("command.palette"))
-  await expect(keybindButton).toBeVisible()
-
-  const initialKeybind = await keybindButton.textContent()
-  expect(initialKeybind).toContain("P")
-
-  await keybindButton.click()
-  await expect(keybindButton).toHaveText(/press/i)
-
-  await page.keyboard.press(`${modKey}+Shift+KeyK`)
-  await page.waitForTimeout(100)
-
-  const newKeybind = await keybindButton.textContent()
-  expect(newKeybind).toContain("K")
-
-  const stored = await page.evaluate(() => {
-    const raw = localStorage.getItem("settings.v3")
-    return raw ? JSON.parse(raw) : null
-  })
-  expect(stored?.keybinds?.["command.palette"]).toBe("mod+shift+k")
-
-  await closeDialog(page, dialog)
-
-  const palette = page.getByRole("dialog").filter({ has: page.getByRole("textbox").first() })
-  await expect(palette).toHaveCount(0)
-
-  await page.keyboard.press(`${modKey}+Shift+K`)
-  await page.waitForTimeout(100)
-
-  await expect(palette).toBeVisible()
-  await expect(palette.getByRole("textbox").first()).toBeVisible()
-
-  await page.keyboard.press("Escape")
-  await expect(palette).toHaveCount(0)
-})

+ 0 - 122
packages/app/e2e/settings/settings-models.spec.ts

@@ -1,122 +0,0 @@
-import { test, expect } from "../fixtures"
-import { promptSelector } from "../selectors"
-import { closeDialog, openSettings } from "../actions"
-
-test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-
-  const command = page.locator('[data-slash-id="model.choose"]')
-  await expect(command).toBeVisible()
-  await command.hover()
-  await page.keyboard.press("Enter")
-
-  const picker = page.getByRole("dialog")
-  await expect(picker).toBeVisible()
-
-  const target = picker.locator('[data-slot="list-item"]').first()
-  await expect(target).toBeVisible()
-
-  const key = await target.getAttribute("data-key")
-  if (!key) throw new Error("Failed to resolve model key from list item")
-
-  const name = (await target.locator("span").first().innerText()).trim()
-  if (!name) throw new Error("Failed to resolve model name from list item")
-
-  await page.keyboard.press("Escape")
-  await expect(picker).toHaveCount(0)
-
-  const settings = await openSettings(page)
-
-  await settings.getByRole("tab", { name: "Models" }).click()
-  const search = settings.getByPlaceholder("Search models")
-  await expect(search).toBeVisible()
-  await search.fill(name)
-
-  const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
-  const input = toggle.locator('[data-slot="switch-input"]')
-  await expect(toggle).toBeVisible()
-  await expect(input).toHaveAttribute("aria-checked", "true")
-  await toggle.locator('[data-slot="switch-control"]').click()
-  await expect(input).toHaveAttribute("aria-checked", "false")
-
-  await closeDialog(page, settings)
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-  await expect(command).toBeVisible()
-  await command.hover()
-  await page.keyboard.press("Enter")
-
-  const pickerAgain = page.getByRole("dialog")
-  await expect(pickerAgain).toBeVisible()
-  await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
-
-  await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
-
-  await page.keyboard.press("Escape")
-  await expect(pickerAgain).toHaveCount(0)
-})
-
-test("showing a hidden model restores it to the model picker", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-
-  const command = page.locator('[data-slash-id="model.choose"]')
-  await expect(command).toBeVisible()
-  await command.hover()
-  await page.keyboard.press("Enter")
-
-  const picker = page.getByRole("dialog")
-  await expect(picker).toBeVisible()
-
-  const target = picker.locator('[data-slot="list-item"]').first()
-  await expect(target).toBeVisible()
-
-  const key = await target.getAttribute("data-key")
-  if (!key) throw new Error("Failed to resolve model key from list item")
-
-  const name = (await target.locator("span").first().innerText()).trim()
-  if (!name) throw new Error("Failed to resolve model name from list item")
-
-  await page.keyboard.press("Escape")
-  await expect(picker).toHaveCount(0)
-
-  const settings = await openSettings(page)
-
-  await settings.getByRole("tab", { name: "Models" }).click()
-  const search = settings.getByPlaceholder("Search models")
-  await expect(search).toBeVisible()
-  await search.fill(name)
-
-  const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
-  const input = toggle.locator('[data-slot="switch-input"]')
-  await expect(toggle).toBeVisible()
-  await expect(input).toHaveAttribute("aria-checked", "true")
-
-  await toggle.locator('[data-slot="switch-control"]').click()
-  await expect(input).toHaveAttribute("aria-checked", "false")
-
-  await toggle.locator('[data-slot="switch-control"]').click()
-  await expect(input).toHaveAttribute("aria-checked", "true")
-
-  await closeDialog(page, settings)
-
-  await page.locator(promptSelector).click()
-  await page.keyboard.type("/model")
-  await expect(command).toBeVisible()
-  await command.hover()
-  await page.keyboard.press("Enter")
-
-  const pickerAgain = page.getByRole("dialog")
-  await expect(pickerAgain).toBeVisible()
-
-  await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toBeVisible()
-
-  await page.keyboard.press("Escape")
-  await expect(pickerAgain).toHaveCount(0)
-})

+ 0 - 136
packages/app/e2e/settings/settings-providers.spec.ts

@@ -1,136 +0,0 @@
-import { test, expect } from "../fixtures"
-import { closeDialog, openSettings } from "../actions"
-
-test("custom provider form can be filled and validates input", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const settings = await openSettings(page)
-  await settings.getByRole("tab", { name: "Providers" }).click()
-
-  const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
-  await expect(customProviderSection).toBeVisible()
-
-  const connectButton = customProviderSection.getByRole("button", { name: "Connect" })
-  await connectButton.click()
-
-  const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
-  await expect(providerDialog).toBeVisible()
-
-  await providerDialog.getByLabel("Provider ID").fill("test-provider")
-  await providerDialog.getByLabel("Display name").fill("Test Provider")
-  await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/fake")
-  await providerDialog.getByLabel("API key").fill("fake-key")
-
-  await providerDialog.getByPlaceholder("model-id").first().fill("test-model")
-  await providerDialog.getByPlaceholder("Display Name").first().fill("Test Model")
-
-  await expect(providerDialog.getByRole("textbox", { name: "Provider ID" })).toHaveValue("test-provider")
-  await expect(providerDialog.getByRole("textbox", { name: "Display name" })).toHaveValue("Test Provider")
-  await expect(providerDialog.getByRole("textbox", { name: "Base URL" })).toHaveValue("http://localhost:9999/fake")
-  await expect(providerDialog.getByRole("textbox", { name: "API key" })).toHaveValue("fake-key")
-  await expect(providerDialog.getByPlaceholder("model-id").first()).toHaveValue("test-model")
-  await expect(providerDialog.getByPlaceholder("Display Name").first()).toHaveValue("Test Model")
-
-  await page.keyboard.press("Escape")
-  await expect(providerDialog).toHaveCount(0)
-
-  await closeDialog(page, settings)
-})
-
-test("custom provider form shows validation errors", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const settings = await openSettings(page)
-  await settings.getByRole("tab", { name: "Providers" }).click()
-
-  const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
-  await customProviderSection.getByRole("button", { name: "Connect" }).click()
-
-  const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
-  await expect(providerDialog).toBeVisible()
-
-  await providerDialog.getByLabel("Provider ID").fill("invalid provider id")
-  await providerDialog.getByLabel("Base URL").fill("not-a-url")
-
-  await providerDialog.getByRole("button", { name: /submit|save/i }).click()
-
-  await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /lowercase/i })).toBeVisible()
-  await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /http/i })).toBeVisible()
-
-  await page.keyboard.press("Escape")
-  await expect(providerDialog).toHaveCount(0)
-
-  await closeDialog(page, settings)
-})
-
-test("custom provider form can add and remove models", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const settings = await openSettings(page)
-  await settings.getByRole("tab", { name: "Providers" }).click()
-
-  const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
-  await customProviderSection.getByRole("button", { name: "Connect" }).click()
-
-  const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
-  await expect(providerDialog).toBeVisible()
-
-  await providerDialog.getByLabel("Provider ID").fill("multi-model-test")
-  await providerDialog.getByLabel("Display name").fill("Multi Model Test")
-  await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/multi")
-
-  await providerDialog.getByPlaceholder("model-id").first().fill("model-1")
-  await providerDialog.getByPlaceholder("Display Name").first().fill("Model 1")
-
-  const idInputsBefore = await providerDialog.getByPlaceholder("model-id").count()
-  await providerDialog.getByRole("button", { name: "Add model" }).click()
-  const idInputsAfter = await providerDialog.getByPlaceholder("model-id").count()
-  expect(idInputsAfter).toBe(idInputsBefore + 1)
-
-  await providerDialog.getByPlaceholder("model-id").nth(1).fill("model-2")
-  await providerDialog.getByPlaceholder("Display Name").nth(1).fill("Model 2")
-
-  await expect(providerDialog.getByPlaceholder("model-id").nth(1)).toHaveValue("model-2")
-  await expect(providerDialog.getByPlaceholder("Display Name").nth(1)).toHaveValue("Model 2")
-
-  await page.keyboard.press("Escape")
-  await expect(providerDialog).toHaveCount(0)
-
-  await closeDialog(page, settings)
-})
-
-test("custom provider form can add and remove headers", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const settings = await openSettings(page)
-  await settings.getByRole("tab", { name: "Providers" }).click()
-
-  const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
-  await customProviderSection.getByRole("button", { name: "Connect" }).click()
-
-  const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
-  await expect(providerDialog).toBeVisible()
-
-  await providerDialog.getByLabel("Provider ID").fill("header-test")
-  await providerDialog.getByLabel("Display name").fill("Header Test")
-  await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/headers")
-
-  await providerDialog.getByPlaceholder("model-id").first().fill("model-x")
-  await providerDialog.getByPlaceholder("Display Name").first().fill("Model X")
-
-  const headerInputsBefore = await providerDialog.getByPlaceholder("Header-Name").count()
-  await providerDialog.getByRole("button", { name: "Add header" }).click()
-  const headerInputsAfter = await providerDialog.getByPlaceholder("Header-Name").count()
-  expect(headerInputsAfter).toBe(headerInputsBefore + 1)
-
-  await providerDialog.getByPlaceholder("Header-Name").first().fill("Authorization")
-  await providerDialog.getByPlaceholder("value").first().fill("Bearer token123")
-
-  await expect(providerDialog.getByPlaceholder("Header-Name").first()).toHaveValue("Authorization")
-  await expect(providerDialog.getByPlaceholder("value").first()).toHaveValue("Bearer token123")
-
-  await page.keyboard.press("Escape")
-  await expect(providerDialog).toHaveCount(0)
-
-  await closeDialog(page, settings)
-})

+ 0 - 718
packages/app/e2e/settings/settings.spec.ts

@@ -1,718 +0,0 @@
-import { test, expect, settingsKey } from "../fixtures"
-import { closeDialog, openSettings } from "../actions"
-import {
-  settingsColorSchemeSelector,
-  settingsCodeFontSelector,
-  settingsLanguageSelectSelector,
-  settingsNotificationsAgentSelector,
-  settingsNotificationsErrorsSelector,
-  settingsNotificationsPermissionsSelector,
-  settingsReleaseNotesSelector,
-  settingsSoundsAgentSelector,
-  settingsSoundsErrorsSelector,
-  settingsSoundsPermissionsSelector,
-  settingsThemeSelector,
-  settingsUIFontSelector,
-  settingsUpdatesStartupSelector,
-} from "../selectors"
-
-test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-
-  await dialog.getByRole("tab", { name: "Shortcuts" }).click()
-  await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible()
-  await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible()
-
-  await closeDialog(page, dialog)
-})
-
-test("changing language updates settings labels", async ({ page, gotoSession }) => {
-  await page.addInitScript(() => {
-    localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
-  })
-
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-
-  const heading = dialog.getByRole("heading", { level: 2 })
-  await expect(heading).toHaveText("General")
-
-  const select = dialog.locator(settingsLanguageSelectSelector)
-  await expect(select).toBeVisible()
-  await select.locator('[data-slot="select-select-trigger"]').click()
-
-  await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
-
-  await expect(heading).toHaveText("Allgemein")
-
-  await select.locator('[data-slot="select-select-trigger"]').click()
-  await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
-  await expect(heading).toHaveText("General")
-})
-
-test("changing color scheme persists in localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const select = dialog.locator(settingsColorSchemeSelector)
-  await expect(select).toBeVisible()
-
-  await select.locator('[data-slot="select-select-trigger"]').click()
-  await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
-
-  const colorScheme = await page.evaluate(() => {
-    return document.documentElement.getAttribute("data-color-scheme")
-  })
-  expect(colorScheme).toBe("dark")
-
-  await select.locator('[data-slot="select-select-trigger"]').click()
-  await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Light" }).click()
-
-  const lightColorScheme = await page.evaluate(() => {
-    return document.documentElement.getAttribute("data-color-scheme")
-  })
-  expect(lightColorScheme).toBe("light")
-})
-
-test("changing theme persists in localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const select = dialog.locator(settingsThemeSelector)
-  await expect(select).toBeVisible()
-
-  const currentThemeId = await page.evaluate(() => {
-    return document.documentElement.getAttribute("data-theme")
-  })
-  const currentTheme = (await select.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
-  const trigger = select.locator('[data-slot="select-select-trigger"]')
-  const items = page.locator('[data-slot="select-select-item"]')
-
-  await trigger.click()
-  const open = await expect
-    .poll(async () => (await items.count()) > 0, { timeout: 5_000 })
-    .toBe(true)
-    .then(() => true)
-    .catch(() => false)
-  if (!open) {
-    await trigger.click()
-    await expect.poll(async () => (await items.count()) > 0, { timeout: 10_000 }).toBe(true)
-  }
-  await expect(items.first()).toBeVisible()
-  const count = await items.count()
-  expect(count).toBeGreaterThan(1)
-
-  const nextTheme = (await items.locator('[data-slot="select-select-item-label"]').allTextContents())
-    .map((x) => x.trim())
-    .find((x) => x && x !== currentTheme)
-  expect(nextTheme).toBeTruthy()
-
-  await items.filter({ hasText: nextTheme! }).first().click()
-
-  await page.keyboard.press("Escape")
-
-  const storedThemeId = await page.evaluate(() => {
-    return localStorage.getItem("opencode-theme-id")
-  })
-
-  expect(storedThemeId).not.toBeNull()
-  expect(storedThemeId).not.toBe(currentThemeId)
-
-  const dataTheme = await page.evaluate(() => {
-    return document.documentElement.getAttribute("data-theme")
-  })
-  expect(dataTheme).toBe(storedThemeId)
-})
-
-test("legacy oc-1 theme migrates to oc-2", async ({ page, gotoSession }) => {
-  await page.addInitScript(() => {
-    localStorage.setItem("opencode-theme-id", "oc-1")
-    localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
-    localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
-  })
-
-  await gotoSession()
-
-  await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2")
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate(() => {
-        return localStorage.getItem("opencode-theme-id")
-      })
-    })
-    .toBe("oc-2")
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate(() => {
-        return localStorage.getItem("opencode-theme-css-light")
-      })
-    })
-    .toBeNull()
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate(() => {
-        return localStorage.getItem("opencode-theme-css-dark")
-      })
-    })
-    .toBeNull()
-})
-
-test("typing a code font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const input = dialog.locator(settingsCodeFontSelector)
-  await expect(input).toBeVisible()
-  await expect(input).toHaveAttribute("placeholder", "System Mono")
-
-  const initialFontFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  const initialUIFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-  expect(initialFontFamily).toContain("ui-monospace")
-
-  const next = "Test Mono"
-
-  await input.click()
-  await input.clear()
-  await input.pressSequentially(next)
-  await expect(input).toHaveValue(next)
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        mono: next,
-      },
-    })
-
-  const newFontFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  const newUIFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-  expect(newFontFamily).toContain(next)
-  expect(newFontFamily).not.toBe(initialFontFamily)
-  expect(newUIFamily).toBe(initialUIFamily)
-})
-
-test("typing a UI font with spaces persists and updates CSS variable", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const input = dialog.locator(settingsUIFontSelector)
-  await expect(input).toBeVisible()
-  await expect(input).toHaveAttribute("placeholder", "System Sans")
-
-  const initialFontFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-  const initialCodeFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  expect(initialFontFamily).toContain("ui-sans-serif")
-
-  const next = "Test Sans"
-
-  await input.click()
-  await input.clear()
-  await input.pressSequentially(next)
-  await expect(input).toHaveValue(next)
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        sans: next,
-      },
-    })
-
-  const newFontFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-  const newCodeFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  expect(newFontFamily).toContain(next)
-  expect(newFontFamily).not.toBe(initialFontFamily)
-  expect(newCodeFamily).toBe(initialCodeFamily)
-})
-
-test("clearing the code font field restores the default placeholder and stack", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const input = dialog.locator(settingsCodeFontSelector)
-  await expect(input).toBeVisible()
-
-  await input.click()
-  await input.clear()
-  await input.pressSequentially("Reset Mono")
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        mono: "Reset Mono",
-      },
-    })
-
-  await input.clear()
-  await input.press("Space")
-  await expect(input).toHaveValue("")
-  await expect(input).toHaveAttribute("placeholder", "System Mono")
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        mono: "",
-      },
-    })
-
-  const fontFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  expect(fontFamily).toContain("ui-monospace")
-  expect(fontFamily).not.toContain("Reset Mono")
-})
-
-test("clearing the UI font field restores the default placeholder and stack", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const input = dialog.locator(settingsUIFontSelector)
-  await expect(input).toBeVisible()
-
-  await input.click()
-  await input.clear()
-  await input.pressSequentially("Reset Sans")
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        sans: "Reset Sans",
-      },
-    })
-
-  await input.clear()
-  await input.press("Space")
-  await expect(input).toHaveValue("")
-  await expect(input).toHaveAttribute("placeholder", "System Sans")
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        sans: "",
-      },
-    })
-
-  const fontFamily = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-  expect(fontFamily).toContain("ui-sans-serif")
-  expect(fontFamily).not.toContain("Reset Sans")
-})
-
-test("color scheme, code font, and UI font rehydrate after reload", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-
-  const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector)
-  await expect(colorSchemeSelect).toBeVisible()
-  await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click()
-  await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
-  await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
-
-  const code = dialog.locator(settingsCodeFontSelector)
-  const ui = dialog.locator(settingsUIFontSelector)
-  await expect(code).toBeVisible()
-  await expect(ui).toBeVisible()
-
-  const initialMono = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  const initialSans = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-
-  const initialSettings = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  const mono = initialSettings?.appearance?.mono === "Reload Mono" ? "Reload Mono 2" : "Reload Mono"
-  const sans = initialSettings?.appearance?.sans === "Reload Sans" ? "Reload Sans 2" : "Reload Sans"
-
-  await code.click()
-  await code.clear()
-  await code.pressSequentially(mono)
-  await expect(code).toHaveValue(mono)
-
-  await ui.click()
-  await ui.clear()
-  await ui.pressSequentially(sans)
-  await expect(ui).toHaveValue(sans)
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        mono,
-        sans,
-      },
-    })
-
-  const updatedSettings = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  const updatedMono = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  const updatedSans = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-  expect(updatedMono).toContain(mono)
-  expect(updatedMono).not.toBe(initialMono)
-  expect(updatedSans).toContain(sans)
-  expect(updatedSans).not.toBe(initialSans)
-  expect(updatedSettings?.appearance?.mono).toBe(mono)
-  expect(updatedSettings?.appearance?.sans).toBe(sans)
-
-  await closeDialog(page, dialog)
-  await page.reload()
-
-  await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      appearance: {
-        mono,
-        sans,
-      },
-    })
-
-  const rehydratedSettings = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate(() =>
-        getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-      )
-    })
-    .toContain(mono)
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate(() =>
-        getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-      )
-    })
-    .toContain(sans)
-
-  const rehydratedMono = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim(),
-  )
-  const rehydratedSans = await page.evaluate(() =>
-    getComputedStyle(document.documentElement).getPropertyValue("--font-family-sans").trim(),
-  )
-  expect(rehydratedMono).toContain(mono)
-  expect(rehydratedMono).not.toBe(initialMono)
-  expect(rehydratedSans).toContain(sans)
-  expect(rehydratedSans).not.toBe(initialSans)
-  expect(rehydratedSettings?.appearance?.mono).toBe(mono)
-  expect(rehydratedSettings?.appearance?.sans).toBe(sans)
-})
-
-test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const switchContainer = dialog.locator(settingsNotificationsAgentSelector)
-  await expect(switchContainer).toBeVisible()
-
-  const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
-  const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(initialState).toBe(true)
-
-  await switchContainer.locator('[data-slot="switch-control"]').click()
-  await page.waitForTimeout(100)
-
-  const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(newState).toBe(false)
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.notifications?.agent).toBe(false)
-})
-
-test("toggling notification permissions switch updates localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const switchContainer = dialog.locator(settingsNotificationsPermissionsSelector)
-  await expect(switchContainer).toBeVisible()
-
-  const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
-  const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(initialState).toBe(true)
-
-  await switchContainer.locator('[data-slot="switch-control"]').click()
-  await page.waitForTimeout(100)
-
-  const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(newState).toBe(false)
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.notifications?.permissions).toBe(false)
-})
-
-test("toggling notification errors switch updates localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const switchContainer = dialog.locator(settingsNotificationsErrorsSelector)
-  await expect(switchContainer).toBeVisible()
-
-  const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
-  const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(initialState).toBe(false)
-
-  await switchContainer.locator('[data-slot="switch-control"]').click()
-  await page.waitForTimeout(100)
-
-  const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(newState).toBe(true)
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.notifications?.errors).toBe(true)
-})
-
-test("changing sound agent selection persists in localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const select = dialog.locator(settingsSoundsAgentSelector)
-  await expect(select).toBeVisible()
-
-  await select.locator('[data-slot="select-select-trigger"]').click()
-
-  const items = page.locator('[data-slot="select-select-item"]')
-  await items.nth(2).click()
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.sounds?.agent).not.toBe("staplebops-01")
-})
-
-test("selecting none disables agent sound", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const select = dialog.locator(settingsSoundsAgentSelector)
-  const trigger = select.locator('[data-slot="select-select-trigger"]')
-  await expect(select).toBeVisible()
-  await expect(trigger).toBeEnabled()
-
-  await trigger.click()
-  const items = page.locator('[data-slot="select-select-item"]')
-  await expect(items.first()).toBeVisible()
-  await items.first().click()
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.sounds?.agentEnabled).toBe(false)
-})
-
-test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector)
-  const errorsSelect = dialog.locator(settingsSoundsErrorsSelector)
-  await expect(permissionsSelect).toBeVisible()
-  await expect(errorsSelect).toBeVisible()
-
-  const initial = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  const permissionsCurrent =
-    (await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
-  await permissionsSelect.locator('[data-slot="select-select-trigger"]').click()
-  const permissionItems = page.locator('[data-slot="select-select-item"]')
-  expect(await permissionItems.count()).toBeGreaterThan(1)
-  if (permissionsCurrent) {
-    await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click()
-  }
-  if (!permissionsCurrent) {
-    await permissionItems.nth(1).click()
-  }
-
-  const errorsCurrent =
-    (await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
-  await errorsSelect.locator('[data-slot="select-select-trigger"]').click()
-  const errorItems = page.locator('[data-slot="select-select-item"]')
-  expect(await errorItems.count()).toBeGreaterThan(1)
-  if (errorsCurrent) {
-    await errorItems.filter({ hasNotText: errorsCurrent }).first().click()
-  }
-  if (!errorsCurrent) {
-    await errorItems.nth(1).click()
-  }
-
-  await expect
-    .poll(async () => {
-      return await page.evaluate((key) => {
-        const raw = localStorage.getItem(key)
-        return raw ? JSON.parse(raw) : null
-      }, settingsKey)
-    })
-    .toMatchObject({
-      sounds: {
-        permissions: expect.any(String),
-        errors: expect.any(String),
-      },
-    })
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions)
-  expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors)
-})
-
-test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const switchContainer = dialog.locator(settingsUpdatesStartupSelector)
-  await expect(switchContainer).toBeVisible()
-
-  const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
-
-  const isDisabled = await toggleInput.evaluate((el: HTMLInputElement) => el.disabled)
-  if (isDisabled) {
-    test.skip()
-    return
-  }
-
-  const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(initialState).toBe(true)
-
-  await switchContainer.locator('[data-slot="switch-control"]').click()
-  await page.waitForTimeout(100)
-
-  const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(newState).toBe(false)
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.updates?.startup).toBe(false)
-})
-
-test("toggling release notes switch updates localStorage", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const dialog = await openSettings(page)
-  const switchContainer = dialog.locator(settingsReleaseNotesSelector)
-  await expect(switchContainer).toBeVisible()
-
-  const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
-  const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(initialState).toBe(true)
-
-  await switchContainer.locator('[data-slot="switch-control"]').click()
-  await page.waitForTimeout(100)
-
-  const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
-  expect(newState).toBe(false)
-
-  const stored = await page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    return raw ? JSON.parse(raw) : null
-  }, settingsKey)
-
-  expect(stored?.general?.releaseNotes).toBe(false)
-})

+ 0 - 109
packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts

@@ -1,109 +0,0 @@
-import { test, expect } from "../fixtures"
-import {
-  defocus,
-  cleanupSession,
-  cleanupTestProject,
-  closeSidebar,
-  createTestProject,
-  hoverSessionItem,
-  openSidebar,
-  waitSession,
-} from "../actions"
-import { projectSwitchSelector } from "../selectors"
-import { dirSlug } from "../utils"
-
-test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
-  const stamp = Date.now()
-
-  const one = await sdk.session.create({ title: `e2e sidebar popover archive 1 ${stamp}` }).then((r) => r.data)
-  const two = await sdk.session.create({ title: `e2e sidebar popover archive 2 ${stamp}` }).then((r) => r.data)
-
-  if (!one?.id) throw new Error("Session create did not return an id")
-  if (!two?.id) throw new Error("Session create did not return an id")
-
-  try {
-    await gotoSession(one.id)
-    await closeSidebar(page)
-
-    const oneItem = page.locator(`[data-session-id="${one.id}"]`).last()
-    const twoItem = page.locator(`[data-session-id="${two.id}"]`).last()
-
-    const project = page.locator(projectSwitchSelector(slug)).first()
-    await expect(project).toBeVisible()
-    await project.hover()
-
-    await expect(oneItem).toBeVisible()
-    await expect(twoItem).toBeVisible()
-
-    const item = await hoverSessionItem(page, one.id)
-    await item
-      .getByRole("button", { name: /archive/i })
-      .first()
-      .click()
-
-    await expect(twoItem).toBeVisible()
-  } finally {
-    await cleanupSession({ sdk, sessionID: one.id })
-    await cleanupSession({ sdk, sessionID: two.id })
-  }
-})
-
-test("open sidebar project popover stays closed after clicking avatar", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const other = await createTestProject()
-  const slug = dirSlug(other)
-
-  try {
-    await project.open({ extra: [other] })
-    await openSidebar(page)
-
-    const projectButton = page.locator(projectSwitchSelector(slug)).first()
-    const card = page.locator('[data-component="hover-card-content"]')
-
-    await expect(projectButton).toBeVisible()
-    await projectButton.hover()
-    await expect(card.getByText(/recent sessions/i)).toBeVisible()
-
-    await projectButton.click()
-    await expect(card).toHaveCount(0)
-
-    await waitSession(page, { directory: other })
-    await expect(card).toHaveCount(0)
-  } finally {
-    await cleanupTestProject(other)
-  }
-})
-
-test("open sidebar project switch activates on first tabbed enter", async ({ page, project }) => {
-  await page.setViewportSize({ width: 1400, height: 800 })
-
-  const other = await createTestProject()
-  const slug = dirSlug(other)
-
-  try {
-    await project.open({ extra: [other] })
-    await openSidebar(page)
-    await defocus(page)
-
-    const projectButton = page.locator(projectSwitchSelector(slug)).first()
-
-    await expect(projectButton).toBeVisible()
-
-    let hit = false
-    for (let i = 0; i < 20; i++) {
-      hit = await projectButton.evaluate((el) => {
-        return el.matches(":focus") || !!el.parentElement?.matches(":focus")
-      })
-      if (hit) break
-      await page.keyboard.press("Tab")
-    }
-
-    expect(hit).toBe(true)
-
-    await page.keyboard.press("Enter")
-    await waitSession(page, { directory: other })
-  } finally {
-    await cleanupTestProject(other)
-  }
-})

+ 0 - 30
packages/app/e2e/sidebar/sidebar-session-links.spec.ts

@@ -1,30 +0,0 @@
-import { test, expect } from "../fixtures"
-import { cleanupSession, openSidebar, withSession } from "../actions"
-import { promptSelector } from "../selectors"
-
-test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => {
-  const stamp = Date.now()
-
-  const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data)
-  const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data)
-
-  if (!one?.id) throw new Error("Session create did not return an id")
-  if (!two?.id) throw new Error("Session create did not return an id")
-
-  try {
-    await gotoSession(one.id)
-
-    await openSidebar(page)
-
-    const target = page.locator(`[data-session-id="${two.id}"] a`).first()
-    await expect(target).toBeVisible()
-    await target.click()
-
-    await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
-    await expect(page.locator(promptSelector)).toBeVisible()
-    await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/)
-  } finally {
-    await cleanupSession({ sdk, sessionID: one.id })
-    await cleanupSession({ sdk, sessionID: two.id })
-  }
-})

+ 0 - 40
packages/app/e2e/sidebar/sidebar.spec.ts

@@ -1,40 +0,0 @@
-import { test, expect } from "../fixtures"
-import { openSidebar, toggleSidebar, withSession } from "../actions"
-
-test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await openSidebar(page)
-  const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
-  await expect(button).toHaveAttribute("aria-expanded", "true")
-
-  await toggleSidebar(page)
-  await expect(button).toHaveAttribute("aria-expanded", "false")
-
-  await toggleSidebar(page)
-  await expect(button).toHaveAttribute("aria-expanded", "true")
-})
-
-test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => {
-  await withSession(sdk, "sidebar persist session 1", async (session1) => {
-    await withSession(sdk, "sidebar persist session 2", async (session2) => {
-      await gotoSession(session1.id)
-
-      await openSidebar(page)
-      const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
-      await toggleSidebar(page)
-      await expect(button).toHaveAttribute("aria-expanded", "false")
-
-      await gotoSession(session2.id)
-      await expect(button).toHaveAttribute("aria-expanded", "false")
-
-      await page.reload()
-      await expect(button).toHaveAttribute("aria-expanded", "false")
-
-      const opened = await page.evaluate(
-        () => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened,
-      )
-      await expect(opened).toBe(false)
-    })
-  })
-})

+ 0 - 94
packages/app/e2e/status/status-popover.spec.ts

@@ -1,94 +0,0 @@
-import { test, expect } from "../fixtures"
-import { openStatusPopover } from "../actions"
-
-test("status popover opens and shows tabs", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const { popoverBody } = await openStatusPopover(page)
-
-  await expect(popoverBody.getByRole("tab", { name: /servers/i })).toBeVisible()
-  await expect(popoverBody.getByRole("tab", { name: /mcp/i })).toBeVisible()
-  await expect(popoverBody.getByRole("tab", { name: /lsp/i })).toBeVisible()
-  await expect(popoverBody.getByRole("tab", { name: /plugins/i })).toBeVisible()
-
-  await page.keyboard.press("Escape")
-  await expect(popoverBody).toHaveCount(0)
-})
-
-test("status popover servers tab shows current server", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const { popoverBody } = await openStatusPopover(page)
-
-  const serversTab = popoverBody.getByRole("tab", { name: /servers/i })
-  await expect(serversTab).toHaveAttribute("aria-selected", "true")
-
-  const serverList = popoverBody.locator('[role="tabpanel"]').first()
-  await expect(serverList.locator("button").first()).toBeVisible()
-})
-
-test("status popover can switch to mcp tab", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const { popoverBody } = await openStatusPopover(page)
-
-  const mcpTab = popoverBody.getByRole("tab", { name: /mcp/i })
-  await mcpTab.click()
-
-  const ariaSelected = await mcpTab.getAttribute("aria-selected")
-  expect(ariaSelected).toBe("true")
-
-  const mcpContent = popoverBody.locator('[role="tabpanel"]:visible').first()
-  await expect(mcpContent).toBeVisible()
-})
-
-test("status popover can switch to lsp tab", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const { popoverBody } = await openStatusPopover(page)
-
-  const lspTab = popoverBody.getByRole("tab", { name: /lsp/i })
-  await lspTab.click()
-
-  const ariaSelected = await lspTab.getAttribute("aria-selected")
-  expect(ariaSelected).toBe("true")
-
-  const lspContent = popoverBody.locator('[role="tabpanel"]:visible').first()
-  await expect(lspContent).toBeVisible()
-})
-
-test("status popover can switch to plugins tab", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const { popoverBody } = await openStatusPopover(page)
-
-  const pluginsTab = popoverBody.getByRole("tab", { name: /plugins/i })
-  await pluginsTab.click()
-
-  const ariaSelected = await pluginsTab.getAttribute("aria-selected")
-  expect(ariaSelected).toBe("true")
-
-  const pluginsContent = popoverBody.locator('[role="tabpanel"]:visible').first()
-  await expect(pluginsContent).toBeVisible()
-})
-
-test("status popover closes on escape", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const { popoverBody } = await openStatusPopover(page)
-  await expect(popoverBody).toBeVisible()
-
-  await page.keyboard.press("Escape")
-  await expect(popoverBody).toHaveCount(0)
-})
-
-test("status popover closes when clicking outside", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const { popoverBody } = await openStatusPopover(page)
-  await expect(popoverBody).toBeVisible()
-
-  await page.getByRole("main").click({ position: { x: 5, y: 5 } })
-
-  await expect(popoverBody).toHaveCount(0)
-})

+ 0 - 28
packages/app/e2e/terminal/terminal-init.spec.ts

@@ -1,28 +0,0 @@
-import { test, expect } from "../fixtures"
-import { waitTerminalFocusIdle, waitTerminalReady } from "../actions"
-import { promptSelector, terminalSelector } from "../selectors"
-import { terminalToggleKey } from "../utils"
-
-test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const terminals = page.locator(terminalSelector)
-  const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
-  const opened = await terminals.first().isVisible()
-
-  if (!opened) {
-    await page.keyboard.press(terminalToggleKey)
-  }
-
-  await waitTerminalFocusIdle(page, { term: terminals.first() })
-  await expect(terminals).toHaveCount(1)
-
-  // Ghostty captures a lot of keybinds when focused; move focus back
-  // to the app shell before triggering `terminal.new`.
-  await page.locator(promptSelector).click()
-  await page.keyboard.press("Control+Alt+T")
-
-  await expect(tabs).toHaveCount(2)
-  await expect(terminals).toHaveCount(1)
-  await waitTerminalReady(page, { term: terminals.first() })
-})

+ 0 - 45
packages/app/e2e/terminal/terminal-reconnect.spec.ts

@@ -1,45 +0,0 @@
-import type { Page } from "@playwright/test"
-import { disconnectTerminal, runTerminal, terminalConnects, waitTerminalReady } from "../actions"
-import { test, expect } from "../fixtures"
-import { terminalSelector } from "../selectors"
-import { terminalToggleKey } from "../utils"
-
-async function open(page: Page) {
-  const term = page.locator(terminalSelector).first()
-  const visible = await term.isVisible().catch(() => false)
-  if (!visible) await page.keyboard.press(terminalToggleKey)
-  await waitTerminalReady(page, { term })
-  return term
-}
-
-test("terminal reconnects without replacing the pty", async ({ page, project }) => {
-  await project.open()
-  const name = `OPENCODE_E2E_RECONNECT_${Date.now()}`
-  const token = `E2E_RECONNECT_${Date.now()}`
-
-  await project.gotoSession()
-
-  const term = await open(page)
-  const id = await term.getAttribute("data-pty-id")
-  if (!id) throw new Error("Active terminal missing data-pty-id")
-
-  const prev = await terminalConnects(page, { term })
-
-  await runTerminal(page, {
-    term,
-    cmd: `export ${name}=${token}; echo ${token}`,
-    token,
-  })
-
-  await disconnectTerminal(page, { term })
-
-  await expect.poll(() => terminalConnects(page, { term }), { timeout: 15_000 }).toBeGreaterThan(prev)
-  await expect.poll(() => term.getAttribute("data-pty-id"), { timeout: 5_000 }).toBe(id)
-
-  await runTerminal(page, {
-    term,
-    cmd: `echo $${name}`,
-    token,
-    timeout: 15_000,
-  })
-})

+ 0 - 165
packages/app/e2e/terminal/terminal-tabs.spec.ts

@@ -1,165 +0,0 @@
-import type { Page } from "@playwright/test"
-import { runTerminal, waitTerminalReady } from "../actions"
-import { test, expect } from "../fixtures"
-import { dropdownMenuContentSelector, terminalSelector } from "../selectors"
-import { terminalToggleKey, workspacePersistKey } from "../utils"
-
-type State = {
-  active?: string
-  all: Array<{
-    id: string
-    title: string
-    titleNumber: number
-    buffer?: string
-  }>
-}
-
-async function open(page: Page) {
-  const terminal = page.locator(terminalSelector)
-  const visible = await terminal.isVisible().catch(() => false)
-  if (!visible) await page.keyboard.press(terminalToggleKey)
-  await waitTerminalReady(page, { term: terminal })
-}
-
-async function store(page: Page, key: string) {
-  return page.evaluate((key) => {
-    const raw = localStorage.getItem(key)
-    if (raw) return JSON.parse(raw) as State
-
-    for (let i = 0; i < localStorage.length; i++) {
-      const next = localStorage.key(i)
-      if (!next?.endsWith(":workspace:terminal")) continue
-      const value = localStorage.getItem(next)
-      if (!value) continue
-      return JSON.parse(value) as State
-    }
-  }, key)
-}
-
-test("inactive terminal tab buffers persist across tab switches", async ({ page, project }) => {
-  await project.open()
-  const key = workspacePersistKey(project.directory, "terminal")
-  const one = `E2E_TERM_ONE_${Date.now()}`
-  const two = `E2E_TERM_TWO_${Date.now()}`
-  const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
-  const first = tabs.filter({ hasText: /Terminal 1/ }).first()
-  const second = tabs.filter({ hasText: /Terminal 2/ }).first()
-
-  await project.gotoSession()
-  await open(page)
-
-  await runTerminal(page, { cmd: `echo ${one}`, token: one })
-
-  await page.getByRole("button", { name: /new terminal/i }).click()
-  await expect(tabs).toHaveCount(2)
-
-  await runTerminal(page, { cmd: `echo ${two}`, token: two })
-
-  await first.click()
-  await expect(first).toHaveAttribute("aria-selected", "true")
-
-  await expect
-    .poll(
-      async () => {
-        const state = await store(page, key)
-        const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
-        const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
-        return {
-          first: first.includes(one),
-          second: second.includes(two),
-        }
-      },
-      { timeout: 5_000 },
-    )
-    .toEqual({ first: false, second: true })
-
-  await second.click()
-  await expect(second).toHaveAttribute("aria-selected", "true")
-  await expect
-    .poll(
-      async () => {
-        const state = await store(page, key)
-        const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
-        const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
-        return {
-          first: first.includes(one),
-          second: second.includes(two),
-        }
-      },
-      { timeout: 5_000 },
-    )
-    .toEqual({ first: true, second: false })
-})
-
-test("closing the active terminal tab falls back to the previous tab", async ({ page, project }) => {
-  await project.open()
-  const key = workspacePersistKey(project.directory, "terminal")
-  const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
-
-  await project.gotoSession()
-  await open(page)
-
-  await page.getByRole("button", { name: /new terminal/i }).click()
-  await expect(tabs).toHaveCount(2)
-
-  const second = tabs.filter({ hasText: /Terminal 2/ }).first()
-  await second.click()
-  await expect(second).toHaveAttribute("aria-selected", "true")
-
-  await second.hover()
-  await page
-    .getByRole("button", { name: /close terminal/i })
-    .nth(1)
-    .click({ force: true })
-
-  const first = tabs.filter({ hasText: /Terminal 1/ }).first()
-  await expect(tabs).toHaveCount(1)
-  await expect(first).toHaveAttribute("aria-selected", "true")
-  await expect
-    .poll(
-      async () => {
-        const state = await store(page, key)
-        return {
-          count: state?.all.length ?? 0,
-          first: state?.all.some((item) => item.titleNumber === 1) ?? false,
-        }
-      },
-      { timeout: 15_000 },
-    )
-    .toEqual({ count: 1, first: true })
-})
-
-test("terminal tab can be renamed from the context menu", async ({ page, project }) => {
-  await project.open()
-  const key = workspacePersistKey(project.directory, "terminal")
-  const rename = `E2E term ${Date.now()}`
-  const tab = page.locator('#terminal-panel [data-slot="tabs-trigger"]').first()
-
-  await project.gotoSession()
-  await open(page)
-
-  await expect(tab).toContainText(/Terminal 1/)
-  await tab.click({ button: "right" })
-
-  const menu = page.locator(dropdownMenuContentSelector).first()
-  await expect(menu).toBeVisible()
-  await menu.getByRole("menuitem", { name: /^Rename$/i }).click()
-  await expect(menu).toHaveCount(0)
-
-  const input = page.locator('#terminal-panel input[type="text"]').first()
-  await expect(input).toBeVisible()
-  await input.fill(rename)
-  await input.press("Enter")
-
-  await expect(input).toHaveCount(0)
-  await expect(tab).toContainText(rename)
-  await expect
-    .poll(
-      async () => {
-        const state = await store(page, key)
-        return state?.all[0]?.title
-      },
-      { timeout: 5_000 },
-    )
-    .toBe(rename)
-})

+ 0 - 18
packages/app/e2e/terminal/terminal.spec.ts

@@ -1,18 +0,0 @@
-import { test, expect } from "../fixtures"
-import { waitTerminalReady } from "../actions"
-import { terminalSelector } from "../selectors"
-import { terminalToggleKey } from "../utils"
-
-test("terminal panel can be toggled", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  const terminal = page.locator(terminalSelector)
-  const initiallyOpen = await terminal.isVisible()
-  if (initiallyOpen) {
-    await page.keyboard.press(terminalToggleKey)
-    await expect(terminal).toHaveCount(0)
-  }
-
-  await page.keyboard.press(terminalToggleKey)
-  await waitTerminalReady(page, { term: terminal })
-})

+ 0 - 25
packages/app/e2e/thinking-level.spec.ts

@@ -1,25 +0,0 @@
-import { test, expect } from "./fixtures"
-import { modelVariantCycleSelector } from "./selectors"
-
-test("smoke model variant cycle updates label", async ({ page, gotoSession }) => {
-  await gotoSession()
-
-  await page.addStyleTag({
-    content: `${modelVariantCycleSelector} { display: inline-block !important; }`,
-  })
-
-  const button = page.locator(modelVariantCycleSelector)
-  const exists = (await button.count()) > 0
-  test.skip(!exists, "current model has no variants")
-  if (!exists) return
-
-  await expect(button).toBeVisible()
-
-  const before = (await button.innerText()).trim()
-  await button.click()
-  await expect(button).not.toHaveText(before)
-
-  const after = (await button.innerText()).trim()
-  await button.click()
-  await expect(button).not.toHaveText(after)
-})

+ 11 - 0
packages/app/e2e/todo.spec.ts

@@ -0,0 +1,11 @@
+import { test } from "@playwright/test"
+
+test(
+  "test something cool",
+  {
+    annotation: { type: "todo" },
+  },
+  async () => {
+    test.fixme()
+  },
+)

+ 1 - 1
packages/app/e2e/tsconfig.json

@@ -5,5 +5,5 @@
     "rootDir": "..",
     "types": ["node", "bun"]
   },
-  "include": ["./**/*.ts", "../src/testing/terminal.ts"]
+  "include": ["./**/*.ts"]
 }

+ 0 - 63
packages/app/e2e/utils.ts

@@ -1,63 +0,0 @@
-import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
-import { base64Encode, checksum } from "@opencode-ai/util/encode"
-
-export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
-export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
-
-export const serverUrl = `http://${serverHost}:${serverPort}`
-export const serverName = `${serverHost}:${serverPort}`
-
-const localHosts = ["127.0.0.1", "localhost"]
-
-const serverLabels = (() => {
-  const url = new URL(serverUrl)
-  if (!localHosts.includes(url.hostname)) return [serverName]
-  return localHosts.map((host) => `${host}:${url.port}`)
-})()
-
-export const serverNames = [...new Set(serverLabels)]
-
-export const serverUrls = serverNames.map((name) => `http://${name}`)
-
-const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
-
-export const serverNamePattern = new RegExp(`(?:${serverNames.map(escape).join("|")})`)
-
-export const modKey = process.platform === "darwin" ? "Meta" : "Control"
-export const terminalToggleKey = "Control+Backquote"
-
-export function createSdk(directory?: string, baseUrl = serverUrl) {
-  return createOpencodeClient({ baseUrl, directory, throwOnError: true })
-}
-
-export async function resolveDirectory(directory: string, baseUrl = serverUrl) {
-  return createSdk(directory, baseUrl)
-    .path.get()
-    .then((x) => x.data?.directory ?? directory)
-}
-
-export async function getWorktree(baseUrl = serverUrl) {
-  const sdk = createSdk(undefined, baseUrl)
-  const result = await sdk.path.get()
-  const data = result.data
-  if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${baseUrl}/path`)
-  return data.worktree
-}
-
-export function dirSlug(directory: string) {
-  return base64Encode(directory)
-}
-
-export function dirPath(directory: string) {
-  return `/${dirSlug(directory)}`
-}
-
-export function sessionPath(directory: string, sessionID?: string) {
-  return `${dirPath(directory)}/session${sessionID ? `/${sessionID}` : ""}`
-}
-
-export function workspacePersistKey(directory: string, key: string) {
-  const head = (directory.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
-  const sum = checksum(directory) ?? "0"
-  return `opencode.workspace.${head}.${sum}.dat:workspace:${key}`
-}

+ 2 - 2
packages/app/package.json

@@ -19,14 +19,14 @@
     "test:unit": "bun test --preload ./happydom.ts ./src",
     "test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
     "test:e2e": "playwright test",
-    "test:e2e:local": "bun script/e2e-local.ts",
+    "test:e2e:local": "playwright test",
     "test:e2e:ui": "playwright test --ui",
     "test:e2e:report": "playwright show-report e2e/playwright-report"
   },
   "license": "MIT",
   "devDependencies": {
     "@happy-dom/global-registrator": "20.0.11",
-    "@playwright/test": "1.57.0",
+    "@playwright/test": "catalog:",
     "@tailwindcss/vite": "catalog:",
     "@tsconfig/bun": "1.0.9",
     "@types/bun": "catalog:",

+ 0 - 180
packages/app/script/e2e-local.ts

@@ -1,180 +0,0 @@
-import fs from "node:fs/promises"
-import net from "node:net"
-import os from "node:os"
-import path from "node:path"
-
-async function freePort() {
-  return await new Promise<number>((resolve, reject) => {
-    const server = net.createServer()
-    server.once("error", reject)
-    server.listen(0, () => {
-      const address = server.address()
-      if (!address || typeof address === "string") {
-        server.close(() => reject(new Error("Failed to acquire a free port")))
-        return
-      }
-      server.close((err) => {
-        if (err) {
-          reject(err)
-          return
-        }
-        resolve(address.port)
-      })
-    })
-  })
-}
-
-async function waitForHealth(url: string) {
-  const timeout = Date.now() + 120_000
-  const errors: string[] = []
-  while (Date.now() < timeout) {
-    const result = await fetch(url)
-      .then((r) => ({ ok: r.ok, error: undefined }))
-      .catch((error) => ({
-        ok: false,
-        error: error instanceof Error ? error.message : String(error),
-      }))
-    if (result.ok) return
-    if (result.error) errors.push(result.error)
-    await new Promise((r) => setTimeout(r, 250))
-  }
-  const last = errors.length ? ` (last error: ${errors[errors.length - 1]})` : ""
-  throw new Error(`Timed out waiting for server health: ${url}${last}`)
-}
-
-const appDir = process.cwd()
-const repoDir = path.resolve(appDir, "../..")
-const opencodeDir = path.join(repoDir, "packages", "opencode")
-
-const extraArgs = (() => {
-  const args = process.argv.slice(2)
-  if (args[0] === "--") return args.slice(1)
-  return args
-})()
-
-const [serverPort, webPort] = await Promise.all([freePort(), freePort()])
-
-const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-"))
-const keepSandbox = process.env.OPENCODE_E2E_KEEP_SANDBOX === "1"
-
-const serverEnv = {
-  ...process.env,
-  OPENCODE_DISABLE_SHARE: process.env.OPENCODE_DISABLE_SHARE ?? "true",
-  OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
-  OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
-  OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
-  OPENCODE_TEST_HOME: path.join(sandbox, "home"),
-  XDG_DATA_HOME: path.join(sandbox, "share"),
-  XDG_CACHE_HOME: path.join(sandbox, "cache"),
-  XDG_CONFIG_HOME: path.join(sandbox, "config"),
-  XDG_STATE_HOME: path.join(sandbox, "state"),
-  OPENCODE_E2E_PROJECT_DIR: repoDir,
-  OPENCODE_E2E_SESSION_TITLE: "E2E Session",
-  OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
-  OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL ?? "opencode/gpt-5-nano",
-  OPENCODE_CLIENT: "app",
-  OPENCODE_STRICT_CONFIG_DEPS: "true",
-} satisfies Record<string, string>
-
-const runnerEnv = {
-  ...serverEnv,
-  PLAYWRIGHT_SERVER_HOST: "127.0.0.1",
-  PLAYWRIGHT_SERVER_PORT: String(serverPort),
-  VITE_OPENCODE_SERVER_HOST: "127.0.0.1",
-  VITE_OPENCODE_SERVER_PORT: String(serverPort),
-  PLAYWRIGHT_PORT: String(webPort),
-} satisfies Record<string, string>
-
-let seed: ReturnType<typeof Bun.spawn> | undefined
-let runner: ReturnType<typeof Bun.spawn> | undefined
-let server: { stop: (close?: boolean) => Promise<void> | void } | undefined
-let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined
-let cleaned = false
-
-const cleanup = async () => {
-  if (cleaned) return
-  cleaned = true
-
-  if (seed && seed.exitCode === null) seed.kill("SIGTERM")
-  if (runner && runner.exitCode === null) runner.kill("SIGTERM")
-
-  const jobs = [
-    inst?.Instance.disposeAll(),
-    typeof server?.stop === "function" ? server.stop() : undefined,
-    keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }),
-  ].filter(Boolean)
-  await Promise.allSettled(jobs)
-}
-
-const shutdown = (code: number, reason: string) => {
-  process.exitCode = code
-  void cleanup().finally(() => {
-    console.error(`e2e-local shutdown: ${reason}`)
-    process.exit(code)
-  })
-}
-
-const reportInternalError = (reason: string, error: unknown) => {
-  console.warn(`e2e-local ignored server error: ${reason}`)
-  console.warn(error)
-}
-
-process.once("SIGINT", () => shutdown(130, "SIGINT"))
-process.once("SIGTERM", () => shutdown(143, "SIGTERM"))
-process.once("SIGHUP", () => shutdown(129, "SIGHUP"))
-process.once("uncaughtException", (error) => {
-  reportInternalError("uncaughtException", error)
-})
-process.once("unhandledRejection", (error) => {
-  reportInternalError("unhandledRejection", error)
-})
-
-let code = 1
-
-try {
-  seed = Bun.spawn(["bun", "script/seed-e2e.ts"], {
-    cwd: opencodeDir,
-    env: serverEnv,
-    stdout: "inherit",
-    stderr: "inherit",
-  })
-
-  const seedExit = await seed.exited
-  if (seedExit !== 0) {
-    code = seedExit
-  } else {
-    Object.assign(process.env, serverEnv)
-    process.env.AGENT = "1"
-    process.env.OPENCODE = "1"
-    process.env.OPENCODE_PID = String(process.pid)
-
-    const log = await import("../../opencode/src/util/log")
-    const install = await import("../../opencode/src/installation")
-    await log.Log.init({
-      print: true,
-      dev: install.Installation.isLocal(),
-      level: "WARN",
-    })
-
-    const servermod = await import("../../opencode/src/server/server")
-    inst = await import("../../opencode/src/project/instance")
-    server = await servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" })
-    console.log(`opencode server listening on http://127.0.0.1:${serverPort}`)
-
-    await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`)
-    runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], {
-      cwd: appDir,
-      env: runnerEnv,
-      stdout: "inherit",
-      stderr: "inherit",
-    })
-    code = await runner.exited
-  }
-} catch (error) {
-  console.error(error)
-  code = 1
-} finally {
-  await cleanup()
-}
-
-process.exit(code)

+ 0 - 17
packages/app/src/components/prompt-input.tsx

@@ -35,7 +35,6 @@ import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { createSessionTabs } from "@/pages/session/helpers"
-import { promptEnabled, promptProbe } from "@/testing/prompt"
 import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
 import { createPromptAttachments } from "./prompt-input/attachments"
 import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
@@ -639,7 +638,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
   const handleSlashSelect = (cmd: SlashCommand | undefined) => {
     if (!cmd) return
-    promptProbe.select(cmd.id)
     closePopover()
     const images = imageAttachments()
 
@@ -728,21 +726,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
     })
   })
-
-  if (promptEnabled()) {
-    createEffect(() => {
-      promptProbe.set({
-        popover: store.popover,
-        slash: {
-          active: slashActive() ?? null,
-          ids: slashFlat().map((cmd) => cmd.id),
-        },
-      })
-    })
-
-    onCleanup(() => promptProbe.clear())
-  }
-
   const selectPopoverActive = () => {
     if (store.popover === "at") {
       const items = atFlat()

+ 0 - 3
packages/app/src/components/prompt-input/submit.ts

@@ -13,7 +13,6 @@ import { usePermission } from "@/context/permission"
 import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
 import { useSDK } from "@/context/sdk"
 import { useSync } from "@/context/sync"
-import { promptProbe } from "@/testing/prompt"
 import { Identifier } from "@/utils/id"
 import { Worktree as WorktreeState } from "@/utils/worktree"
 import { buildRequestParts } from "./build-request-parts"
@@ -307,7 +306,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
 
     input.addToHistory(currentPrompt, mode)
     input.resetHistoryNavigation()
-    promptProbe.start()
 
     const projectDirectory = sdk.directory
     const isNewSession = !params.id
@@ -427,7 +425,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
       return
     }
 
-    promptProbe.submit({ sessionID: session.id, directory: sessionDirectory })
     input.onSubmit?.()
 
     if (mode === "shell") {

+ 0 - 16
packages/app/src/components/terminal.tsx

@@ -13,7 +13,6 @@ import { useSDK } from "@/context/sdk"
 import { useServer } from "@/context/server"
 import { monoFontFamily, useSettings } from "@/context/settings"
 import type { LocalPTY } from "@/context/terminal"
-import { terminalAttr, terminalProbe } from "@/testing/terminal"
 import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
 import { terminalWriter } from "@/utils/terminal-writer"
 
@@ -178,7 +177,6 @@ export const Terminal = (props: TerminalProps) => {
   let container!: HTMLDivElement
   const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
   const id = local.pty.id
-  const probe = terminalProbe(id)
   const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : ""
   const restoreSize =
     restore &&
@@ -349,9 +347,6 @@ export const Terminal = (props: TerminalProps) => {
   }
 
   onMount(() => {
-    probe.init()
-    cleanups.push(() => probe.drop())
-
     const run = async () => {
       const loaded = await loadGhostty()
       if (disposed) return
@@ -381,8 +376,6 @@ export const Terminal = (props: TerminalProps) => {
       term = t
       output = terminalWriter((data, done) =>
         t.write(data, () => {
-          probe.render(data)
-          probe.settle()
           done?.()
         }),
       )
@@ -534,7 +527,6 @@ export const Terminal = (props: TerminalProps) => {
         const handleOpen = () => {
           if (disposed) return
           tries = 0
-          probe.connect()
           local.onConnect?.()
           scheduleSize(t.cols, t.rows)
         }
@@ -599,13 +591,6 @@ export const Terminal = (props: TerminalProps) => {
         socket.addEventListener("close", handleClose)
       }
 
-      probe.control({
-        disconnect: () => {
-          if (!ws) return
-          ws.close(4_000, "e2e")
-        },
-      })
-
       open()
     }
 
@@ -645,7 +630,6 @@ export const Terminal = (props: TerminalProps) => {
     <div
       ref={container}
       data-component="terminal"
-      {...{ [terminalAttr]: id }}
       data-prevent-autofocus
       tabIndex={-1}
       style={{ "background-color": terminalColors().background }}

+ 1 - 49
packages/app/src/context/local.tsx

@@ -1,11 +1,10 @@
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { base64Encode } from "@opencode-ai/util/encode"
 import { useParams } from "@solidjs/router"
-import { batch, createEffect, createMemo, onCleanup } from "solid-js"
+import { batch, createEffect, createMemo } from "solid-js"
 import { createStore } from "solid-js/store"
 import { useModels } from "@/context/models"
 import { useProviders } from "@/hooks/use-providers"
-import { modelEnabled, modelProbe } from "@/testing/model-selection"
 import { Persist, persisted } from "@/utils/persist"
 import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
 import { useSDK } from "./sdk"
@@ -388,53 +387,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         },
       },
     }
-
-    if (modelEnabled()) {
-      const probe = Symbol("model-probe")
-
-      modelProbe.bind(probe, {
-        setAgent: agent.set,
-        setModel: model.set,
-        setVariant: model.variant.set,
-      })
-
-      createEffect(() => {
-        const agent = result.agent.current()
-        const model = result.model.current()
-        modelProbe.set(probe, {
-          dir: sdk.directory,
-          sessionID: id(),
-          last: store.last,
-          agent: agent?.name,
-          model: model
-            ? {
-                providerID: model.provider.id,
-                modelID: model.id,
-                name: model.name,
-              }
-            : undefined,
-          variant: result.model.variant.current() ?? null,
-          selected: result.model.variant.selected(),
-          configured: result.model.variant.configured(),
-          pick: scope(),
-          base: undefined,
-          current: store.current,
-          variants: result.model.variant.list(),
-          models: result.model
-            .list()
-            .filter((item) => result.model.visible({ providerID: item.provider.id, modelID: item.id }))
-            .map((item) => ({
-              providerID: item.provider.id,
-              modelID: item.id,
-              name: item.name,
-            })),
-          agents: result.agent.list().map((item) => ({ name: item.name })),
-        })
-      })
-
-      onCleanup(() => modelProbe.clear(probe))
-    }
-
     return result
   },
 })

+ 1 - 9
packages/app/src/pages/error.tsx

@@ -1,12 +1,11 @@
 import { TextField } from "@opencode-ai/ui/text-field"
 import { Logo } from "@opencode-ai/ui/logo"
 import { Button } from "@opencode-ai/ui/button"
-import { Component, Show, onMount } from "solid-js"
+import { Component, Show } from "solid-js"
 import { createStore } from "solid-js/store"
 import { usePlatform } from "@/context/platform"
 import { useLanguage } from "@/context/language"
 import { Icon } from "@opencode-ai/ui/icon"
-import type { E2EWindow } from "@/testing/terminal"
 
 export type InitError = {
   name: string
@@ -227,13 +226,6 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
     actionError: undefined as string | undefined,
   })
 
-  onMount(() => {
-    const win = window as E2EWindow
-    if (!win.__opencode_e2e) return
-    const detail = formatError(props.error, language.t)
-    console.error(`[e2e:error-boundary] ${window.location.pathname}\n${detail}`)
-  })
-
   async function checkForUpdates() {
     if (!platform.checkUpdate) return
     setStore("checking", true)

+ 2 - 53
packages/app/src/pages/session/composer/session-composer-state.ts

@@ -1,6 +1,5 @@
-import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
+import { createEffect, createMemo, on, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
-import { makeEventListener } from "@solid-primitives/event-listener"
 import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
 import { useParams } from "@solidjs/router"
 import { showToast } from "@opencode-ai/ui/toast"
@@ -9,7 +8,6 @@ import { useLanguage } from "@/context/language"
 import { usePermission } from "@/context/permission"
 import { useSDK } from "@/context/sdk"
 import { useSync } from "@/context/sync"
-import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer"
 import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
 
 export const todoState = (input: {
@@ -49,49 +47,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
     return !!permissionRequest() || !!questionRequest()
   })
 
-  const [test, setTest] = createStore({
-    on: false,
-    live: undefined as boolean | undefined,
-    todos: undefined as Todo[] | undefined,
-  })
-
-  const pull = () => {
-    const id = params.id
-    if (!id) {
-      setTest({ on: false, live: undefined, todos: undefined })
-      return
-    }
-
-    const next = composerDriver(id)
-    if (!next) {
-      setTest({ on: false, live: undefined, todos: undefined })
-      return
-    }
-
-    setTest({
-      on: true,
-      live: next.live,
-      todos: next.todos?.map((todo) => ({ ...todo })),
-    })
-  }
-
-  onMount(() => {
-    if (!composerEnabled()) return
-
-    pull()
-    createEffect(on(() => params.id, pull, { defer: true }))
-
-    const onEvent = (event: Event) => {
-      const detail = (event as CustomEvent<{ sessionID?: string }>).detail
-      if (detail?.sessionID !== params.id) return
-      pull()
-    }
-
-    makeEventListener(window, composerEvent, onEvent)
-  })
-
   const todos = createMemo((): Todo[] => {
-    if (test.on && test.todos !== undefined) return test.todos
     const id = params.id
     if (!id) return []
     return globalSync.data.session_todo[id] ?? []
@@ -108,10 +64,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
   })
 
   const busy = createMemo(() => status().type !== "idle")
-  const live = createMemo(() => {
-    if (test.on && test.live !== undefined) return test.live
-    return busy() || blocked()
-  })
+  const live = createMemo(() => busy() || blocked())
 
   const [store, setStore] = createStore({
     responding: undefined as string | undefined,
@@ -163,10 +116,6 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
 
   // Keep stale turn todos from reopening if the model never clears them.
   const clear = () => {
-    if (test.on && test.todos !== undefined) {
-      setTest("todos", [])
-      return
-    }
     const id = params.id
     if (!id) return
     globalSync.todo.set(id, [])

+ 1 - 21
packages/app/src/pages/session/composer/session-todo-dock.tsx

@@ -7,9 +7,8 @@ import { useSpring } from "@opencode-ai/ui/motion-spring"
 import { TextReveal } from "@opencode-ai/ui/text-reveal"
 import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
-import { Index, createEffect, createMemo, onCleanup } from "solid-js"
+import { Index, createEffect, createMemo } from "solid-js"
 import { createStore } from "solid-js/store"
-import { composerEnabled, composerProbe } from "@/testing/session-composer"
 import { useLanguage } from "@/context/language"
 
 const doneToken = "\u0000done\u0000"
@@ -81,8 +80,6 @@ export function SessionTodoDock(props: {
   const off = createMemo(() => hide() > 0.98)
   const turn = createMemo(() => Math.max(0, Math.min(1, value())))
   const full = createMemo(() => Math.max(78, store.height))
-  const e2e = composerEnabled()
-  const probe = composerProbe(props.sessionID)
   let contentRef: HTMLDivElement | undefined
 
   createEffect(() => {
@@ -95,23 +92,6 @@ export function SessionTodoDock(props: {
     createResizeObserver(el, update)
   })
 
-  createEffect(() => {
-    if (!e2e) return
-
-    probe.set({
-      mounted: true,
-      collapsed: store.collapsed,
-      hidden: store.collapsed || off(),
-      count: props.todos.length,
-      states: props.todos.map((todo) => todo.status),
-    })
-  })
-
-  onCleanup(() => {
-    if (!e2e) return
-    probe.drop()
-  })
-
   return (
     <DockTray
       data-component="session-todo-dock"

+ 0 - 6
packages/app/src/pages/session/terminal-panel.tsx

@@ -19,7 +19,6 @@ import { terminalTabLabel } from "@/pages/session/terminal-label"
 import { createSizing, focusTerminalById } from "@/pages/session/helpers"
 import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
 import { useSessionLayout } from "@/pages/session/session-layout"
-import { terminalProbe } from "@/testing/terminal"
 
 export function TerminalPanel() {
   const delays = [120, 240]
@@ -78,12 +77,9 @@ export function TerminalPanel() {
   )
 
   const focus = (id: string) => {
-    const probe = terminalProbe(id)
-    probe.focus(delays.length + 1)
     focusTerminalById(id)
 
     const frame = requestAnimationFrame(() => {
-      probe.step()
       if (!opened()) return
       if (terminal.active() !== id) return
       focusTerminalById(id)
@@ -91,7 +87,6 @@ export function TerminalPanel() {
 
     const timers = delays.map((ms) =>
       window.setTimeout(() => {
-        probe.step()
         if (!opened()) return
         if (terminal.active() !== id) return
         focusTerminalById(id)
@@ -99,7 +94,6 @@ export function TerminalPanel() {
     )
 
     return () => {
-      probe.focus(0)
       cancelAnimationFrame(frame)
       for (const timer of timers) clearTimeout(timer)
     }

+ 0 - 109
packages/app/src/testing/model-selection.ts

@@ -1,109 +0,0 @@
-type ModelKey = {
-  providerID: string
-  modelID: string
-}
-
-type ModelItem = ModelKey & {
-  name: string
-}
-
-type AgentItem = {
-  name: string
-}
-
-type State = {
-  agent?: string
-  model?: ModelKey | null
-  variant?: string | null
-}
-
-export type ModelProbeState = {
-  dir?: string
-  sessionID?: string
-  last?: {
-    type: "agent" | "model" | "variant"
-    agent?: string
-    model?: ModelKey | null
-    variant?: string | null
-  }
-  agent?: string
-  model?: (ModelKey & { name?: string }) | undefined
-  variant?: string | null
-  selected?: string | null
-  configured?: string
-  pick?: State
-  base?: State
-  current?: string
-  variants?: string[]
-  models?: ModelItem[]
-  agents?: AgentItem[]
-}
-
-export type ModelWindow = Window & {
-  __opencode_e2e?: {
-    model?: {
-      enabled?: boolean
-      current?: ModelProbeState
-      controls?: {
-        setAgent?: (name: string | undefined) => void
-        setModel?: (value: ModelKey | undefined) => void
-        setVariant?: (value: string | undefined) => void
-      }
-    }
-  }
-}
-
-const clone = (state?: State) => {
-  if (!state) return undefined
-  return {
-    ...state,
-    model: state.model ? { ...state.model } : state.model,
-  }
-}
-
-let active: symbol | undefined
-
-export const modelEnabled = () => {
-  if (typeof window === "undefined") return false
-  return (window as ModelWindow).__opencode_e2e?.model?.enabled === true
-}
-
-const root = () => {
-  if (!modelEnabled()) return
-  return (window as ModelWindow).__opencode_e2e?.model
-}
-
-export const modelProbe = {
-  bind(id: symbol, input: NonNullable<NonNullable<ModelWindow["__opencode_e2e"]>["model"]>["controls"]) {
-    const state = root()
-    if (!state) return
-    active = id
-    state.controls = input
-  },
-  set(id: symbol, input: ModelProbeState) {
-    const state = root()
-    if (!state || active !== id) return
-    state.current = {
-      ...input,
-      model: input.model ? { ...input.model } : undefined,
-      last: input.last
-        ? {
-            ...input.last,
-            model: input.last.model ? { ...input.last.model } : input.last.model,
-          }
-        : undefined,
-      pick: clone(input.pick),
-      base: clone(input.base),
-      variants: input.variants?.slice(),
-      models: input.models?.map((item) => ({ ...item })),
-      agents: input.agents?.map((item) => ({ ...item })),
-    }
-  },
-  clear(id: symbol) {
-    const state = root()
-    if (!state || active !== id) return
-    active = undefined
-    state.current = undefined
-    state.controls = undefined
-  },
-}

+ 0 - 83
packages/app/src/testing/prompt.ts

@@ -1,83 +0,0 @@
-import type { E2EWindow } from "./terminal"
-
-export type PromptProbeState = {
-  popover: "at" | "slash" | null
-  slash: {
-    active: string | null
-    ids: string[]
-  }
-  selected: string | null
-  selects: number
-}
-
-export type PromptSendState = {
-  started: number
-  count: number
-  sessionID?: string
-  directory?: string
-}
-
-export const promptEnabled = () => {
-  if (typeof window === "undefined") return false
-  return (window as E2EWindow).__opencode_e2e?.prompt?.enabled === true
-}
-
-const root = () => {
-  if (!promptEnabled()) return
-  return (window as E2EWindow).__opencode_e2e?.prompt
-}
-
-export const promptProbe = {
-  set(input: Omit<PromptProbeState, "selected" | "selects">) {
-    const state = root()
-    if (!state) return
-    state.current = {
-      popover: input.popover,
-      slash: {
-        active: input.slash.active,
-        ids: [...input.slash.ids],
-      },
-      selected: state.current?.selected ?? null,
-      selects: state.current?.selects ?? 0,
-    }
-  },
-  select(id: string) {
-    const state = root()
-    if (!state) return
-    const prev = state.current
-    state.current = {
-      popover: prev?.popover ?? null,
-      slash: {
-        active: prev?.slash.active ?? null,
-        ids: [...(prev?.slash.ids ?? [])],
-      },
-      selected: id,
-      selects: (prev?.selects ?? 0) + 1,
-    }
-  },
-  clear() {
-    const state = root()
-    if (!state) return
-    state.current = undefined
-  },
-  start() {
-    const state = root()
-    if (!state) return
-    state.sent = {
-      started: (state.sent?.started ?? 0) + 1,
-      count: state.sent?.count ?? 0,
-      sessionID: state.sent?.sessionID,
-      directory: state.sent?.directory,
-    }
-  },
-  submit(input: { sessionID: string; directory: string }) {
-    const state = root()
-    if (!state) return
-    state.sent = {
-      started: state.sent?.started ?? 0,
-      count: (state.sent?.count ?? 0) + 1,
-      sessionID: input.sessionID,
-      directory: input.directory,
-    }
-  },
-}

+ 0 - 84
packages/app/src/testing/session-composer.ts

@@ -1,84 +0,0 @@
-import type { Todo } from "@opencode-ai/sdk/v2"
-
-export const composerEvent = "opencode:e2e:composer"
-
-export type ComposerDriverState = {
-  live?: boolean
-  todos?: Array<Pick<Todo, "content" | "status" | "priority">>
-}
-
-export type ComposerProbeState = {
-  mounted: boolean
-  collapsed: boolean
-  hidden: boolean
-  count: number
-  states: Todo["status"][]
-}
-
-type ComposerState = {
-  driver?: ComposerDriverState
-  probe?: ComposerProbeState
-}
-
-export type ComposerWindow = Window & {
-  __opencode_e2e?: {
-    composer?: {
-      enabled?: boolean
-      sessions?: Record<string, ComposerState>
-    }
-  }
-}
-
-const clone = (driver: ComposerDriverState) => ({
-  live: driver.live,
-  todos: driver.todos?.map((todo) => ({ ...todo })),
-})
-
-export const composerEnabled = () => {
-  if (typeof window === "undefined") return false
-  return (window as ComposerWindow).__opencode_e2e?.composer?.enabled === true
-}
-
-const root = () => {
-  if (!composerEnabled()) return
-  const state = (window as ComposerWindow).__opencode_e2e?.composer
-  if (!state) return
-  state.sessions ??= {}
-  return state.sessions
-}
-
-export const composerDriver = (sessionID?: string) => {
-  if (!sessionID) return
-  const state = root()?.[sessionID]?.driver
-  if (!state) return
-  return clone(state)
-}
-
-export const composerProbe = (sessionID?: string) => {
-  const set = (next: ComposerProbeState) => {
-    if (!sessionID) return
-    const sessions = root()
-    if (!sessions) return
-    const prev = sessions[sessionID] ?? {}
-    sessions[sessionID] = {
-      ...prev,
-      probe: {
-        ...next,
-        states: [...next.states],
-      },
-    }
-  }
-
-  return {
-    set,
-    drop() {
-      set({
-        mounted: false,
-        collapsed: false,
-        hidden: true,
-        count: 0,
-        states: [],
-      })
-    },
-  }
-}

+ 0 - 119
packages/app/src/testing/terminal.ts

@@ -1,119 +0,0 @@
-import type { ModelProbeState } from "./model-selection"
-
-export const terminalAttr = "data-pty-id"
-
-export type TerminalProbeState = {
-  connected: boolean
-  connects: number
-  rendered: string
-  settled: number
-  focusing: number
-}
-
-type TerminalProbeControl = {
-  disconnect?: VoidFunction
-}
-
-export type E2EWindow = Window & {
-  __opencode_e2e?: {
-    model?: {
-      enabled?: boolean
-      current?: ModelProbeState
-    }
-    prompt?: {
-      enabled?: boolean
-      current?: import("./prompt").PromptProbeState
-      sent?: import("./prompt").PromptSendState
-    }
-    terminal?: {
-      enabled?: boolean
-      terminals?: Record<string, TerminalProbeState>
-      controls?: Record<string, TerminalProbeControl>
-    }
-  }
-}
-
-const seed = (): TerminalProbeState => ({
-  connected: false,
-  connects: 0,
-  rendered: "",
-  settled: 0,
-  focusing: 0,
-})
-
-const root = () => {
-  if (typeof window === "undefined") return
-  const state = (window as E2EWindow).__opencode_e2e?.terminal
-  if (!state?.enabled) return
-  return state
-}
-
-const terms = () => {
-  const state = root()
-  if (!state) return
-  state.terminals ??= {}
-  return state.terminals
-}
-
-const controls = () => {
-  const state = root()
-  if (!state) return
-  state.controls ??= {}
-  return state.controls
-}
-
-export const terminalProbe = (id: string) => {
-  const set = (next: Partial<TerminalProbeState>) => {
-    const state = terms()
-    if (!state) return
-    state[id] = { ...(state[id] ?? seed()), ...next }
-  }
-
-  return {
-    init() {
-      set(seed())
-    },
-    connect() {
-      const state = terms()
-      if (!state) return
-      const prev = state[id] ?? seed()
-      state[id] = {
-        ...prev,
-        connected: true,
-        connects: prev.connects + 1,
-      }
-    },
-    render(data: string) {
-      const state = terms()
-      if (!state) return
-      const prev = state[id] ?? seed()
-      state[id] = { ...prev, rendered: prev.rendered + data }
-    },
-    settle() {
-      const state = terms()
-      if (!state) return
-      const prev = state[id] ?? seed()
-      state[id] = { ...prev, settled: prev.settled + 1 }
-    },
-    focus(count: number) {
-      set({ focusing: Math.max(0, count) })
-    },
-    step() {
-      const state = terms()
-      if (!state) return
-      const prev = state[id] ?? seed()
-      state[id] = { ...prev, focusing: Math.max(0, prev.focusing - 1) }
-    },
-    control(next: Partial<TerminalProbeControl>) {
-      const state = controls()
-      if (!state) return
-      state[id] = { ...(state[id] ?? {}), ...next }
-    },
-    drop() {
-      const state = terms()
-      if (state) delete state[id]
-      const control = controls()
-      if (control) delete control[id]
-    },
-  }
-}

+ 0 - 66
packages/app/test/e2e/mock.test.ts

@@ -1,66 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { bodyText, inputMatch, promptMatch } from "../../e2e/prompt/mock"
-
-function hit(body: Record<string, unknown>) {
-  return { body }
-}
-
-describe("promptMatch", () => {
-  test("matches token in serialized body", () => {
-    const match = promptMatch("hello")
-    expect(match(hit({ messages: [{ role: "user", content: "say hello" }] }))).toBe(true)
-    expect(match(hit({ messages: [{ role: "user", content: "say goodbye" }] }))).toBe(false)
-  })
-})
-
-describe("inputMatch", () => {
-  test("matches exact tool input in chat completions body", () => {
-    const input = { questions: [{ header: "Need input", question: "Pick one" }] }
-    const match = inputMatch(input)
-
-    // The seed prompt embeds JSON.stringify(input) in the user message
-    const prompt = `Use this JSON input: ${JSON.stringify(input)}`
-    const body = { messages: [{ role: "user", content: prompt }] }
-    expect(match(hit(body))).toBe(true)
-  })
-
-  test("matches exact tool input in responses API body", () => {
-    const input = { questions: [{ header: "Need input", question: "Pick one" }] }
-    const match = inputMatch(input)
-
-    const prompt = `Use this JSON input: ${JSON.stringify(input)}`
-    const body = { model: "test", input: [{ role: "user", content: [{ type: "input_text", text: prompt }] }] }
-    expect(match(hit(body))).toBe(true)
-  })
-
-  test("matches patchText with newlines", () => {
-    const patchText = "*** Begin Patch\n*** Add File: test.txt\n+line1\n*** End Patch"
-    const match = inputMatch({ patchText })
-
-    const prompt = `Use this JSON input: ${JSON.stringify({ patchText })}`
-    const body = { messages: [{ role: "user", content: prompt }] }
-    expect(match(hit(body))).toBe(true)
-
-    // Also works in responses API format
-    const respBody = { model: "test", input: [{ role: "user", content: [{ type: "input_text", text: prompt }] }] }
-    expect(match(hit(respBody))).toBe(true)
-  })
-
-  test("does not match unrelated requests", () => {
-    const input = { questions: [{ header: "Need input" }] }
-    const match = inputMatch(input)
-
-    expect(match(hit({ messages: [{ role: "user", content: "hello" }] }))).toBe(false)
-    expect(match(hit({ model: "test", input: [] }))).toBe(false)
-  })
-
-  test("does not match partial input", () => {
-    const input = { questions: [{ header: "Need input", question: "Pick one" }] }
-    const match = inputMatch(input)
-
-    // Only header, missing question
-    const partial = `Use this JSON input: ${JSON.stringify({ questions: [{ header: "Need input" }] })}`
-    const body = { messages: [{ role: "user", content: partial }] }
-    expect(match(hit(body))).toBe(false)
-  })
-})

+ 0 - 27
packages/app/test/e2e/no-real-llm.test.ts

@@ -1,27 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import path from "node:path"
-import { fileURLToPath } from "node:url"
-
-const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../e2e")
-
-function hasPrompt(src: string) {
-  if (!src.includes("withProject(")) return false
-  if (src.includes("withNoReplyPrompt(")) return false
-  if (src.includes("session.promptAsync({") && !src.includes("noReply: true")) return true
-  if (!src.includes("promptSelector")) return false
-  return src.includes('keyboard.press("Enter")') || src.includes('prompt.press("Enter")')
-}
-
-describe("e2e llm guard", () => {
-  test("withProject specs do not submit prompt replies", async () => {
-    const bad: string[] = []
-
-    for await (const file of new Bun.Glob("**/*.spec.ts").scan({ cwd: dir, absolute: true })) {
-      const src = await Bun.file(file).text()
-      if (!hasPrompt(src)) continue
-      bad.push(path.relative(dir, file))
-    }
-
-    expect(bad).toEqual([])
-  })
-})

+ 0 - 76
packages/opencode/script/seed-e2e.ts

@@ -1,76 +0,0 @@
-import { AppRuntime } from "@/effect/app-runtime"
-
-const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
-const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
-const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
-const model = process.env.OPENCODE_E2E_MODEL ?? "opencode/gpt-5-nano"
-const parts = model.split("/")
-const providerID = parts[0] ?? "opencode"
-const modelID = parts[1] ?? "gpt-5-nano"
-const now = Date.now()
-
-const seed = async () => {
-  const { Instance } = await import("../src/project/instance")
-  const { InstanceBootstrap } = await import("../src/project/bootstrap")
-  const { Config } = await import("../src/config/config")
-  const { Session } = await import("../src/session")
-  const { MessageID, PartID } = await import("../src/session/schema")
-  const { Project } = await import("../src/project/project")
-  const { ModelID, ProviderID } = await import("../src/provider/schema")
-  const { ToolRegistry } = await import("../src/tool/registry")
-  const { Effect } = await import("effect")
-
-  try {
-    await Instance.provide({
-      directory: dir,
-      init: () => AppRuntime.runPromise(InstanceBootstrap),
-      fn: async () => {
-        await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.waitForDependencies()))
-        await AppRuntime.runPromise(
-          Effect.gen(function* () {
-            const registry = yield* ToolRegistry.Service
-            yield* registry.ids()
-          }),
-        )
-
-        await AppRuntime.runPromise(
-          Effect.gen(function* () {
-            const session = yield* Session.Service
-            const result = yield* session.create({ title })
-            const messageID = MessageID.ascending()
-            const partID = PartID.ascending()
-            const message = {
-              id: messageID,
-              sessionID: result.id,
-              role: "user" as const,
-              time: { created: now },
-              agent: "build",
-              model: {
-                providerID: ProviderID.make(providerID),
-                modelID: ModelID.make(modelID),
-              },
-            }
-            const part = {
-              id: partID,
-              sessionID: result.id,
-              messageID,
-              type: "text" as const,
-              text,
-              time: { start: now },
-            }
-            yield* session.updateMessage(message)
-            yield* session.updatePart(part)
-          }),
-        )
-        await AppRuntime.runPromise(
-          Project.Service.use((svc) => svc.update({ projectID: Instance.project.id, name: "E2E Project" })),
-        )
-      },
-    })
-  } finally {
-    await Instance.disposeAll().catch(() => {})
-    await AppRuntime.dispose().catch(() => {})
-  }
-}
-
-await seed()

+ 0 - 15
packages/opencode/src/provider/provider.ts

@@ -1529,21 +1529,6 @@ export namespace Provider {
         if (s.models.has(key)) return s.models.get(key)!
 
         return yield* Effect.promise(async () => {
-          const url = (() => {
-            const item = envs["OPENCODE_E2E_LLM_URL"]
-            if (typeof item !== "string" || item === "") return
-            return item
-          })()
-          if (url) {
-            const language = createOpenAICompatible({
-              name: model.providerID,
-              apiKey: "test-key",
-              baseURL: url,
-            }).chatModel(model.api.id)
-            s.models.set(key, language)
-            return language
-          }
-
           const provider = s.providers[model.providerID]
           const sdk = await resolveSDK(model, s, envs)
 

+ 1 - 7
packages/opencode/src/tool/registry.ts

@@ -36,7 +36,6 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { Ripgrep } from "../file/ripgrep"
 import { Format } from "../format"
 import { InstanceState } from "@/effect/instance-state"
-import { Env } from "../env"
 import { Question } from "../question"
 import { Todo } from "../session/todo"
 import { LSP } from "../lsp"
@@ -78,7 +77,6 @@ export namespace ToolRegistry {
     Service,
     never,
     | Config.Service
-    | Env.Service
     | Plugin.Service
     | Question.Service
     | Todo.Service
@@ -100,7 +98,6 @@ export namespace ToolRegistry {
     Service,
     Effect.gen(function* () {
       const config = yield* Config.Service
-      const env = yield* Env.Service
       const plugin = yield* Plugin.Service
       const agents = yield* Agent.Service
       const skill = yield* Skill.Service
@@ -274,15 +271,13 @@ export namespace ToolRegistry {
       })
 
       const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
-        const e2e = !!(yield* env.get("OPENCODE_E2E_LLM_URL"))
         const filtered = (yield* all()).filter((tool) => {
           if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) {
             return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
           }
 
           const usePatch =
-            e2e ||
-            (input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4"))
+            input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")
           if (tool.id === ApplyPatchTool.id) return usePatch
           if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch
 
@@ -328,7 +323,6 @@ export namespace ToolRegistry {
   export const defaultLayer = Layer.suspend(() =>
     layer.pipe(
       Layer.provide(Config.defaultLayer),
-      Layer.provide(Env.defaultLayer),
       Layer.provide(Plugin.defaultLayer),
       Layer.provide(Question.defaultLayer),
       Layer.provide(Todo.defaultLayer),