)
@@ -196,7 +194,9 @@ export const SortableWorkspace = (props: {
when={workspaceEditActive()}
fallback={
Date: Sat, 7 Feb 2026 04:43:58 -0600
Subject: [PATCH 057/399] chore: cleanup
---
specs/01-persist-payload-limits.md | 206 --------------
specs/02-cache-eviction.md | 141 ----------
specs/03-request-throttling.md | 145 ----------
specs/04-scroll-spy-optimization.md | 125 ---------
specs/05-modularize-and-dedupe.md | 153 -----------
specs/06-app-i18n-audit.md | 237 ----------------
specs/07-ui-i18n-audit.md | 156 -----------
specs/08-app-e2e-smoke-suite.md | 255 ------------------
specs/09-session-page-decomposition.md | 113 --------
specs/09-session-page-hot-paths.md | 105 --------
specs/10-file-content-eviction-accounting.md | 99 -------
specs/10-layout-page-decomposition.md | 109 --------
specs/11-layout-view-tabs-reactivity.md | 92 -------
specs/11-prompt-input-and-optimistic-state.md | 121 ---------
specs/12-global-sync-domain-split.md | 105 --------
specs/12-session-context-metrics-shared.md | 96 -------
specs/13-file-context-domain-split.md | 111 --------
specs/13-file-tree-fetch-discipline.md | 88 ------
specs/14-comments-aggregation-index.md | 87 ------
specs/14-server-health-and-row-dedupe.md | 108 --------
specs/15-prompt-input-modularization.md | 104 -------
specs/15-runtime-adapter-type-safety.md | 106 --------
specs/16-i18n-hardening-and-parity.md | 107 --------
specs/16-terminal-cache-key-clarity.md | 82 ------
specs/17-unit-test-foundation.md | 101 -------
specs/18-parallel-workstream-map.md | 51 ----
specs/parallel-agent-plan.md | 59 ----
specs/perf-roadmap.md | 196 --------------
28 files changed, 3458 deletions(-)
delete mode 100644 specs/01-persist-payload-limits.md
delete mode 100644 specs/02-cache-eviction.md
delete mode 100644 specs/03-request-throttling.md
delete mode 100644 specs/04-scroll-spy-optimization.md
delete mode 100644 specs/05-modularize-and-dedupe.md
delete mode 100644 specs/06-app-i18n-audit.md
delete mode 100644 specs/07-ui-i18n-audit.md
delete mode 100644 specs/08-app-e2e-smoke-suite.md
delete mode 100644 specs/09-session-page-decomposition.md
delete mode 100644 specs/09-session-page-hot-paths.md
delete mode 100644 specs/10-file-content-eviction-accounting.md
delete mode 100644 specs/10-layout-page-decomposition.md
delete mode 100644 specs/11-layout-view-tabs-reactivity.md
delete mode 100644 specs/11-prompt-input-and-optimistic-state.md
delete mode 100644 specs/12-global-sync-domain-split.md
delete mode 100644 specs/12-session-context-metrics-shared.md
delete mode 100644 specs/13-file-context-domain-split.md
delete mode 100644 specs/13-file-tree-fetch-discipline.md
delete mode 100644 specs/14-comments-aggregation-index.md
delete mode 100644 specs/14-server-health-and-row-dedupe.md
delete mode 100644 specs/15-prompt-input-modularization.md
delete mode 100644 specs/15-runtime-adapter-type-safety.md
delete mode 100644 specs/16-i18n-hardening-and-parity.md
delete mode 100644 specs/16-terminal-cache-key-clarity.md
delete mode 100644 specs/17-unit-test-foundation.md
delete mode 100644 specs/18-parallel-workstream-map.md
delete mode 100644 specs/parallel-agent-plan.md
delete mode 100644 specs/perf-roadmap.md
diff --git a/specs/01-persist-payload-limits.md b/specs/01-persist-payload-limits.md
deleted file mode 100644
index 0e6d422f34ac..000000000000
--- a/specs/01-persist-payload-limits.md
+++ /dev/null
@@ -1,206 +0,0 @@
-## Payload limits
-
-Prevent blocking storage writes and runaway persisted size
-
----
-
-### Summary
-
-Large payloads (base64 images, terminal buffers) are currently persisted inside key-value stores:
-
-- web: `localStorage` (sync, blocks the main thread)
-- desktop: Tauri Store-backed async storage files (still expensive when values are huge)
-
-We’ll introduce size-aware persistence policies plus a dedicated “blob store” for large/binary data (IndexedDB on web; separate files on desktop). Prompt/history state will persist only lightweight references to blobs and load them on demand.
-
----
-
-### Goals
-
-- Stop persisting image `dataUrl` blobs inside web `localStorage`
-- Stop persisting image `dataUrl` blobs inside desktop store `.dat` files
-- Store image payloads out-of-band (blob store) and load lazily when needed (e.g. when restoring a history item)
-- Prevent terminal buffer persistence from exceeding safe size limits
-- Keep persistence behavior predictable across web (sync) and desktop (async)
-- Provide escape hatches via flags and per-key size caps
-
----
-
-### Non-goals
-
-- Cross-device sync of images or terminal buffers
-- Lossless persistence of full terminal scrollback on web
-- Perfect blob deduplication or a complex reference-counting system on day one
-
----
-
-### Current state
-
-- `packages/app/src/utils/persist.ts` uses `localStorage` (sync) on web and async storage only on desktop.
-- Desktop storage is implemented via `@tauri-apps/plugin-store` and writes to named `.dat` files (see `packages/desktop/src/index.tsx`). Large values bloat these files and increase flush costs.
-- Prompt history persists under `Persist.global("prompt-history")` (`packages/app/src/components/prompt-input.tsx`) and can include image parts (`dataUrl`).
-- Prompt draft persistence uses `packages/app/src/context/prompt.tsx` and can also include image parts (`dataUrl`).
-- Terminal buffer is serialized in `packages/app/src/components/terminal.tsx` and persisted in `packages/app/src/context/terminal.tsx`.
-
----
-
-### Proposed approach
-
-#### 1) Add per-key persistence policies (KV store guardrails)
-
-In `packages/app/src/utils/persist.ts`, add policy hooks for each persisted key:
-
-- `warnBytes` (soft warning threshold)
-- `maxBytes` (hard cap)
-- `transformIn` / `transformOut` for lossy persistence (e.g. strip or refactor fields)
-- `onOversize` strategy: `drop`, `truncate`, or `migrateToBlobRef`
-
-This protects both:
-
-- web (`localStorage` is sync)
-- desktop (async, but still expensive to store/flush giant values)
-
-#### 2) Add a dedicated blob store for large data
-
-Introduce a small blob-store abstraction used by the app layer:
-
-- web backend: IndexedDB (store `Blob` values keyed by `id`)
-- desktop backend: filesystem directory under the app data directory (store one file per blob)
-
-Store _references_ to blobs inside the persisted JSON instead of the blob contents.
-
-#### 3) Persist image parts as references (not base64 payloads)
-
-Update the prompt image model so the in-memory shape can still use a `dataUrl` for UI, but the persisted representation is reference-based.
-
-Suggested approach:
-
-- Keep `ImageAttachmentPart` with:
- - required: `id`, `filename`, `mime`
- - optional/ephemeral: `dataUrl?: string`
- - new: `blobID?: string` (or `ref: string`)
-
-Persistence rules:
-
-- When writing persisted prompt/history state:
- - ensure each image part is stored in blob store (`blobID`)
- - persist only metadata + `blobID` (no `dataUrl`)
-- When reading persisted prompt/history state:
- - do not eagerly load blob payloads
- - hydrate `dataUrl` only when needed:
- - when applying a history entry into the editor
- - before submission (ensure all image parts have usable `dataUrl`)
- - when rendering an attachment preview, if required
-
----
-
-### Phased implementation steps
-
-1. Add guardrails in `persist.ts`
-
-- Implement size estimation in `packages/app/src/utils/persist.ts` using `TextEncoder` byte length on JSON strings.
-- Add a policy registry keyed by persist name (e.g. `"prompt-history"`, `"prompt"`, `"terminal"`).
-- Add a feature flag (e.g. `persist.payloadLimits`) to enable enforcement gradually.
-
-2. Add blob-store abstraction + platform hooks
-
-- Add a new app-level module (e.g. `packages/app/src/utils/blob.ts`) defining:
- - `put(id, bytes|Blob)`
- - `get(id)`
- - `remove(id)`
-- Extend the `Platform` interface (`packages/app/src/context/platform.tsx`) with optional blob methods, or provide a default web implementation and override on desktop:
- - web: implement via IndexedDB
- - desktop: implement via filesystem files (requires adding a Tauri fs plugin or `invoke` wrappers)
-
-3. Update prompt history + prompt draft persistence to use blob refs
-
-- Update prompt/history serialization paths to ensure image parts are stored as blob refs:
- - Prompt history: `packages/app/src/components/prompt-input.tsx`
- - Prompt draft: `packages/app/src/context/prompt.tsx`
-- Ensure “apply history prompt” hydrates image blobs only when applying the prompt (not during background load).
-
-4. One-time migration for existing persisted base64 images
-
-- On read, detect legacy persisted image parts that include `dataUrl`.
-- If a `dataUrl` is found:
- - write it into the blob store (convert dataUrl → bytes)
- - replace persisted payload with `{ blobID, filename, mime, id }` only
- - re-save the reduced version
-- If migration fails (missing permissions, quota, etc.), fall back to:
- - keep the prompt entry but drop the image payload and mark as unavailable
-
-5. Fix terminal persistence (bounded snapshot)
-
-- In `packages/app/src/context/terminal.tsx`, persist only:
- - last `maxLines` and/or
- - last `maxBytes` of combined text
-- In `packages/app/src/components/terminal.tsx`, keep the full in-memory buffer unchanged.
-
-6. Add basic blob lifecycle cleanup
- To avoid “blob directory grows forever”, add one of:
-
-- TTL-based cleanup: store `lastAccessed` per blob and delete blobs older than N days
-- Reference scan cleanup: periodically scan prompt-history + prompt drafts, build a set of referenced `blobID`s, and delete unreferenced blobs
-
-Start with TTL-based cleanup (simpler, fewer cross-store dependencies), then consider scan-based cleanup if needed.
-
----
-
-### Data migration / backward compatibility
-
-- KV store data:
- - policies should be tolerant of missing fields (e.g. `dataUrl` missing)
-- Image parts:
- - treat missing `dataUrl` as “not hydrated yet”
- - treat missing `blobID` (legacy) as “not persisted” or “needs migration”
-- Desktop:
- - blob files should be namespaced (e.g. `opencode/blobs/
`) to avoid collisions
-
----
-
-### Risk + mitigations
-
-- Risk: blob store is unavailable (IndexedDB disabled, desktop fs permissions).
- - Mitigation: keep base state functional; persist prompts without image payloads and show a clear placeholder.
-- Risk: lazy hydration introduces edge cases when submitting.
- - Mitigation: add a pre-submit “ensure images hydrated” step; if hydration fails, block submission with a clear error or submit without images.
-- Risk: dataUrl→bytes conversion cost during migration.
- - Mitigation: migrate incrementally (only when reading an entry) and/or use `requestIdleCallback` on web.
-- Risk: blob cleanup deletes blobs still needed.
- - Mitigation: TTL default should be conservative; scan-based cleanup should only delete blobs unreferenced by current persisted state.
-
----
-
-### Validation plan
-
-- Unit-level:
- - size estimation + policy enforcement in `persist.ts`
- - blob store put/get/remove round trips (web + desktop backends)
-- Manual scenarios:
- - attach multiple images, reload, and confirm:
- - KV store files do not balloon
- - images can be restored when selecting history items
- - open terminal with large output and confirm reload restores bounded snapshot quickly
- - confirm prompt draft persistence still works in `packages/app/src/context/prompt.tsx`
-
----
-
-### Rollout plan
-
-- Phase 1: ship with `persist.payloadLimits` off; log oversize detections in dev.
-- Phase 2: enable image blob refs behind `persist.imageBlobs` (web + desktop).
-- Phase 3: enable terminal truncation and enforce hard caps for known hot keys.
-- Phase 4: enable blob cleanup behind `persist.blobGc` (TTL first).
-- Provide quick kill switches by disabling each flag independently.
-
----
-
-### Open questions
-
-- What should the canonical persisted image schema be (`blobID` field name, placeholder shape, etc.)?
-- Desktop implementation detail:
- - add `@tauri-apps/plugin-fs` vs custom `invoke()` commands for blob read/write?
- - where should blob files live (appDataDir) and what retention policy is acceptable?
-- Web implementation detail:
- - do we store `Blob` directly in IndexedDB, or store base64 strings?
-- Should prompt-history images be retained indefinitely, or only for the last `MAX_HISTORY` entries?
diff --git a/specs/02-cache-eviction.md b/specs/02-cache-eviction.md
deleted file mode 100644
index 08b1c34857cb..000000000000
--- a/specs/02-cache-eviction.md
+++ /dev/null
@@ -1,141 +0,0 @@
-## Cache eviction
-
-Add explicit bounds for long-lived in-memory state
-
----
-
-### Summary
-
-Several in-memory caches grow without limits during long sessions. We’ll introduce explicit eviction (LRU + TTL + size caps) for sessions/messages/file contents and global per-directory sync stores.
-
----
-
-### Goals
-
-- Prevent unbounded memory growth from caches that survive navigation
-- Add consistent eviction primitives shared across contexts
-- Keep UI responsive under heavy usage (many sessions, large files)
-
----
-
-### Non-goals
-
-- Perfect cache hit rates or prefetch strategies
-- Changing server APIs or adding background jobs
-- Persisting caches for offline use
-
----
-
-### Current state
-
-- Global sync uses per-directory child stores without eviction in `packages/app/src/context/global-sync.tsx`.
-- File contents cached in `packages/app/src/context/file.tsx` with no cap.
-- Session-heavy pages include `packages/app/src/pages/session.tsx` and `packages/app/src/pages/layout.tsx`.
-
----
-
-### Proposed approach
-
-- Introduce a shared cache utility that supports:
- - `maxEntries`, `maxBytes` (approx), and `ttlMs`
- - LRU ordering with explicit `touch(key)` on access
- - deterministic `evict()` and `clear()` APIs
-- Apply the utility to:
- - global-sync per-directory child stores (cap number of directories kept “hot”)
- - file contents cache (cap by entries + bytes, with TTL)
- - session/message caches (cap by session count, and optionally message count)
-- Add feature flags per cache domain to allow partial rollout (e.g. `cache.eviction.files`).
-
----
-
-### Phased implementation steps
-
-1. Add a generic cache helper
-
-- Create `packages/app/src/utils/cache.ts` with a small, dependency-free LRU+TTL.
-- Keep it framework-agnostic and usable from Solid contexts.
-
-Sketch:
-
-```ts
-type CacheOpts = {
- maxEntries: number
- ttlMs?: number
- maxBytes?: number
- sizeOf?: (value: unknown) => number
-}
-
-function createLruCache(opts: CacheOpts) {
- // get, set, delete, clear, evictExpired, stats
-}
-```
-
-2. Apply eviction to file contents
-
-- In `packages/app/src/context/file.tsx`:
- - wrap the existing file-content map in the LRU helper
- - approximate size via `TextEncoder` length of content strings
- - evict on `set` and periodically via `requestIdleCallback` when available
-- Add a small TTL (e.g. 10–30 minutes) to discard stale contents.
-
-3. Apply eviction to global-sync child stores
-
-- In `packages/app/src/context/global-sync.tsx`:
- - track child stores by directory key in an LRU with `maxEntries`
- - call a `dispose()` hook on eviction to release subscriptions and listeners
-- Ensure “currently active directory” is always `touch()`’d to avoid surprise evictions.
-
-4. Apply eviction to session/message caches
-
-- Identify the session/message caching touchpoints used by `packages/app/src/pages/session.tsx`.
-- Add caps that reflect UI needs (e.g. last 10–20 sessions kept, last N messages per session if cached).
-
-5. Add developer tooling
-
-- Add a debug-only stats readout (console or dev panel) for cache sizes and eviction counts.
-- Add a one-click “clear caches” action for troubleshooting.
-
----
-
-### Data migration / backward compatibility
-
-- No persisted schema changes are required since this targets in-memory caches.
-- If any cache is currently mirrored into persistence, keep keys stable and only change in-memory retention.
-
----
-
-### Risk + mitigations
-
-- Risk: evicting content still needed causes extra refetches and flicker.
- - Mitigation: always pin “active” entities and evict least-recently-used first.
-- Risk: disposing global-sync child stores could leak listeners if not cleaned up correctly.
- - Mitigation: require an explicit `dispose()` contract and add dev assertions for listener counts.
-- Risk: approximate byte sizing is imprecise.
- - Mitigation: combine entry caps with byte caps and keep thresholds conservative.
-
----
-
-### Validation plan
-
-- Add tests for `createLruCache` covering TTL expiry, LRU ordering, and eviction triggers.
-- Manual scenarios:
- - open many files and confirm memory stabilizes and UI remains responsive
- - switch across many directories and confirm global-sync does not continuously grow
- - long session navigation loop and confirm caches plateau
-
----
-
-### Rollout plan
-
-- Land cache utility first with flags default off.
-- Enable file cache eviction first (lowest behavioral risk).
-- Enable global-sync eviction next with conservative caps and strong logging in dev.
-- Enable session/message eviction last after observing real usage patterns.
-
----
-
-### Open questions
-
-- What are the current session/message cache structures and their ownership boundaries?
-- Which child stores in `global-sync.tsx` have resources that must be disposed explicitly?
-- What caps are acceptable for typical workflows (files open, directories visited, sessions viewed)?
diff --git a/specs/03-request-throttling.md b/specs/03-request-throttling.md
deleted file mode 100644
index 0cd8c7fb0971..000000000000
--- a/specs/03-request-throttling.md
+++ /dev/null
@@ -1,145 +0,0 @@
-## Request throttling
-
-Debounce and cancel high-frequency server calls
-
----
-
-### Summary
-
-Some user interactions trigger bursts of server requests that can overlap and return out of order. We’ll debounce frequent triggers and cancel in-flight requests (or ignore stale results) for file search and LSP refresh.
-
----
-
-### Goals
-
-- Reduce redundant calls from file search and LSP refresh
-- Prevent stale responses from overwriting newer UI state
-- Preserve responsive typing and scrolling during high activity
-
----
-
-### Non-goals
-
-- Changing server-side behavior or adding new endpoints
-- Implementing global request queues for all SDK calls
-- Persisting search results across reloads
-
----
-
-### Current state
-
-- File search calls `sdk.client.find.files` via `files.searchFilesAndDirectories`.
-- LSP refresh is triggered frequently (exact call sites vary, but the refresh behavior is high-frequency).
-- Large UI modules involved include `packages/app/src/pages/layout.tsx` and `packages/app/src/components/prompt-input.tsx`.
-
----
-
-### Proposed approach
-
-- Add a small request coordinator utility:
- - debounced triggering (leading/trailing configurable)
- - cancellation via `AbortController` when supported
- - stale-result protection via monotonic request ids when abort is not supported
-- Integrate coordinator into:
- - `files.searchFilesAndDirectories` (wrap `sdk.client.find.files`)
- - LSP refresh call path (wrap refresh invocation and ensure only latest applies)
-
----
-
-### Phased implementation steps
-
-1. Add a debounced + cancellable helper
-
-- Create `packages/app/src/utils/requests.ts` with:
- - `createDebouncedAsync(fn, delayMs)`
- - `createLatestOnlyAsync(fn)` that drops stale responses
-- Prefer explicit, readable primitives over a single complex abstraction.
-
-Sketch:
-
-```ts
-function createLatestOnlyAsync(
- fn: (args: { input: TArgs; signal?: AbortSignal }) => Promise,
-) {
- let id = 0
- let controller: AbortController | undefined
-
- return async (...input: TArgs) => {
- id += 1
- const current = id
- controller?.abort()
- controller = new AbortController()
-
- const result = await fn({ input, signal: controller.signal })
- if (current !== id) return
- return result
- }
-}
-```
-
-2. Apply to file search
-
-- Update `files.searchFilesAndDirectories` to:
- - debounce input changes (e.g. 150–300 ms)
- - abort prior request when a new query begins
- - ignore results if they are stale
-- Ensure “empty query” is handled locally without calling the server.
-
-3. Apply to LSP refresh
-
-- Identify the refresh trigger points used during typing and file switching.
-- Add:
- - debounce for rapid triggers (e.g. 250–500 ms)
- - cancellation for in-flight refresh if supported
- - last-write-wins behavior for applying diagnostics/results
-
-4. Add feature flags and metrics
-
-- Add flags:
- - `requests.debounce.fileSearch`
- - `requests.latestOnly.lspRefresh`
-- Add simple dev-only counters for “requests started / aborted / applied”.
-
----
-
-### Data migration / backward compatibility
-
-- No persisted data changes.
-- Behavior is compatible as long as UI state updates only when the “latest” request resolves.
-
----
-
-### Risk + mitigations
-
-- Risk: aggressive debounce makes UI feel laggy.
- - Mitigation: keep delays small and tune separately for search vs refresh.
-- Risk: aborting requests may surface as errors in logs.
- - Mitigation: treat `AbortError` as expected and do not log it as a failure.
-- Risk: SDK method may not accept `AbortSignal`.
- - Mitigation: use request-id stale protection even without true cancellation.
-
----
-
-### Validation plan
-
-- Manual scenarios:
- - type quickly in file search and confirm requests collapse and results stay correct
- - trigger LSP refresh repeatedly and confirm diagnostics do not flicker backward
-- Add a small unit test for latest-only behavior (stale results are ignored).
-
----
-
-### Rollout plan
-
-- Ship helpers behind flags default off.
-- Enable file search debounce first (high impact, easy to validate).
-- Enable LSP latest-only next, then add cancellation if SDK supports signals.
-- Keep a quick rollback by disabling the flags.
-
----
-
-### Open questions
-
-- Does `sdk.client.find.files` accept an abort signal today, or do we need stale-result protection only?
-- Where is LSP refresh initiated, and does it have a single chokepoint we can wrap?
-- What debounce values feel best for common repos and slower machines?
diff --git a/specs/04-scroll-spy-optimization.md b/specs/04-scroll-spy-optimization.md
deleted file mode 100644
index 19f5a76ff88b..000000000000
--- a/specs/04-scroll-spy-optimization.md
+++ /dev/null
@@ -1,125 +0,0 @@
-## Spy acceleration
-
-Replace O(N) DOM scans in session view
-
----
-
-### Summary
-
-The session scroll-spy currently scans the DOM with `querySelectorAll` and walks message nodes, which becomes expensive as message count grows. We’ll replace the scan with an observer-based or indexed approach that scales smoothly.
-
----
-
-### Goals
-
-- Remove repeated full DOM scans during scroll in the session view
-- Keep “current message” tracking accurate during streaming and layout shifts
-- Provide a safe fallback path for older browsers and edge cases
-
----
-
-### Non-goals
-
-- Visual redesign of the session page
-- Changing message rendering structure or IDs
-- Perfect accuracy during extreme layout thrash
-
----
-
-### Current state
-
-- `packages/app/src/pages/session.tsx` uses `querySelectorAll('[data-message-id]')` for scroll-spy.
-- The page is large and handles many responsibilities, increasing the chance of perf regressions.
-
----
-
-### Proposed approach
-
-Implement a two-tier scroll-spy:
-
-- Primary: `IntersectionObserver` to track which message elements are visible, updated incrementally.
-- Secondary: binary search over precomputed offsets when observer is unavailable or insufficient.
-- Use `ResizeObserver` (and a lightweight “dirty” flag) to refresh offsets only when layout changes.
-
----
-
-### Phased implementation steps
-
-1. Extract a dedicated scroll-spy module
-
-- Create `packages/app/src/pages/session/scroll-spy.ts` (or similar) that exposes:
- - `register(el, id)` and `unregister(id)`
- - `getActiveId()` signal/store
-- Keep DOM operations centralized and easy to profile.
-
-2. Add IntersectionObserver tracking
-
-- Observe each `[data-message-id]` element once, on mount.
-- Maintain a small map of `id -> intersectionRatio` (or visible boolean).
-- Pick the active id by:
- - highest intersection ratio, then
- - nearest to top of viewport as a tiebreaker
-
-3. Add binary search fallback
-
-- Maintain an ordered list of `{ id, top }` positions.
-- On scroll (throttled via `requestAnimationFrame`), compute target Y and binary search to find nearest message.
-- Refresh the positions list on:
- - message list mutations (new messages)
- - container resize events (ResizeObserver)
- - explicit “layout changed” events after streaming completes
-
-4. Remove `querySelectorAll` hot path
-
-- Keep a one-time initial query only as a bridge during rollout, then remove it.
-- Ensure newly rendered messages are registered via refs rather than scanning the whole DOM.
-
-5. Add a feature flag and fallback
-
-- Add `session.scrollSpyOptimized` flag.
-- If observer setup fails, fall back to the existing scan behavior temporarily.
-
----
-
-### Data migration / backward compatibility
-
-- No persisted data changes.
-- IDs remain sourced from existing `data-message-id` attributes.
-
----
-
-### Risk + mitigations
-
-- Risk: observer ordering differs from previous “active message” logic.
- - Mitigation: keep selection rules simple, document them, and add a small tolerance for tie cases.
-- Risk: layout shifts cause incorrect offset indexing.
- - Mitigation: refresh offsets with ResizeObserver and after message streaming batches.
-- Risk: performance regressions from observing too many nodes.
- - Mitigation: prefer one observer instance and avoid per-node observers.
-
----
-
-### Validation plan
-
-- Manual scenarios:
- - very long sessions (hundreds of messages) and continuous scrolling
- - streaming responses that append content and change heights
- - resizing the window and toggling side panels
-- Add a dev-only profiler hook to log time spent in scroll-spy updates per second.
-
----
-
-### Rollout plan
-
-- Land extracted module first, still using the old scan internally.
-- Add observer implementation behind `session.scrollSpyOptimized` off by default.
-- Enable flag for internal testing, then default on after stability.
-- Keep fallback code for one release cycle, then remove scan path.
-
----
-
-### Open questions
-
-- What is the exact definition of “active” used elsewhere (URL hash, sidebar highlight, breadcrumb)?
-- Are messages virtualized today, or are all DOM nodes mounted at once?
-- Which container is the scroll root (window vs an inner div), and does it change by layout mode?
diff --git a/specs/05-modularize-and-dedupe.md b/specs/05-modularize-and-dedupe.md
deleted file mode 100644
index 85e83f312597..000000000000
--- a/specs/05-modularize-and-dedupe.md
+++ /dev/null
@@ -1,153 +0,0 @@
-## Component modularity
-
-Split mega-components and dedupe scoped caches
-
----
-
-### Summary
-
-Several large UI files combine rendering, state, persistence, and caching patterns, including repeated “scoped session cache” infrastructure. We’ll extract reusable primitives and break large components into smaller units without changing user-facing behavior.
-
----
-
-### Goals
-
-- Reduce complexity in:
- - `packages/app/src/pages/session.tsx`
- - `packages/app/src/pages/layout.tsx`
- - `packages/app/src/components/prompt-input.tsx`
-- Deduplicate “scoped session cache” logic into a shared utility
-- Make performance fixes (eviction, throttling) easier to implement safely
-
----
-
-### Non-goals
-
-- Large redesign of routing or page structure
-- Moving to a different state management approach
-- Rewriting all contexts in one pass
-
----
-
-### Current state
-
-- Session page is large and mixes concerns (`packages/app/src/pages/session.tsx`).
-- Layout is also large and likely coordinates multiple global concerns (`packages/app/src/pages/layout.tsx`).
-- Prompt input is large and includes persistence and interaction logic (`packages/app/src/components/prompt-input.tsx`).
-- Similar “scoped cache” patterns appear in multiple places (session-bound maps, per-session stores, ad hoc memoization).
-
----
-
-### Proposed approach
-
-- Introduce a shared “scoped store” utility to standardize session-bound caches:
- - keyed by `sessionId`
- - automatic cleanup via TTL or explicit `dispose(sessionId)`
- - optional LRU cap for many sessions
-- Break mega-components into focused modules with clear boundaries:
- - “view” components (pure rendering)
- - “controller” hooks (state + effects)
- - “services” (SDK calls, persistence adapters)
-
----
-
-### Phased implementation steps
-
-1. Inventory and name the repeated pattern
-
-- Identify the repeated “scoped session cache” usage sites in:
- - `packages/app/src/pages/session.tsx`
- - `packages/app/src/pages/layout.tsx`
- - `packages/app/src/components/prompt-input.tsx`
-- Write down the common operations (get-or-create, clear-on-session-change, dispose).
-
-2. Add a shared scoped-cache utility
-
-- Create `packages/app/src/utils/scoped-cache.ts`:
- - `createScopedCache(createValue, opts)` returning `get(key)`, `peek(key)`, `delete(key)`, `clear()`
- - optional TTL + LRU caps to avoid leak-by-design
-- Keep the API tiny and explicit so call sites stay readable.
-
-Sketch:
-
-```ts
-type ScopedOpts = { maxEntries?: number; ttlMs?: number }
-
-function createScopedCache(createValue: (key: string) => T, opts: ScopedOpts) {
- // store + eviction + dispose hooks
-}
-```
-
-3. Extract session page submodules
-
-- Split `packages/app/src/pages/session.tsx` into:
- - `session/view.tsx` for rendering layout
- - `session/messages.tsx` for message list
- - `session/composer.tsx` for input wiring
- - `session/scroll-spy.ts` for active message tracking
-- Keep exports stable so routing code changes minimally.
-
-4. Extract layout coordination logic
-
-- Split `packages/app/src/pages/layout.tsx` into:
- - shell layout view
- - navigation/controller logic
- - global keyboard shortcuts (if present)
-- Ensure each extracted piece has a narrow prop surface and no hidden globals.
-
-5. Extract prompt-input state machine
-
-- Split `packages/app/src/components/prompt-input.tsx` into:
- - `usePromptComposer()` hook (draft, submission, attachments)
- - presentational input component
-- Route persistence through existing `packages/app/src/context/prompt.tsx`, but isolate wiring code.
-
-6. Replace ad hoc scoped caches with the shared utility
-
-- Swap one call site at a time and keep behavior identical.
-- Add a flag `scopedCache.shared` to fall back to the old implementation if needed.
-
----
-
-### Data migration / backward compatibility
-
-- No persisted schema changes are required by modularization alone.
-- If any cache keys change due to refactors, keep a compatibility reader for one release cycle.
-
----
-
-### Risk + mitigations
-
-- Risk: refactors cause subtle behavior changes (focus, keyboard shortcuts, scroll position).
- - Mitigation: extract without logic changes first, then improve behavior in later diffs.
-- Risk: new shared cache introduces lifecycle bugs.
- - Mitigation: require explicit cleanup hooks and add dev assertions for retained keys.
-- Risk: increased file count makes navigation harder temporarily.
- - Mitigation: use consistent naming and keep the folder structure shallow.
-
----
-
-### Validation plan
-
-- Manual regression checklist:
- - compose, attach images, submit, and reload draft
- - navigate between sessions and confirm caches don’t bleed across IDs
- - verify terminal, file search, and scroll-spy still behave normally
-- Add lightweight unit tests for `createScopedCache` eviction and disposal behavior.
-
----
-
-### Rollout plan
-
-- Phase 1: introduce `createScopedCache` unused, then adopt in one low-risk area.
-- Phase 2: extract session submodules with no behavior changes.
-- Phase 3: flip remaining scoped caches to shared utility behind `scopedCache.shared`.
-- Phase 4: remove old duplicated implementations after confidence.
-
----
-
-### Open questions
-
-- Where exactly is “scoped session cache” duplicated today, and what are the differing lifecycle rules?
-- Which extracted modules must remain synchronous for Solid reactivity to behave correctly?
-- Are there implicit dependencies in the large files (module-level state) that need special handling?
diff --git a/specs/06-app-i18n-audit.md b/specs/06-app-i18n-audit.md
deleted file mode 100644
index 0362ec212a17..000000000000
--- a/specs/06-app-i18n-audit.md
+++ /dev/null
@@ -1,237 +0,0 @@
-# App i18n Audit (Remaining Work)
-
-Scope: `packages/app/`
-
-Date: 2026-01-20
-
-This report documents the remaining user-facing strings in `packages/app/src` that are still hardcoded (not routed through `useLanguage().t(...)` / translation keys), plus i18n-adjacent issues like locale-sensitive formatting.
-
-## Current State
-
-- The app uses `useLanguage().t("...")` with dictionaries in `packages/app/src/i18n/en.ts` and `packages/app/src/i18n/zh.ts`.
-- Recent progress (already translated): `packages/app/src/pages/home.tsx`, `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/session.tsx`, `packages/app/src/components/prompt-input.tsx`, `packages/app/src/components/dialog-connect-provider.tsx`, `packages/app/src/components/session/session-header.tsx`, `packages/app/src/pages/error.tsx`, `packages/app/src/components/session/session-new-view.tsx`, `packages/app/src/components/session-context-usage.tsx`, `packages/app/src/components/session/session-context-tab.tsx`, `packages/app/src/components/session-lsp-indicator.tsx`, `packages/app/src/components/session/session-sortable-tab.tsx`, `packages/app/src/components/titlebar.tsx`, `packages/app/src/components/dialog-select-model.tsx`, `packages/app/src/context/notification.tsx`, `packages/app/src/context/global-sync.tsx`, `packages/app/src/context/file.tsx`, `packages/app/src/context/local.tsx`, `packages/app/src/utils/prompt.ts`, `packages/app/src/context/terminal.tsx`, `packages/app/src/components/session/session-sortable-terminal-tab.tsx` (plus new keys added in both dictionaries).
-- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (373 keys each; no missing or extra keys).
-
-## Methodology
-
-- Scanned `packages/app/src` (excluding `packages/app/src/i18n/*` and tests).
-- Grepped for:
- - Hardcoded JSX text nodes (e.g. `>Some text<`)
- - Hardcoded prop strings (e.g. `title="..."`, `placeholder="..."`, `label="..."`, `description="..."`, `Tooltip value="..."`)
- - Toast/notification strings, default fallbacks, and error message templates.
-- Manually reviewed top hits to distinguish:
- - User-facing UI copy (needs translation)
- - Developer-only logs (`console.*`) (typically does not need translation)
- - Technical identifiers (e.g. `MCP`, `LSP`, URLs) (may remain untranslated by choice).
-
-## Highest Priority: Pages
-
-### 1) Error Page
-
-File: `packages/app/src/pages/error.tsx`
-
-Completed (2026-01-20):
-
-- Localized page UI copy via `error.page.*` keys (title, description, buttons, report text, version label).
-- Localized error chain framing and common init error templates via `error.chain.*` keys.
-- Kept raw server/provider error messages as-is when provided (only localizing labels and structure).
-
-## Highest Priority: Components
-
-### 2) Prompt Input
-
-File: `packages/app/src/components/prompt-input.tsx`
-
-Completed (2026-01-20):
-
-- Localized placeholder examples by replacing the hardcoded `PLACEHOLDERS` list with `prompt.example.*` keys.
-- Localized toast titles/descriptions via `prompt.toast.*` and reused `common.requestFailed` for fallback error text.
-- Localized popover empty states and drag/drop overlay copy (`prompt.popover.*`, `prompt.dropzone.label`).
-- Localized smaller labels (slash "custom" badge, attach button tooltip, Send/Stop tooltip labels).
-- Kept the `ESC` keycap itself untranslated (key label).
-
-### 3) Provider Connection / Auth Flow
-
-File: `packages/app/src/components/dialog-connect-provider.tsx`
-
-Completed (2026-01-20):
-
-- Localized all user-visible copy via `provider.connect.*` keys (titles, statuses, validations, instructions, OpenCode Zen onboarding).
-- Added `common.submit` and used it for both API + OAuth submit buttons.
-- Localized the success toast via `provider.connect.toast.connected.*`.
-
-### 4) Session Header (Share/Publish UI)
-
-File: `packages/app/src/components/session/session-header.tsx`
-
-Completed (2026-01-20):
-
-- Localized search placeholder via `session.header.search.placeholder`.
-- Localized share/publish UI via `session.share.*` keys (popover title/description, button states, copy tooltip).
-- Reused existing command keys for toggle/share tooltips (`command.review.toggle`, `command.terminal.toggle`, `command.session.share`).
-
-## Medium Priority: Components
-
-### 5) New Session View
-
-File: `packages/app/src/components/session/session-new-view.tsx`
-
-Completed (2026-01-20):
-
-- Reused existing `command.session.new` for the heading.
-- Localized worktree labels via `session.new.worktree.*` (main branch, main branch w/ branch name, create worktree).
-- Localized "Last modified" via `session.new.lastModified` and used `language.locale()` for Luxon relative time.
-
-### 6) Context Usage Tooltip
-
-File: `packages/app/src/components/session-context-usage.tsx`
-
-Completed (2026-01-20):
-
-- Localized tooltip labels + CTA via `context.usage.*` keys.
-- Switched currency and number formatting to the active locale (`language.locale()`).
-
-### 7) Session Context Tab (Formatting)
-
-File: `packages/app/src/components/session/session-context-tab.tsx`
-
-Completed (2026-01-20):
-
-- Switched currency formatting to the active locale (`language.locale()`).
-- Also used `language.locale()` for number/date formatting.
-- Note: "—" placeholders remain hardcoded; optional to localize.
-
-### 8) LSP Indicator
-
-File: `packages/app/src/components/session-lsp-indicator.tsx`
-
-Completed (2026-01-20):
-
-- Localized tooltip/label framing via `lsp.*` keys (kept the acronym itself).
-
-### 9) Session Tab Close Tooltip
-
-File: `packages/app/src/components/session/session-sortable-tab.tsx`
-
-Completed (2026-01-20):
-
-- Reused `common.closeTab` for the close tooltip.
-
-### 10) Titlebar Tooltip
-
-File: `packages/app/src/components/titlebar.tsx`
-
-Completed (2026-01-20):
-
-- Reused `command.sidebar.toggle` for the tooltip title.
-
-### 11) Model Selection "Recent" Group
-
-File: `packages/app/src/components/dialog-select-model.tsx`
-
-Completed (2026-01-20):
-
-- Removed the unused hardcoded "Recent" group comparisons to avoid locale-coupled sorting.
-
-### 12) Select Server Dialog Placeholder (Optional)
-
-File: `packages/app/src/components/dialog-select-server.tsx`
-
-Completed (2026-01-20):
-
-- Moved the placeholder example URL behind `dialog.server.add.placeholder` (value unchanged).
-
-## Medium Priority: Context Modules
-
-### 13) OS/Desktop Notifications
-
-File: `packages/app/src/context/notification.tsx`
-
-Completed (2026-01-20):
-
-- Localized OS notification titles/fallback copy via `notification.session.*` keys.
-
-### 14) Global Sync (Bootstrap Errors + Toast)
-
-File: `packages/app/src/context/global-sync.tsx`
-
-Completed (2026-01-20):
-
-- Localized the sessions list failure toast via `toast.session.listFailed.title`.
-- Localized the bootstrap connection error via `error.globalSync.connectFailed`.
-
-### 15) File Load Failure Toast (Duplicate)
-
-Files:
-
-- `packages/app/src/context/file.tsx`
-- `packages/app/src/context/local.tsx`
-
-Completed (2026-01-20):
-
-- Introduced `toast.file.loadFailed.title` and reused it in both contexts.
-
-### 16) Terminal Naming (Tricky)
-
-File: `packages/app/src/context/terminal.tsx`
-
-Completed (2026-01-20):
-
-- Terminal display labels are now rendered from a stable numeric `titleNumber` and localized via `terminal.title.*`.
-- Added a one-time migration to backfill missing `titleNumber` by parsing the stored title string.
-
-## Low Priority: Utils / Dev-Only Copy
-
-### 17) Default Attachment Filename
-
-File: `packages/app/src/utils/prompt.ts`
-
-Completed (2026-01-20):
-
-- Added `common.attachment` and plumbed it into `extractPromptFromParts(...)` as `opts.attachmentName`.
-
-### 18) Dev-only Root Mount Error
-
-File: `packages/app/src/entry.tsx`
-
-Completed (2026-01-20):
-
-- Localized the DEV-only root mount error via `error.dev.rootNotFound`.
-- Selected locale using `navigator.languages` to match the app’s default detection.
-
-## Prioritized Implementation Plan
-
-No remaining work in `packages/app/` as of 2026-01-20.
-
-## Suggested Key Naming Conventions
-
-To keep the dictionaries navigable, prefer grouping by surface:
-
-- `error.page.*`, `error.chain.*`
-- `prompt.*` (including examples, tooltips, empty states, toasts)
-- `provider.connect.*` (auth flow UI + validation + success)
-- `session.share.*` (publish/unpublish/copy link)
-- `context.usage.*` (Tokens/Usage/Cost + call to action)
-- `lsp.*` (and potentially `mcp.*` if expanded)
-- `notification.session.*`
-- `toast.file.*`, `toast.session.*`
-
-Also reuse existing command keys for tooltip titles whenever possible (e.g. `command.sidebar.toggle`, `command.review.toggle`, `command.terminal.toggle`).
-
-## Appendix: Remaining Files At-a-Glance
-
-Pages:
-
-- (none)
-
-Components:
-
-- (none)
-
-Context:
-
-- (none)
-
-Utils:
-
-- (none)
diff --git a/specs/07-ui-i18n-audit.md b/specs/07-ui-i18n-audit.md
deleted file mode 100644
index e3c74686781e..000000000000
--- a/specs/07-ui-i18n-audit.md
+++ /dev/null
@@ -1,156 +0,0 @@
-# UI i18n Audit (Remaining Work)
-
-Scope: `packages/ui/` (and consumers: `packages/app/`, `packages/enterprise/`)
-
-Date: 2026-01-20
-
-This report documents the remaining user-facing strings in `packages/ui/src` that are still hardcoded (not routed through a translation function), and proposes an i18n architecture that works long-term across multiple packages.
-
-## Current State
-
-- `packages/app/` already has i18n via `useLanguage().t("...")` with dictionaries in `packages/app/src/i18n/en.ts` and `packages/app/src/i18n/zh.ts`.
-- `packages/ui/` is a shared component library used by:
- - `packages/app/src/pages/session.tsx` (Session UI)
- - `packages/enterprise/src/routes/share/[shareID].tsx` (shared session rendering)
-- `packages/ui/` currently has **hardcoded English UI copy** in several components (notably `session-turn.tsx`, `session-review.tsx`, `message-part.tsx`).
-- `packages/enterprise/` does not currently have an i18n system, so any i18n approach must be usable without depending on `packages/app/`.
-
-## Decision: How We Should Add i18n To `@opencode-ai/ui`
-
-Introduce a small, app-agnostic i18n interface in `packages/ui/` and keep UI-owned strings in UI-owned dictionaries.
-
-Why this is the best long-term shape:
-
-- Keeps dependency direction clean: `packages/enterprise/` (and any future consumer) can translate UI without importing `packages/app/` dictionaries.
-- Avoids prop-drilling strings through shared components.
-- Allows each package to own its strings while still rendering a single, coherent locale in the product.
-
-### Proposed Architecture
-
-1. **UI provides an i18n context (no persistence)**
-
-- Add `packages/ui/src/context/i18n.tsx`:
- - Exports `I18nProvider` and `useI18n()`.
- - Context value includes:
- - `t(key, params?)` translation function (template interpolation supported by the consumer).
- - `locale()` accessor for locale-sensitive formatting (Luxon/Intl).
- - Context should have a safe default (English) so UI components can render even if a consumer forgets the provider.
-
-2. **UI owns UI strings (dictionaries live in UI)**
-
-- Add `packages/ui/src/i18n/en.ts` and `packages/ui/src/i18n/zh.ts`.
-- Export them from `@opencode-ai/ui` via `packages/ui/package.json` exports (e.g. `"./i18n/*": "./src/i18n/*.ts"`).
-- Use a clear namespace prefix for all UI keys to avoid collisions:
- - Recommended: `ui.*` (e.g. `ui.sessionReview.title`).
-
-3. **Consumers merge dictionaries and provide `t`/`locale` once**
-
-- `packages/app/`:
- - Keep `packages/app/src/context/language.tsx` as the source of truth for locale selection/persistence.
- - Extend it to merge UI dictionaries into its translation table.
- - Add a tiny bridge provider in `packages/app/src/app.tsx` to feed `useLanguage()` into `@opencode-ai/ui`'s `I18nProvider`.
-
-- `packages/enterprise/`:
- - Add a lightweight locale detector (similar to `packages/app/src/context/language.tsx`), likely based on `Accept-Language` on the server and/or `navigator.languages` on the client.
- - Merge `@opencode-ai/ui` dictionaries and (optionally) enterprise-local dictionaries.
- - Wrap the share route in `I18nProvider`.
-
-### Key Naming Conventions (UI)
-
-- Prefer component + semantic grouping:
- - `ui.sessionReview.title`
- - `ui.sessionReview.diffStyle.unified`
- - `ui.sessionReview.diffStyle.split`
- - `ui.sessionReview.expandAll`
- - `ui.sessionReview.collapseAll`
-
-- For `SessionTurn`:
- - `ui.sessionTurn.steps.show`
- - `ui.sessionTurn.steps.hide`
- - `ui.sessionTurn.summary.response`
- - `ui.sessionTurn.diff.more` (use templating: `Show more changes ({{count}})`)
- - `ui.sessionTurn.retry.retrying` / `ui.sessionTurn.retry.inSeconds` / etc (avoid string concatenation that is English-order dependent)
- - Status text:
- - `ui.sessionTurn.status.delegating`
- - `ui.sessionTurn.status.planning`
- - `ui.sessionTurn.status.gatheringContext`
- - `ui.sessionTurn.status.searchingCode`
- - `ui.sessionTurn.status.searchingWeb`
- - `ui.sessionTurn.status.makingEdits`
- - `ui.sessionTurn.status.runningCommands`
- - `ui.sessionTurn.status.thinking`
- - `ui.sessionTurn.status.thinkingWithTopic` (template: `Thinking - {{topic}}`)
- - `ui.sessionTurn.status.gatheringThoughts`
- - `ui.sessionTurn.status.consideringNextSteps` (fallback)
-
-## Locale-Sensitive Formatting (UI)
-
-`SessionTurn` currently formats durations via Luxon `Interval.toDuration(...).toHuman(...)` without an explicit locale.
-
-When i18n is added:
-
-- Use `useI18n().locale()` and pass locale explicitly:
- - Luxon: `duration.toHuman({ locale: locale(), ... })` (or set `.setLocale(locale())` where applicable).
- - Intl numbers/currency (if added later): `new Intl.NumberFormat(locale(), ...)`.
-
-## Initial Hardcoded Strings (Audit Findings)
-
-These are the highest-impact UI surfaces to translate first.
-
-### 1) `packages/ui/src/components/session-review.tsx`
-
-- `Session changes`
-- `Unified` / `Split`
-- `Collapse all` / `Expand all`
-
-### 2) `packages/ui/src/components/session-turn.tsx`
-
-- Tool/task status strings (e.g. `Delegating work`, `Searching the codebase`)
-- Steps toggle labels: `Show steps` / `Hide steps`
-- Summary section title: `Response`
-- Pagination CTA: `Show more changes ({{count}})`
-
-### 3) `packages/ui/src/components/message-part.tsx`
-
-Examples (non-exhaustive):
-
-- `Error`
-- `Edit`
-- `Write`
-- `Type your own answer`
-- `Review your answers`
-
-### 4) Additional Hardcoded Strings (Full Audit)
-
-Found during a full `packages/ui/src/components` + `packages/ui/src/context` sweep:
-
-- `packages/ui/src/components/list.tsx`
- - `Loading`
- - `No results`
- - `No results for "{{filter}}"`
-- `packages/ui/src/components/message-nav.tsx`
- - `New message`
-- `packages/ui/src/components/text-field.tsx`
- - `Copied`
- - `Copy to clipboard`
-- `packages/ui/src/components/image-preview.tsx`
- - `Image preview` (alt text)
-
-## Prioritized Implementation Plan
-
-1. Completed (2026-01-20): Add `@opencode-ai/ui` i18n context (`packages/ui/src/context/i18n.tsx`) + export it.
-2. Completed (2026-01-20): Add UI dictionaries (`packages/ui/src/i18n/en.ts`, `packages/ui/src/i18n/zh.ts`) + export them.
-3. Completed (2026-01-20): Wire `I18nProvider` into:
- - `packages/app/src/app.tsx`
- - `packages/enterprise/src/app.tsx`
-4. Completed (2026-01-20): Convert `packages/ui/src/components/session-review.tsx` and `packages/ui/src/components/session-turn.tsx` to use `useI18n().t(...)`.
-5. Completed (2026-01-20): Convert `packages/ui/src/components/message-part.tsx`.
-6. Completed (2026-01-20): Do a full `packages/ui/src/components` + `packages/ui/src/context` audit for additional hardcoded copy.
-
-## Notes / Risks
-
-- **SSR:** Enterprise share pages render on the server. Ensure the i18n provider works in SSR and does not assume `window`/`navigator`.
-- **Key collisions:** Use a consistent `ui.*` prefix to avoid clashing with app keys.
-- **Fallback behavior:** Decide whether missing keys should:
- - fall back to English, or
- - render the key (useful for catching missing translations).
diff --git a/specs/08-app-e2e-smoke-suite.md b/specs/08-app-e2e-smoke-suite.md
deleted file mode 100644
index c034399a38c4..000000000000
--- a/specs/08-app-e2e-smoke-suite.md
+++ /dev/null
@@ -1,255 +0,0 @@
-## App E2E Smoke Suite (CI)
-
-Implement a small set of high-signal, low-flake Playwright tests to run in CI.
-
-These tests are intended to catch regressions in the “core shell” of the app (navigation, dialogs, prompt UX, file viewer, terminal), without relying on model output.
-
----
-
-### Summary
-
-Add 6 smoke tests to `packages/app/e2e/`:
-
-- Settings dialog: open, switch tabs, close
-- Prompt slash command: `/open` opens the file picker dialog
-- Prompt @mention: `@` inserts a file pill token
-- Model picker: open model selection and choose a model
-- File viewer: open a known file and assert contents render
-- Terminal: open terminal, verify Ghostty mounts, create a second terminal
-
----
-
-### Progress
-
-- [x] 1. Settings dialog open / switch / close (`packages/app/e2e/settings.spec.ts`)
-- [x] 2. Prompt slash command path: `/open` opens file picker (`packages/app/e2e/prompt-slash-open.spec.ts`)
-- [x] 3. Prompt @mention inserts a file pill token (`packages/app/e2e/prompt-mention.spec.ts`)
-- [x] 4. Model selection UI works end-to-end (`packages/app/e2e/model-picker.spec.ts`)
-- [x] 5. File viewer renders real file content (`packages/app/e2e/file-viewer.spec.ts`)
-- [x] 8. Terminal init + create new terminal (`packages/app/e2e/terminal-init.spec.ts`)
-
----
-
-### Goals
-
-- Tests run reliably in CI using the existing local runner (`packages/app/script/e2e-local.ts`).
-- Cover “wiring” regressions across UI + backend APIs:
- - dialogs + command routing
- - prompt contenteditable parsing
- - file search + file read + code viewer render
- - terminal open + pty creation + Ghostty mount
-- Avoid assertions that depend on LLM output.
-- Keep runtime low (these should be “smoke”, not full workflows).
-
----
-
-### Non-goals
-
-- Verifying complex model behavior, streaming correctness, or tool call semantics.
-- Testing provider auth flows (CI has no secrets).
-- Testing share, MCP, or LSP download flows (disabled in the e2e runner).
-
----
-
-### Current State
-
-Existing tests in `packages/app/e2e/` already cover:
-
-- Home renders + server picker opens
-- Directory route redirects to `/session`
-- Sidebar collapse/expand
-- Command palette opens/closes
-- Basic session open + prompt input + (optional) prompt/reply flow
-- File open via palette (but shallow assertion: tab exists)
-- Terminal panel toggles (but doesn’t assert Ghostty mounted)
-- Context panel open
-
-We want to add a focused smoke layer that increases coverage of the most regression-prone UI paths.
-
----
-
-### Proposed Tests
-
-All tests should use the shared fixtures in:
-
-- `packages/app/e2e/fixtures.ts` (for `sdk`, `directory`, `gotoSession`)
-- `packages/app/e2e/utils.ts` (for `modKey`, `promptSelector`, `terminalToggleKey`)
-
-Prefer creating new spec files rather than overloading existing ones, so it’s easy to run these tests as a group via grep.
-
-Suggested file layout:
-
-- `packages/app/e2e/settings.spec.ts`
-- `packages/app/e2e/prompt-slash-open.spec.ts`
-- `packages/app/e2e/prompt-mention.spec.ts`
-- `packages/app/e2e/model-picker.spec.ts`
-- `packages/app/e2e/file-viewer.spec.ts`
-- `packages/app/e2e/terminal-init.spec.ts`
-
-Name each test with a “smoke” prefix so CI can run only this suite if needed.
-
-#### 1) Settings dialog open / switch / close
-
-Purpose: catch regressions in dialog infra, settings rendering, tabs.
-
-Steps:
-
-1. `await gotoSession()`.
-2. Open settings via keybind (preferred for stability): `await page.keyboard.press(`${modKey}+Comma`)`.
-3. Assert dialog visible (`page.getByRole('dialog')`).
-4. Click the "Shortcuts" tab (role `tab`, name "Shortcuts").
-5. Assert shortcuts view renders (e.g. the search field placeholder or reset button exists).
-6. Close with `Escape` and assert dialog removed.
-
-Notes:
-
-- If `Meta+Comma` / `Control+Comma` key name is flaky, fall back to clicking the sidebar settings icon.
-- Favor role-based selectors over brittle class selectors.
-- If `Escape` doesn’t dismiss reliably (tooltips can intercept), fall back to clicking the dialog overlay.
-
-Implementation: `packages/app/e2e/settings.spec.ts`
-
-Acceptance criteria:
-
-- Settings dialog opens reliably.
-- Switching to Shortcuts tab works.
-- Escape closes the dialog.
-
-#### 2) Prompt slash command path: `/open` opens file picker
-
-Purpose: validate contenteditable parsing + slash popover + builtin command dispatch (distinct from `mod+p`).
-
-Steps:
-
-1. `await gotoSession()`.
-2. Click prompt (`promptSelector`).
-3. Type `/open`.
-4. Press `Enter` (while slash popover is active).
-5. Assert a dialog appears and contains a textbox (the file picker search input).
-6. Close dialog with `Escape`.
-
-Acceptance criteria:
-
-- `/open` triggers `file.open` and opens `DialogSelectFile`.
-
-#### 3) Prompt @mention inserts a file pill token
-
-Purpose: validate the most fragile prompt behavior: structured tokens inside contenteditable.
-
-Steps:
-
-1. `await gotoSession()`.
-2. Focus the prompt.
-3. Type `@packages/app/package.json`.
-4. Press `Tab` to accept the active @mention suggestion.
-5. Assert a pill element is inserted:
- - `page.locator('[data-component="prompt-input"] [data-type="file"][data-path="packages/app/package.json"]')` exists.
-
-Acceptance criteria:
-
-- A file pill is inserted and has the expected `data-*` attributes.
-- Prompt editor remains interactable (e.g. typing a trailing space works).
-
-#### 4) Model selection UI works end-to-end
-
-Purpose: validate model list rendering, selection wiring, and prompt footer updating.
-
-Implementation approach:
-
-- Use `/model` to open the model selection dialog (builtin command).
-
-Steps:
-
-1. `await gotoSession()`.
-2. Focus prompt, type `/model`, press `Enter`.
-3. In the model dialog, pick a visible model that is not the current selection (if available).
-4. Use the search field to filter to that model (use its id from the list item's `data-key` to avoid time-based model visibility drift).
-5. Select the filtered model.
-6. Assert dialog closed.
-7. Assert the prompt footer now shows the chosen model name.
-
-Acceptance criteria:
-
-- A model can be selected without requiring provider auth.
-- The prompt footer reflects the new selection.
-
-#### 5) File viewer renders real file content
-
-Purpose: ensure file search + open + file.read + code viewer render all work.
-
-Steps:
-
-1. `await gotoSession()`.
-2. Open file picker (either `mod+p` or `/open`).
-3. Search for `packages/app/package.json`.
-4. Click the matching file result.
-5. Ensure the new file tab is active (click the `package.json` tab if needed so the viewer mounts).
-6. Assert the code viewer contains a known substring:
- - `"name": "@opencode-ai/app"`.
-7. Optionally assert the file tab is active and visible.
-
-Acceptance criteria:
-
-- Code view shows expected content (not just “tab exists”).
-
-#### 8) Terminal init + create new terminal
-
-Purpose: ensure terminal isn’t only “visible”, but actually mounted and functional.
-
-Steps:
-
-1. `await gotoSession()`.
-2. Open terminal with `terminalToggleKey` (currently `Control+Backquote`).
-3. Assert terminal container exists and is visible: `[data-component="terminal"]`.
-4. Assert Ghostty textarea exists: `[data-component="terminal"] textarea`.
-5. Create a new terminal via keybind (`terminal.new` is `ctrl+alt+t`).
-6. Assert terminal tab count increases to 2.
-
-Acceptance criteria:
-
-- Ghostty mounts (textarea present).
-- Creating a new terminal results in a second tab.
-
----
-
-### CI Stability + Flake Avoidance
-
-These tests run with `fullyParallel: true` in `packages/app/playwright.config.ts`. Keep them isolated and deterministic.
-
-- Avoid ordering-based assertions: never assume a “first” session/project/file is stable unless you filtered by unique text.
-- Prefer deterministic targets:
- - use `packages/app/package.json` rather than bare `package.json` (multiple hits possible)
- - for models, avoid hardcoding a single model id; pick from the visible list and filter by its `data-key` instead
-- Prefer robust selectors:
- - role selectors: `getByRole('dialog')`, `getByRole('textbox')`, `getByRole('tab')`
- - stable data attributes already present: `promptSelector`, `[data-component="terminal"]`
-- Keep tests local and fast:
- - do not submit prompts that require real model replies
- - avoid `page.waitForTimeout`; use `expect(...).toBeVisible()` and `expect.poll` when needed
-- Watch for silent UI failures:
- - capture `page.on('pageerror')` and fail test if any are emitted
- - optionally capture console errors (`page.on('console', ...)`) and fail on `type==='error'`
-- Cleanup:
- - these tests should not need to create sessions
- - if a test ever creates sessions or PTYs directly, clean up with SDK calls in `finally`
-
----
-
-### Validation Plan
-
-Run locally:
-
-- `cd packages/app`
-- `bun run test:e2e:local -- --grep smoke`
-
-Verify:
-
-- all new tests pass consistently across multiple runs
-- overall e2e suite time does not increase significantly
-
----
-
-### Open Questions
-
-- Should we add a small helper in `packages/app/e2e/utils.ts` for “type into prompt contenteditable” to reduce duplication?
-- Do we want to gate these smoke tests with a dedicated `@smoke` naming convention (or `test.describe('smoke', ...)`) so CI can target them explicitly?
diff --git a/specs/09-session-page-decomposition.md b/specs/09-session-page-decomposition.md
deleted file mode 100644
index 8f39edc94c44..000000000000
--- a/specs/09-session-page-decomposition.md
+++ /dev/null
@@ -1,113 +0,0 @@
-## Session page decomposition
-
-Split `pages/session.tsx` into focused modules without behavior changes.
-
----
-
-### Summary
-
-`packages/app/src/pages/session.tsx` is still a large (~3,655 LOC) route coordinator. Recent refactoring already extracted `packages/app/src/pages/session/helpers.ts` and `packages/app/src/pages/session/scroll-spy.ts`, but review-panel wiring, message timeline orchestration, file-tab rendering, and terminal coordination remain tightly coupled. This spec continues the decomposition from that updated baseline.
-
----
-
-### Goals
-
-- Reduce complexity in `packages/app/src/pages/session.tsx`.
-- Isolate major concerns into dedicated modules under `packages/app/src/pages/session/`.
-- Keep behavior and route/API contracts unchanged.
-- Preserve current keyboard, scroll, hash, and review interactions.
-
----
-
-### Non-goals
-
-- No redesign of session UX.
-- No changes to SDK contracts.
-- No refactor of `context/global-sync.tsx`, `context/file.tsx`, or `components/prompt-input.tsx` in this workstream.
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/pages/session.tsx`
-- New files under `packages/app/src/pages/session/**`
-
-This workstream must not edit:
-
-- `packages/app/src/pages/layout.tsx` (owned by spec 10)
-- `packages/app/src/components/prompt-input.tsx` (owned by spec 11)
-- `packages/app/src/context/global-sync.tsx` (owned by spec 12)
-- `packages/app/src/context/file.tsx` (owned by spec 13)
-
----
-
-### Current state
-
-- File size: ~3,655 LOC.
-- Existing extracted modules:
- - `packages/app/src/pages/session/helpers.ts` (terminal focus and shared handlers)
- - `packages/app/src/pages/session/scroll-spy.ts` (message visibility + active-section tracking)
-- High effect density (`createEffect`) and local-state density (`createStore` + `createSignal`) remain in `session.tsx`.
-- Remaining interleaved responsibilities:
- - review panel state + scrolling integration
- - message timeline + hash navigation wiring
- - file tab renderers + per-tab scroll sync
- - terminal panel and tab coordination
-
----
-
-### Proposed module split
-
-Build on the existing `packages/app/src/pages/session/` directory and keep current extracted helpers in place. Add modules such as:
-
-- `review-panel.tsx` - review tab rendering and focused diff logic.
-- `message-timeline.tsx` - session turn rendering and active message tracking UI wiring.
-- `file-tabs.tsx` - file tab content rendering, file scroll persistence, and line-comment overlays.
-- `terminal-panel.tsx` - terminal tabs and focus behavior.
-- `use-session-page-state.ts` - page-level derived state and imperative handlers.
-
-`packages/app/src/pages/session.tsx` remains the route entry and orchestrator only.
-
----
-
-### Phased steps
-
-1. Keep `helpers.ts` and `scroll-spy.ts` as baseline; extract any additional pure helpers first (no behavior changes).
-2. Extract review panel subtree and related handlers.
-3. Extract file-tab subtree and scroll synchronization logic.
-4. Extract terminal panel subtree.
-5. Move page-level state/effects into `use-session-page-state.ts`.
-6. Reduce `session.tsx` to composition and routing glue.
-
----
-
-### Acceptance criteria
-
-- `packages/app/src/pages/session.tsx` is reduced substantially (target: under 1,400 LOC).
-- No user-facing behavior changes in session, review, file tabs, or terminal tabs.
-- Event listeners and observers are still correctly cleaned up.
-- New modules have clear prop boundaries and minimal hidden coupling.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Targeted e2e checks:
- - `e2e/session/session.spec.ts`
- - `e2e/files/file-viewer.spec.ts`
- - `e2e/terminal/terminal.spec.ts`
-- Manual checks:
- - message hash navigation
- - review diff focus + open-file action
- - terminal tab create/reorder/focus behavior
-
----
-
-### Handoff notes
-
-- Keep module interfaces narrow and data-oriented.
-- Prefer extracting code unchanged before doing any cleanup refactors.
-- If a helper is useful to other specs, place it under `pages/session/` for now; cross-spec shared utilities can be unified later.
diff --git a/specs/09-session-page-hot-paths.md b/specs/09-session-page-hot-paths.md
deleted file mode 100644
index cc15106fbab7..000000000000
--- a/specs/09-session-page-hot-paths.md
+++ /dev/null
@@ -1,105 +0,0 @@
-## Session hot paths
-
-Reduce render work and duplication in `session.tsx`
-
----
-
-### Summary
-
-`packages/app/src/pages/session.tsx` mixes routing, commands, tab rendering, review panel wiring, terminal focus logic, and message scrolling. This spec targets hot-path performance + local code quality improvements that can ship together in one session-page-focused PR. It should follow the keyed command-registration pattern introduced in `packages/app/src/context/command.tsx`.
-
----
-
-### Goals
-
-- Render heavy file-tab content only for the active tab
-- Deduplicate review-panel wiring used in desktop and mobile paths
-- Centralize terminal-focus DOM logic into one helper
-- Reduce churn in command registration setup
-
----
-
-### Non-goals
-
-- Scroll-spy rewrite (covered by `specs/04-scroll-spy-optimization.md`)
-- Large routing/layout redesign
-- Behavior changes to prompt submission or session history
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/pages/session.tsx`
-- New files under `packages/app/src/pages/session/*` (if extracted)
-
-This spec should not modify:
-
-- `packages/app/src/context/*`
-- `packages/app/src/components/prompt-input.tsx`
-- `packages/app/src/components/file-tree.tsx`
-
----
-
-### Implementation plan
-
-1. Add shared helpers for repeated session-page actions
-
-- Extract `openReviewFile(path)` helper to replace repeated inline `onViewFile` bodies.
-- Extract `focusTerminalById(id)` helper and reuse in both:
- - terminal active change effect
- - terminal drag-end focus restoration
-
-2. Deduplicate review panel construction
-
-- Build a shared review props factory (or local render helper) so desktop/mobile paths do not duplicate comment wiring, `onViewFile`, and classes glue.
-- Keep per-surface differences limited to layout classes and diff style.
-
-3. Gate heavy file-tab rendering by active tab
-
-- Keep tab trigger list rendered for all opened tabs.
-- Render `Tabs.Content` body only for `activeTab()`, plus lightweight placeholders as needed.
-- Ensure per-tab scroll state restore still works when reactivating a tab.
-
-4. Reduce command registry reallocation
-
-- Register session commands with a stable key (`command.register("session", ...)`) so remounts replace prior session command entries.
-- Move large command-array construction into smaller memoized blocks:
- - stable command definitions
- - dynamic state fields (`disabled`, titles) as narrow computed closures
- - Keep command IDs, keybinds, and behavior identical.
-
----
-
-### Acceptance criteria
-
-- File tab bodies are not all mounted at once for large open-tab sets.
-- `onViewFile` review behavior is defined in one shared helper.
-- Terminal focus query/dispatch logic lives in one function and is reused.
-- Session command registration uses a stable key (`"session"`) and `command.register` no longer contains one monolithic inline array with repeated inline handlers for shared actions.
-- Session UX remains unchanged for:
- - opening files from review
- - drag-reordering terminal tabs
- - keyboard command execution
-
----
-
-### Validation plan
-
-- Manual:
- - Open 12+ file tabs, switch quickly, verify active tab restore and no blank states.
- - Open review panel (desktop and mobile), use "view file" from diffs, verify same behavior as before.
- - Drag terminal tab, ensure terminal input focus is restored.
- - Run key commands: `mod+p`, `mod+w`, `mod+shift+r`, `ctrl+``.
-- Perf sanity:
- - Compare CPU usage while switching tabs with many opened files before/after.
-
----
-
-### Risks and mitigations
-
-- Risk: unmounted tab content loses transient editor state.
- - Mitigation: keep persisted scroll/selection restore path intact and verify reactivation behavior.
-- Risk: command refactor subtly changes command ordering.
- - Mitigation: keep IDs and registration order stable, diff against current command list in dev.
diff --git a/specs/10-file-content-eviction-accounting.md b/specs/10-file-content-eviction-accounting.md
deleted file mode 100644
index 2f7f1f8cc97d..000000000000
--- a/specs/10-file-content-eviction-accounting.md
+++ /dev/null
@@ -1,99 +0,0 @@
-## File cache accounting
-
-Make file-content eviction bookkeeping O(1)
-
----
-
-### Summary
-
-`packages/app/src/context/file.tsx` currently recomputes total cached bytes by reducing the entire LRU map inside the eviction loop. This creates avoidable overhead on large file sets. We will switch to incremental byte accounting while keeping LRU behavior unchanged.
-
----
-
-### Goals
-
-- Remove repeated full-map reductions from eviction path
-- Maintain accurate total byte tracking incrementally
-- Preserve existing eviction semantics (entry count + byte cap)
-
----
-
-### Non-goals
-
-- Changing cache limits
-- Changing file loading API behavior
-- Introducing cross-session shared caches
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/context/file.tsx`
-- Optional tests in `packages/app/src/context/*file*.test.ts`
-
-This spec should not modify:
-
-- `packages/app/src/pages/session.tsx`
-- `packages/app/src/components/file-tree.tsx`
-
----
-
-### Implementation plan
-
-1. Introduce incremental byte counters
-
-- Add module-level `contentBytesTotal`.
-- Add helper(s):
- - `setContentBytes(path, nextBytes)`
- - `removeContentBytes(path)`
- - `resetContentBytes()`
-
-2. Refactor LRU touch/update path
-
-- Keep `contentLru` as LRU order map.
-- Update byte total only when a path is inserted/updated/removed.
-- Ensure replacing existing byte value updates total correctly.
-
-3. Refactor eviction loop
-
-- Use `contentBytesTotal` in loop condition instead of `Array.from(...).reduce(...)`.
-- On eviction, remove from both `contentLru` and byte counter.
-
-4. Keep scope reset correct
-
-- On directory scope change, clear inflight maps + `contentLru` + byte counter.
-
----
-
-### Acceptance criteria
-
-- `evictContent` performs no full-map reduction per iteration.
-- Total bytes remain accurate after:
- - loading file A
- - loading file B
- - force-reloading file A with a different size
- - evicting entries
- - scope reset
-- Existing caps (`MAX_FILE_CONTENT_ENTRIES`, `MAX_FILE_CONTENT_BYTES`) continue to enforce correctly.
-
----
-
-### Validation plan
-
-- Manual:
- - Open many files with mixed sizes and verify old files still evict as before.
- - Switch directory scope and verify cache clears safely.
-- Optional unit coverage:
- - size counter updates on overwrite + delete.
- - eviction condition uses count and bytes as expected.
-
----
-
-### Risks and mitigations
-
-- Risk: byte counter drifts from map contents.
- - Mitigation: route all updates through centralized helpers.
-- Risk: stale bytes retained on early returns.
- - Mitigation: assert cleanup paths in `finally`/scope reset still execute.
diff --git a/specs/10-layout-page-decomposition.md b/specs/10-layout-page-decomposition.md
deleted file mode 100644
index d48002ea80d8..000000000000
--- a/specs/10-layout-page-decomposition.md
+++ /dev/null
@@ -1,109 +0,0 @@
-## Layout page decomposition
-
-Split `pages/layout.tsx` into composable layout modules with stable behavior.
-
----
-
-### Summary
-
-`packages/app/src/pages/layout.tsx` is a 3,000+ line coordinator for sidebar navigation, project/workspace controls, deep-link handling, dialogs, drag/drop overlays, and global shell interactions. This spec decomposes it into focused modules to improve maintainability and reduce merge risk for future features.
-
----
-
-### Goals
-
-- Break up `packages/app/src/pages/layout.tsx` into smaller units.
-- Separate rendering concerns from orchestration/state concerns.
-- Keep existing URL/navigation semantics and sidebar behavior.
-- Preserve all current command and dialog entry points.
-
----
-
-### Non-goals
-
-- No major UX redesign of the sidebar or project/workspace UI.
-- No changes to server/global-sync contracts.
-- No refactor of `pages/session.tsx` in this workstream.
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/pages/layout.tsx`
-- New files under `packages/app/src/pages/layout/**`
-
-This workstream must not edit:
-
-- `packages/app/src/pages/session.tsx` (spec 09)
-- `packages/app/src/components/prompt-input.tsx` (spec 11)
-- `packages/app/src/context/global-sync.tsx` (spec 12)
-
----
-
-### Current state
-
-- File size: ~3,004 LOC.
-- Contains mixed concerns:
- - app-shell rendering
- - sidebar/project/workspace UI + drag/drop
- - deep-link handling and startup flows
- - workspace reset/delete actions and toasts
-
----
-
-### Proposed module split
-
-Create `packages/app/src/pages/layout/` modules such as:
-
-- `use-layout-page-state.ts` - orchestration state and handlers.
-- `sidebar-panel.tsx` - sidebar shell and root interactions.
-- `project-item.tsx` - project-level row and actions.
-- `workspace-item.tsx` - workspace row, sessions list, and workspace actions.
-- `deep-links.ts` - deep-link parsing/draining/handler utilities.
-
-Keep `packages/app/src/pages/layout.tsx` as route-level composition and provider wiring.
-
----
-
-### Phased steps
-
-1. Extract pure helpers first (deep-link parse, shared label helpers, small utility functions).
-2. Extract workspace subtree and action handlers.
-3. Extract project subtree and menu actions.
-4. Extract sidebar shell and drag overlay components.
-5. Move orchestration logic into `use-layout-page-state.ts`.
-6. Reduce `layout.tsx` to composition-only entry.
-
----
-
-### Acceptance criteria
-
-- `packages/app/src/pages/layout.tsx` is significantly smaller (target: under 1,200 LOC).
-- Behavior parity for:
- - project open/close/rename
- - workspace expand/collapse/reset/delete
- - deep-link handling
- - drag/drop ordering
-- No regressions in keyboard navigation and dialog actions.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Targeted e2e checks:
- - `e2e/sidebar/sidebar.spec.ts`
- - `e2e/projects/workspaces.spec.ts`
- - `e2e/projects/project-edit.spec.ts`
- - `e2e/app/navigation.spec.ts`
-- Manual check: deep-link open-project flow still opens and navigates correctly.
-
----
-
-### Handoff notes
-
-- Keep action handlers close to their domain module.
-- Do not merge in behavior cleanups during extraction; preserve semantics first.
-- If shared components are needed, add them under `pages/layout/` for now to avoid cross-spec conflicts.
diff --git a/specs/11-layout-view-tabs-reactivity.md b/specs/11-layout-view-tabs-reactivity.md
deleted file mode 100644
index c7680440cb9f..000000000000
--- a/specs/11-layout-view-tabs-reactivity.md
+++ /dev/null
@@ -1,92 +0,0 @@
-## Layout reactivity
-
-Reduce per-call reactive overhead in `useLayout`
-
----
-
-### Summary
-
-`packages/app/src/context/layout.tsx` creates reactive effects inside `view(sessionKey)` and `tabs(sessionKey)` each time these helpers are called. Multiple consumers for the same key can accumulate duplicate watchers. This spec simplifies the API internals so calls stay lightweight while preserving behavior.
-
----
-
-### Goals
-
-- Remove avoidable per-call `createEffect` allocations in `view()` and `tabs()`
-- Preserve scroll seeding, pruning, and touch semantics
-- Keep external `useLayout` API stable
-
----
-
-### Non-goals
-
-- Persistence schema migration
-- Session tab behavior redesign
-- New layout features
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/context/layout.tsx`
-- `packages/app/src/context/layout-scroll.test.ts` (if updates needed)
-
-This spec should not modify:
-
-- `packages/app/src/pages/session.tsx`
-- `packages/app/src/components/session/*`
-
----
-
-### Implementation plan
-
-1. Consolidate key-touch logic
-
-- Introduce shared internal helper, e.g. `ensureSessionKey(key)` that performs:
- - `touch(key)`
- - `scroll.seed(key)`
-
-2. Remove per-call effects in `view()` / `tabs()`
-
-- Replace internal `createEffect(on(key, ...))` usage with lazy key reads inside accessors/memos.
-- Ensure reads still invoke `ensureSessionKey` at safe points.
-
-3. Keep return API stable
-
-- Preserve current method names and behavior:
- - `view(...).scroll`, `setScroll`, `terminal`, `reviewPanel`, `review`
- - `tabs(...).active`, `all`, `open`, `close`, `move`, etc.
-
-4. Verify pruning behavior
-
-- Ensure session-key pruning still runs when key set grows and active key changes.
-
----
-
-### Acceptance criteria
-
-- `view()` and `tabs()` no longer instantiate per-call key-change effects.
-- Existing callers do not require API changes.
-- Scroll restore and tab persistence still work across session navigation.
-- No regressions in handoff/pending-message behavior.
-
----
-
-### Validation plan
-
-- Manual:
- - Navigate across multiple sessions; verify tabs + review open state + scroll positions restore.
- - Toggle terminal/review panels and confirm persisted state remains consistent.
-- Tests:
- - Update/add targeted tests for key seeding/pruning if behavior changed.
-
----
-
-### Risks and mitigations
-
-- Risk: subtle key-touch ordering changes affect prune timing.
- - Mitigation: keep `touch` and `seed` coupled through one helper and verify prune boundaries.
-- Risk: removing effects misses updates for dynamic accessor keys.
- - Mitigation: ensure every public accessor path reads current key and calls helper.
diff --git a/specs/11-prompt-input-and-optimistic-state.md b/specs/11-prompt-input-and-optimistic-state.md
deleted file mode 100644
index 47008fb1fa33..000000000000
--- a/specs/11-prompt-input-and-optimistic-state.md
+++ /dev/null
@@ -1,121 +0,0 @@
-## Prompt input and optimistic-state consolidation
-
-Decompose prompt-input and unify optimistic message mutations.
-
----
-
-### Summary
-
-`packages/app/src/components/prompt-input.tsx` has already been partially decomposed and is now ~1,391 LOC. Editor DOM helpers, attachments, history, and submit flow were extracted into `packages/app/src/components/prompt-input/*.ts`, but optimistic mutation ownership and some UI/controller responsibilities are still split across call sites. This spec continues from that refactored baseline.
-
----
-
-### Goals
-
-- Split `prompt-input.tsx` into modular UI + controller pieces.
-- Centralize optimistic message add/remove behavior behind sync-context APIs.
-- Remove unsafe cast path around optimistic parts (`as unknown as Part[]`).
-- Keep existing prompt UX and submission semantics unchanged.
-
----
-
-### Non-goals
-
-- No redesign of prompt input visuals.
-- No changes to session protocol or backend APIs.
-- No changes to unrelated page modules (`pages/session.tsx`, `pages/layout.tsx`).
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/components/prompt-input.tsx`
-- New files under `packages/app/src/components/prompt-input/**`
-- `packages/app/src/context/sync.tsx` (optimistic API surface only)
-
-This workstream must not edit:
-
-- `packages/app/src/pages/session.tsx` (spec 09)
-- `packages/app/src/pages/layout.tsx` (spec 10)
-- `packages/app/src/context/global-sync.tsx` (spec 12)
-- `packages/app/src/context/file.tsx` (spec 13)
-
----
-
-### Current state
-
-- File size: ~1,391 LOC for `prompt-input.tsx`.
-- Existing extracted modules:
- - `prompt-input/editor-dom.ts`
- - `prompt-input/attachments.ts`
- - `prompt-input/history.ts`
- - `prompt-input/submit.ts`
-- Optimistic mutation and request-part casting still need consolidation (including remaining `as unknown as Part[]` in submit path).
-- Remaining concerns still tightly coupled in `prompt-input.tsx`:
- - slash/mention UI rendering and keyboard orchestration
- - context pill interactions and focus behavior
- - composition glue across history/attachments/submit
-
----
-
-### Proposed structure
-
-Build on the existing `packages/app/src/components/prompt-input/` modules by adding/further splitting modules such as:
-
-- `use-prompt-composer.ts` - state machine for submit/abort/history.
-- `build-request-parts.ts` - typed request-part construction.
-- `slash-popover.tsx` - slash command list rendering.
-- `context-items.tsx` - context pills and interactions.
-
-Keep existing lower-level modules (`attachments.ts`, `editor-dom.ts`, `history.ts`, `submit.ts`) and narrow their responsibilities where needed.
-
-Add sync-level optimistic APIs (in `context/sync.tsx` or `context/sync-optimistic.ts`):
-
-- `session.optimistic.add(...)`
-- `session.optimistic.remove(...)`
-
-Prompt input should call these APIs instead of directly mutating message/part stores.
-
----
-
-### Phased steps
-
-1. Extract typed request-part builder (likely from `prompt-input/submit.ts`) to remove ad hoc casting.
-2. Introduce sync optimistic APIs with current behavior.
-3. Replace remaining direct `produce(...)` optimistic mutations with optimistic APIs.
-4. Extract remaining UI subtrees (slash popover, context items, toolbar controls).
-5. Extract controller hook and keep route component as composition shell.
-
----
-
-### Acceptance criteria
-
-- Optimistic update logic exists in one place only.
-- `prompt-input.tsx` is significantly smaller (target: under 1,200 LOC).
-- Prompt submit/abort/history behavior remains unchanged.
-- No `as unknown as Part[]` in optimistic request construction path.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Targeted e2e checks:
- - `e2e/prompt/prompt.spec.ts`
- - `e2e/prompt/context.spec.ts`
- - `e2e/prompt/prompt-slash-open.spec.ts`
- - `e2e/prompt/prompt-mention.spec.ts`
-- Manual check:
- - submit with file/image/context attachments
- - abort in-flight turn
- - history up/down restore behavior
-
----
-
-### Handoff notes
-
-- Preserve sequence semantics around optimistic insert, worktree wait, send, and rollback.
-- Keep sync optimistic API data-oriented and reusable by future callers.
-- Do not mix this with broader sync/global-sync refactors in the same diff.
diff --git a/specs/12-global-sync-domain-split.md b/specs/12-global-sync-domain-split.md
deleted file mode 100644
index a0b600a19e5b..000000000000
--- a/specs/12-global-sync-domain-split.md
+++ /dev/null
@@ -1,105 +0,0 @@
-## Global sync domain split
-
-Refactor `context/global-sync.tsx` into domain modules while preserving behavior.
-
----
-
-### Summary
-
-`packages/app/src/context/global-sync.tsx` is a large multi-domain module (1,000+ LOC) that currently owns queue scheduling, bootstrap, child store creation, persistence bridges, session trimming, and event reduction. This workstream splits it into clear domains without changing runtime behavior.
-
----
-
-### Goals
-
-- Decompose global sync internals into maintainable modules.
-- Keep `useGlobalSync()` public API unchanged.
-- Isolate pure logic (session trimming, ordering, grouping) from side effects.
-- Keep event handling deterministic and easier to test.
-
----
-
-### Non-goals
-
-- No protocol/API changes to server events.
-- No behavior changes in session ordering, trimming, or cache semantics.
-- No changes to page-level UI logic.
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/context/global-sync.tsx`
-- New files under `packages/app/src/context/global-sync/**`
-
-This workstream must not edit:
-
-- `packages/app/src/context/file.tsx` (spec 13)
-- `packages/app/src/components/prompt-input.tsx` (spec 11)
-- `packages/app/src/pages/session.tsx` and `packages/app/src/pages/layout.tsx` (specs 09/10)
-
----
-
-### Current state
-
-- Single large module with many responsibilities.
-- Event reducer is embedded in component lifecycle code.
-- Queue/scheduler, bootstrap, and child-store lifecycle are tightly interwoven.
-
----
-
-### Proposed module split
-
-Create `packages/app/src/context/global-sync/` modules like:
-
-- `types.ts` - shared types.
-- `queue.ts` - refresh queue and drain scheduler.
-- `child-store.ts` - child store creation, persistence wiring, cache maps.
-- `session-trim.ts` - pure session sorting/trimming helpers.
-- `bootstrap.ts` - global and per-directory bootstrap flows.
-- `event-reducer.ts` - event handlers for SDK event stream.
-
-Keep `global-sync.tsx` as provider/composition entry point.
-
----
-
-### Phased steps
-
-1. Extract pure helpers (`cmp`, session trim/recent logic) first.
-2. Extract queue/drain scheduler.
-3. Extract child-store creation and persisted cache wiring.
-4. Extract bootstrap flows.
-5. Extract event reducer and wire into existing listener.
-6. Keep API surface stable and documented.
-
----
-
-### Acceptance criteria
-
-- Public API of `useGlobalSync()` remains backward compatible.
-- `global-sync.tsx` is substantially reduced (target: under 500 LOC).
-- Event handling logic is isolated and easier to trace.
-- No behavior regressions in project/session/provider sync.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Targeted e2e checks:
- - `e2e/app/session.spec.ts`
- - `e2e/sidebar/sidebar-session-links.spec.ts`
- - `e2e/projects/projects-switch.spec.ts`
-- Manual checks:
- - switching directories/projects still hydrates child stores correctly
- - session list/pagination behavior remains stable
-
----
-
-### Handoff notes
-
-- Favor function extraction with unchanged code first.
-- Keep event handler ordering explicit; avoid implicit fallthrough behaviors.
-- Add focused tests only for extracted pure helpers if practical, but avoid broad test-suite changes here.
diff --git a/specs/12-session-context-metrics-shared.md b/specs/12-session-context-metrics-shared.md
deleted file mode 100644
index ef534a815338..000000000000
--- a/specs/12-session-context-metrics-shared.md
+++ /dev/null
@@ -1,96 +0,0 @@
-## Context metrics shared
-
-Unify duplicate session usage calculations
-
----
-
-### Summary
-
-`session-context-tab.tsx` and `session-context-usage.tsx` both compute overlapping session metrics (cost, last assistant token totals, provider/model context usage). This creates duplicate loops and raises drift risk. We will centralize shared calculations in one helper module and have both components consume it.
-
----
-
-### Goals
-
-- Compute shared session usage metrics in one place
-- Remove duplicate loops for cost and latest-token context usage
-- Keep UI output unchanged in both components
-
----
-
-### Non-goals
-
-- Rewriting the detailed context breakdown estimator logic
-- Changing translations or labels
-- Moving metrics into backend API responses
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/components/session/session-context-tab.tsx`
-- `packages/app/src/components/session-context-usage.tsx`
-- New helper in `packages/app/src/components/session/*` or `packages/app/src/utils/*`
-
-This spec should not modify:
-
-- `packages/app/src/pages/session.tsx`
-- `packages/app/src/context/sync.tsx`
-
----
-
-### Implementation plan
-
-1. Add shared metrics helper
-
-- Create helper for raw metrics from message list + provider map, e.g.:
- - `totalCost`
- - `lastAssistantWithTokens`
- - `tokenTotal`
- - `tokenUsagePercent`
- - provider/model labels
-- Return raw numeric values; keep locale formatting in consumers.
-
-2. Add memoization guard
-
-- Use reference-based memoization (e.g. by message-array identity) inside helper or component-level memo to avoid duplicate recalculation on unchanged arrays.
-
-3. Migrate both components
-
-- Replace duplicated loops in:
- - `session-context-tab.tsx`
- - `session-context-usage.tsx`
-- Keep existing UI structure and i18n keys unchanged.
-
----
-
-### Acceptance criteria
-
-- Shared cost + token calculations are defined in one module.
-- Both components read from the shared helper.
-- Rendered values remain identical for:
- - total cost
- - token totals
- - usage percentage
- - provider/model fallback labels
-
----
-
-### Validation plan
-
-- Manual:
- - Open session context tab and compare values with header/context indicator tooltip.
- - Verify values update correctly while new assistant messages stream in.
-- Regression:
- - locale change still formats numbers/currency correctly.
-
----
-
-### Risks and mitigations
-
-- Risk: helper changes semantic edge cases (no provider, no model, missing token fields).
- - Mitigation: preserve existing fallback behavior (`"—"`, null percent).
-- Risk: memoization over-caches stale values.
- - Mitigation: key cache by message-array reference and dependent IDs only.
diff --git a/specs/13-file-context-domain-split.md b/specs/13-file-context-domain-split.md
deleted file mode 100644
index 41a578e2f951..000000000000
--- a/specs/13-file-context-domain-split.md
+++ /dev/null
@@ -1,111 +0,0 @@
-## File context domain split
-
-Refactor `context/file.tsx` into focused modules with unchanged API.
-
----
-
-### Summary
-
-`packages/app/src/context/file.tsx` still combines path normalization, file-content caching/eviction, file-tree loading, watcher event handling, and file-view persistence orchestration. Recent refactoring extracted generic scoped-cache primitives to `packages/app/src/utils/scoped-cache.ts`, but most file-domain behavior remains in one module. This spec separates those concerns while preserving the existing `useFile()` interface.
-
----
-
-### Goals
-
-- Keep `useFile()` API stable for all callers.
-- Extract independent domains into dedicated modules.
-- Improve readability and lower risk for future file-tree/perf changes.
-- Preserve current caching and watcher semantics.
-
----
-
-### Non-goals
-
-- No redesign of file tree UI.
-- No change to backend file APIs.
-- No simultaneous refactor of `components/file-tree.tsx` in this workstream.
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/context/file.tsx`
-- New files under `packages/app/src/context/file/**`
-- `packages/app/src/utils/scoped-cache.ts` (only when required for file-view cache extraction)
-
-This workstream must not edit:
-
-- `packages/app/src/context/global-sync.tsx` (spec 12)
-- `packages/app/src/pages/session.tsx` (spec 09)
-- `packages/app/src/components/prompt-input.tsx` (spec 11)
-
----
-
-### Current state
-
-- File size: ~751 LOC.
-- `packages/app/src/utils/scoped-cache.ts` now exists as a shared cache primitive used by file view persistence.
-- Multiple domains in one module:
- - path normalization/parsing
- - LRU content memory management
- - tree node/directory state management
- - event-driven watcher invalidation
- - per-session view cache bootstrapping
-
----
-
-### Proposed module split
-
-Create `packages/app/src/context/file/` modules such as:
-
-- `path.ts` - normalize/strip helpers.
-- `content-cache.ts` - content LRU + byte caps.
-- `view-cache.ts` - per-session file view persistence cache (building on `createScopedCache`).
-- `tree-store.ts` - directory/node store and list/expand/collapse actions.
-- `watcher.ts` - watcher event handling and invalidation routines.
-
-`file.tsx` remains the provider entry that composes these modules.
-
----
-
-### Phased steps
-
-1. Extract path helper functions with no behavior changes.
-2. Extract content cache and eviction logic.
-3. Extract file-specific view-cache loading/pruning logic on top of `createScopedCache`.
-4. Extract tree-store list/refresh/toggle actions.
-5. Extract watcher update handler and wire cleanup.
-6. Keep `useFile()` return shape unchanged.
-
----
-
-### Acceptance criteria
-
-- `useFile()` API remains backward compatible.
-- `context/file.tsx` is reduced significantly (target: under 350 LOC).
-- Tree loading/refresh and content eviction behavior remain unchanged.
-- Watcher-driven reload behavior still works for changed/added/deleted files.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Targeted e2e checks:
- - `e2e/files/file-tree.spec.ts`
- - `e2e/files/file-viewer.spec.ts`
- - `e2e/files/file-open.spec.ts`
-- Manual checks:
- - directory expand/collapse and refresh
- - large file navigation and cache reuse
- - watcher-driven updates in active file tabs
-
----
-
-### Handoff notes
-
-- Keep tree/data stores colocated with their mutation helpers.
-- Avoid changing persisted key names or cache key shapes in this pass.
-- Save broader API cleanups for a follow-up once modules are stable.
diff --git a/specs/13-file-tree-fetch-discipline.md b/specs/13-file-tree-fetch-discipline.md
deleted file mode 100644
index 7fe71fc7abc3..000000000000
--- a/specs/13-file-tree-fetch-discipline.md
+++ /dev/null
@@ -1,88 +0,0 @@
-## File tree fetches
-
-Make directory listing triggers explicit and minimal
-
----
-
-### Summary
-
-`packages/app/src/components/file-tree.tsx` currently invokes `file.tree.list(path)` from a generic effect in each tree instance. Even with inflight guards, this pattern causes avoidable list calls and makes load behavior harder to reason about. This spec tightens fetch triggers.
-
----
-
-### Goals
-
-- Avoid redundant list invocations from passive rerenders
-- Fetch directory data only when needed (mount + expansion + explicit refresh)
-- Keep tree behavior unchanged for users
-
----
-
-### Non-goals
-
-- Replacing recursive tree rendering with virtualization
-- Changing file-tree visual design
-- Backend/API changes for file listing
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/components/file-tree.tsx`
-
-This spec should not modify:
-
-- `packages/app/src/context/file.tsx`
-- `packages/app/src/pages/session.tsx`
-
----
-
-### Implementation plan
-
-1. Replace broad list effect with explicit triggers
-
-- Load root path on mount.
-- For nested directories, list only when:
- - node is expanded, or
- - parent explicitly requests refresh.
-
-2. Guard expansion-driven fetches
-
-- Keep `file.tree.expand(path)` as the primary source of truth for expansion fetches.
-- Ensure passive rerenders do not retrigger `list(path)` calls for already loaded dirs.
-
-3. Keep filter auto-expand behavior
-
-- Preserve existing "allowed filter" directory auto-expansion.
-- Ensure auto-expanded directories still fetch exactly once unless force refresh occurs.
-
----
-
-### Acceptance criteria
-
-- `file-tree.tsx` no longer calls `file.tree.list(path)` from an unscoped rerender effect.
-- Expanding a folder still loads its children correctly.
-- Filtering by `allowed` still opens and shows required parent directories.
-- No regressions in change/all tabs where `FileTree` is used.
-
----
-
-### Validation plan
-
-- Manual:
- - Expand/collapse deep directory trees repeatedly.
- - Switch between "changes" and "all" tree tabs.
- - Open review, click files, verify tree stays responsive.
-- Optional instrumentation:
- - count list calls per user action and compare before/after.
-
----
-
-### Risks and mitigations
-
-- Risk: directories fail to load when expansion timing changes.
- - Mitigation: rely on `expand()` path and verify for root + nested nodes.
-- Risk: filter-driven auto-expand misses one level.
- - Mitigation: keep existing auto-expand iteration and add regression checks.
diff --git a/specs/14-comments-aggregation-index.md b/specs/14-comments-aggregation-index.md
deleted file mode 100644
index 3afb1ecfefed..000000000000
--- a/specs/14-comments-aggregation-index.md
+++ /dev/null
@@ -1,87 +0,0 @@
-## Comments indexing
-
-Avoid repeated flatten+sort for comment aggregates
-
----
-
-### Summary
-
-`packages/app/src/context/comments.tsx` derives `all` by flattening all file comment arrays and sorting on every change. This is simple but can become expensive with many comments. We will maintain an indexed aggregate structure incrementally.
-
----
-
-### Goals
-
-- Keep `comments.list(file)` behavior unchanged
-- Make `comments.all()` retrieval near O(1) for reads
-- Preserve chronological ordering guarantees
-
----
-
-### Non-goals
-
-- Persisting comments in a new schema
-- Adding new comment metadata fields
-- UI changes for comment display
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/context/comments.tsx`
-- Optional tests for comments context
-
-This spec should not modify:
-
-- `packages/app/src/pages/session.tsx`
-- `packages/ui/src/components/line-comment.tsx`
-
----
-
-### Implementation plan
-
-1. Add aggregate index state
-
-- Maintain `commentsByFile` (existing) plus an `allComments` array in chronological order.
-- Keep both updated through the same mutator paths.
-
-2. Update mutators
-
-- `add`: append new comment to file list and aggregate list.
-- `remove`: remove from file list and aggregate list by id/file.
-- `clear`: reset both structures and focus/active state.
-
-3. Simplify selectors
-
-- `list(file)` reads file list directly.
-- `all()` returns pre-indexed aggregate list without per-read flatten+sort.
-
----
-
-### Acceptance criteria
-
-- `comments.all()` no longer flattens and sorts every reactive run.
-- Comment order stays chronological by `time`.
-- `add/remove/clear/focus/active` semantics remain unchanged.
-
----
-
-### Validation plan
-
-- Manual:
- - Add multiple comments across different files.
- - Remove one comment and verify both file-level and global views update correctly.
- - Submit prompt (which clears comments) and verify reset behavior.
-- Optional unit test:
- - add/remove/clear keeps aggregate ordering and integrity.
-
----
-
-### Risks and mitigations
-
-- Risk: aggregate list and per-file lists diverge.
- - Mitigation: funnel all writes through centralized mutators; avoid direct store writes elsewhere.
-- Risk: ID collision edge cases.
- - Mitigation: keep UUID creation unchanged and remove by `file + id` pair.
diff --git a/specs/14-server-health-and-row-dedupe.md b/specs/14-server-health-and-row-dedupe.md
deleted file mode 100644
index ded825180ae2..000000000000
--- a/specs/14-server-health-and-row-dedupe.md
+++ /dev/null
@@ -1,108 +0,0 @@
-## Server health and row dedupe
-
-Unify server health checks and deduplicate server-row UI logic.
-
----
-
-### Summary
-
-Server health logic is duplicated across multiple files, and server row rendering/truncation logic is repeated in both the status popover and server dialog. This creates drift risk and inconsistent behavior. This spec centralizes health checks and row rendering while preserving existing UX.
-
----
-
-### Goals
-
-- Introduce one shared server-health checker.
-- Use consistent timeout and error semantics in all server health call sites.
-- Deduplicate repeated server row truncation/tooltip behavior.
-- Keep current polling interval and status semantics unless explicitly changed.
-
----
-
-### Non-goals
-
-- No redesign of the status popover or server dialog.
-- No changes to server persistence model.
-- No broad refactor of unrelated status tabs (MCP/LSP/plugins).
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/components/dialog-select-server.tsx`
-- `packages/app/src/components/status-popover.tsx`
-- `packages/app/src/context/server.tsx`
-- New files under `packages/app/src/components/server/**` and/or `packages/app/src/utils/server-health.ts`
-
-This workstream must not edit:
-
-- `packages/app/src/components/terminal.tsx` (spec 15)
-- `packages/app/src/pages/session.tsx` and `packages/app/src/pages/layout.tsx` (specs 09/10)
-
----
-
-### Current state
-
-- Duplicate `checkHealth` implementation in:
- - `components/dialog-select-server.tsx`
- - `components/status-popover.tsx`
-- Similar health check logic in `context/server.tsx`.
-- Duplicate row truncation + resize listener logic in status and dialog server lists.
-
----
-
-### Proposed approach
-
-1. Add shared health utility:
-
-- `checkServerHealth(url, fetch, opts)`
-- one timeout strategy
-- one return shape: `{ healthy: boolean, version?: string }`
-
-2. Add shared server row primitive:
-
-- common rendering for status dot, truncated name/version handling, tooltip content
-- optional action slots for per-screen controls
-
-3. Adopt utility and row primitive in both consumers.
-
----
-
-### Phased steps
-
-1. Create `utils/server-health.ts` and migrate all health call sites.
-2. Create shared row component (`components/server/server-row.tsx`).
-3. Replace duplicated row logic in server dialog and status popover.
-4. Confirm polling and active/default server behavior still match existing UX.
-
----
-
-### Acceptance criteria
-
-- Exactly one app-level server health check implementation remains.
-- Server row truncation/tooltip behavior is shared, not duplicated.
-- No regressions when switching active/default server.
-- Existing status dot semantics are preserved.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Targeted e2e checks:
- - `e2e/status/status-popover.spec.ts`
- - `e2e/app/server-default.spec.ts`
-- Manual checks:
- - add/edit/remove server
- - blocked unhealthy server behavior
- - default server toggles and persistence
-
----
-
-### Handoff notes
-
-- Keep shared server row API minimal and composable.
-- Avoid introducing new global state for this refactor.
-- Prefer deterministic helper behavior over UI-specific branching inside the utility.
diff --git a/specs/15-prompt-input-modularization.md b/specs/15-prompt-input-modularization.md
deleted file mode 100644
index f80785f0ea00..000000000000
--- a/specs/15-prompt-input-modularization.md
+++ /dev/null
@@ -1,104 +0,0 @@
-## Prompt input split
-
-Modularize `prompt-input.tsx` without behavior changes
-
----
-
-### Summary
-
-`packages/app/src/components/prompt-input.tsx` is a very large component that combines editor DOM parsing, popovers, history, drag/drop + paste uploads, worktree/session creation, optimistic messages, and send/abort flow. This spec splits it into focused modules so future changes are safer.
-
----
-
-### Goals
-
-- Reduce `prompt-input.tsx` complexity and file size
-- Extract cohesive logic into testable hooks/helpers
-- Keep runtime behavior and UX unchanged
-
----
-
-### Non-goals
-
-- Replacing contenteditable editor approach
-- Major UX redesign of composer controls
-- API contract changes for prompt submission
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/components/prompt-input.tsx`
-- New files under `packages/app/src/components/prompt-input/*`
-
-This spec should not modify:
-
-- `packages/app/src/pages/session.tsx`
-- `packages/app/src/context/prompt.tsx` (except minor type-only imports if needed)
-
----
-
-### Implementation plan
-
-1. Extract editor DOM helpers
-
-- Move pure DOM/selection helpers into `prompt-input/editor-dom.ts`:
- - `createTextFragment`
- - `getNodeLength`
- - `getTextLength`
- - cursor get/set helpers
-
-2. Extract history controller
-
-- Move prompt history read/write/navigation logic into `prompt-input/history.ts` hook.
-- Keep existing persisted keys and history semantics unchanged.
-
-3. Extract attachment interactions
-
-- Move image/file paste + drag/drop + file-input attachment flows to `prompt-input/attachments.ts` hook.
-
-4. Extract submit pipeline
-
-- Move send/abort/optimistic message pipeline to `prompt-input/submit.ts` service/hook.
-- Keep existing error toasts, worktree handling, and rollback behavior.
-
-5. Keep composition shell stable
-
-- `PromptInput` component remains the integration shell that wires hooks + JSX.
-- Preserve exported component API and props.
-
----
-
-### Acceptance criteria
-
-- `prompt-input.tsx` becomes primarily orchestration + view code.
-- Extracted modules contain the heavy imperative logic.
-- All existing behaviors remain intact:
- - slash and @ popovers
- - history up/down navigation
- - image attach/paste/drag-drop
- - shell mode submit/abort
- - optimistic message + rollback on failure
-
----
-
-### Validation plan
-
-- Manual regression checklist:
- - type prompt, submit, stop, retry
- - use `/` command selection and `@` selector
- - history navigation with arrows
- - paste image, drag image, remove attachment
- - start in new session + worktree create path
- - failure path restores prompt and context comments
-
----
-
-### Risks and mitigations
-
-- Risk: subtle ordering changes in submit rollback logic.
- - Mitigation: migrate logic mechanically first, then cleanup.
-- Risk: editor selection bugs after helper extraction.
- - Mitigation: keep existing cursor helpers unchanged and add focused manual checks.
diff --git a/specs/15-runtime-adapter-type-safety.md b/specs/15-runtime-adapter-type-safety.md
deleted file mode 100644
index 99c8f6d303bd..000000000000
--- a/specs/15-runtime-adapter-type-safety.md
+++ /dev/null
@@ -1,106 +0,0 @@
-## Runtime adapter type safety
-
-Reduce unsafe casts at browser and third-party integration boundaries.
-
----
-
-### Summary
-
-Several integration points rely on `as any` or `unknown as` casts (terminal internals, speech recognition, add-on internals, generic trigger props). This spec introduces typed adapters and narrow interfaces to improve maintainability and make type errors actionable.
-
----
-
-### Goals
-
-- Remove or significantly reduce unsafe casts in scoped files.
-- Introduce explicit adapter interfaces around unstable third-party APIs.
-- Preserve behavior with no UX changes.
-- Improve maintainability of terminal and speech integrations.
-
----
-
-### Non-goals
-
-- No server health dedupe work (owned by spec 14).
-- No large architectural changes to terminal or speech subsystems.
-- No changes to business logic semantics.
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/components/terminal.tsx`
-- `packages/app/src/utils/speech.ts`
-- `packages/app/src/addons/serialize.ts`
-- `packages/app/src/components/dialog-select-model.tsx`
-- New utility files under `packages/app/src/utils/**` related to adapter typing
-
-This workstream must not edit:
-
-- `components/dialog-select-server.tsx`, `components/status-popover.tsx`, `context/server.tsx` (spec 14)
-- `components/prompt-input.tsx` (spec 11)
-
----
-
-### Current state
-
-- Explicit `as any` appears in `serialize.ts` and `speech.ts`.
-- Multiple `unknown as` casts in `terminal.tsx` for option/disposable access.
-- Generic trigger props in `dialog-select-model.tsx` use `as any` spread.
-
----
-
-### Proposed approach
-
-1. Add narrow adapter types for third-party internals:
-
-- terminal option setter/disposable handles
-- speech recognition constructor on `window`
-- serialize addon internal terminal buffer access
-
-2. Introduce tiny helper guards/utilities:
-
-- `isDisposable(value): value is { dispose(): void }`
-- `hasSetOption(value): value is { setOption(...): void }`
-
-3. Replace broad casts with adapter functions and runtime checks.
-
----
-
-### Phased steps
-
-1. Refactor terminal helpers (`setOption`, disposal cleanups) to typed guards.
-2. Refactor speech recognition window access to typed constructor lookup.
-3. Replace `serialize.ts` `as any` internals with explicit local interface.
-4. Remove `dialog-select-model.tsx` `as any` trigger props cast via stricter generic typing.
-
----
-
-### Acceptance criteria
-
-- No `as any` remains in the scoped files (or document unavoidable cases inline).
-- `unknown as` usage in scoped files is minimized and justified.
-- Typecheck passes with no new suppression comments.
-- Runtime behavior remains unchanged.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Targeted e2e checks:
- - `e2e/terminal/terminal.spec.ts`
- - `e2e/models/model-picker.spec.ts`
-- Manual checks:
- - terminal open/connect/resize/cleanup
- - speech start/stop and interim/final behavior
-
----
-
-### Handoff notes
-
-- Prefer small typed wrapper functions over inline complex narrowing.
-- Keep adapter names explicit and local to their integration point.
-- If a cast cannot be removed safely, add a short comment describing why.
diff --git a/specs/16-i18n-hardening-and-parity.md b/specs/16-i18n-hardening-and-parity.md
deleted file mode 100644
index 58cbeeaf8604..000000000000
--- a/specs/16-i18n-hardening-and-parity.md
+++ /dev/null
@@ -1,107 +0,0 @@
-## i18n hardening and parity
-
-Strengthen locale correctness and remove remaining hardcoded copy.
-
----
-
-### Summary
-
-The app has broad translation coverage but still has maintainability gaps: locale dictionaries are typed as `Partial`, some non-English dictionaries contain English values for specific keys, and a few user-facing strings are still hardcoded in components/pages. This spec hardens i18n guarantees and cleans up remaining drift.
-
----
-
-### Goals
-
-- Enforce stricter dictionary key parity across all app locales.
-- Remove known English fallback strings from non-English locale files.
-- Localize remaining hardcoded user-facing strings in scoped files.
-- Keep existing localization architecture (`useLanguage().t(...)`) intact.
-
----
-
-### Non-goals
-
-- No translation quality rewrite for all strings.
-- No locale expansion beyond existing languages.
-- No changes to non-user-facing log/diagnostic strings.
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/src/context/language.tsx`
-- `packages/app/src/i18n/*.ts`
-- `packages/app/src/components/dialog-custom-provider.tsx`
-- `packages/app/src/pages/directory-layout.tsx`
-
-This workstream must not edit:
-
-- `pages/session.tsx`, `pages/layout.tsx`, `components/prompt-input.tsx`
-- server/terminal integration files owned by specs 14/15
-
----
-
-### Current state
-
-- Locale files are large and manually maintained.
-- Non-English locales are typed with `Partial>`, which allows silent missing keys.
-- Known untranslated strings exist for keys like:
- - `command.session.previous.unseen`
- - `command.session.next.unseen`
-- Some user-facing strings remain hardcoded in scoped files.
-
----
-
-### Proposed approach
-
-1. Tighten locale typing:
-
-- Move from `Partial>` to stricter parity enforcement.
-- Keep `en.ts` as source-of-truth key set.
-
-2. Fix known untranslated key values in non-English dictionaries.
-
-3. Localize scoped hardcoded strings by adding translation keys and using `language.t(...)`.
-
----
-
-### Phased steps
-
-1. Add/adjust shared locale typing pattern for parity safety.
-2. Update all locale files to satisfy stricter typing.
-3. Translate known English carry-over keys in non-English dictionaries.
-4. Replace hardcoded copy in:
-
-- `components/dialog-custom-provider.tsx`
-- `pages/directory-layout.tsx`
-
-5. Run typecheck and parity checks.
-
----
-
-### Acceptance criteria
-
-- Locale files enforce full key parity against `en` (compile-time).
-- No known English carry-over values remain for the targeted keys in non-English locales.
-- Scoped hardcoded user-facing strings are replaced with translation keys.
-- Typecheck passes.
-
----
-
-### Validation plan
-
-- Typecheck: `bun run typecheck` (from `packages/app`).
-- Grep sanity checks:
- - targeted keys no longer English in non-English locales
- - scoped files no longer contain hardcoded user-facing copy
-- Manual spot checks in at least 2 locales (for example: `de`, `zh`).
-
----
-
-### Handoff notes
-
-- Keep key naming consistent with existing conventions.
-- Avoid broad copy changes outside scoped files to reduce review surface.
-- If translation wording is uncertain, keep it simple and literal for now; quality passes can follow.
diff --git a/specs/16-terminal-cache-key-clarity.md b/specs/16-terminal-cache-key-clarity.md
deleted file mode 100644
index 667f4f3c3cc3..000000000000
--- a/specs/16-terminal-cache-key-clarity.md
+++ /dev/null
@@ -1,82 +0,0 @@
-## Terminal cache scope
-
-Clarify workspace-only terminal cache semantics
-
----
-
-### Summary
-
-`packages/app/src/context/terminal.tsx` accepts `(dir, session)` but currently keys cache entries as `${dir}:${WORKSPACE_KEY}`. The behavior is workspace-scoped, but the API shape suggests session-scoped caching. This spec aligns naming and implementation to avoid confusion and future bugs.
-
----
-
-### Goals
-
-- Make terminal cache scope explicit (workspace-scoped)
-- Remove misleading unused session-keying surface
-- Preserve existing runtime behavior
-
----
-
-### Non-goals
-
-- Changing terminal persistence behavior
-- Moving terminals to per-session isolation
-- UI changes to terminal tabs
-
----
-
-### Parallel execution contract
-
-This spec owns:
-
-- `packages/app/src/context/terminal.tsx`
-
-This spec should not modify:
-
-- `packages/app/src/pages/session.tsx`
-- `packages/app/src/components/session/session-sortable-terminal-tab.tsx`
-
----
-
-### Implementation plan
-
-1. Rename internals for clarity
-
-- Update internal function names/variables from session-oriented to workspace-oriented where applicable.
-
-2. Remove unused session cache-key parametering
-
-- Simplify `load`/factory signatures so keying intent is explicit.
-- Keep key format workspace-only by directory.
-
-3. Add inline documentation
-
-- Add short comment near cache key creation clarifying why terminals are shared across sessions in the same workspace.
-
-4. Keep behavior stable
-
-- Ensure active terminal, tab order, clone/new/close behavior remain unchanged.
-
----
-
-### Acceptance criteria
-
-- No unused session-derived cache key logic remains.
-- Code communicates workspace-scoped terminal lifecycle clearly.
-- No functional changes to terminal operations.
-
----
-
-### Validation plan
-
-- Manual:
- - Create multiple terminals, navigate between sessions in same workspace, confirm state continuity.
- - Switch workspace directory, confirm separate terminal state.
-
----
-
-### Risks and mitigations
-
-- Risk: accidental behavior change to session-scoped terminals.
- - Mitigation: keep cache key unchanged; refactor naming/signatures only.
diff --git a/specs/17-unit-test-foundation.md b/specs/17-unit-test-foundation.md
deleted file mode 100644
index 2b6a9394539e..000000000000
--- a/specs/17-unit-test-foundation.md
+++ /dev/null
@@ -1,101 +0,0 @@
-## Unit test foundation
-
-Establish reliable unit coverage for core app logic.
-
----
-
-### Summary
-
-`packages/app` is still e2e-first, but recent refactoring added a first wave of active source-unit tests (session helpers/scroll spy, prompt-input modules, file-tree, comments/layout/terminal/file context, and scoped-cache). This spec focuses on turning that momentum into a stable, explicit unit-test baseline in CI/local and unblocking the remaining skipped legacy suites.
-
----
-
-### Goals
-
-- Add a clear unit-test command for app source tests.
-- Unskip and stabilize existing skipped unit tests.
-- Add fast tests for high-value pure logic.
-- Keep unit suite independent of full e2e environment.
-
----
-
-### Non-goals
-
-- No replacement of e2e tests.
-- No broad product-code refactors unless required to make logic testable.
-- No flaky browser-automation tests added here.
-
----
-
-### Parallel ownership (important)
-
-This workstream owns:
-
-- `packages/app/package.json` (test scripts only)
-- `packages/app/happydom.ts` (if harness tweaks are needed)
-- `packages/app/src/**/*.test.ts`
-- `packages/app/src/**/*.test.tsx`
-
-This workstream should avoid editing product code files owned by other specs, unless a tiny testability export is strictly required.
-
----
-
-### Current state
-
-- Active unit coverage now exists across several `src/**/*.test.*` files (including context, pages/session, components/prompt-input, and utils).
-- Remaining skipped legacy suites:
- - `src/context/layout-scroll.test.ts` (`test.skip`)
- - `src/addons/serialize.test.ts` (`describe.skip`)
-- `package.json` scripts still focus on Playwright e2e and do not expose a dedicated `test:unit` entrypoint.
-
----
-
-### Proposed approach
-
-1. Add dedicated unit-test script(s), for example:
-
-- `test:unit` using Bun test + happydom preload where needed.
-
-2. Unskip and stabilize remaining skipped legacy tests:
-
-- make `layout-scroll.test.ts` deterministic
-- enable a reliable subset of `serialize.test.ts` (or split smoke vs heavy integration cases)
-
-3. Add/expand fast unit tests for high-value pure logic not yet covered:
-
-- keybind parsing/formatting/matching (`context/command.tsx` exports)
-- worktree state machine (`utils/worktree.ts`)
-
----
-
-### Phased steps
-
-1. Wire `test:unit` in `package.json`.
-2. Make existing skipped tests runnable and stable.
-3. Add at least 2 new unit test files for core pure logic.
-4. Ensure unit suite can run standalone without Playwright server setup.
-
----
-
-### Acceptance criteria
-
-- `bun run test:unit` exists and passes locally.
-- No full-file `describe.skip`/`test.skip` remains in `packages/app/src/**/*.test.*` (unless documented as intentionally quarantined with reason).
-- Unit suite includes meaningful assertions for keybind + worktree logic.
-- Runtime for unit suite remains fast (target: under 15 seconds locally, excluding first install).
-
----
-
-### Validation plan
-
-- Run: `bun run test:unit`.
-- Run: `bun run typecheck`.
-- Verify unit tests can execute without starting full app/backend servers.
-
----
-
-### Handoff notes
-
-- Keep tests implementation-focused, not duplicated business logic.
-- Avoid mocks where practical; prefer real small-scope code paths.
-- If integration-heavy serialize cases remain flaky, separate them into a clearly named non-default test target.
diff --git a/specs/18-parallel-workstream-map.md b/specs/18-parallel-workstream-map.md
deleted file mode 100644
index 8a769f673c9e..000000000000
--- a/specs/18-parallel-workstream-map.md
+++ /dev/null
@@ -1,51 +0,0 @@
-## Parallel workstream map
-
-Use this as the assignment sheet for running multiple agents at once.
-
----
-
-### Workstreams
-
-1. `specs/09-session-page-decomposition.md`
-2. `specs/10-layout-page-decomposition.md`
-3. `specs/11-prompt-input-and-optimistic-state.md`
-4. `specs/12-global-sync-domain-split.md`
-5. `specs/13-file-context-domain-split.md`
-6. `specs/14-server-health-and-row-dedupe.md`
-7. `specs/15-runtime-adapter-type-safety.md`
-8. `specs/16-i18n-hardening-and-parity.md`
-9. `specs/17-unit-test-foundation.md`
-
----
-
-### File-ownership matrix
-
-| Spec | Primary ownership | Avoid editing |
-| ---- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
-| 09 | `pages/session.tsx`, `pages/session/**` | `pages/layout.tsx`, `components/prompt-input.tsx`, `context/global-sync.tsx`, `context/file.tsx` |
-| 10 | `pages/layout.tsx`, `pages/layout/**` | `pages/session.tsx`, `components/prompt-input.tsx`, `context/global-sync.tsx` |
-| 11 | `components/prompt-input.tsx`, `components/prompt-input/**`, `context/sync.tsx` (optimistic API only) | `pages/session.tsx`, `pages/layout.tsx`, `context/global-sync.tsx`, `context/file.tsx` |
-| 12 | `context/global-sync.tsx`, `context/global-sync/**` | `context/file.tsx`, `components/prompt-input.tsx`, page files |
-| 13 | `context/file.tsx`, `context/file/**`, `utils/scoped-cache.ts` (only when file-view cache extraction needs it) | `context/global-sync.tsx`, `components/prompt-input.tsx`, page files |
-| 14 | `components/dialog-select-server.tsx`, `components/status-popover.tsx`, `context/server.tsx`, shared server utility/component | terminal/speech/serialize files |
-| 15 | `components/terminal.tsx`, `utils/speech.ts`, `addons/serialize.ts`, `components/dialog-select-model.tsx`, adapter utilities | server status/dialog/context files |
-| 16 | `context/language.tsx`, `i18n/*.ts`, `components/dialog-custom-provider.tsx`, `pages/directory-layout.tsx` | major page/context refactors |
-| 17 | `package.json` (test scripts), `happydom.ts`, `src/**/*.test.*` | product code files in other specs unless strictly needed |
-
----
-
-### Recommended execution order (if all start together)
-
-- Start all 9 in parallel.
-- Merge low-conflict streams first: 12, 13, 14, 15, 16, 17.
-- Then merge 09, 10, 11 (largest diff sizes and highest rebase probability).
-
----
-
-### Integration checkpoint
-
-After all streams merge, run a full verification pass in `packages/app`:
-
-- `bun run typecheck`
-- `bun run test:unit` (from spec 17)
-- targeted e2e smoke for session/layout/prompt/server/terminal flows
diff --git a/specs/parallel-agent-plan.md b/specs/parallel-agent-plan.md
deleted file mode 100644
index f7398c669072..000000000000
--- a/specs/parallel-agent-plan.md
+++ /dev/null
@@ -1,59 +0,0 @@
-## Parallel agent plan
-
-Execution map for session-page improvement concerns
-
----
-
-### New specs added
-
-- `specs/09-session-page-hot-paths.md`
-- `specs/10-file-content-eviction-accounting.md`
-- `specs/11-layout-view-tabs-reactivity.md`
-- `specs/12-session-context-metrics-shared.md`
-- `specs/13-file-tree-fetch-discipline.md`
-- `specs/14-comments-aggregation-index.md`
-- `specs/15-prompt-input-modularization.md`
-- `specs/16-terminal-cache-key-clarity.md`
-
----
-
-### Existing related specs
-
-- `specs/04-scroll-spy-optimization.md` (session scroll-spy concern)
-- `specs/05-modularize-and-dedupe.md` (broad modularization roadmap)
-
----
-
-### Parallel-safe batching
-
-Batch A (run one at a time, shared `session.tsx` surface):
-
-- `specs/09-session-page-hot-paths.md`
-- `specs/04-scroll-spy-optimization.md`
-
-Batch B (parallel with each other and with Batch A):
-
-- `specs/10-file-content-eviction-accounting.md`
-- `specs/11-layout-view-tabs-reactivity.md`
-- `specs/12-session-context-metrics-shared.md`
-- `specs/13-file-tree-fetch-discipline.md`
-- `specs/14-comments-aggregation-index.md`
-- `specs/15-prompt-input-modularization.md`
-- `specs/16-terminal-cache-key-clarity.md`
-
-Batch C (broad follow-up after focused specs land):
-
-- `specs/05-modularize-and-dedupe.md`
-
----
-
-### Suggested assignment
-
-1. Agent A: `specs/09-session-page-hot-paths.md`
-2. Agent B: `specs/10-file-content-eviction-accounting.md`
-3. Agent C: `specs/11-layout-view-tabs-reactivity.md`
-4. Agent D: `specs/12-session-context-metrics-shared.md`
-5. Agent E: `specs/13-file-tree-fetch-discipline.md`
-6. Agent F: `specs/14-comments-aggregation-index.md`
-7. Agent G: `specs/15-prompt-input-modularization.md`
-8. Agent H: `specs/16-terminal-cache-key-clarity.md`
diff --git a/specs/perf-roadmap.md b/specs/perf-roadmap.md
deleted file mode 100644
index 7d5ac93cbe54..000000000000
--- a/specs/perf-roadmap.md
+++ /dev/null
@@ -1,196 +0,0 @@
-## Performance roadmap
-
-Sequenced delivery plan for app scalability + maintainability
-
----
-
-### Objective
-
-Deliver the top 5 app improvements (performance + long-term flexibility) in a safe, incremental sequence that:
-
-- minimizes regression risk
-- keeps changes reviewable (small PRs)
-- provides escape hatches (flags / caps)
-- validates improvements with targeted measurements
-
-This roadmap ties together:
-
-- `specs/01-persist-payload-limits.md`
-- `specs/02-cache-eviction.md`
-- `specs/03-request-throttling.md`
-- `specs/04-scroll-spy-optimization.md`
-- `specs/05-modularize-and-dedupe.md`
-
----
-
-### Guiding principles
-
-- Prefer “guardrails first”: add caps/limits and do no harm, then optimize.
-- Always ship behind flags if behavior changes (especially persistence and eviction).
-- Optimize at chokepoints (SDK call wrappers, storage wrappers, scroll-spy module) instead of fixing symptoms at every call site.
-- Make “hot paths” explicitly measurable in dev (e.g. via `packages/app/src/utils/perf.ts`).
-
----
-
-### Phase 0 — Baseline + flags (prep)
-
-**Goal:** make later changes safe to land and easy to revert.
-
-**Deliverables**
-
-- Feature-flag plumbing for:
- - persistence payload limits (`persist.payloadLimits`)
- - request debouncing/latest-only (`requests.*`)
- - cache eviction (`cache.eviction.*`)
- - optimized scroll spy (`session.scrollSpyOptimized`)
- - shared scoped cache (`scopedCache.shared`)
-- Dev-only counters/logs for:
- - persist oversize detections
- - request aborts/stale drops
- - eviction counts and retained sizes
- - scroll-spy compute time per second
-
-**Exit criteria**
-
-- Flags exist but default “off” for behavior changes.
-- No user-visible behavior changes.
-
-**Effort / risk**: `S–M` / low
-
----
-
-### Phase 1 — Stop the worst “jank generators” (storage + request storms)
-
-**Goal:** remove the highest-frequency sources of main-thread blocking and redundant work.
-
-**Work items**
-
-- Implement file search debounce + stale-result protection
- - Spec: `specs/03-request-throttling.md`
- - Start with file search only (lowest risk, easy to observe).
-- Add persistence payload size checks + warnings (no enforcement yet)
- - Spec: `specs/01-persist-payload-limits.md`
- - Focus on detecting oversized keys and preventing repeated write attempts.
-- Ship prompt-history “strip image dataUrl” behind a flag
- - Spec: `specs/01-persist-payload-limits.md`
- - Keep image metadata placeholders so UI remains coherent.
-
-**Exit criteria**
-
-- Fast typing in file search generates at most 1 request per debounce window.
-- Oversize persisted keys are detected and do not cause repeated blocking writes.
-- Prompt history reload does not attempt to restore base64 `dataUrl` on web when flag enabled.
-
-**Effort / risk**: `M` / low–med
-
----
-
-### Phase 2 — Bound memory growth (in-memory eviction)
-
-**Goal:** stabilize memory footprint for long-running sessions and “project hopping”.
-
-**Work items**
-
-- Introduce shared LRU/TTL cache helper
- - Spec: `specs/02-cache-eviction.md`
-- Apply eviction to file contents cache first
- - Spec: `specs/02-cache-eviction.md`
- - Pin open tabs / active file to prevent flicker.
-- Add conservative eviction for global-sync per-directory child stores
- - Spec: `specs/02-cache-eviction.md`
- - Ensure evicted children are fully disposed.
-- (Optional) session/message eviction if memory growth persists after the above
- - Spec: `specs/02-cache-eviction.md`
-
-**Exit criteria**
-
-- Opening many files does not continuously increase JS heap without bound.
-- Switching across many directories does not keep all directory stores alive indefinitely.
-- Eviction never removes currently active session/file content.
-
-**Effort / risk**: `M–L` / med
-
----
-
-### Phase 3 — Large session scroll scalability (scroll spy)
-
-**Goal:** keep scrolling smooth as message count increases.
-
-**Work items**
-
-- Extract scroll-spy logic into a dedicated module (no behavior change)
- - Spec: `specs/04-scroll-spy-optimization.md`
-- Implement IntersectionObserver tracking behind flag
- - Spec: `specs/04-scroll-spy-optimization.md`
-- Add binary search fallback for non-observer environments
- - Spec: `specs/04-scroll-spy-optimization.md`
-
-**Exit criteria**
-
-- Scroll handler no longer calls `querySelectorAll('[data-message-id]')` on every scroll tick.
-- Long sessions (hundreds of messages) maintain smooth scrolling.
-- Active message selection remains stable during streaming/layout shifts.
-
-**Effort / risk**: `M` / med
-
----
-
-### Phase 4 — “Make it easy to keep fast” (modularity + dedupe)
-
-**Goal:** reduce maintenance cost and make future perf work cheaper.
-
-**Work items**
-
-- Introduce shared scoped-cache utility and adopt in one low-risk area
- - Spec: `specs/05-modularize-and-dedupe.md`
-- Incrementally split mega-components (one PR per extraction)
- - Spec: `specs/05-modularize-and-dedupe.md`
- - Prioritize extracting:
- - session scroll/backfill logic
- - prompt editor model/history
- - layout event/shortcut wiring
-- Remove duplicated patterns after confidence + one release cycle
-
-**Exit criteria**
-
-- Each mega-file drops below a target size (suggestion):
- - `session.tsx` < ~800 LOC
- - `prompt-input.tsx` < ~900 LOC
-- “Scoped cache” has a single implementation used across contexts.
-- Future perf fixes land in isolated modules with minimal cross-cutting change.
-
-**Effort / risk**: `L` / med–high
-
----
-
-### Recommended PR slicing (keeps reviews safe)
-
-- PR A: add request helpers + file search debounce (flagged)
-- PR B: persist size detection + logs (no behavior change)
-- PR C: prompt history strip images (flagged)
-- PR D: cache helper + file content eviction (flagged)
-- PR E: global-sync child eviction (flagged)
-- PR F: scroll-spy extraction (no behavior change)
-- PR G: optimized scroll-spy implementation (flagged)
-- PR H+: modularization PRs (small, mechanical refactors)
-
----
-
-### Rollout strategy
-
-- Keep defaults conservative and ship flags “off” first.
-- Enable flags internally (dev builds) to gather confidence.
-- Flip defaults in this order:
- 1. file search debounce
- 2. prompt-history image stripping
- 3. file-content eviction
- 4. global-sync child eviction
- 5. optimized scroll-spy
-
----
-
-### Open questions
-
-- What are acceptable defaults for storage caps and cache sizes for typical OpenCode usage?
-- Does the SDK support `AbortSignal` end-to-end for cancellation, or do we rely on stale-result dropping?
-- Should web and desktop persistence semantics be aligned (even if desktop has async storage available)?
From 6490fb01489bfa5644349d650f978c80d0ab7365 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:21:59 -0600
Subject: [PATCH 058/399] fix(console): zen workspace translation cleanup
---
packages/console/app/src/i18n/ar.ts | 64 +++++++-------
packages/console/app/src/i18n/da.ts | 66 +++++++-------
packages/console/app/src/i18n/de.ts | 68 +++++++-------
packages/console/app/src/i18n/it.ts | 50 +++++------
packages/console/app/src/i18n/zh.ts | 94 ++++++++++----------
packages/console/app/src/i18n/zht.ts | 94 ++++++++++----------
specs/19-workspace-i18n-audit-overview.md | 76 ++++++++++++++++
specs/20-workspace-i18n-audit-ar.md | 99 +++++++++++++++++++++
specs/21-workspace-i18n-audit-da.md | 103 ++++++++++++++++++++++
specs/22-workspace-i18n-audit-de.md | 95 ++++++++++++++++++++
specs/23-workspace-i18n-audit-it.md | 86 ++++++++++++++++++
11 files changed, 677 insertions(+), 218 deletions(-)
create mode 100644 specs/19-workspace-i18n-audit-overview.md
create mode 100644 specs/20-workspace-i18n-audit-ar.md
create mode 100644 specs/21-workspace-i18n-audit-da.md
create mode 100644 specs/22-workspace-i18n-audit-de.md
create mode 100644 specs/23-workspace-i18n-audit-it.md
diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts
index 8ba7c37e2dfb..4e3206715c20 100644
--- a/packages/console/app/src/i18n/ar.ts
+++ b/packages/console/app/src/i18n/ar.ts
@@ -351,8 +351,8 @@ export const dict = {
"changelog.empty":
"\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u064a \u0625\u062f\u062e\u0627\u0644\u0627\u062a \u0641\u064a \u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a.",
"changelog.viewJson": "\u0639\u0631\u0636 JSON",
- "workspace.nav.zen": "زين",
- "workspace.nav.apiKeys": "API المفاتيح",
+ "workspace.nav.zen": "Zen",
+ "workspace.nav.apiKeys": "مفاتيح API",
"workspace.nav.members": "أعضاء",
"workspace.nav.billing": "الفواتير",
"workspace.nav.settings": "إعدادات",
@@ -365,14 +365,14 @@ export const dict = {
"workspace.newUser.feature.quality.title": "أعلى جودة",
"workspace.newUser.feature.quality.body":
"الوصول إلى النماذج التي تم تكوينها لتحقيق الأداء الأمثل - لا يوجد تخفيضات أو توجيه إلى موفري الخدمة الأرخص.",
- "workspace.newUser.feature.lockin.title": "لا يوجد قفل",
+ "workspace.newUser.feature.lockin.title": "بدون احتجاز بمزوّد واحد",
"workspace.newUser.feature.lockin.body":
"استخدم Zen مع أي وكيل ترميز، واستمر في استخدام موفري الخدمات الآخرين مع opencode وقتما تشاء.",
"workspace.newUser.copyApiKey": "انسخ مفتاح API",
"workspace.newUser.copyKey": "نسخ المفتاح",
"workspace.newUser.copied": "منسوخ!",
"workspace.newUser.step.enableBilling": "تمكين الفوترة",
- "workspace.newUser.step.login.before": "يجري",
+ "workspace.newUser.step.login.before": "شغّل",
"workspace.newUser.step.login.after": "وحدد opencode",
"workspace.newUser.step.pasteKey": "الصق مفتاح API الخاص بك",
"workspace.newUser.step.models.before": "ابدأ opencode ثم قم بالتشغيل",
@@ -390,7 +390,7 @@ export const dict = {
"workspace.providers.saving": "توفير...",
"workspace.providers.save": "يحفظ",
"workspace.providers.table.provider": "مزود",
- "workspace.providers.table.apiKey": "API المفتاح",
+ "workspace.providers.table.apiKey": "مفتاح API",
"workspace.usage.title": "تاريخ الاستخدام",
"workspace.usage.subtitle": "استخدام وتكاليف API الأخيرة.",
"workspace.usage.empty": "قم بإجراء أول مكالمة API للبدء.",
@@ -398,25 +398,25 @@ export const dict = {
"workspace.usage.table.model": "نموذج",
"workspace.usage.table.input": "مدخل",
"workspace.usage.table.output": "الإخراج",
- "workspace.usage.table.cost": "يكلف",
+ "workspace.usage.table.cost": "التكلفة",
"workspace.usage.breakdown.input": "مدخل",
"workspace.usage.breakdown.cacheRead": "قراءة ذاكرة التخزين المؤقت",
"workspace.usage.breakdown.cacheWrite": "كتابة ذاكرة التخزين المؤقت",
"workspace.usage.breakdown.output": "الإخراج",
"workspace.usage.breakdown.reasoning": "المنطق",
"workspace.usage.subscription": "الاشتراك (${{amount}})",
- "workspace.cost.title": "يكلف",
+ "workspace.cost.title": "التكلفة",
"workspace.cost.subtitle": "تكاليف الاستخدام مقسمة حسب النموذج.",
- "workspace.cost.allModels": "جميع الموديلات",
+ "workspace.cost.allModels": "جميع النماذج",
"workspace.cost.allKeys": "جميع المفاتيح",
"workspace.cost.deletedSuffix": "(محذوف)",
"workspace.cost.empty": "لا توجد بيانات استخدام متاحة للفترة المحددة.",
- "workspace.cost.subscriptionShort": "الفرعية",
- "workspace.keys.title": "API المفاتيح",
+ "workspace.cost.subscriptionShort": "اشتراك",
+ "workspace.keys.title": "مفاتيح API",
"workspace.keys.subtitle": "إدارة مفاتيح API الخاصة بك للوصول إلى خدمات opencode.",
"workspace.keys.create": "قم بإنشاء مفتاح API",
"workspace.keys.placeholder": "أدخل اسم المفتاح",
- "workspace.keys.empty": "قم بإنشاء مفتاح opencode للبوابة API",
+ "workspace.keys.empty": "أنشئ مفتاح API لبوابة opencode",
"workspace.keys.table.name": "اسم",
"workspace.keys.table.key": "مفتاح",
"workspace.keys.table.createdBy": "تم الإنشاء بواسطة",
@@ -442,14 +442,14 @@ export const dict = {
"workspace.members.table.email": "بريد إلكتروني",
"workspace.members.table.role": "دور",
"workspace.members.table.monthLimit": "حد الشهر",
- "workspace.members.role.admin": "مسؤل",
+ "workspace.members.role.admin": "مسؤول",
"workspace.members.role.adminDescription": "يمكن إدارة النماذج، والأعضاء، والفواتير",
"workspace.members.role.member": "عضو",
"workspace.members.role.memberDescription": "يمكنهم فقط إنشاء مفاتيح API لأنفسهم",
"workspace.settings.title": "إعدادات",
"workspace.settings.subtitle": "قم بتحديث اسم مساحة العمل الخاصة بك وتفضيلاتك.",
"workspace.settings.workspaceName": "اسم مساحة العمل",
- "workspace.settings.defaultName": "تقصير",
+ "workspace.settings.defaultName": "الافتراضي",
"workspace.settings.updating": "جارٍ التحديث...",
"workspace.settings.save": "يحفظ",
"workspace.settings.edit": "يحرر",
@@ -461,37 +461,37 @@ export const dict = {
"workspace.billing.add": "أضف $",
"workspace.billing.enterAmount": "أدخل المبلغ",
"workspace.billing.loading": "تحميل...",
- "workspace.billing.addAction": "يضيف",
+ "workspace.billing.addAction": "إضافة",
"workspace.billing.addBalance": "إضافة الرصيد",
- "workspace.billing.linkedToStripe": "مرتبطة بالشريط",
- "workspace.billing.manage": "يدير",
+ "workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
+ "workspace.billing.manage": "إدارة",
"workspace.billing.enable": "تمكين الفوترة",
"workspace.monthlyLimit.title": "الحد الشهري",
"workspace.monthlyLimit.subtitle": "قم بتعيين حد الاستخدام الشهري لحسابك.",
"workspace.monthlyLimit.placeholder": "50",
- "workspace.monthlyLimit.setting": "جلسة...",
+ "workspace.monthlyLimit.setting": "جارٍ التعيين...",
"workspace.monthlyLimit.set": "تعيين",
"workspace.monthlyLimit.edit": "تحرير الحد",
"workspace.monthlyLimit.noLimit": "لم يتم تعيين حد الاستخدام.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي ل",
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
- "workspace.reload.title": "إعادة التحميل التلقائي",
- "workspace.reload.disabled.before": "إعادة التحميل التلقائي هو",
- "workspace.reload.disabled.state": "عاجز",
- "workspace.reload.disabled.after": "تمكين إعادة التحميل تلقائيًا عندما يكون الرصيد منخفضًا.",
- "workspace.reload.enabled.before": "إعادة التحميل التلقائي هو",
+ "workspace.reload.title": "إعادة الشحن التلقائي",
+ "workspace.reload.disabled.before": "إعادة الشحن التلقائي",
+ "workspace.reload.disabled.state": "معطّل",
+ "workspace.reload.disabled.after": "فعّلها لإعادة شحن الرصيد تلقائيًا عندما يكون منخفضًا.",
+ "workspace.reload.enabled.before": "إعادة الشحن التلقائي",
"workspace.reload.enabled.state": "ممكّن",
- "workspace.reload.enabled.middle": "سنقوم بإعادة التحميل",
+ "workspace.reload.enabled.middle": "سنعيد شحن رصيدك بمبلغ",
"workspace.reload.processingFee": "رسوم المعالجة",
- "workspace.reload.enabled.after": "عندما يصل التوازن",
+ "workspace.reload.enabled.after": "عندما يصل الرصيد إلى",
"workspace.reload.edit": "يحرر",
- "workspace.reload.enable": "يُمكَِن",
- "workspace.reload.enableAutoReload": "تمكين إعادة التحميل التلقائي",
- "workspace.reload.reloadAmount": "إعادة تحميل $",
+ "workspace.reload.enable": "تفعيل",
+ "workspace.reload.enableAutoReload": "تفعيل إعادة الشحن التلقائي",
+ "workspace.reload.reloadAmount": "مبلغ إعادة الشحن $",
"workspace.reload.whenBalanceReaches": "عندما يصل الرصيد إلى $",
"workspace.reload.saving": "توفير...",
"workspace.reload.save": "يحفظ",
- "workspace.reload.failedAt": "فشلت عملية إعادة التحميل عند",
+ "workspace.reload.failedAt": "فشلت إعادة الشحن في",
"workspace.reload.reason": "سبب:",
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع الخاصة بك والمحاولة مرة أخرى.",
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
@@ -500,11 +500,11 @@ export const dict = {
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
"workspace.payments.table.date": "تاريخ",
"workspace.payments.table.paymentId": "معرف الدفع",
- "workspace.payments.table.amount": "كمية",
+ "workspace.payments.table.amount": "المبلغ",
"workspace.payments.table.receipt": "إيصال",
"workspace.payments.type.credit": "ائتمان",
"workspace.payments.type.subscription": "الاشتراك",
- "workspace.payments.view": "منظر",
+ "workspace.payments.view": "عرض",
"workspace.black.loading": "تحميل...",
"workspace.black.time.day": "يوم",
"workspace.black.time.days": "أيام",
@@ -521,8 +521,8 @@ export const dict = {
"workspace.black.subscription.resetsIn": "إعادة تعيين في",
"workspace.black.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.black.waitlist.title": "قائمة الانتظار",
- "workspace.black.waitlist.joined": "أنت على قائمة الانتظار للخطة السوداء {{plan}} دولار شهريًا OpenCode.",
- "workspace.black.waitlist.ready": "نحن على استعداد لتسجيلك في خطة Black {{plan}} الشهرية OpenCode.",
+ "workspace.black.waitlist.joined": "أنت على قائمة الانتظار لخطة OpenCode Black بقيمة ${{plan}} شهريًا.",
+ "workspace.black.waitlist.ready": "نحن مستعدون لتسجيلك في خطة OpenCode Black بقيمة ${{plan}} شهريًا.",
"workspace.black.waitlist.leave": "ترك قائمة الانتظار",
"workspace.black.waitlist.leaving": "مغادرة...",
"workspace.black.waitlist.left": "غادر",
diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts
index 908d4b605fc1..719c2b96150a 100644
--- a/packages/console/app/src/i18n/da.ts
+++ b/packages/console/app/src/i18n/da.ts
@@ -294,18 +294,18 @@ export const dict = {
"workspace.home.billing.currentBalance": "Nuværende saldo",
"workspace.newUser.feature.tested.title": "Testede og verificerede modeller",
"workspace.newUser.feature.tested.body":
- "Vi har benchmarket og testet modeller specifikt til kodningsmidler for at sikre den bedste ydeevne.",
+ "Vi har benchmarket og testet modeller specifikt til kodningsagenter for at sikre den bedste ydeevne.",
"workspace.newUser.feature.quality.title": "Højeste kvalitet",
"workspace.newUser.feature.quality.body":
"Få adgang til modeller konfigureret til optimal ydeevne - ingen nedgraderinger eller routing til billigere udbydere.",
"workspace.newUser.feature.lockin.title": "Ingen indlåsning",
"workspace.newUser.feature.lockin.body":
"Brug Zen med en hvilken som helst kodningsagent, og fortsæt med at bruge andre udbydere med opencode, når du vil.",
- "workspace.newUser.copyApiKey": "Kopiér nøglen API",
+ "workspace.newUser.copyApiKey": "Kopiér API-nøgle",
"workspace.newUser.copyKey": "Kopier nøgle",
"workspace.newUser.copied": "Kopieret!",
"workspace.newUser.step.enableBilling": "Aktiver fakturering",
- "workspace.newUser.step.login.before": "Løbe",
+ "workspace.newUser.step.login.before": "Kør",
"workspace.newUser.step.login.after": "og vælg opencode",
"workspace.newUser.step.pasteKey": "Indsæt din API nøgle",
"workspace.newUser.step.models.before": "Start opencode og kør",
@@ -316,12 +316,12 @@ export const dict = {
"workspace.models.table.enabled": "Aktiveret",
"workspace.providers.title": "Medbring din egen nøgle",
"workspace.providers.subtitle": "Konfigurer dine egne API nøgler fra AI-udbydere.",
- "workspace.providers.placeholder": "Indtast nøglen {{provider}} API ({{prefix}}...)",
+ "workspace.providers.placeholder": "Indtast {{provider}} API-nøgle ({{prefix}}...)",
"workspace.providers.configure": "Konfigurer",
- "workspace.providers.edit": "Redigere",
+ "workspace.providers.edit": "Rediger",
"workspace.providers.delete": "Slet",
"workspace.providers.saving": "Gemmer...",
- "workspace.providers.save": "Spare",
+ "workspace.providers.save": "Gem",
"workspace.providers.table.provider": "Udbyder",
"workspace.providers.table.apiKey": "API Nøgle",
"workspace.usage.title": "Brugshistorik",
@@ -330,15 +330,15 @@ export const dict = {
"workspace.usage.table.date": "Dato",
"workspace.usage.table.model": "Model",
"workspace.usage.table.input": "Input",
- "workspace.usage.table.output": "Produktion",
- "workspace.usage.table.cost": "Koste",
+ "workspace.usage.table.output": "Output",
+ "workspace.usage.table.cost": "Omkostning",
"workspace.usage.breakdown.input": "Input",
"workspace.usage.breakdown.cacheRead": "Cache læst",
"workspace.usage.breakdown.cacheWrite": "Cache skriv",
- "workspace.usage.breakdown.output": "Produktion",
+ "workspace.usage.breakdown.output": "Output",
"workspace.usage.breakdown.reasoning": "Ræsonnement",
"workspace.usage.subscription": "abonnement (${{amount}})",
- "workspace.cost.title": "Koste",
+ "workspace.cost.title": "Omkostninger",
"workspace.cost.subtitle": "Brugsomkostninger opdelt efter model.",
"workspace.cost.allModels": "Alle modeller",
"workspace.cost.allKeys": "Alle nøgler",
@@ -354,7 +354,7 @@ export const dict = {
"workspace.keys.table.key": "Nøgle",
"workspace.keys.table.createdBy": "Skabt af",
"workspace.keys.table.lastUsed": "Sidst brugt",
- "workspace.keys.copyApiKey": "Kopiér nøglen API",
+ "workspace.keys.copyApiKey": "Kopiér API-nøgle",
"workspace.keys.delete": "Slet",
"workspace.members.title": "Medlemmer",
"workspace.members.subtitle": "Administrer arbejdsområdemedlemmer og deres tilladelser.",
@@ -368,10 +368,10 @@ export const dict = {
"workspace.members.noLimit": "Ingen grænse",
"workspace.members.noLimitLowercase": "ingen grænse",
"workspace.members.invited": "inviteret",
- "workspace.members.edit": "Redigere",
+ "workspace.members.edit": "Rediger",
"workspace.members.delete": "Slet",
"workspace.members.saving": "Gemmer...",
- "workspace.members.save": "Spare",
+ "workspace.members.save": "Gem",
"workspace.members.table.email": "E-mail",
"workspace.members.table.role": "Rolle",
"workspace.members.table.monthLimit": "Månedsgrænse",
@@ -382,10 +382,10 @@ export const dict = {
"workspace.settings.title": "Indstillinger",
"workspace.settings.subtitle": "Opdater dit arbejdsområdes navn og præferencer.",
"workspace.settings.workspaceName": "Arbejdsområdets navn",
- "workspace.settings.defaultName": "Misligholdelse",
+ "workspace.settings.defaultName": "Standard",
"workspace.settings.updating": "Opdaterer...",
- "workspace.settings.save": "Spare",
- "workspace.settings.edit": "Redigere",
+ "workspace.settings.save": "Gem",
+ "workspace.settings.edit": "Rediger",
"workspace.billing.title": "Fakturering",
"workspace.billing.subtitle.beforeLink": "Administrer betalingsmetoder.",
"workspace.billing.contactUs": "Kontakt os",
@@ -394,10 +394,10 @@ export const dict = {
"workspace.billing.add": "Tilføj $",
"workspace.billing.enterAmount": "Indtast beløb",
"workspace.billing.loading": "Indlæser...",
- "workspace.billing.addAction": "Tilføje",
+ "workspace.billing.addAction": "Tilføj",
"workspace.billing.addBalance": "Tilføj balance",
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
- "workspace.billing.manage": "Styre",
+ "workspace.billing.manage": "Administrer",
"workspace.billing.enable": "Aktiver fakturering",
"workspace.monthlyLimit.title": "Månedlig grænse",
"workspace.monthlyLimit.subtitle": "Indstil en månedlig forbrugsgrænse for din konto.",
@@ -408,23 +408,23 @@ export const dict = {
"workspace.monthlyLimit.noLimit": "Ingen forbrugsgrænse angivet.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
- "workspace.reload.title": "Automatisk genindlæsning",
- "workspace.reload.disabled.before": "Automatisk genindlæsning er",
- "workspace.reload.disabled.state": "handicappet",
- "workspace.reload.disabled.after": "Aktiver for automatisk at genindlæse, når balancen er lav.",
- "workspace.reload.enabled.before": "Automatisk genindlæsning er",
+ "workspace.reload.title": "Automatisk genopfyldning",
+ "workspace.reload.disabled.before": "Automatisk genopfyldning er",
+ "workspace.reload.disabled.state": "deaktiveret",
+ "workspace.reload.disabled.after": "Aktiver for automatisk at genopfylde, når saldoen er lav.",
+ "workspace.reload.enabled.before": "Automatisk genopfyldning er",
"workspace.reload.enabled.state": "aktiveret",
- "workspace.reload.enabled.middle": "Vi genindlæser",
+ "workspace.reload.enabled.middle": "Vi genopfylder",
"workspace.reload.processingFee": "ekspeditionsgebyr",
"workspace.reload.enabled.after": "når balancen er nået",
- "workspace.reload.edit": "Redigere",
+ "workspace.reload.edit": "Rediger",
"workspace.reload.enable": "Aktiver",
- "workspace.reload.enableAutoReload": "Aktiver automatisk genindlæsning",
- "workspace.reload.reloadAmount": "Genindlæs $",
+ "workspace.reload.enableAutoReload": "Aktiver automatisk genopfyldning",
+ "workspace.reload.reloadAmount": "Genopfyld $",
"workspace.reload.whenBalanceReaches": "Når saldoen når $",
"workspace.reload.saving": "Gemmer...",
- "workspace.reload.save": "Spare",
- "workspace.reload.failedAt": "Genindlæsning mislykkedes kl",
+ "workspace.reload.save": "Gem",
+ "workspace.reload.failedAt": "Genopfyldning mislykkedes kl",
"workspace.reload.reason": "Årsag:",
"workspace.reload.updatePaymentMethod": "Opdater din betalingsmetode, og prøv igen.",
"workspace.reload.retrying": "Prøver igen...",
@@ -434,10 +434,10 @@ export const dict = {
"workspace.payments.table.date": "Dato",
"workspace.payments.table.paymentId": "Betalings-id",
"workspace.payments.table.amount": "Beløb",
- "workspace.payments.table.receipt": "Modtagelse",
+ "workspace.payments.table.receipt": "Kvittering",
"workspace.payments.type.credit": "kredit",
"workspace.payments.type.subscription": "abonnement",
- "workspace.payments.view": "Udsigt",
+ "workspace.payments.view": "Vis",
"workspace.black.loading": "Indlæser...",
"workspace.black.time.day": "dag",
"workspace.black.time.days": "dage",
@@ -458,8 +458,8 @@ export const dict = {
"workspace.black.waitlist.ready": "Vi er klar til at tilmelde dig ${{plan}} per måned OpenCode Black plan.",
"workspace.black.waitlist.leave": "Forlad venteliste",
"workspace.black.waitlist.leaving": "Forlader...",
- "workspace.black.waitlist.left": "Venstre",
- "workspace.black.waitlist.enroll": "Indskrive",
+ "workspace.black.waitlist.left": "Forladt",
+ "workspace.black.waitlist.enroll": "Tilmeld",
"workspace.black.waitlist.enrolling": "Tilmelder...",
"workspace.black.waitlist.enrolled": "Tilmeldt",
"workspace.black.waitlist.enrollNote":
diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts
index f37cbc91fc85..ea20893a62dc 100644
--- a/packages/console/app/src/i18n/de.ts
+++ b/packages/console/app/src/i18n/de.ts
@@ -306,27 +306,27 @@ export const dict = {
"workspace.newUser.feature.lockin.title": "Kein Lock-in",
"workspace.newUser.feature.lockin.body":
"Verwenden Sie Zen mit einem beliebigen Codierungsagenten und nutzen Sie weiterhin andere Anbieter mit opencode, wann immer Sie möchten.",
- "workspace.newUser.copyApiKey": "Kopieren Sie den Schlüssel API",
+ "workspace.newUser.copyApiKey": "API-Schlüssel kopieren",
"workspace.newUser.copyKey": "Schlüssel kopieren",
"workspace.newUser.copied": "Kopiert!",
"workspace.newUser.step.enableBilling": "Abrechnung aktivieren",
- "workspace.newUser.step.login.before": "Laufen",
+ "workspace.newUser.step.login.before": "Führe",
"workspace.newUser.step.login.after": "und wählen Sie opencode",
"workspace.newUser.step.pasteKey": "Fügen Sie Ihren API-Schlüssel ein",
- "workspace.newUser.step.models.before": "Starten Sie opencode und führen Sie es aus",
+ "workspace.newUser.step.models.before": "Starte opencode und führe",
"workspace.newUser.step.models.after": "um ein Modell auszuwählen",
"workspace.models.title": "Modelle",
"workspace.models.subtitle.beforeLink":
"Verwalten Sie, auf welche Modelle Arbeitsbereichsmitglieder zugreifen können.",
"workspace.models.table.model": "Modell",
- "workspace.models.table.enabled": "Ermöglicht",
+ "workspace.models.table.enabled": "Aktiviert",
"workspace.providers.title": "Bringen Sie Ihren eigenen Schlüssel mit",
"workspace.providers.subtitle": "Konfigurieren Sie Ihre eigenen API-Schlüssel von KI-Anbietern.",
"workspace.providers.placeholder": "Geben Sie den Schlüssel {{provider}} API ein ({{prefix}}...)",
"workspace.providers.configure": "Konfigurieren",
"workspace.providers.edit": "Bearbeiten",
"workspace.providers.delete": "Löschen",
- "workspace.providers.saving": "Sparen...",
+ "workspace.providers.saving": "Wird gespeichert...",
"workspace.providers.save": "Speichern",
"workspace.providers.table.provider": "Anbieter",
"workspace.providers.table.apiKey": "API-Schlüssel",
@@ -335,14 +335,14 @@ export const dict = {
"workspace.usage.empty": "Machen Sie Ihren ersten API-Aufruf, um loszulegen.",
"workspace.usage.table.date": "Datum",
"workspace.usage.table.model": "Modell",
- "workspace.usage.table.input": "Eingang",
- "workspace.usage.table.output": "Ausgabe",
+ "workspace.usage.table.input": "Input",
+ "workspace.usage.table.output": "Output",
"workspace.usage.table.cost": "Kosten",
- "workspace.usage.breakdown.input": "Eingang",
+ "workspace.usage.breakdown.input": "Input",
"workspace.usage.breakdown.cacheRead": "Cache-Lesen",
"workspace.usage.breakdown.cacheWrite": "Cache-Schreiben",
- "workspace.usage.breakdown.output": "Ausgabe",
- "workspace.usage.breakdown.reasoning": "Argumentation",
+ "workspace.usage.breakdown.output": "Output",
+ "workspace.usage.breakdown.reasoning": "Reasoning",
"workspace.usage.subscription": "Abonnement (${{amount}})",
"workspace.cost.title": "Kosten",
"workspace.cost.subtitle": "Nutzungskosten aufgeschlüsselt nach Modell.",
@@ -360,12 +360,12 @@ export const dict = {
"workspace.keys.table.key": "Schlüssel",
"workspace.keys.table.createdBy": "Erstellt von",
"workspace.keys.table.lastUsed": "Zuletzt verwendet",
- "workspace.keys.copyApiKey": "Kopieren Sie den Schlüssel API",
+ "workspace.keys.copyApiKey": "API-Schlüssel kopieren",
"workspace.keys.delete": "Löschen",
"workspace.members.title": "Mitglieder",
"workspace.members.subtitle": "Verwalten Sie Arbeitsbereichsmitglieder und ihre Berechtigungen.",
"workspace.members.invite": "Mitglied einladen",
- "workspace.members.inviting": "Einladend...",
+ "workspace.members.inviting": "Wird eingeladen...",
"workspace.members.beta.beforeLink": "Während der Betaversion sind Arbeitsbereiche für Teams kostenlos.",
"workspace.members.form.invitee": "Eingeladen",
"workspace.members.form.emailPlaceholder": "Geben Sie Ihre E-Mail-Adresse ein",
@@ -376,7 +376,7 @@ export const dict = {
"workspace.members.invited": "eingeladen",
"workspace.members.edit": "Bearbeiten",
"workspace.members.delete": "Löschen",
- "workspace.members.saving": "Sparen...",
+ "workspace.members.saving": "Wird gespeichert...",
"workspace.members.save": "Speichern",
"workspace.members.table.email": "E-Mail",
"workspace.members.table.role": "Rolle",
@@ -408,30 +408,30 @@ export const dict = {
"workspace.monthlyLimit.title": "Monatliches Limit",
"workspace.monthlyLimit.subtitle": "Legen Sie ein monatliches Nutzungslimit für Ihr Konto fest.",
"workspace.monthlyLimit.placeholder": "50",
- "workspace.monthlyLimit.setting": "Einstellung...",
- "workspace.monthlyLimit.set": "Satz",
+ "workspace.monthlyLimit.setting": "Wird gesetzt...",
+ "workspace.monthlyLimit.set": "Festlegen",
"workspace.monthlyLimit.edit": "Limit bearbeiten",
"workspace.monthlyLimit.noLimit": "Kein Nutzungslimit festgelegt.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
- "workspace.reload.title": "Automatisches Neuladen",
- "workspace.reload.disabled.before": "Automatisches Nachladen ist",
+ "workspace.reload.title": "Automatische Aufladung",
+ "workspace.reload.disabled.before": "Automatische Aufladung ist",
"workspace.reload.disabled.state": "deaktiviert",
"workspace.reload.disabled.after":
- "Aktivieren Sie diese Option, um das Guthaben automatisch neu zu laden, wenn das Guthaben niedrig ist.",
- "workspace.reload.enabled.before": "Automatisches Nachladen ist",
- "workspace.reload.enabled.state": "ermöglicht",
- "workspace.reload.enabled.middle": "Wir laden nach",
+ "Aktivieren Sie diese Option, damit bei niedrigem Kontostand automatisch aufgeladen wird.",
+ "workspace.reload.enabled.before": "Automatische Aufladung ist",
+ "workspace.reload.enabled.state": "aktiviert",
+ "workspace.reload.enabled.middle": "Wir laden auf",
"workspace.reload.processingFee": "Bearbeitungsgebühr",
- "workspace.reload.enabled.after": "wenn das Gleichgewicht erreicht ist",
+ "workspace.reload.enabled.after": "sobald der Kontostand",
"workspace.reload.edit": "Bearbeiten",
"workspace.reload.enable": "Aktivieren",
- "workspace.reload.enableAutoReload": "Aktivieren Sie das automatische Neuladen",
- "workspace.reload.reloadAmount": "$ neu laden",
- "workspace.reload.whenBalanceReaches": "Wenn der Saldo $ erreicht",
- "workspace.reload.saving": "Sparen...",
+ "workspace.reload.enableAutoReload": "Automatische Aufladung aktivieren",
+ "workspace.reload.reloadAmount": "Aufladebetrag $",
+ "workspace.reload.whenBalanceReaches": "Wenn der Kontostand $ erreicht",
+ "workspace.reload.saving": "Wird gespeichert...",
"workspace.reload.save": "Speichern",
- "workspace.reload.failedAt": "Neuladen fehlgeschlagen bei",
+ "workspace.reload.failedAt": "Aufladung fehlgeschlagen am",
"workspace.reload.reason": "Grund:",
"workspace.reload.updatePaymentMethod": "Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut.",
"workspace.reload.retrying": "Erneuter Versuch...",
@@ -440,11 +440,11 @@ export const dict = {
"workspace.payments.subtitle": "Letzte Zahlungsvorgänge.",
"workspace.payments.table.date": "Datum",
"workspace.payments.table.paymentId": "Zahlungs-ID",
- "workspace.payments.table.amount": "Menge",
+ "workspace.payments.table.amount": "Betrag",
"workspace.payments.table.receipt": "Quittung",
"workspace.payments.type.credit": "Kredit",
"workspace.payments.type.subscription": "Abonnement",
- "workspace.payments.view": "Sicht",
+ "workspace.payments.view": "Anzeigen",
"workspace.black.loading": "Laden...",
"workspace.black.time.day": "Tag",
"workspace.black.time.days": "Tage",
@@ -454,21 +454,21 @@ export const dict = {
"workspace.black.time.minutes": "Minuten",
"workspace.black.time.fewSeconds": "ein paar Sekunden",
"workspace.black.subscription.title": "Abonnement",
- "workspace.black.subscription.message": "Sie haben OpenCode Black für {{plan}} pro Monat abonniert.",
+ "workspace.black.subscription.message": "Sie haben OpenCode Black für ${{plan}} pro Monat abonniert.",
"workspace.black.subscription.manage": "Abonnement verwalten",
"workspace.black.subscription.rollingUsage": "5-stündige Nutzung",
"workspace.black.subscription.weeklyUsage": "Wöchentliche Nutzung",
- "workspace.black.subscription.resetsIn": "Wird zurückgesetzt",
+ "workspace.black.subscription.resetsIn": "Zurückgesetzt in",
"workspace.black.subscription.useBalance":
"Nutzen Sie Ihr verfügbares Guthaben, nachdem Sie die Nutzungslimits erreicht haben",
"workspace.black.waitlist.title": "Warteliste",
"workspace.black.waitlist.joined":
- "Sie stehen auf der Warteliste für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode.",
+ "Sie stehen auf der Warteliste für den OpenCode Black Tarif für ${{plan}} pro Monat.",
"workspace.black.waitlist.ready":
- "Wir sind bereit, Sie für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode anzumelden.",
+ "Wir können Sie jetzt in den OpenCode Black Tarif für ${{plan}} pro Monat aufnehmen.",
"workspace.black.waitlist.leave": "Warteliste verlassen",
"workspace.black.waitlist.leaving": "Verlassen...",
- "workspace.black.waitlist.left": "Links",
+ "workspace.black.waitlist.left": "Verlassen",
"workspace.black.waitlist.enroll": "Einschreiben",
"workspace.black.waitlist.enrolling": "Anmeldung...",
"workspace.black.waitlist.enrolled": "Eingeschrieben",
diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts
index fb03a01e321d..daf9e5625c60 100644
--- a/packages/console/app/src/i18n/it.ts
+++ b/packages/console/app/src/i18n/it.ts
@@ -284,8 +284,8 @@ export const dict = {
"changelog.hero.subtitle": "Nuovi aggiornamenti e miglioramenti per OpenCode",
"changelog.empty": "Nessuna voce di changelog trovata.",
"changelog.viewJson": "Visualizza JSON",
- "workspace.nav.zen": "zen",
- "workspace.nav.apiKeys": "API Chiavi",
+ "workspace.nav.zen": "Zen",
+ "workspace.nav.apiKeys": "Chiavi API",
"workspace.nav.members": "Membri",
"workspace.nav.billing": "Fatturazione",
"workspace.nav.settings": "Impostazioni",
@@ -299,14 +299,14 @@ export const dict = {
"workspace.newUser.feature.quality.title": "Massima qualità",
"workspace.newUser.feature.quality.body":
"Modelli di accesso configurati per prestazioni ottimali: senza downgrade o instradamento verso fornitori più economici.",
- "workspace.newUser.feature.lockin.title": "Nessun blocco",
+ "workspace.newUser.feature.lockin.title": "Nessun lock-in",
"workspace.newUser.feature.lockin.body":
"Utilizza Zen con qualsiasi agente di codifica e continua a utilizzare altri provider con opencode ogni volta che vuoi.",
"workspace.newUser.copyApiKey": "Copia la chiave API",
"workspace.newUser.copyKey": "Copia chiave",
"workspace.newUser.copied": "Copiato!",
"workspace.newUser.step.enableBilling": "Abilita fatturazione",
- "workspace.newUser.step.login.before": "Correre",
+ "workspace.newUser.step.login.before": "Esegui",
"workspace.newUser.step.login.after": "e seleziona opencode",
"workspace.newUser.step.pasteKey": "Incolla la tua chiave API",
"workspace.newUser.step.models.before": "Avvia opencode ed esegui",
@@ -315,16 +315,16 @@ export const dict = {
"workspace.models.subtitle.beforeLink": "Gestire i modelli a cui possono accedere i membri dell'area di lavoro.",
"workspace.models.table.model": "Modello",
"workspace.models.table.enabled": "Abilitato",
- "workspace.providers.title": "Porta la tua chiave",
+ "workspace.providers.title": "Bring Your Own Key (BYOK)",
"workspace.providers.subtitle": "Configura le tue chiavi API dai fornitori di intelligenza artificiale.",
"workspace.providers.placeholder": "Inserisci la chiave {{provider}} API ({{prefix}}...)",
"workspace.providers.configure": "Configura",
"workspace.providers.edit": "Modificare",
"workspace.providers.delete": "Eliminare",
- "workspace.providers.saving": "Risparmio...",
+ "workspace.providers.saving": "Salvataggio in corso...",
"workspace.providers.save": "Salva",
"workspace.providers.table.provider": "Fornitore",
- "workspace.providers.table.apiKey": "API Chiave",
+ "workspace.providers.table.apiKey": "Chiave API",
"workspace.usage.title": "Cronologia dell'utilizzo",
"workspace.usage.subtitle": "Utilizzo e costi recenti di API.",
"workspace.usage.empty": "Effettua la tua prima chiamata API per iniziare.",
@@ -346,7 +346,7 @@ export const dict = {
"workspace.cost.deletedSuffix": "(eliminato)",
"workspace.cost.empty": "Nessun dato di utilizzo disponibile per il periodo selezionato.",
"workspace.cost.subscriptionShort": "sub",
- "workspace.keys.title": "API Chiavi",
+ "workspace.keys.title": "Chiavi API",
"workspace.keys.subtitle": "Gestisci le tue chiavi API per accedere ai servizi opencode.",
"workspace.keys.create": "Crea chiave API",
"workspace.keys.placeholder": "Inserisci il nome della chiave",
@@ -360,7 +360,7 @@ export const dict = {
"workspace.members.title": "Membri",
"workspace.members.subtitle": "Gestire i membri dell'area di lavoro e le relative autorizzazioni.",
"workspace.members.invite": "Invita membro",
- "workspace.members.inviting": "Invitante...",
+ "workspace.members.inviting": "Invito in corso...",
"workspace.members.beta.beforeLink": "Gli spazi di lavoro sono gratuiti per i team durante la beta.",
"workspace.members.form.invitee": "Invitato",
"workspace.members.form.emailPlaceholder": "Inserisci l'e-mail",
@@ -371,12 +371,12 @@ export const dict = {
"workspace.members.invited": "invitato",
"workspace.members.edit": "Modificare",
"workspace.members.delete": "Eliminare",
- "workspace.members.saving": "Risparmio...",
+ "workspace.members.saving": "Salvataggio in corso...",
"workspace.members.save": "Salva",
"workspace.members.table.email": "E-mail",
"workspace.members.table.role": "Ruolo",
"workspace.members.table.monthLimit": "Limite mensile",
- "workspace.members.role.admin": "Ammin",
+ "workspace.members.role.admin": "Admin",
"workspace.members.role.adminDescription": "Può gestire modelli, membri e fatturazione",
"workspace.members.role.member": "Membro",
"workspace.members.role.memberDescription": "Possono generare chiavi API solo per se stessi",
@@ -388,42 +388,42 @@ export const dict = {
"workspace.settings.save": "Salva",
"workspace.settings.edit": "Modificare",
"workspace.billing.title": "Fatturazione",
- "workspace.billing.subtitle.beforeLink": "Gestire i metodi di pagamento.",
+ "workspace.billing.subtitle.beforeLink": "Gestisci i metodi di pagamento.",
"workspace.billing.contactUs": "Contattaci",
"workspace.billing.subtitle.afterLink": "se hai qualche domanda",
"workspace.billing.currentBalance": "Saldo attuale",
"workspace.billing.add": "Aggiungi $",
"workspace.billing.enterAmount": "Inserisci l'importo",
"workspace.billing.loading": "Caricamento...",
- "workspace.billing.addAction": "Aggiungere",
+ "workspace.billing.addAction": "Aggiungi",
"workspace.billing.addBalance": "Aggiungi saldo",
"workspace.billing.linkedToStripe": "Collegato a Stripe",
- "workspace.billing.manage": "Maneggio",
+ "workspace.billing.manage": "Gestisci",
"workspace.billing.enable": "Abilita fatturazione",
"workspace.monthlyLimit.title": "Limite mensile",
"workspace.monthlyLimit.subtitle": "Imposta un limite di utilizzo mensile per il tuo account.",
"workspace.monthlyLimit.placeholder": "50",
- "workspace.monthlyLimit.setting": "Collocamento...",
+ "workspace.monthlyLimit.setting": "Impostazione in corso...",
"workspace.monthlyLimit.set": "Impostato",
"workspace.monthlyLimit.edit": "Modifica limite",
"workspace.monthlyLimit.noLimit": "Nessun limite di utilizzo impostato.",
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
"workspace.reload.title": "Ricarica automatica",
- "workspace.reload.disabled.before": "La ricarica automatica lo è",
+ "workspace.reload.disabled.before": "La ricarica automatica è",
"workspace.reload.disabled.state": "disabilitato",
"workspace.reload.disabled.after": "Abilita la ricarica automatica quando il saldo è basso.",
- "workspace.reload.enabled.before": "La ricarica automatica lo è",
+ "workspace.reload.enabled.before": "La ricarica automatica è",
"workspace.reload.enabled.state": "abilitato",
"workspace.reload.enabled.middle": "Ricaricheremo",
"workspace.reload.processingFee": "tassa di elaborazione",
- "workspace.reload.enabled.after": "quando l'equilibrio raggiunge",
+ "workspace.reload.enabled.after": "quando il saldo raggiunge",
"workspace.reload.edit": "Modificare",
"workspace.reload.enable": "Abilitare",
"workspace.reload.enableAutoReload": "Abilita ricarica automatica",
"workspace.reload.reloadAmount": "Ricarica $",
"workspace.reload.whenBalanceReaches": "Quando il saldo raggiunge $",
- "workspace.reload.saving": "Risparmio...",
+ "workspace.reload.saving": "Salvataggio in corso...",
"workspace.reload.save": "Salva",
"workspace.reload.failedAt": "Ricarica non riuscita a",
"workspace.reload.reason": "Motivo:",
@@ -434,11 +434,11 @@ export const dict = {
"workspace.payments.subtitle": "Transazioni di pagamento recenti.",
"workspace.payments.table.date": "Data",
"workspace.payments.table.paymentId": "ID pagamento",
- "workspace.payments.table.amount": "Quantità",
+ "workspace.payments.table.amount": "Importo",
"workspace.payments.table.receipt": "Ricevuta",
"workspace.payments.type.credit": "credito",
"workspace.payments.type.subscription": "sottoscrizione",
- "workspace.payments.view": "Visualizzazione",
+ "workspace.payments.view": "Visualizza",
"workspace.black.loading": "Caricamento...",
"workspace.black.time.day": "giorno",
"workspace.black.time.days": "giorni",
@@ -452,14 +452,14 @@ export const dict = {
"workspace.black.subscription.manage": "Gestisci abbonamento",
"workspace.black.subscription.rollingUsage": "Utilizzo di 5 ore",
"workspace.black.subscription.weeklyUsage": "Utilizzo settimanale",
- "workspace.black.subscription.resetsIn": "Si reimposta",
+ "workspace.black.subscription.resetsIn": "Si reimposta tra",
"workspace.black.subscription.useBalance": "Utilizza il saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.black.waitlist.title": "Lista d'attesa",
- "workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano nero ${{plan}} al mese OpenCode.",
+ "workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano OpenCode Black da ${{plan}} al mese.",
"workspace.black.waitlist.ready": "Siamo pronti per iscriverti al piano OpenCode Black da ${{plan}} al mese.",
"workspace.black.waitlist.leave": "Lascia la lista d'attesa",
- "workspace.black.waitlist.leaving": "In partenza...",
- "workspace.black.waitlist.left": "Sinistra",
+ "workspace.black.waitlist.leaving": "Uscita in corso...",
+ "workspace.black.waitlist.left": "Uscito dalla lista d'attesa",
"workspace.black.waitlist.enroll": "Iscriversi",
"workspace.black.waitlist.enrolling": "Iscrizione...",
"workspace.black.waitlist.enrolled": "Iscritto",
diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts
index 4a5553429f2e..b24ad2684c34 100644
--- a/packages/console/app/src/i18n/zh.ts
+++ b/packages/console/app/src/i18n/zh.ts
@@ -293,9 +293,9 @@ export const dict = {
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u4e0e\u6539\u8fdb",
"changelog.empty": "\u672a\u627e\u5230\u66f4\u65b0\u65e5\u5fd7\u6761\u76ee\u3002",
"changelog.viewJson": "\u67e5\u770b JSON",
- "workspace.nav.zen": "禅",
+ "workspace.nav.zen": "Zen",
"workspace.nav.apiKeys": "API 键",
- "workspace.nav.members": "会员",
+ "workspace.nav.members": "成员",
"workspace.nav.billing": "计费",
"workspace.nav.settings": "设置",
"workspace.home.banner.beforeLink": "编码代理的可靠优化模型。",
@@ -310,26 +310,26 @@ export const dict = {
"workspace.newUser.feature.lockin.body":
"将 Zen 与任何编码代理结合使用,并在需要时继续将其他提供程序与 opencode 结合使用。",
"workspace.newUser.copyApiKey": "复制 API 密钥",
- "workspace.newUser.copyKey": "复制钥匙",
- "workspace.newUser.copied": "复制了!",
+ "workspace.newUser.copyKey": "复制密钥",
+ "workspace.newUser.copied": "已复制!",
"workspace.newUser.step.enableBilling": "启用计费",
- "workspace.newUser.step.login.before": "跑步",
+ "workspace.newUser.step.login.before": "运行",
"workspace.newUser.step.login.after": "并选择 opencode",
"workspace.newUser.step.pasteKey": "粘贴您的 API 密钥",
"workspace.newUser.step.models.before": "启动 opencode 并运行",
- "workspace.newUser.step.models.after": "选择型号",
- "workspace.models.title": "型号",
+ "workspace.newUser.step.models.after": "选择模型",
+ "workspace.models.title": "模型",
"workspace.models.subtitle.beforeLink": "管理工作区成员可以访问哪些模型。",
"workspace.models.table.model": "模型",
"workspace.models.table.enabled": "启用",
- "workspace.providers.title": "带上你自己的钥匙",
+ "workspace.providers.title": "自带密钥",
"workspace.providers.subtitle": "从 AI 提供商处配置您自己的 API 密钥。",
"workspace.providers.placeholder": "输入 {{provider}} API 密钥({{prefix}}...)",
"workspace.providers.configure": "配置",
"workspace.providers.edit": "编辑",
"workspace.providers.delete": "删除",
"workspace.providers.saving": "保存...",
- "workspace.providers.save": "节省",
+ "workspace.providers.save": "保存",
"workspace.providers.table.provider": "提供者",
"workspace.providers.table.apiKey": "API 密钥",
"workspace.usage.title": "使用历史",
@@ -348,25 +348,25 @@ export const dict = {
"workspace.usage.subscription": "订阅 (${{amount}})",
"workspace.cost.title": "成本",
"workspace.cost.subtitle": "按型号细分的使用成本。",
- "workspace.cost.allModels": "所有型号",
- "workspace.cost.allKeys": "所有按键",
+ "workspace.cost.allModels": "所有模型",
+ "workspace.cost.allKeys": "所有密钥",
"workspace.cost.deletedSuffix": "(已删除)",
"workspace.cost.empty": "所选期间没有可用的使用数据。",
- "workspace.cost.subscriptionShort": "子",
+ "workspace.cost.subscriptionShort": "订",
"workspace.keys.title": "API 键",
"workspace.keys.subtitle": "管理您的 API 密钥以访问 opencode 服务。",
"workspace.keys.create": "创建 API 密钥",
- "workspace.keys.placeholder": "输入按键名称",
+ "workspace.keys.placeholder": "输入密钥名称",
"workspace.keys.empty": "创建 opencode 网关 API 密钥",
- "workspace.keys.table.name": "姓名",
- "workspace.keys.table.key": "钥匙",
+ "workspace.keys.table.name": "名称",
+ "workspace.keys.table.key": "密钥",
"workspace.keys.table.createdBy": "创建者",
"workspace.keys.table.lastUsed": "最后使用",
"workspace.keys.copyApiKey": "复制 API 密钥",
"workspace.keys.delete": "删除",
- "workspace.members.title": "会员",
+ "workspace.members.title": "成员",
"workspace.members.subtitle": "管理工作区成员及其权限。",
- "workspace.members.invite": "邀请会员",
+ "workspace.members.invite": "邀请成员",
"workspace.members.inviting": "邀请...",
"workspace.members.beta.beforeLink": "测试期间,工作空间对团队免费。",
"workspace.members.form.invitee": "受邀者",
@@ -379,11 +379,11 @@ export const dict = {
"workspace.members.edit": "编辑",
"workspace.members.delete": "删除",
"workspace.members.saving": "保存...",
- "workspace.members.save": "节省",
+ "workspace.members.save": "保存",
"workspace.members.table.email": "电子邮件",
"workspace.members.table.role": "角色",
- "workspace.members.table.monthLimit": "月份限制",
- "workspace.members.role.admin": "行政",
+ "workspace.members.table.monthLimit": "月限额",
+ "workspace.members.role.admin": "管理员",
"workspace.members.role.adminDescription": "可以管理模型、成员和计费",
"workspace.members.role.member": "成员",
"workspace.members.role.memberDescription": "只能为自己生成 API 密钥",
@@ -392,7 +392,7 @@ export const dict = {
"workspace.settings.workspaceName": "工作区名称",
"workspace.settings.defaultName": "默认",
"workspace.settings.updating": "更新中...",
- "workspace.settings.save": "节省",
+ "workspace.settings.save": "保存",
"workspace.settings.edit": "编辑",
"workspace.billing.title": "计费",
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
@@ -404,35 +404,35 @@ export const dict = {
"workspace.billing.loading": "加载中...",
"workspace.billing.addAction": "添加",
"workspace.billing.addBalance": "添加余额",
- "workspace.billing.linkedToStripe": "链接到条纹",
+ "workspace.billing.linkedToStripe": "已绑定 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "启用计费",
"workspace.monthlyLimit.title": "每月限额",
"workspace.monthlyLimit.subtitle": "为您的帐户设置每月使用限额。",
"workspace.monthlyLimit.placeholder": "50",
- "workspace.monthlyLimit.setting": "环境...",
- "workspace.monthlyLimit.set": "放",
+ "workspace.monthlyLimit.setting": "设置中...",
+ "workspace.monthlyLimit.set": "设置",
"workspace.monthlyLimit.edit": "编辑限制",
"workspace.monthlyLimit.noLimit": "没有设置使用限制。",
- "workspace.monthlyLimit.currentUsage.beforeMonth": "当前使用情况为",
- "workspace.monthlyLimit.currentUsage.beforeAmount": "是 $",
- "workspace.reload.title": "自动重新加载",
- "workspace.reload.disabled.before": "自动重新加载是",
- "workspace.reload.disabled.state": "残疾人",
- "workspace.reload.disabled.after": "启用余额不足时自动充值。",
- "workspace.reload.enabled.before": "自动重新加载是",
+ "workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
+ "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
+ "workspace.reload.title": "自动充值",
+ "workspace.reload.disabled.before": "自动充值已",
+ "workspace.reload.disabled.state": "停用",
+ "workspace.reload.disabled.after": "启用后将在余额较低时自动充值。",
+ "workspace.reload.enabled.before": "自动充值已",
"workspace.reload.enabled.state": "已启用",
- "workspace.reload.enabled.middle": "我们将重新加载",
- "workspace.reload.processingFee": "加工费",
+ "workspace.reload.enabled.middle": "我们将自动充值",
+ "workspace.reload.processingFee": "手续费",
"workspace.reload.enabled.after": "当余额达到",
"workspace.reload.edit": "编辑",
- "workspace.reload.enable": "使能够",
- "workspace.reload.enableAutoReload": "启用自动重新加载",
- "workspace.reload.reloadAmount": "重新加载 $",
+ "workspace.reload.enable": "启用",
+ "workspace.reload.enableAutoReload": "启用自动充值",
+ "workspace.reload.reloadAmount": "充值 $",
"workspace.reload.whenBalanceReaches": "当余额达到 $",
"workspace.reload.saving": "保存...",
- "workspace.reload.save": "节省",
- "workspace.reload.failedAt": "重新加载失败于",
+ "workspace.reload.save": "保存",
+ "workspace.reload.failedAt": "充值失败于",
"workspace.reload.reason": "原因:",
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
"workspace.reload.retrying": "正在重试...",
@@ -441,11 +441,11 @@ export const dict = {
"workspace.payments.subtitle": "最近的付款交易。",
"workspace.payments.table.date": "日期",
"workspace.payments.table.paymentId": "付款ID",
- "workspace.payments.table.amount": "数量",
+ "workspace.payments.table.amount": "金额",
"workspace.payments.table.receipt": "收据",
"workspace.payments.type.credit": "信用",
"workspace.payments.type.subscription": "订阅",
- "workspace.payments.view": "看法",
+ "workspace.payments.view": "查看",
"workspace.black.loading": "加载中...",
"workspace.black.time.day": "天",
"workspace.black.time.days": "天",
@@ -455,20 +455,20 @@ export const dict = {
"workspace.black.time.minutes": "分钟",
"workspace.black.time.fewSeconds": "几秒钟",
"workspace.black.subscription.title": "订阅",
- "workspace.black.subscription.message": "您已订阅 OpenCode Black,每月费用为 {{plan}} 美元。",
+ "workspace.black.subscription.message": "您已订阅 OpenCode Black,费用为每月 ${{plan}}。",
"workspace.black.subscription.manage": "管理订阅",
"workspace.black.subscription.rollingUsage": "5小时使用",
"workspace.black.subscription.weeklyUsage": "每周使用量",
"workspace.black.subscription.resetsIn": "重置于",
"workspace.black.subscription.useBalance": "达到使用限额后使用您的可用余额",
"workspace.black.waitlist.title": "候补名单",
- "workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色计划。",
- "workspace.black.waitlist.ready": "我们已准备好让您加入每月 {{plan}} 美元的 OpenCode 黑色计划。",
+ "workspace.black.waitlist.joined": "您已加入每月 ${{plan}} 的 OpenCode Black 方案候补名单。",
+ "workspace.black.waitlist.ready": "我们已准备好将您加入每月 ${{plan}} 的 OpenCode Black 方案。",
"workspace.black.waitlist.leave": "离开候补名单",
"workspace.black.waitlist.leaving": "离开...",
- "workspace.black.waitlist.left": "左边",
- "workspace.black.waitlist.enroll": "注册",
- "workspace.black.waitlist.enrolling": "正在报名...",
- "workspace.black.waitlist.enrolled": "已注册",
+ "workspace.black.waitlist.left": "已退出",
+ "workspace.black.waitlist.enroll": "加入",
+ "workspace.black.waitlist.enrolling": "加入中...",
+ "workspace.black.waitlist.enrolled": "已加入",
"workspace.black.waitlist.enrollNote": "单击“注册”后,您的订阅将立即开始,并且将从您的卡中扣费。",
} satisfies Dict
diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts
index 38bd448e303c..2de7c3996015 100644
--- a/packages/console/app/src/i18n/zht.ts
+++ b/packages/console/app/src/i18n/zht.ts
@@ -293,9 +293,9 @@ export const dict = {
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u8207\u6539\u5584",
"changelog.empty": "\u627e\u4e0d\u5230\u66f4\u65b0\u65e5\u8a8c\u9805\u76ee\u3002",
"changelog.viewJson": "\u6aa2\u8996 JSON",
- "workspace.nav.zen": "禪",
+ "workspace.nav.zen": "Zen",
"workspace.nav.apiKeys": "API 鍵",
- "workspace.nav.members": "會員",
+ "workspace.nav.members": "成員",
"workspace.nav.billing": "計費",
"workspace.nav.settings": "設定",
"workspace.home.banner.beforeLink": "編碼代理的可靠優化模型。",
@@ -310,26 +310,26 @@ export const dict = {
"workspace.newUser.feature.lockin.body":
"將 Zen 與任何編碼代理結合使用,並在需要時繼續將其他提供程序與 opencode 結合使用。",
"workspace.newUser.copyApiKey": "複製 API 密鑰",
- "workspace.newUser.copyKey": "複製鑰匙",
- "workspace.newUser.copied": "複製了!",
+ "workspace.newUser.copyKey": "複製密鑰",
+ "workspace.newUser.copied": "已複製!",
"workspace.newUser.step.enableBilling": "啟用計費",
- "workspace.newUser.step.login.before": "跑步",
+ "workspace.newUser.step.login.before": "執行",
"workspace.newUser.step.login.after": "並選擇 opencode",
"workspace.newUser.step.pasteKey": "粘貼您的 API 密鑰",
"workspace.newUser.step.models.before": "啟動 opencode 並運行",
- "workspace.newUser.step.models.after": "選擇型號",
- "workspace.models.title": "型號",
+ "workspace.newUser.step.models.after": "選擇模型",
+ "workspace.models.title": "模型",
"workspace.models.subtitle.beforeLink": "管理工作區成員可以訪問哪些模型。",
"workspace.models.table.model": "模型",
"workspace.models.table.enabled": "啟用",
- "workspace.providers.title": "帶上你自己的鑰匙",
+ "workspace.providers.title": "自帶密鑰",
"workspace.providers.subtitle": "從 AI 提供商處配置您自己的 API 密鑰。",
"workspace.providers.placeholder": "輸入 {{provider}} API 密鑰({{prefix}}...)",
"workspace.providers.configure": "配置",
"workspace.providers.edit": "編輯",
"workspace.providers.delete": "刪除",
"workspace.providers.saving": "保存...",
- "workspace.providers.save": "節省",
+ "workspace.providers.save": "儲存",
"workspace.providers.table.provider": "提供者",
"workspace.providers.table.apiKey": "API 密鑰",
"workspace.usage.title": "使用歷史",
@@ -348,25 +348,25 @@ export const dict = {
"workspace.usage.subscription": "訂閱 (${{amount}})",
"workspace.cost.title": "成本",
"workspace.cost.subtitle": "按型號細分的使用成本。",
- "workspace.cost.allModels": "所有型號",
- "workspace.cost.allKeys": "所有按鍵",
+ "workspace.cost.allModels": "所有模型",
+ "workspace.cost.allKeys": "所有密鑰",
"workspace.cost.deletedSuffix": "(已刪除)",
"workspace.cost.empty": "所選期間沒有可用的使用數據。",
- "workspace.cost.subscriptionShort": "子",
+ "workspace.cost.subscriptionShort": "訂",
"workspace.keys.title": "API 鍵",
"workspace.keys.subtitle": "管理您的 API 密鑰以訪問 opencode 服務。",
"workspace.keys.create": "創建 API 密鑰",
- "workspace.keys.placeholder": "輸入按鍵名稱",
+ "workspace.keys.placeholder": "輸入密鑰名稱",
"workspace.keys.empty": "創建 opencode 網關 API 密鑰",
- "workspace.keys.table.name": "姓名",
- "workspace.keys.table.key": "鑰匙",
+ "workspace.keys.table.name": "名稱",
+ "workspace.keys.table.key": "密鑰",
"workspace.keys.table.createdBy": "創建者",
"workspace.keys.table.lastUsed": "最後使用",
"workspace.keys.copyApiKey": "複製 API 密鑰",
"workspace.keys.delete": "刪除",
- "workspace.members.title": "會員",
+ "workspace.members.title": "成員",
"workspace.members.subtitle": "管理工作區成員及其權限。",
- "workspace.members.invite": "邀請會員",
+ "workspace.members.invite": "邀請成員",
"workspace.members.inviting": "邀請...",
"workspace.members.beta.beforeLink": "測試期間,工作空間對團隊免費。",
"workspace.members.form.invitee": "受邀者",
@@ -379,11 +379,11 @@ export const dict = {
"workspace.members.edit": "編輯",
"workspace.members.delete": "刪除",
"workspace.members.saving": "保存...",
- "workspace.members.save": "節省",
+ "workspace.members.save": "儲存",
"workspace.members.table.email": "電子郵件",
"workspace.members.table.role": "角色",
- "workspace.members.table.monthLimit": "月份限制",
- "workspace.members.role.admin": "行政",
+ "workspace.members.table.monthLimit": "月限額",
+ "workspace.members.role.admin": "管理員",
"workspace.members.role.adminDescription": "可以管理模型、成員和計費",
"workspace.members.role.member": "成員",
"workspace.members.role.memberDescription": "只能為自己生成 API 密鑰",
@@ -392,7 +392,7 @@ export const dict = {
"workspace.settings.workspaceName": "工作區名稱",
"workspace.settings.defaultName": "預設",
"workspace.settings.updating": "更新中...",
- "workspace.settings.save": "節省",
+ "workspace.settings.save": "儲存",
"workspace.settings.edit": "編輯",
"workspace.billing.title": "計費",
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
@@ -404,35 +404,35 @@ export const dict = {
"workspace.billing.loading": "載入中...",
"workspace.billing.addAction": "添加",
"workspace.billing.addBalance": "添加餘額",
- "workspace.billing.linkedToStripe": "鏈接到條紋",
+ "workspace.billing.linkedToStripe": "已連結 Stripe",
"workspace.billing.manage": "管理",
"workspace.billing.enable": "啟用計費",
"workspace.monthlyLimit.title": "每月限額",
"workspace.monthlyLimit.subtitle": "為您的帳戶設置每月使用限額。",
"workspace.monthlyLimit.placeholder": "50",
- "workspace.monthlyLimit.setting": "環境...",
- "workspace.monthlyLimit.set": "放",
+ "workspace.monthlyLimit.setting": "設定中...",
+ "workspace.monthlyLimit.set": "設定",
"workspace.monthlyLimit.edit": "編輯限制",
"workspace.monthlyLimit.noLimit": "沒有設置使用限制。",
- "workspace.monthlyLimit.currentUsage.beforeMonth": "當前使用情況為",
- "workspace.monthlyLimit.currentUsage.beforeAmount": "是 $",
- "workspace.reload.title": "自動重新加載",
- "workspace.reload.disabled.before": "自動重新加載是",
- "workspace.reload.disabled.state": "殘疾人",
- "workspace.reload.disabled.after": "啟用餘額不足時自動充值。",
- "workspace.reload.enabled.before": "自動重新加載是",
+ "workspace.monthlyLimit.currentUsage.beforeMonth": "當前",
+ "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
+ "workspace.reload.title": "自動儲值",
+ "workspace.reload.disabled.before": "自動儲值已",
+ "workspace.reload.disabled.state": "停用",
+ "workspace.reload.disabled.after": "啟用後會在餘額偏低時自動儲值。",
+ "workspace.reload.enabled.before": "自動儲值已",
"workspace.reload.enabled.state": "已啟用",
- "workspace.reload.enabled.middle": "我們將重新加載",
- "workspace.reload.processingFee": "加工費",
+ "workspace.reload.enabled.middle": "我們將自動儲值",
+ "workspace.reload.processingFee": "手續費",
"workspace.reload.enabled.after": "當餘額達到",
"workspace.reload.edit": "編輯",
- "workspace.reload.enable": "使能夠",
- "workspace.reload.enableAutoReload": "啟用自動重新加載",
- "workspace.reload.reloadAmount": "重新加載 $",
+ "workspace.reload.enable": "啟用",
+ "workspace.reload.enableAutoReload": "啟用自動儲值",
+ "workspace.reload.reloadAmount": "儲值 $",
"workspace.reload.whenBalanceReaches": "當餘額達到 $",
"workspace.reload.saving": "保存...",
- "workspace.reload.save": "節省",
- "workspace.reload.failedAt": "重新加載失敗於",
+ "workspace.reload.save": "儲存",
+ "workspace.reload.failedAt": "儲值失敗於",
"workspace.reload.reason": "原因:",
"workspace.reload.updatePaymentMethod": "請更新您的付款方式並重試。",
"workspace.reload.retrying": "正在重試...",
@@ -441,11 +441,11 @@ export const dict = {
"workspace.payments.subtitle": "最近的付款交易。",
"workspace.payments.table.date": "日期",
"workspace.payments.table.paymentId": "付款ID",
- "workspace.payments.table.amount": "數量",
+ "workspace.payments.table.amount": "金額",
"workspace.payments.table.receipt": "收據",
"workspace.payments.type.credit": "信用",
"workspace.payments.type.subscription": "訂閱",
- "workspace.payments.view": "看法",
+ "workspace.payments.view": "查看",
"workspace.black.loading": "載入中...",
"workspace.black.time.day": "天",
"workspace.black.time.days": "天",
@@ -455,20 +455,20 @@ export const dict = {
"workspace.black.time.minutes": "分鐘",
"workspace.black.time.fewSeconds": "幾秒鐘",
"workspace.black.subscription.title": "訂閱",
- "workspace.black.subscription.message": "您已訂閱 OpenCode Black,每月費用為 {{plan}} 美元。",
+ "workspace.black.subscription.message": "您已訂閱 OpenCode Black,費用為每月 ${{plan}}。",
"workspace.black.subscription.manage": "管理訂閱",
"workspace.black.subscription.rollingUsage": "5小時使用",
"workspace.black.subscription.weeklyUsage": "每週使用量",
"workspace.black.subscription.resetsIn": "重置於",
"workspace.black.subscription.useBalance": "達到使用限額後使用您的可用餘額",
"workspace.black.waitlist.title": "候補名單",
- "workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色計劃。",
- "workspace.black.waitlist.ready": "我們已準備好讓您加入每月 {{plan}} 美元的 OpenCode 黑色計劃。",
+ "workspace.black.waitlist.joined": "您已加入每月 ${{plan}} 的 OpenCode Black 方案候補名單。",
+ "workspace.black.waitlist.ready": "我們已準備好將您加入每月 ${{plan}} 的 OpenCode Black 方案。",
"workspace.black.waitlist.leave": "離開候補名單",
"workspace.black.waitlist.leaving": "離開...",
- "workspace.black.waitlist.left": "左邊",
- "workspace.black.waitlist.enroll": "註冊",
- "workspace.black.waitlist.enrolling": "正在報名...",
- "workspace.black.waitlist.enrolled": "已註冊",
+ "workspace.black.waitlist.left": "已退出",
+ "workspace.black.waitlist.enroll": "加入",
+ "workspace.black.waitlist.enrolling": "加入中...",
+ "workspace.black.waitlist.enrolled": "已加入",
"workspace.black.waitlist.enrollNote": "單擊“註冊”後,您的訂閱將立即開始,並且將從您的卡中扣費。",
} satisfies Dict
diff --git a/specs/19-workspace-i18n-audit-overview.md b/specs/19-workspace-i18n-audit-overview.md
new file mode 100644
index 000000000000..42599a2f6169
--- /dev/null
+++ b/specs/19-workspace-i18n-audit-overview.md
@@ -0,0 +1,76 @@
+## workspace i18n audit overview (zen workspace routes)
+
+Audit objective: verify translation accuracy for customer-facing workspace management pages used to manage Zen (AI inference provider) usage, with strict terminology and brand integrity.
+
+---
+
+### scope
+
+- Routes audited: `packages/console/app/src/routes/workspace/**` (all workspace TSX routes, including billing/keys/members/settings)
+- Locale files audited: `packages/console/app/src/i18n/ar.ts`, `packages/console/app/src/i18n/da.ts`, `packages/console/app/src/i18n/de.ts`, `packages/console/app/src/i18n/it.ts`
+- Source-of-truth copy: `packages/console/app/src/i18n/en.ts`
+- Total unique translation keys used by workspace routes: **181**
+
+---
+
+### deliverables
+
+- Arabic audit and fix spec: `specs/20-workspace-i18n-audit-ar.md`
+- Danish audit and fix spec: `specs/21-workspace-i18n-audit-da.md`
+- German audit and fix spec: `specs/22-workspace-i18n-audit-de.md`
+- Italian audit and fix spec: `specs/23-workspace-i18n-audit-it.md`
+
+These files are intentionally execution-ready so follow-up agents can apply string fixes without re-auditing.
+
+---
+
+### non-negotiable terminology contract
+
+1. Product names and brands:
+ - `Zen` must remain exactly `Zen` (never translated, never lowercased).
+ - `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models` remain literal.
+2. AI domain terms must preserve technical meaning:
+ - `model` = AI model, not fashion/product model.
+ - `provider` = AI provider/vendor.
+ - `Bring Your Own Key` = BYOK concept (user-provided API credential).
+3. Billing/top-up language must stay financial:
+ - `reload` in billing pages means balance top-up/recharge, not page refresh.
+ - `amount`, `cost`, `usage`, `monthly spending limit` must map to metering/billing context.
+4. Placeholder and interpolation safety:
+ - Keep placeholders unchanged (`{{provider}}`, `{{prefix}}`, `{{amount}}`, etc).
+ - Keep currency symbols and key ordering intact.
+
+---
+
+### cross-locale risk summary
+
+- Critical risk focus across locales: billing `reload` text must consistently mean balance top-up/recharge (not page refresh), while product literals and CLI command verbs remain exact.
+- `ar` (Arabic): **31** flagged keys (critical: 4, major: 23, minor: 4), with critical literals broken for `Zen`, `Stripe`, and `OpenCode Black` waitlist strings.
+- `da` (Danish): **33** flagged keys (critical: 8, major: 18, minor: 7), concentrated in reload/top-up semantics plus onboarding/default-label blockers.
+- `de` (German): **34** flagged keys (critical: 6, major: 27, minor: 1), centered on reload/top-up semantics, onboarding command phrasing, and usage metric wording.
+- `it` (Italian): **25** flagged keys (critical: 2, major: 18, minor: 5), led by `Zen` casing and waitlist status semantics with broader API/billing wording drift.
+
+---
+
+### remediation workflow for follow-up agents
+
+1. Pick one locale spec (`20`/`21`/`22`/`23`) and apply only mapped key updates in that locale file.
+2. Do not add/remove keys; only edit string values.
+3. Preserve placeholders and punctuation contracts.
+4. Run app typecheck:
+ - `bun run typecheck` in `packages/console/app`
+5. Run post-fix terminology sanity checks:
+ - Literal checks in locale files: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
+ - `workspace.reload.*` language matches top-up/recharge semantics.
+ - `API key` labels are ordered naturally in target language.
+ - Placeholders remain unchanged (`{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`, etc).
+
+---
+
+### acceptance criteria
+
+- All mapped critical and major issues in locale specs are fixed.
+- `workspace.nav.zen` is exactly `Zen` in all locales.
+- `workspace.billing.linkedToStripe` preserves `Stripe` literal in all locales.
+- Workspace billing and usage terminology is technically accurate for AI inference workflows.
+- No placeholder regressions.
diff --git a/specs/20-workspace-i18n-audit-ar.md b/specs/20-workspace-i18n-audit-ar.md
new file mode 100644
index 000000000000..5eab6fed985a
--- /dev/null
+++ b/specs/20-workspace-i18n-audit-ar.md
@@ -0,0 +1,99 @@
+## workspace i18n audit - ar
+
+Arabic translation audit for workspace routes with focus on Zen/AI/billing terminology accuracy.
+
+---
+
+### coverage
+
+- Route scope: `packages/console/app/src/routes/workspace/**`
+- Locale file: `packages/console/app/src/i18n/ar.ts`
+- Source reference: `packages/console/app/src/i18n/en.ts`
+- Unique keys in scope: **181**
+- Flagged keys: **31** (critical: 4, major: 23, minor: 4)
+
+---
+
+### glossary decisions (ar)
+
+- `Zen`: keep exactly `Zen`
+- `OpenCode Black`: keep exactly `OpenCode Black`
+- `Stripe`: keep exactly `Stripe`
+- `opencode`: keep exactly `opencode`
+- `API`: keep exactly `API`
+- `/models`: keep exactly `/models`
+- `model`: `نموذج` / `نماذج`
+- `provider`: `مزوّد`
+- `API key`: `مفتاح API`
+- `Bring Your Own Key`: `استخدم مفتاحك الخاص`
+- `usage`: `الاستخدام`
+- `cost`: `التكلفة`
+- `billing`: `الفوترة`
+- `monthly spending limit`: `حد الإنفاق الشهري`
+- `auto reload` (billing context): `إعادة الشحن التلقائي`
+- Keep placeholders exactly as-is: `{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`.
+
+---
+
+### required key updates
+
+| key | replace with | severity | reason |
+| -------------------------------------- | ------------------------------------------------------------------ | -------- | ---------------------------------------------------- |
+| workspace.nav.zen | Zen | critical | Product literal must not be translated. |
+| workspace.billing.linkedToStripe | مرتبط بـ Stripe | critical | Keep payment brand literal `Stripe`. |
+| workspace.black.waitlist.joined | أنت على قائمة الانتظار لخطة OpenCode Black بقيمة ${{plan}} شهريًا. | critical | Preserve exact literal `OpenCode Black`. |
+| workspace.black.waitlist.ready | نحن مستعدون لتسجيلك في خطة OpenCode Black بقيمة ${{plan}} شهريًا. | critical | Preserve exact literal `OpenCode Black`. |
+| workspace.nav.apiKeys | مفاتيح API | major | Correct `API` key word order. |
+| workspace.keys.title | مفاتيح API | major | Correct `API` key word order. |
+| workspace.providers.table.apiKey | مفتاح API | major | Correct `API` key word order. |
+| workspace.cost.title | التكلفة | major | `Cost` mistranslated as verb. |
+| workspace.usage.table.cost | التكلفة | major | `Cost` mistranslated as verb. |
+| workspace.cost.allModels | جميع النماذج | major | Prefer standard AI term `نماذج`. |
+| workspace.cost.subscriptionShort | اشتراك | major | Current value is semantically wrong. |
+| workspace.payments.table.amount | المبلغ | major | Financial amount mistranslated as quantity. |
+| workspace.payments.view | عرض | major | CTA mistranslated as noun “scenery”. |
+| workspace.monthlyLimit.setting | جارٍ التعيين... | major | Progress state mistranslated. |
+| workspace.settings.defaultName | الافتراضي | major | Default label mistranslated. |
+| workspace.newUser.step.login.before | شغّل | major | CLI imperative “Run” mistranslated. |
+| workspace.members.role.admin | مسؤول | major | Spelling error. |
+| workspace.reload.title | إعادة الشحن التلقائي | major | Billing `reload` means top-up/recharge, not refresh. |
+| workspace.reload.enableAutoReload | تفعيل إعادة الشحن التلقائي | major | Must reflect top-up semantics. |
+| workspace.reload.reloadAmount | مبلغ إعادة الشحن $ | major | Must reflect recharge amount. |
+| workspace.reload.failedAt | فشلت إعادة الشحن في | major | Must reflect recharge failure. |
+| workspace.reload.disabled.state | معطّل | major | Incorrect toggle state term. |
+| workspace.reload.enabled.after | عندما يصل الرصيد إلى | major | `balance` mistranslated as “equilibrium”. |
+| workspace.reload.enable | تفعيل | major | CTA form mistranslated. |
+| workspace.reload.disabled.before | إعادة الشحن التلقائي | major | Align phrase with recharge semantics. |
+| workspace.reload.enabled.before | إعادة الشحن التلقائي | major | Align phrase with recharge semantics. |
+| workspace.reload.enabled.middle | سنعيد شحن رصيدك بمبلغ | major | Must explicitly mean topping up balance. |
+| workspace.billing.addAction | إضافة | minor | CTA grammatical form fix. |
+| workspace.billing.manage | إدارة | minor | CTA grammatical form fix. |
+| workspace.keys.empty | أنشئ مفتاح API لبوابة opencode | minor | Improve technical clarity and ordering. |
+| workspace.newUser.feature.lockin.title | بدون احتجاز بمزوّد واحد | minor | Better conveys “No Lock-in”. |
+
+---
+
+### implementation batches
+
+1. **Brand literals first (critical)**
+ - `workspace.nav.zen`, `workspace.billing.linkedToStripe`, `workspace.black.waitlist.joined`, `workspace.black.waitlist.ready`
+2. **Reload/recharge semantics**
+ - `workspace.reload.*` flagged keys to ensure top-up meaning (not page refresh)
+3. **Billing/payment terminology**
+ - `workspace.cost.title`, `workspace.usage.table.cost`, `workspace.payments.table.amount`, `workspace.payments.view`, `workspace.monthlyLimit.setting`
+4. **API/workspace wording fixes**
+ - API-key ordering keys, `workspace.settings.defaultName`, `workspace.members.role.admin`, `workspace.newUser.step.login.before`
+5. **Minor CTA/clarity polish**
+ - `workspace.billing.addAction`, `workspace.billing.manage`, `workspace.keys.empty`, `workspace.newUser.feature.lockin.title`
+
+---
+
+### acceptance checks for follow-up fix agent
+
+- `workspace.nav.zen` value is exactly `Zen`.
+- `workspace.billing.linkedToStripe` contains `Stripe` literal.
+- `workspace.black.waitlist.joined` and `workspace.black.waitlist.ready` keep `OpenCode Black` exactly.
+- No `workspace.reload.*` string implies page refresh; all imply balance top-up/recharge.
+- API key labels use `مفتاح API` / `مفاتيح API` ordering.
+- Preserve literals exactly: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
+- All placeholders remain unchanged (`{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`).
diff --git a/specs/21-workspace-i18n-audit-da.md b/specs/21-workspace-i18n-audit-da.md
new file mode 100644
index 000000000000..c0c0ef0c0578
--- /dev/null
+++ b/specs/21-workspace-i18n-audit-da.md
@@ -0,0 +1,103 @@
+## workspace i18n audit - da
+
+Danish translation audit for workspace routes with emphasis on AI-provider, billing, and workspace-management accuracy.
+
+---
+
+### coverage
+
+- Route scope: `packages/console/app/src/routes/workspace/**`
+- Locale file: `packages/console/app/src/i18n/da.ts`
+- Source reference: `packages/console/app/src/i18n/en.ts`
+- Unique locale keys used in scope: **181**
+- Flagged keys: **33** (critical: 8, major: 18, minor: 7)
+
+---
+
+### glossary decisions (da)
+
+- `Zen`: keep exactly `Zen`
+- `Stripe`: keep exactly `Stripe`
+- `OpenCode Black`: keep exactly `OpenCode Black`
+- `opencode`: keep exactly `opencode`
+- `API`: keep exactly `API`
+- `/models`: keep exactly `/models`
+- `API key`: `API-nøgle`
+- `billing`: `fakturering`
+- `reload` in wallet context: `genopfyldning` (top-up), never page refresh wording
+
+---
+
+### required key updates
+
+| key | current da | replace with | severity | reason |
+| ------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------- |
+| workspace.newUser.step.login.before | Løbe | Kør | critical | Wrong verb for CLI instruction (`opencode auth login`). |
+| workspace.settings.defaultName | Misligholdelse | Standard | critical | Wrong meaning for default-label UI text. |
+| workspace.reload.title | Automatisk genindlæsning | Automatisk genopfyldning | critical | `reload` here is balance top-up, not page reload. |
+| workspace.reload.disabled.before | Automatisk genindlæsning er | Automatisk genopfyldning er | critical | Same billing top-up semantic issue. |
+| workspace.reload.enabled.before | Automatisk genindlæsning er | Automatisk genopfyldning er | critical | Same billing top-up semantic issue. |
+| workspace.reload.enableAutoReload | Aktiver automatisk genindlæsning | Aktiver automatisk genopfyldning | critical | Same billing top-up semantic issue. |
+| workspace.reload.reloadAmount | Genindlæs $ | Genopfyld $ | critical | Top-up amount label, not page reload action. |
+| workspace.reload.failedAt | Genindlæsning mislykkedes kl | Genopfyldning mislykkedes kl | critical | Error refers to failed recharge event. |
+| workspace.reload.disabled.after | Aktiver for automatisk at genindlæse, når balancen er lav. | Aktiver for automatisk at genopfylde, når saldoen er lav. | major | Must describe low-balance auto top-up behavior. |
+| workspace.reload.enabled.middle | Vi genindlæser | Vi genopfylder | major | Must describe automatic recharge action. |
+| workspace.reload.disabled.state | handicappet | deaktiveret | major | Wrong/off-tone toggle-state wording. |
+| workspace.members.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
+| workspace.providers.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
+| workspace.reload.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
+| workspace.settings.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
+| workspace.keys.copyApiKey | Kopiér nøglen API | Kopiér API-nøgle | major | Incorrect technical phrase order. |
+| workspace.newUser.copyApiKey | Kopiér nøglen API | Kopiér API-nøgle | major | Incorrect technical phrase order. |
+| workspace.providers.placeholder | Indtast nøglen {{provider}} API ({{prefix}}...) | Indtast {{provider}} API-nøgle ({{prefix}}...) | major | Technical phrase is malformed; placeholders must be preserved exactly. |
+| workspace.newUser.feature.tested.body | Vi har benchmarket og testet modeller specifikt til kodningsmidler for at sikre den bedste ydeevne. | Vi har benchmarket og testet modeller specifikt til kodningsagenter for at sikre den bedste ydeevne. | major | Wrong domain noun (`kodningsmidler` != coding agents). |
+| workspace.payments.table.receipt | Modtagelse | Kvittering | major | Wrong payment term for receipt column. |
+| workspace.payments.view | Udsigt | Vis | major | Wrong CTA meaning for “View” action. |
+| workspace.black.waitlist.left | Venstre | Forladt | major | Wrong meaning (“left” direction vs status). |
+| workspace.usage.table.output | Produktion | Output | major | Wrong AI metering term in token table. |
+| workspace.usage.breakdown.output | Produktion | Output | major | Wrong AI metering term in token breakdown. |
+| workspace.usage.table.cost | Koste | Omkostning | major | Wrong part of speech for metric label. |
+| workspace.cost.title | Koste | Omkostninger | major | Wrong part of speech for section heading. |
+| workspace.members.edit | Redigere | Rediger | minor | Use imperative CTA form. |
+| workspace.providers.edit | Redigere | Rediger | minor | Use imperative CTA form. |
+| workspace.reload.edit | Redigere | Rediger | minor | Use imperative CTA form. |
+| workspace.settings.edit | Redigere | Rediger | minor | Use imperative CTA form. |
+| workspace.billing.addAction | Tilføje | Tilføj | minor | Use imperative CTA form. |
+| workspace.billing.manage | Styre | Administrer | minor | Better management CTA wording in product UI. |
+| workspace.black.waitlist.enroll | Indskrive | Tilmeld | minor | Better enrollment CTA for subscription/waitlist flow. |
+
+---
+
+### implementation batches
+
+1. **Critical blockers: onboarding + default + reload semantics**
+ - `workspace.newUser.step.login.before`
+ - `workspace.settings.defaultName`
+ - `workspace.reload.title`
+ - `workspace.reload.disabled.before`
+ - `workspace.reload.enabled.before`
+ - `workspace.reload.enableAutoReload`
+ - `workspace.reload.reloadAmount`
+ - `workspace.reload.failedAt`
+2. **Billing top-up and save-action correctness**
+ - Remaining `workspace.reload.*` in table
+ - `workspace.members.save`, `workspace.providers.save`, `workspace.reload.save`, `workspace.settings.save`
+3. **API and provider terminology cleanup**
+ - `workspace.keys.copyApiKey`, `workspace.newUser.copyApiKey`, `workspace.providers.placeholder`
+4. **Usage/payments/workspace label fixes**
+ - `workspace.newUser.feature.tested.body`
+ - `workspace.payments.table.receipt`, `workspace.payments.view`
+ - `workspace.black.waitlist.left`
+ - `workspace.usage.table.output`, `workspace.usage.breakdown.output`, `workspace.usage.table.cost`, `workspace.cost.title`
+5. **CTA polish (minor imperative consistency)**
+ - all `*.edit`, `workspace.billing.addAction`, `workspace.billing.manage`, `workspace.black.waitlist.enroll`
+
+---
+
+### acceptance checks for follow-up fix agent
+
+- Preserve literals exactly: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
+- Preserve placeholders exactly (eg `{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`).
+- All `workspace.reload.*` labels must mean balance top-up/recharge (`genopfyld*`), never page refresh (`genindlæs*`).
+- Save CTAs must reflect saving changes (`Gem` / `Gemmer...`), not savings-money wording.
+- AI metering labels in usage tables remain technically correct (`Input`, `Output`, cost nouns).
diff --git a/specs/22-workspace-i18n-audit-de.md b/specs/22-workspace-i18n-audit-de.md
new file mode 100644
index 000000000000..cfc782008e26
--- /dev/null
+++ b/specs/22-workspace-i18n-audit-de.md
@@ -0,0 +1,95 @@
+## workspace i18n audit - de
+
+German translation audit for workspace routes with focus on CLI onboarding clarity, AI metric terminology, and billing top-up semantics.
+
+---
+
+### coverage
+
+- Route scope: `packages/console/app/src/routes/workspace/**`
+- Locale file: `packages/console/app/src/i18n/de.ts`
+- Source reference: `packages/console/app/src/i18n/en.ts`
+- Unique keys in scope: **181**
+- Flagged keys: **34** (critical: 6, major: 27, minor: 1)
+
+---
+
+### glossary decisions (de)
+
+- Preserve literals exactly: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`
+- Preserve placeholders exactly (`{{provider}}`, `{{amount}}`, `{{plan}}`, `{{prefix}}`, etc.)
+- `API key`: `API-Schlüssel`
+- `usage`: `Nutzung`
+- `cost`: `Kosten`; payments column `Betrag`
+- `billing`: `Abrechnung`
+- Billing `reload` semantics: `Aufladung` / `aufladen` (never `Neuladen`)
+- AI cost metrics: keep `Input`, `Output`, `Reasoning`
+
+---
+
+### required key updates
+
+| key | replace with | severity | reason |
+| ------------------------------------- | ---------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------- |
+| workspace.newUser.step.login.before | Führe | critical | Wrong CLI verb in onboarding instruction. |
+| workspace.monthlyLimit.set | Festlegen | critical | CTA mistranslated as noun (`Satz`). |
+| workspace.reload.title | Automatische Aufladung | critical | Billing top-up semantics, not page reload. |
+| workspace.reload.disabled.after | Aktivieren Sie diese Option, damit bei niedrigem Kontostand automatisch aufgeladen wird. | critical | Uses `neu laden`; must use billing top-up phrasing. |
+| workspace.reload.enableAutoReload | Automatische Aufladung aktivieren | critical | Primary toggle must follow billing top-up semantics. |
+| workspace.black.subscription.message | Sie haben OpenCode Black für ${{plan}} pro Monat abonniert. | critical | Missing `$` before `{{plan}}`. |
+| workspace.newUser.step.models.before | Starte opencode und führe | major | Unidiomatic command phrasing before `/models`. |
+| workspace.newUser.copyApiKey | API-Schlüssel kopieren | major | Broken technical term word order. |
+| workspace.keys.copyApiKey | API-Schlüssel kopieren | major | Broken technical term word order. |
+| workspace.models.table.enabled | Aktiviert | major | Wrong toggle-state label. |
+| workspace.providers.saving | Wird gespeichert... | major | `Sparen...` means saving money, not save state. |
+| workspace.members.inviting | Wird eingeladen... | major | Incorrect invite loading state wording. |
+| workspace.members.saving | Wird gespeichert... | major | `Sparen...` means saving money, not save state. |
+| workspace.monthlyLimit.setting | Wird gesetzt... | major | In-progress state should be verbal action. |
+| workspace.payments.table.amount | Betrag | major | Financial column mistranslated as quantity. |
+| workspace.payments.view | Anzeigen | major | CTA mistranslated as noun (`Sicht`). |
+| workspace.usage.table.input | Input | major | AI billing metric should remain technical term. |
+| workspace.usage.table.output | Output | major | AI billing metric should remain technical term. |
+| workspace.usage.breakdown.input | Input | major | AI billing metric should remain technical term. |
+| workspace.usage.breakdown.output | Output | major | AI billing metric should remain technical term. |
+| workspace.usage.breakdown.reasoning | Reasoning | major | AI billing metric should remain technical term. |
+| workspace.reload.disabled.before | Automatische Aufladung ist | major | Should use top-up semantics instead of `Nachladen`. |
+| workspace.reload.enabled.before | Automatische Aufladung ist | major | Should use top-up semantics instead of `Nachladen`. |
+| workspace.reload.enabled.state | aktiviert | major | `ermöglicht` is wrong state adjective for toggle. |
+| workspace.reload.enabled.middle | Wir laden auf | major | Should be account top-up verb (`aufladen`). |
+| workspace.reload.enabled.after | sobald der Kontostand | major | `Gleichgewicht` is wrong meaning for account balance. |
+| workspace.reload.reloadAmount | Aufladebetrag $ | major | Label should reflect top-up amount semantics. |
+| workspace.reload.whenBalanceReaches | Wenn der Kontostand $ erreicht | major | Use account-balance term `Kontostand`. |
+| workspace.reload.failedAt | Aufladung fehlgeschlagen am | major | Error copy should use top-up semantics. |
+| workspace.reload.saving | Wird gespeichert... | major | `Sparen...` means saving money, not save state. |
+| workspace.black.waitlist.left | Verlassen | major | `Links` is incorrect meaning. |
+| workspace.black.waitlist.joined | Sie stehen auf der Warteliste für den OpenCode Black Tarif für ${{plan}} pro Monat. | major | Product name order/phrase is broken. |
+| workspace.black.waitlist.ready | Wir können Sie jetzt in den OpenCode Black Tarif für ${{plan}} pro Monat aufnehmen. | major | Product name order/phrase is broken. |
+| workspace.black.subscription.resetsIn | Zurückgesetzt in | minor | Improve pre-duration label grammar. |
+
+---
+
+### implementation batches
+
+1. **Critical blockers first**
+ - `workspace.newUser.step.login.before`, `workspace.monthlyLimit.set`, `workspace.reload.title`, `workspace.reload.disabled.after`, `workspace.reload.enableAutoReload`, `workspace.black.subscription.message`
+2. **Reload semantics sweep**
+ - All `workspace.reload.*` keys listed above (state labels, helper text, error text, amount labels)
+3. **Onboarding + API key wording**
+ - `workspace.newUser.step.models.before`, `workspace.newUser.copyApiKey`, `workspace.keys.copyApiKey`
+4. **Async/CTA quality fixes**
+ - `workspace.providers.saving`, `workspace.members.inviting`, `workspace.members.saving`, `workspace.monthlyLimit.setting`, `workspace.payments.view`, `workspace.reload.saving`
+5. **AI usage/payments terminology**
+ - usage `Input/Output/Reasoning` keys + `workspace.payments.table.amount`
+6. **OpenCode Black waitlist copy**
+ - `workspace.black.waitlist.left`, `workspace.black.waitlist.joined`, `workspace.black.waitlist.ready`, `workspace.black.subscription.resetsIn`
+
+---
+
+### acceptance checks for follow-up fix agent
+
+- Flagged totals align with this spec: **34** (`critical` 6, `major` 27, `minor` 1).
+- `workspace.nav.zen` remains exactly `Zen`.
+- Literals remain exact where present: `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
+- Billing top-up copy consistently uses `Aufladung` / `aufladen` (not `Neuladen`/`Nachladen`).
+- Usage metrics remain technical terms: `Input`, `Output`, `Reasoning`.
+- Placeholders remain unchanged in all updated strings.
diff --git a/specs/23-workspace-i18n-audit-it.md b/specs/23-workspace-i18n-audit-it.md
new file mode 100644
index 000000000000..da1b72826973
--- /dev/null
+++ b/specs/23-workspace-i18n-audit-it.md
@@ -0,0 +1,86 @@
+## workspace i18n audit - it
+
+Italian translation audit for workspace routes with focus on Zen brand integrity and AI/billing term precision.
+
+---
+
+### coverage
+
+- Route scope: `packages/console/app/src/routes/workspace/**`
+- Locale file: `packages/console/app/src/i18n/it.ts`
+- Source reference: `packages/console/app/src/i18n/en.ts`
+- Unique keys in scope: **181** (`177` `workspace.*` + `4` `common.*`)
+- Flagged keys: **25** (critical: 2, major: 18, minor: 5)
+
+---
+
+### glossary decisions (it)
+
+- `Zen`: keep exactly `Zen`
+- `Stripe`: keep exactly `Stripe`
+- `OpenCode Black`: keep exactly `OpenCode Black`
+- `opencode`: keep exactly `opencode`
+- `API`: keep exactly `API`
+- `/models`: keep exactly `/models`
+- `API key`: `chiave API` / `chiavi API`
+- `billing`: `fatturazione`
+- `auto reload` (billing): `ricarica automatica`
+- `reload` in billing context: top-up/recharge semantics (never page refresh)
+
+---
+
+### required key updates
+
+| key | current_it | replace with | severity | reason |
+| -------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------- | --------------------------------------------------------- |
+| workspace.nav.zen | zen | Zen | critical | Brand literal must match exactly. |
+| workspace.black.waitlist.left | Sinistra | Uscito dalla lista d'attesa | critical | `Left` is waitlist status, not direction. |
+| workspace.billing.manage | Maneggio | Gestisci | major | Wrong lexical meaning for CTA. |
+| workspace.billing.subtitle.beforeLink | Gestire i metodi di pagamento. | Gestisci i metodi di pagamento. | major | UI copy should be imperative, not infinitive. |
+| workspace.black.waitlist.joined | Sei in lista d'attesa per il piano nero ${{plan}} al mese OpenCode. | Sei in lista d'attesa per il piano OpenCode Black da ${{plan}} al mese. | major | Preserve exact product naming and natural billing phrase. |
+| workspace.black.waitlist.leaving | In partenza... | Uscita in corso... | major | Wrong async action meaning for waitlist exit. |
+| workspace.black.subscription.resetsIn | Si reimposta | Si reimposta tra | major | Missing connector for duration composition. |
+| workspace.members.inviting | Invitante... | Invito in corso... | major | Incorrect grammar for in-progress invite state. |
+| workspace.members.role.admin | Ammin | Admin | major | Role label typo / non-standard abbreviation. |
+| workspace.members.saving | Risparmio... | Salvataggio in corso... | major | Persistence action mistranslated as money saving. |
+| workspace.providers.saving | Risparmio... | Salvataggio in corso... | major | Persistence action mistranslated as money saving. |
+| workspace.reload.saving | Risparmio... | Salvataggio in corso... | major | Persistence action mistranslated as money saving. |
+| workspace.monthlyLimit.setting | Collocamento... | Impostazione in corso... | major | Wrong lexical meaning for `Setting...`. |
+| workspace.newUser.step.login.before | Correre | Esegui | major | CLI instruction must convey `Run`. |
+| workspace.payments.table.amount | Quantità | Importo | major | Financial amount requires `Importo`. |
+| workspace.keys.title | API Chiavi | Chiavi API | major | Technical term order is incorrect. |
+| workspace.nav.apiKeys | API Chiavi | Chiavi API | major | Technical term order is incorrect. |
+| workspace.providers.table.apiKey | API Chiave | Chiave API | major | Technical term order is incorrect. |
+| workspace.providers.title | Porta la tua chiave | Bring Your Own Key (BYOK) | major | Preserve BYOK concept explicitly in AI-provider context. |
+| workspace.reload.enabled.after | quando l'equilibrio raggiunge | quando il saldo raggiunge | major | Billing semantic error (`saldo`, not physical balance). |
+| workspace.billing.addAction | Aggiungere | Aggiungi | minor | CTA should be imperative form. |
+| workspace.payments.view | Visualizzazione | Visualizza | minor | CTA should be verb, not noun. |
+| workspace.reload.disabled.before | La ricarica automatica lo è | La ricarica automatica è | minor | Remove incorrect pronoun for grammatical sentence. |
+| workspace.reload.enabled.before | La ricarica automatica lo è | La ricarica automatica è | minor | Remove incorrect pronoun for grammatical sentence. |
+| workspace.newUser.feature.lockin.title | Nessun blocco | Nessun lock-in | minor | Prefer established product/industry term. |
+
+---
+
+### implementation batches
+
+1. **Critical brand/status fixes**
+ - `workspace.nav.zen`, `workspace.black.waitlist.left`
+2. **Billing and waitlist semantics**
+ - `workspace.billing.manage`, `workspace.billing.subtitle.beforeLink`, `workspace.black.waitlist.joined`, `workspace.black.waitlist.leaving`, `workspace.black.subscription.resetsIn`, `workspace.reload.enabled.after`, `workspace.payments.table.amount`
+3. **Async/action grammar correctness**
+ - `workspace.members.inviting`, all `*.saving` keys, `workspace.monthlyLimit.setting`, `workspace.newUser.step.login.before`
+4. **AI/API terminology consistency**
+ - `workspace.keys.title`, `workspace.nav.apiKeys`, `workspace.providers.table.apiKey`, `workspace.providers.title`
+5. **Minor CTA/style normalizations**
+ - `workspace.billing.addAction`, `workspace.payments.view`, `workspace.reload.disabled.before`, `workspace.reload.enabled.before`, `workspace.newUser.feature.lockin.title`
+
+---
+
+### acceptance checks for follow-up fix agent
+
+- `workspace.nav.zen` is exactly `Zen`.
+- Waitlist status strings use state semantics (`joined`, `leaving`, `left`) and preserve `OpenCode Black`.
+- Billing `reload` copy clearly means balance top-up/recharge.
+- `Chiave API` / `Chiavi API` ordering is consistent across workspace UI.
+- BYOK meaning remains explicit in provider setup title.
+- All placeholders are preserved exactly (`{{plan}}`, `{{provider}}`, `{{prefix}}`, `{{amount}}`, etc.).
From 9ac54adbb2443b6b8c11f2509aadc29b7ed87368 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Sun, 8 Feb 2026 16:23:24 -0600
Subject: [PATCH 059/399] chore: cleanup
---
specs/19-workspace-i18n-audit-overview.md | 76 ----------------
specs/20-workspace-i18n-audit-ar.md | 99 ---------------------
specs/21-workspace-i18n-audit-da.md | 103 ----------------------
specs/22-workspace-i18n-audit-de.md | 95 --------------------
specs/23-workspace-i18n-audit-it.md | 86 ------------------
5 files changed, 459 deletions(-)
delete mode 100644 specs/19-workspace-i18n-audit-overview.md
delete mode 100644 specs/20-workspace-i18n-audit-ar.md
delete mode 100644 specs/21-workspace-i18n-audit-da.md
delete mode 100644 specs/22-workspace-i18n-audit-de.md
delete mode 100644 specs/23-workspace-i18n-audit-it.md
diff --git a/specs/19-workspace-i18n-audit-overview.md b/specs/19-workspace-i18n-audit-overview.md
deleted file mode 100644
index 42599a2f6169..000000000000
--- a/specs/19-workspace-i18n-audit-overview.md
+++ /dev/null
@@ -1,76 +0,0 @@
-## workspace i18n audit overview (zen workspace routes)
-
-Audit objective: verify translation accuracy for customer-facing workspace management pages used to manage Zen (AI inference provider) usage, with strict terminology and brand integrity.
-
----
-
-### scope
-
-- Routes audited: `packages/console/app/src/routes/workspace/**` (all workspace TSX routes, including billing/keys/members/settings)
-- Locale files audited: `packages/console/app/src/i18n/ar.ts`, `packages/console/app/src/i18n/da.ts`, `packages/console/app/src/i18n/de.ts`, `packages/console/app/src/i18n/it.ts`
-- Source-of-truth copy: `packages/console/app/src/i18n/en.ts`
-- Total unique translation keys used by workspace routes: **181**
-
----
-
-### deliverables
-
-- Arabic audit and fix spec: `specs/20-workspace-i18n-audit-ar.md`
-- Danish audit and fix spec: `specs/21-workspace-i18n-audit-da.md`
-- German audit and fix spec: `specs/22-workspace-i18n-audit-de.md`
-- Italian audit and fix spec: `specs/23-workspace-i18n-audit-it.md`
-
-These files are intentionally execution-ready so follow-up agents can apply string fixes without re-auditing.
-
----
-
-### non-negotiable terminology contract
-
-1. Product names and brands:
- - `Zen` must remain exactly `Zen` (never translated, never lowercased).
- - `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models` remain literal.
-2. AI domain terms must preserve technical meaning:
- - `model` = AI model, not fashion/product model.
- - `provider` = AI provider/vendor.
- - `Bring Your Own Key` = BYOK concept (user-provided API credential).
-3. Billing/top-up language must stay financial:
- - `reload` in billing pages means balance top-up/recharge, not page refresh.
- - `amount`, `cost`, `usage`, `monthly spending limit` must map to metering/billing context.
-4. Placeholder and interpolation safety:
- - Keep placeholders unchanged (`{{provider}}`, `{{prefix}}`, `{{amount}}`, etc).
- - Keep currency symbols and key ordering intact.
-
----
-
-### cross-locale risk summary
-
-- Critical risk focus across locales: billing `reload` text must consistently mean balance top-up/recharge (not page refresh), while product literals and CLI command verbs remain exact.
-- `ar` (Arabic): **31** flagged keys (critical: 4, major: 23, minor: 4), with critical literals broken for `Zen`, `Stripe`, and `OpenCode Black` waitlist strings.
-- `da` (Danish): **33** flagged keys (critical: 8, major: 18, minor: 7), concentrated in reload/top-up semantics plus onboarding/default-label blockers.
-- `de` (German): **34** flagged keys (critical: 6, major: 27, minor: 1), centered on reload/top-up semantics, onboarding command phrasing, and usage metric wording.
-- `it` (Italian): **25** flagged keys (critical: 2, major: 18, minor: 5), led by `Zen` casing and waitlist status semantics with broader API/billing wording drift.
-
----
-
-### remediation workflow for follow-up agents
-
-1. Pick one locale spec (`20`/`21`/`22`/`23`) and apply only mapped key updates in that locale file.
-2. Do not add/remove keys; only edit string values.
-3. Preserve placeholders and punctuation contracts.
-4. Run app typecheck:
- - `bun run typecheck` in `packages/console/app`
-5. Run post-fix terminology sanity checks:
- - Literal checks in locale files: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
- - `workspace.reload.*` language matches top-up/recharge semantics.
- - `API key` labels are ordered naturally in target language.
- - Placeholders remain unchanged (`{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`, etc).
-
----
-
-### acceptance criteria
-
-- All mapped critical and major issues in locale specs are fixed.
-- `workspace.nav.zen` is exactly `Zen` in all locales.
-- `workspace.billing.linkedToStripe` preserves `Stripe` literal in all locales.
-- Workspace billing and usage terminology is technically accurate for AI inference workflows.
-- No placeholder regressions.
diff --git a/specs/20-workspace-i18n-audit-ar.md b/specs/20-workspace-i18n-audit-ar.md
deleted file mode 100644
index 5eab6fed985a..000000000000
--- a/specs/20-workspace-i18n-audit-ar.md
+++ /dev/null
@@ -1,99 +0,0 @@
-## workspace i18n audit - ar
-
-Arabic translation audit for workspace routes with focus on Zen/AI/billing terminology accuracy.
-
----
-
-### coverage
-
-- Route scope: `packages/console/app/src/routes/workspace/**`
-- Locale file: `packages/console/app/src/i18n/ar.ts`
-- Source reference: `packages/console/app/src/i18n/en.ts`
-- Unique keys in scope: **181**
-- Flagged keys: **31** (critical: 4, major: 23, minor: 4)
-
----
-
-### glossary decisions (ar)
-
-- `Zen`: keep exactly `Zen`
-- `OpenCode Black`: keep exactly `OpenCode Black`
-- `Stripe`: keep exactly `Stripe`
-- `opencode`: keep exactly `opencode`
-- `API`: keep exactly `API`
-- `/models`: keep exactly `/models`
-- `model`: `نموذج` / `نماذج`
-- `provider`: `مزوّد`
-- `API key`: `مفتاح API`
-- `Bring Your Own Key`: `استخدم مفتاحك الخاص`
-- `usage`: `الاستخدام`
-- `cost`: `التكلفة`
-- `billing`: `الفوترة`
-- `monthly spending limit`: `حد الإنفاق الشهري`
-- `auto reload` (billing context): `إعادة الشحن التلقائي`
-- Keep placeholders exactly as-is: `{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`.
-
----
-
-### required key updates
-
-| key | replace with | severity | reason |
-| -------------------------------------- | ------------------------------------------------------------------ | -------- | ---------------------------------------------------- |
-| workspace.nav.zen | Zen | critical | Product literal must not be translated. |
-| workspace.billing.linkedToStripe | مرتبط بـ Stripe | critical | Keep payment brand literal `Stripe`. |
-| workspace.black.waitlist.joined | أنت على قائمة الانتظار لخطة OpenCode Black بقيمة ${{plan}} شهريًا. | critical | Preserve exact literal `OpenCode Black`. |
-| workspace.black.waitlist.ready | نحن مستعدون لتسجيلك في خطة OpenCode Black بقيمة ${{plan}} شهريًا. | critical | Preserve exact literal `OpenCode Black`. |
-| workspace.nav.apiKeys | مفاتيح API | major | Correct `API` key word order. |
-| workspace.keys.title | مفاتيح API | major | Correct `API` key word order. |
-| workspace.providers.table.apiKey | مفتاح API | major | Correct `API` key word order. |
-| workspace.cost.title | التكلفة | major | `Cost` mistranslated as verb. |
-| workspace.usage.table.cost | التكلفة | major | `Cost` mistranslated as verb. |
-| workspace.cost.allModels | جميع النماذج | major | Prefer standard AI term `نماذج`. |
-| workspace.cost.subscriptionShort | اشتراك | major | Current value is semantically wrong. |
-| workspace.payments.table.amount | المبلغ | major | Financial amount mistranslated as quantity. |
-| workspace.payments.view | عرض | major | CTA mistranslated as noun “scenery”. |
-| workspace.monthlyLimit.setting | جارٍ التعيين... | major | Progress state mistranslated. |
-| workspace.settings.defaultName | الافتراضي | major | Default label mistranslated. |
-| workspace.newUser.step.login.before | شغّل | major | CLI imperative “Run” mistranslated. |
-| workspace.members.role.admin | مسؤول | major | Spelling error. |
-| workspace.reload.title | إعادة الشحن التلقائي | major | Billing `reload` means top-up/recharge, not refresh. |
-| workspace.reload.enableAutoReload | تفعيل إعادة الشحن التلقائي | major | Must reflect top-up semantics. |
-| workspace.reload.reloadAmount | مبلغ إعادة الشحن $ | major | Must reflect recharge amount. |
-| workspace.reload.failedAt | فشلت إعادة الشحن في | major | Must reflect recharge failure. |
-| workspace.reload.disabled.state | معطّل | major | Incorrect toggle state term. |
-| workspace.reload.enabled.after | عندما يصل الرصيد إلى | major | `balance` mistranslated as “equilibrium”. |
-| workspace.reload.enable | تفعيل | major | CTA form mistranslated. |
-| workspace.reload.disabled.before | إعادة الشحن التلقائي | major | Align phrase with recharge semantics. |
-| workspace.reload.enabled.before | إعادة الشحن التلقائي | major | Align phrase with recharge semantics. |
-| workspace.reload.enabled.middle | سنعيد شحن رصيدك بمبلغ | major | Must explicitly mean topping up balance. |
-| workspace.billing.addAction | إضافة | minor | CTA grammatical form fix. |
-| workspace.billing.manage | إدارة | minor | CTA grammatical form fix. |
-| workspace.keys.empty | أنشئ مفتاح API لبوابة opencode | minor | Improve technical clarity and ordering. |
-| workspace.newUser.feature.lockin.title | بدون احتجاز بمزوّد واحد | minor | Better conveys “No Lock-in”. |
-
----
-
-### implementation batches
-
-1. **Brand literals first (critical)**
- - `workspace.nav.zen`, `workspace.billing.linkedToStripe`, `workspace.black.waitlist.joined`, `workspace.black.waitlist.ready`
-2. **Reload/recharge semantics**
- - `workspace.reload.*` flagged keys to ensure top-up meaning (not page refresh)
-3. **Billing/payment terminology**
- - `workspace.cost.title`, `workspace.usage.table.cost`, `workspace.payments.table.amount`, `workspace.payments.view`, `workspace.monthlyLimit.setting`
-4. **API/workspace wording fixes**
- - API-key ordering keys, `workspace.settings.defaultName`, `workspace.members.role.admin`, `workspace.newUser.step.login.before`
-5. **Minor CTA/clarity polish**
- - `workspace.billing.addAction`, `workspace.billing.manage`, `workspace.keys.empty`, `workspace.newUser.feature.lockin.title`
-
----
-
-### acceptance checks for follow-up fix agent
-
-- `workspace.nav.zen` value is exactly `Zen`.
-- `workspace.billing.linkedToStripe` contains `Stripe` literal.
-- `workspace.black.waitlist.joined` and `workspace.black.waitlist.ready` keep `OpenCode Black` exactly.
-- No `workspace.reload.*` string implies page refresh; all imply balance top-up/recharge.
-- API key labels use `مفتاح API` / `مفاتيح API` ordering.
-- Preserve literals exactly: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
-- All placeholders remain unchanged (`{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`).
diff --git a/specs/21-workspace-i18n-audit-da.md b/specs/21-workspace-i18n-audit-da.md
deleted file mode 100644
index c0c0ef0c0578..000000000000
--- a/specs/21-workspace-i18n-audit-da.md
+++ /dev/null
@@ -1,103 +0,0 @@
-## workspace i18n audit - da
-
-Danish translation audit for workspace routes with emphasis on AI-provider, billing, and workspace-management accuracy.
-
----
-
-### coverage
-
-- Route scope: `packages/console/app/src/routes/workspace/**`
-- Locale file: `packages/console/app/src/i18n/da.ts`
-- Source reference: `packages/console/app/src/i18n/en.ts`
-- Unique locale keys used in scope: **181**
-- Flagged keys: **33** (critical: 8, major: 18, minor: 7)
-
----
-
-### glossary decisions (da)
-
-- `Zen`: keep exactly `Zen`
-- `Stripe`: keep exactly `Stripe`
-- `OpenCode Black`: keep exactly `OpenCode Black`
-- `opencode`: keep exactly `opencode`
-- `API`: keep exactly `API`
-- `/models`: keep exactly `/models`
-- `API key`: `API-nøgle`
-- `billing`: `fakturering`
-- `reload` in wallet context: `genopfyldning` (top-up), never page refresh wording
-
----
-
-### required key updates
-
-| key | current da | replace with | severity | reason |
-| ------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------- |
-| workspace.newUser.step.login.before | Løbe | Kør | critical | Wrong verb for CLI instruction (`opencode auth login`). |
-| workspace.settings.defaultName | Misligholdelse | Standard | critical | Wrong meaning for default-label UI text. |
-| workspace.reload.title | Automatisk genindlæsning | Automatisk genopfyldning | critical | `reload` here is balance top-up, not page reload. |
-| workspace.reload.disabled.before | Automatisk genindlæsning er | Automatisk genopfyldning er | critical | Same billing top-up semantic issue. |
-| workspace.reload.enabled.before | Automatisk genindlæsning er | Automatisk genopfyldning er | critical | Same billing top-up semantic issue. |
-| workspace.reload.enableAutoReload | Aktiver automatisk genindlæsning | Aktiver automatisk genopfyldning | critical | Same billing top-up semantic issue. |
-| workspace.reload.reloadAmount | Genindlæs $ | Genopfyld $ | critical | Top-up amount label, not page reload action. |
-| workspace.reload.failedAt | Genindlæsning mislykkedes kl | Genopfyldning mislykkedes kl | critical | Error refers to failed recharge event. |
-| workspace.reload.disabled.after | Aktiver for automatisk at genindlæse, når balancen er lav. | Aktiver for automatisk at genopfylde, når saldoen er lav. | major | Must describe low-balance auto top-up behavior. |
-| workspace.reload.enabled.middle | Vi genindlæser | Vi genopfylder | major | Must describe automatic recharge action. |
-| workspace.reload.disabled.state | handicappet | deaktiveret | major | Wrong/off-tone toggle-state wording. |
-| workspace.members.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
-| workspace.providers.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
-| workspace.reload.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
-| workspace.settings.save | Spare | Gem | major | Current word means monetary savings, not save changes. |
-| workspace.keys.copyApiKey | Kopiér nøglen API | Kopiér API-nøgle | major | Incorrect technical phrase order. |
-| workspace.newUser.copyApiKey | Kopiér nøglen API | Kopiér API-nøgle | major | Incorrect technical phrase order. |
-| workspace.providers.placeholder | Indtast nøglen {{provider}} API ({{prefix}}...) | Indtast {{provider}} API-nøgle ({{prefix}}...) | major | Technical phrase is malformed; placeholders must be preserved exactly. |
-| workspace.newUser.feature.tested.body | Vi har benchmarket og testet modeller specifikt til kodningsmidler for at sikre den bedste ydeevne. | Vi har benchmarket og testet modeller specifikt til kodningsagenter for at sikre den bedste ydeevne. | major | Wrong domain noun (`kodningsmidler` != coding agents). |
-| workspace.payments.table.receipt | Modtagelse | Kvittering | major | Wrong payment term for receipt column. |
-| workspace.payments.view | Udsigt | Vis | major | Wrong CTA meaning for “View” action. |
-| workspace.black.waitlist.left | Venstre | Forladt | major | Wrong meaning (“left” direction vs status). |
-| workspace.usage.table.output | Produktion | Output | major | Wrong AI metering term in token table. |
-| workspace.usage.breakdown.output | Produktion | Output | major | Wrong AI metering term in token breakdown. |
-| workspace.usage.table.cost | Koste | Omkostning | major | Wrong part of speech for metric label. |
-| workspace.cost.title | Koste | Omkostninger | major | Wrong part of speech for section heading. |
-| workspace.members.edit | Redigere | Rediger | minor | Use imperative CTA form. |
-| workspace.providers.edit | Redigere | Rediger | minor | Use imperative CTA form. |
-| workspace.reload.edit | Redigere | Rediger | minor | Use imperative CTA form. |
-| workspace.settings.edit | Redigere | Rediger | minor | Use imperative CTA form. |
-| workspace.billing.addAction | Tilføje | Tilføj | minor | Use imperative CTA form. |
-| workspace.billing.manage | Styre | Administrer | minor | Better management CTA wording in product UI. |
-| workspace.black.waitlist.enroll | Indskrive | Tilmeld | minor | Better enrollment CTA for subscription/waitlist flow. |
-
----
-
-### implementation batches
-
-1. **Critical blockers: onboarding + default + reload semantics**
- - `workspace.newUser.step.login.before`
- - `workspace.settings.defaultName`
- - `workspace.reload.title`
- - `workspace.reload.disabled.before`
- - `workspace.reload.enabled.before`
- - `workspace.reload.enableAutoReload`
- - `workspace.reload.reloadAmount`
- - `workspace.reload.failedAt`
-2. **Billing top-up and save-action correctness**
- - Remaining `workspace.reload.*` in table
- - `workspace.members.save`, `workspace.providers.save`, `workspace.reload.save`, `workspace.settings.save`
-3. **API and provider terminology cleanup**
- - `workspace.keys.copyApiKey`, `workspace.newUser.copyApiKey`, `workspace.providers.placeholder`
-4. **Usage/payments/workspace label fixes**
- - `workspace.newUser.feature.tested.body`
- - `workspace.payments.table.receipt`, `workspace.payments.view`
- - `workspace.black.waitlist.left`
- - `workspace.usage.table.output`, `workspace.usage.breakdown.output`, `workspace.usage.table.cost`, `workspace.cost.title`
-5. **CTA polish (minor imperative consistency)**
- - all `*.edit`, `workspace.billing.addAction`, `workspace.billing.manage`, `workspace.black.waitlist.enroll`
-
----
-
-### acceptance checks for follow-up fix agent
-
-- Preserve literals exactly: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
-- Preserve placeholders exactly (eg `{{provider}}`, `{{prefix}}`, `{{plan}}`, `{{amount}}`).
-- All `workspace.reload.*` labels must mean balance top-up/recharge (`genopfyld*`), never page refresh (`genindlæs*`).
-- Save CTAs must reflect saving changes (`Gem` / `Gemmer...`), not savings-money wording.
-- AI metering labels in usage tables remain technically correct (`Input`, `Output`, cost nouns).
diff --git a/specs/22-workspace-i18n-audit-de.md b/specs/22-workspace-i18n-audit-de.md
deleted file mode 100644
index cfc782008e26..000000000000
--- a/specs/22-workspace-i18n-audit-de.md
+++ /dev/null
@@ -1,95 +0,0 @@
-## workspace i18n audit - de
-
-German translation audit for workspace routes with focus on CLI onboarding clarity, AI metric terminology, and billing top-up semantics.
-
----
-
-### coverage
-
-- Route scope: `packages/console/app/src/routes/workspace/**`
-- Locale file: `packages/console/app/src/i18n/de.ts`
-- Source reference: `packages/console/app/src/i18n/en.ts`
-- Unique keys in scope: **181**
-- Flagged keys: **34** (critical: 6, major: 27, minor: 1)
-
----
-
-### glossary decisions (de)
-
-- Preserve literals exactly: `Zen`, `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`
-- Preserve placeholders exactly (`{{provider}}`, `{{amount}}`, `{{plan}}`, `{{prefix}}`, etc.)
-- `API key`: `API-Schlüssel`
-- `usage`: `Nutzung`
-- `cost`: `Kosten`; payments column `Betrag`
-- `billing`: `Abrechnung`
-- Billing `reload` semantics: `Aufladung` / `aufladen` (never `Neuladen`)
-- AI cost metrics: keep `Input`, `Output`, `Reasoning`
-
----
-
-### required key updates
-
-| key | replace with | severity | reason |
-| ------------------------------------- | ---------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------- |
-| workspace.newUser.step.login.before | Führe | critical | Wrong CLI verb in onboarding instruction. |
-| workspace.monthlyLimit.set | Festlegen | critical | CTA mistranslated as noun (`Satz`). |
-| workspace.reload.title | Automatische Aufladung | critical | Billing top-up semantics, not page reload. |
-| workspace.reload.disabled.after | Aktivieren Sie diese Option, damit bei niedrigem Kontostand automatisch aufgeladen wird. | critical | Uses `neu laden`; must use billing top-up phrasing. |
-| workspace.reload.enableAutoReload | Automatische Aufladung aktivieren | critical | Primary toggle must follow billing top-up semantics. |
-| workspace.black.subscription.message | Sie haben OpenCode Black für ${{plan}} pro Monat abonniert. | critical | Missing `$` before `{{plan}}`. |
-| workspace.newUser.step.models.before | Starte opencode und führe | major | Unidiomatic command phrasing before `/models`. |
-| workspace.newUser.copyApiKey | API-Schlüssel kopieren | major | Broken technical term word order. |
-| workspace.keys.copyApiKey | API-Schlüssel kopieren | major | Broken technical term word order. |
-| workspace.models.table.enabled | Aktiviert | major | Wrong toggle-state label. |
-| workspace.providers.saving | Wird gespeichert... | major | `Sparen...` means saving money, not save state. |
-| workspace.members.inviting | Wird eingeladen... | major | Incorrect invite loading state wording. |
-| workspace.members.saving | Wird gespeichert... | major | `Sparen...` means saving money, not save state. |
-| workspace.monthlyLimit.setting | Wird gesetzt... | major | In-progress state should be verbal action. |
-| workspace.payments.table.amount | Betrag | major | Financial column mistranslated as quantity. |
-| workspace.payments.view | Anzeigen | major | CTA mistranslated as noun (`Sicht`). |
-| workspace.usage.table.input | Input | major | AI billing metric should remain technical term. |
-| workspace.usage.table.output | Output | major | AI billing metric should remain technical term. |
-| workspace.usage.breakdown.input | Input | major | AI billing metric should remain technical term. |
-| workspace.usage.breakdown.output | Output | major | AI billing metric should remain technical term. |
-| workspace.usage.breakdown.reasoning | Reasoning | major | AI billing metric should remain technical term. |
-| workspace.reload.disabled.before | Automatische Aufladung ist | major | Should use top-up semantics instead of `Nachladen`. |
-| workspace.reload.enabled.before | Automatische Aufladung ist | major | Should use top-up semantics instead of `Nachladen`. |
-| workspace.reload.enabled.state | aktiviert | major | `ermöglicht` is wrong state adjective for toggle. |
-| workspace.reload.enabled.middle | Wir laden auf | major | Should be account top-up verb (`aufladen`). |
-| workspace.reload.enabled.after | sobald der Kontostand | major | `Gleichgewicht` is wrong meaning for account balance. |
-| workspace.reload.reloadAmount | Aufladebetrag $ | major | Label should reflect top-up amount semantics. |
-| workspace.reload.whenBalanceReaches | Wenn der Kontostand $ erreicht | major | Use account-balance term `Kontostand`. |
-| workspace.reload.failedAt | Aufladung fehlgeschlagen am | major | Error copy should use top-up semantics. |
-| workspace.reload.saving | Wird gespeichert... | major | `Sparen...` means saving money, not save state. |
-| workspace.black.waitlist.left | Verlassen | major | `Links` is incorrect meaning. |
-| workspace.black.waitlist.joined | Sie stehen auf der Warteliste für den OpenCode Black Tarif für ${{plan}} pro Monat. | major | Product name order/phrase is broken. |
-| workspace.black.waitlist.ready | Wir können Sie jetzt in den OpenCode Black Tarif für ${{plan}} pro Monat aufnehmen. | major | Product name order/phrase is broken. |
-| workspace.black.subscription.resetsIn | Zurückgesetzt in | minor | Improve pre-duration label grammar. |
-
----
-
-### implementation batches
-
-1. **Critical blockers first**
- - `workspace.newUser.step.login.before`, `workspace.monthlyLimit.set`, `workspace.reload.title`, `workspace.reload.disabled.after`, `workspace.reload.enableAutoReload`, `workspace.black.subscription.message`
-2. **Reload semantics sweep**
- - All `workspace.reload.*` keys listed above (state labels, helper text, error text, amount labels)
-3. **Onboarding + API key wording**
- - `workspace.newUser.step.models.before`, `workspace.newUser.copyApiKey`, `workspace.keys.copyApiKey`
-4. **Async/CTA quality fixes**
- - `workspace.providers.saving`, `workspace.members.inviting`, `workspace.members.saving`, `workspace.monthlyLimit.setting`, `workspace.payments.view`, `workspace.reload.saving`
-5. **AI usage/payments terminology**
- - usage `Input/Output/Reasoning` keys + `workspace.payments.table.amount`
-6. **OpenCode Black waitlist copy**
- - `workspace.black.waitlist.left`, `workspace.black.waitlist.joined`, `workspace.black.waitlist.ready`, `workspace.black.subscription.resetsIn`
-
----
-
-### acceptance checks for follow-up fix agent
-
-- Flagged totals align with this spec: **34** (`critical` 6, `major` 27, `minor` 1).
-- `workspace.nav.zen` remains exactly `Zen`.
-- Literals remain exact where present: `Stripe`, `OpenCode Black`, `opencode`, `API`, `/models`.
-- Billing top-up copy consistently uses `Aufladung` / `aufladen` (not `Neuladen`/`Nachladen`).
-- Usage metrics remain technical terms: `Input`, `Output`, `Reasoning`.
-- Placeholders remain unchanged in all updated strings.
diff --git a/specs/23-workspace-i18n-audit-it.md b/specs/23-workspace-i18n-audit-it.md
deleted file mode 100644
index da1b72826973..000000000000
--- a/specs/23-workspace-i18n-audit-it.md
+++ /dev/null
@@ -1,86 +0,0 @@
-## workspace i18n audit - it
-
-Italian translation audit for workspace routes with focus on Zen brand integrity and AI/billing term precision.
-
----
-
-### coverage
-
-- Route scope: `packages/console/app/src/routes/workspace/**`
-- Locale file: `packages/console/app/src/i18n/it.ts`
-- Source reference: `packages/console/app/src/i18n/en.ts`
-- Unique keys in scope: **181** (`177` `workspace.*` + `4` `common.*`)
-- Flagged keys: **25** (critical: 2, major: 18, minor: 5)
-
----
-
-### glossary decisions (it)
-
-- `Zen`: keep exactly `Zen`
-- `Stripe`: keep exactly `Stripe`
-- `OpenCode Black`: keep exactly `OpenCode Black`
-- `opencode`: keep exactly `opencode`
-- `API`: keep exactly `API`
-- `/models`: keep exactly `/models`
-- `API key`: `chiave API` / `chiavi API`
-- `billing`: `fatturazione`
-- `auto reload` (billing): `ricarica automatica`
-- `reload` in billing context: top-up/recharge semantics (never page refresh)
-
----
-
-### required key updates
-
-| key | current_it | replace with | severity | reason |
-| -------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------- | --------------------------------------------------------- |
-| workspace.nav.zen | zen | Zen | critical | Brand literal must match exactly. |
-| workspace.black.waitlist.left | Sinistra | Uscito dalla lista d'attesa | critical | `Left` is waitlist status, not direction. |
-| workspace.billing.manage | Maneggio | Gestisci | major | Wrong lexical meaning for CTA. |
-| workspace.billing.subtitle.beforeLink | Gestire i metodi di pagamento. | Gestisci i metodi di pagamento. | major | UI copy should be imperative, not infinitive. |
-| workspace.black.waitlist.joined | Sei in lista d'attesa per il piano nero ${{plan}} al mese OpenCode. | Sei in lista d'attesa per il piano OpenCode Black da ${{plan}} al mese. | major | Preserve exact product naming and natural billing phrase. |
-| workspace.black.waitlist.leaving | In partenza... | Uscita in corso... | major | Wrong async action meaning for waitlist exit. |
-| workspace.black.subscription.resetsIn | Si reimposta | Si reimposta tra | major | Missing connector for duration composition. |
-| workspace.members.inviting | Invitante... | Invito in corso... | major | Incorrect grammar for in-progress invite state. |
-| workspace.members.role.admin | Ammin | Admin | major | Role label typo / non-standard abbreviation. |
-| workspace.members.saving | Risparmio... | Salvataggio in corso... | major | Persistence action mistranslated as money saving. |
-| workspace.providers.saving | Risparmio... | Salvataggio in corso... | major | Persistence action mistranslated as money saving. |
-| workspace.reload.saving | Risparmio... | Salvataggio in corso... | major | Persistence action mistranslated as money saving. |
-| workspace.monthlyLimit.setting | Collocamento... | Impostazione in corso... | major | Wrong lexical meaning for `Setting...`. |
-| workspace.newUser.step.login.before | Correre | Esegui | major | CLI instruction must convey `Run`. |
-| workspace.payments.table.amount | Quantità | Importo | major | Financial amount requires `Importo`. |
-| workspace.keys.title | API Chiavi | Chiavi API | major | Technical term order is incorrect. |
-| workspace.nav.apiKeys | API Chiavi | Chiavi API | major | Technical term order is incorrect. |
-| workspace.providers.table.apiKey | API Chiave | Chiave API | major | Technical term order is incorrect. |
-| workspace.providers.title | Porta la tua chiave | Bring Your Own Key (BYOK) | major | Preserve BYOK concept explicitly in AI-provider context. |
-| workspace.reload.enabled.after | quando l'equilibrio raggiunge | quando il saldo raggiunge | major | Billing semantic error (`saldo`, not physical balance). |
-| workspace.billing.addAction | Aggiungere | Aggiungi | minor | CTA should be imperative form. |
-| workspace.payments.view | Visualizzazione | Visualizza | minor | CTA should be verb, not noun. |
-| workspace.reload.disabled.before | La ricarica automatica lo è | La ricarica automatica è | minor | Remove incorrect pronoun for grammatical sentence. |
-| workspace.reload.enabled.before | La ricarica automatica lo è | La ricarica automatica è | minor | Remove incorrect pronoun for grammatical sentence. |
-| workspace.newUser.feature.lockin.title | Nessun blocco | Nessun lock-in | minor | Prefer established product/industry term. |
-
----
-
-### implementation batches
-
-1. **Critical brand/status fixes**
- - `workspace.nav.zen`, `workspace.black.waitlist.left`
-2. **Billing and waitlist semantics**
- - `workspace.billing.manage`, `workspace.billing.subtitle.beforeLink`, `workspace.black.waitlist.joined`, `workspace.black.waitlist.leaving`, `workspace.black.subscription.resetsIn`, `workspace.reload.enabled.after`, `workspace.payments.table.amount`
-3. **Async/action grammar correctness**
- - `workspace.members.inviting`, all `*.saving` keys, `workspace.monthlyLimit.setting`, `workspace.newUser.step.login.before`
-4. **AI/API terminology consistency**
- - `workspace.keys.title`, `workspace.nav.apiKeys`, `workspace.providers.table.apiKey`, `workspace.providers.title`
-5. **Minor CTA/style normalizations**
- - `workspace.billing.addAction`, `workspace.payments.view`, `workspace.reload.disabled.before`, `workspace.reload.enabled.before`, `workspace.newUser.feature.lockin.title`
-
----
-
-### acceptance checks for follow-up fix agent
-
-- `workspace.nav.zen` is exactly `Zen`.
-- Waitlist status strings use state semantics (`joined`, `leaving`, `left`) and preserve `OpenCode Black`.
-- Billing `reload` copy clearly means balance top-up/recharge.
-- `Chiave API` / `Chiavi API` ordering is consistent across workspace UI.
-- BYOK meaning remains explicit in provider setup title.
-- All placeholders are preserved exactly (`{{plan}}`, `{{provider}}`, `{{prefix}}`, `{{amount}}`, etc.).
From a598ecac1f7593ac4533222c4223acb0122d5107 Mon Sep 17 00:00:00 2001
From: Alex Yaroshuk <34632190+alexyaroshuk@users.noreply.github.com>
Date: Mon, 9 Feb 2026 09:14:48 +0800
Subject: [PATCH 060/399] fix(app): localize "close tab" in command pallete
(#12756)
---
packages/app/src/i18n/ar.ts | 1 +
packages/app/src/i18n/br.ts | 1 +
packages/app/src/i18n/da.ts | 1 +
packages/app/src/i18n/de.ts | 1 +
packages/app/src/i18n/es.ts | 1 +
packages/app/src/i18n/fr.ts | 1 +
packages/app/src/i18n/ja.ts | 1 +
packages/app/src/i18n/ko.ts | 1 +
packages/app/src/i18n/no.ts | 1 +
packages/app/src/i18n/pl.ts | 1 +
packages/app/src/i18n/ru.ts | 1 +
packages/app/src/i18n/th.ts | 1 +
packages/app/src/i18n/zh.ts | 1 +
packages/app/src/i18n/zht.ts | 1 +
14 files changed, 14 insertions(+)
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index 3778adcd6799..201d63660a39 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "جلسة جديدة",
"command.file.open": "فتح ملف",
+ "command.tab.close": "إغلاق علامة التبويب",
"command.context.addSelection": "إضافة التحديد إلى السياق",
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
"command.input.focus": "التركيز على حقل الإدخال",
diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts
index 74bfd8707c88..b7f2d74857f7 100644
--- a/packages/app/src/i18n/br.ts
+++ b/packages/app/src/i18n/br.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nova sessão",
"command.file.open": "Abrir arquivo",
+ "command.tab.close": "Fechar aba",
"command.context.addSelection": "Adicionar seleção ao contexto",
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
"command.input.focus": "Focar entrada",
diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts
index 7242fb5849f2..8ea4907c1b67 100644
--- a/packages/app/src/i18n/da.ts
+++ b/packages/app/src/i18n/da.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Ny session",
"command.file.open": "Åbn fil",
+ "command.tab.close": "Luk fane",
"command.context.addSelection": "Tilføj markering til kontekst",
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
"command.input.focus": "Fokuser inputfelt",
diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts
index bd8acae5e8fc..a4884a1033dc 100644
--- a/packages/app/src/i18n/de.ts
+++ b/packages/app/src/i18n/de.ts
@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "Neue Sitzung",
"command.file.open": "Datei öffnen",
+ "command.tab.close": "Tab schließen",
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
"command.input.focus": "Eingabefeld fokussieren",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index f9b11ade8705..50d9060703ea 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nueva sesión",
"command.file.open": "Abrir archivo",
+ "command.tab.close": "Cerrar pestaña",
"command.context.addSelection": "Añadir selección al contexto",
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
"command.input.focus": "Enfocar entrada",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index 0cc81e5ea7a6..7ad39f340639 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nouvelle session",
"command.file.open": "Ouvrir un fichier",
+ "command.tab.close": "Fermer l'onglet",
"command.context.addSelection": "Ajouter la sélection au contexte",
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
"command.input.focus": "Focus input",
diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts
index 337e1b0d349f..a39bfbaf331b 100644
--- a/packages/app/src/i18n/ja.ts
+++ b/packages/app/src/i18n/ja.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "新しいセッション",
"command.file.open": "ファイルを開く",
+ "command.tab.close": "タブを閉じる",
"command.context.addSelection": "選択範囲をコンテキストに追加",
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
"command.input.focus": "入力欄にフォーカス",
diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts
index 283bb6f3bdc6..b5927b210767 100644
--- a/packages/app/src/i18n/ko.ts
+++ b/packages/app/src/i18n/ko.ts
@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "새 세션",
"command.file.open": "파일 열기",
+ "command.tab.close": "탭 닫기",
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
"command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가",
"command.input.focus": "입력창 포커스",
diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts
index bbffd0083d14..7d8cdd27f3dd 100644
--- a/packages/app/src/i18n/no.ts
+++ b/packages/app/src/i18n/no.ts
@@ -47,6 +47,7 @@ export const dict = {
"command.session.new": "Ny sesjon",
"command.file.open": "Åpne fil",
+ "command.tab.close": "Lukk fane",
"command.context.addSelection": "Legg til markering i kontekst",
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
"command.input.focus": "Fokuser inndata",
diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts
index 2d36ca8c1809..76a47ea26f8d 100644
--- a/packages/app/src/i18n/pl.ts
+++ b/packages/app/src/i18n/pl.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Nowa sesja",
"command.file.open": "Otwórz plik",
+ "command.tab.close": "Zamknij kartę",
"command.context.addSelection": "Dodaj zaznaczenie do kontekstu",
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
"command.input.focus": "Fokus na pole wejściowe",
diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts
index 18b0ba5f47d5..e83ce37618c5 100644
--- a/packages/app/src/i18n/ru.ts
+++ b/packages/app/src/i18n/ru.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "Новая сессия",
"command.file.open": "Открыть файл",
+ "command.tab.close": "Закрыть вкладку",
"command.context.addSelection": "Добавить выделение в контекст",
"command.context.addSelection.description": "Добавить выбранные строки из текущего файла",
"command.input.focus": "Фокус на поле ввода",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index d48a7cea665b..2be19d15b174 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -44,6 +44,7 @@ export const dict = {
"command.session.new": "เซสชันใหม่",
"command.file.open": "เปิดไฟล์",
+ "command.tab.close": "ปิดแท็บ",
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
"command.input.focus": "โฟกัสช่องป้อนข้อมูล",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index 070064d1c416..a48f9e549415 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "新建会话",
"command.file.open": "打开文件",
+ "command.tab.close": "关闭标签页",
"command.context.addSelection": "将所选内容添加到上下文",
"command.context.addSelection.description": "添加当前文件中选中的行",
"command.input.focus": "聚焦输入框",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index 39dcd92e2767..60363fc99eff 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -48,6 +48,7 @@ export const dict = {
"command.session.new": "新增工作階段",
"command.file.open": "開啟檔案",
+ "command.tab.close": "關閉分頁",
"command.context.addSelection": "將選取內容加入上下文",
"command.context.addSelection.description": "加入目前檔案中選取的行",
"command.input.focus": "聚焦輸入框",
From 79879b43cee88d0701448725acc3d0cc1d7664d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?=
Date: Mon, 9 Feb 2026 02:43:10 +0100
Subject: [PATCH 061/399] refactor(nix): use native Bun APIs and propagate
errors (#12694)
---
nix/scripts/canonicalize-node-modules.ts | 56 ++++++++++--------------
nix/scripts/normalize-bun-binaries.ts | 16 ++-----
2 files changed, 26 insertions(+), 46 deletions(-)
diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts
index faa6f63402e2..7997a3cd2325 100644
--- a/nix/scripts/canonicalize-node-modules.ts
+++ b/nix/scripts/canonicalize-node-modules.ts
@@ -1,27 +1,32 @@
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
import { join, relative } from "path"
-type SemverLike = {
- valid: (value: string) => string | null
- rcompare: (left: string, right: string) => number
-}
-
type Entry = {
dir: string
version: string
- label: string
}
+async function isDirectory(path: string) {
+ try {
+ const info = await lstat(path)
+ return info.isDirectory()
+ } catch {
+ return false
+ }
+}
+
+const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x")
+
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const linkRoot = join(bunRoot, "node_modules")
const directories = (await readdir(bunRoot)).sort()
+
const versions = new Map()
for (const entry of directories) {
const full = join(bunRoot, entry)
- const info = await lstat(full)
- if (!info.isDirectory()) {
+ if (!(await isDirectory(full))) {
continue
}
const parsed = parseEntry(entry)
@@ -29,37 +34,23 @@ for (const entry of directories) {
continue
}
const list = versions.get(parsed.name) ?? []
- list.push({ dir: full, version: parsed.version, label: entry })
+ list.push({ dir: full, version: parsed.version })
versions.set(parsed.name, list)
}
-const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
- | SemverLike
- | {
- default: SemverLike
- }
-const semver = "default" in semverModule ? semverModule.default : semverModule
const selections = new Map()
for (const [slug, list] of versions) {
list.sort((a, b) => {
- const left = semver.valid(a.version)
- const right = semver.valid(b.version)
- if (left && right) {
- const delta = semver.rcompare(left, right)
- if (delta !== 0) {
- return delta
- }
- }
- if (left && !right) {
- return -1
- }
- if (!left && right) {
- return 1
- }
+ const aValid = isValidSemver(a.version)
+ const bValid = isValidSemver(b.version)
+ if (aValid && bValid) return -Bun.semver.order(a.version, b.version)
+ if (aValid) return -1
+ if (bValid) return 1
return b.version.localeCompare(a.version)
})
- selections.set(slug, list[0])
+ const first = list[0]
+ if (first) selections.set(slug, first)
}
await rm(linkRoot, { recursive: true, force: true })
@@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
await mkdir(parent, { recursive: true })
const linkPath = join(parent, leaf)
const desired = join(entry.dir, "node_modules", slug)
- const exists = await lstat(desired)
- .then((info) => info.isDirectory())
- .catch(() => false)
- if (!exists) {
+ if (!(await isDirectory(desired))) {
continue
}
const relativeTarget = relative(parent, desired)
diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts
index 531d8fd0567a..978ab325b7bb 100644
--- a/nix/scripts/normalize-bun-binaries.ts
+++ b/nix/scripts/normalize-bun-binaries.ts
@@ -8,7 +8,7 @@ type PackageManifest = {
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
-const bunEntries = (await safeReadDir(bunRoot)).sort()
+const bunEntries = (await readdir(bunRoot)).sort()
let rewritten = 0
for (const entry of bunEntries) {
@@ -45,11 +45,11 @@ for (const entry of bunEntries) {
}
}
-console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
+console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`)
async function collectPackages(modulesRoot: string) {
const found: string[] = []
- const topLevel = (await safeReadDir(modulesRoot)).sort()
+ const topLevel = (await readdir(modulesRoot)).sort()
for (const name of topLevel) {
if (name === ".bin" || name === ".bun") {
continue
@@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) {
continue
}
if (name.startsWith("@")) {
- const scoped = (await safeReadDir(full)).sort()
+ const scoped = (await readdir(full)).sort()
for (const child of scoped) {
const scopedDir = join(full, child)
if (await isDirectory(scopedDir)) {
@@ -121,14 +121,6 @@ async function isDirectory(path: string) {
}
}
-async function safeReadDir(path: string) {
- try {
- return await readdir(path)
- } catch {
- return []
- }
-}
-
function normalizeBinName(name: string) {
const slash = name.lastIndexOf("/")
if (slash >= 0) {
From 62f38087b8a1fa5eeebc14c376f0ebfb1d7b3e5c Mon Sep 17 00:00:00 2001
From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Date: Sun, 8 Feb 2026 20:55:41 -0600
Subject: [PATCH 062/399] fix: parse mid stream openai responses style errors
to prevent infinite retries for errors that should STOP execution (#12768)
---
packages/opencode/src/session/message-v2.ts | 71 ++++++++++++++++++-
.../opencode/test/session/message-v2.test.ts | 54 ++++++++++++++
2 files changed, 124 insertions(+), 1 deletion(-)
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 65ac72e050cd..3119c2bce319 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -796,7 +796,76 @@ export namespace MessageV2 {
case e instanceof Error:
return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
default:
- return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
+ try {
+ const json = iife(() => {
+ if (typeof e === "string") {
+ try {
+ return JSON.parse(e)
+ } catch {
+ return undefined
+ }
+ }
+
+ if (typeof e === "object" && e !== null) {
+ return e
+ }
+ return undefined
+ })
+ if (json) {
+ const responseBody = JSON.stringify(json)
+ // Handle Responses API mid stream style errors
+ if (json?.type === "error") {
+ switch (json?.error?.code) {
+ case "context_length_exceeded":
+ return new MessageV2.APIError(
+ {
+ message: "Input exceeds context window of this model",
+ isRetryable: false,
+ responseBody,
+ },
+ {
+ cause: e,
+ },
+ ).toObject()
+ case "insufficient_quota":
+ return new MessageV2.APIError(
+ {
+ message: "Quota exceeded. Check your plan and billing details.",
+ isRetryable: false,
+ responseBody,
+ },
+ {
+ cause: e,
+ },
+ ).toObject()
+ case "usage_not_included":
+ return new MessageV2.APIError(
+ {
+ message:
+ "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
+ isRetryable: false,
+ responseBody,
+ },
+ {
+ cause: e,
+ },
+ ).toObject()
+ case "invalid_prompt":
+ return new MessageV2.APIError(
+ {
+ message: json?.error?.message || "Invalid prompt.",
+ isRetryable: false,
+ responseBody,
+ },
+ {
+ cause: e,
+ },
+ ).toObject()
+ }
+ }
+ }
+ } catch {}
+ return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
}
}
}
diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts
index 2f632ad1cf2b..39c58bb6ec75 100644
--- a/packages/opencode/test/session/message-v2.test.ts
+++ b/packages/opencode/test/session/message-v2.test.ts
@@ -784,3 +784,57 @@ describe("session.message-v2.toModelMessage", () => {
])
})
})
+
+describe("session.message-v2.fromError", () => {
+ test("serializes response error codes", () => {
+ const cases = [
+ {
+ code: "context_length_exceeded",
+ message: "Input exceeds context window of this model",
+ },
+ {
+ code: "insufficient_quota",
+ message: "Quota exceeded. Check your plan and billing details.",
+ },
+ {
+ code: "usage_not_included",
+ message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
+ },
+ {
+ code: "invalid_prompt",
+ message: "Invalid prompt from test",
+ },
+ ]
+
+ cases.forEach((item) => {
+ const input = {
+ type: "error",
+ error: {
+ code: item.code,
+ message: item.code === "invalid_prompt" ? item.message : undefined,
+ },
+ }
+ const result = MessageV2.fromError(input, { providerID: "test" })
+
+ expect(result).toStrictEqual({
+ name: "APIError",
+ data: {
+ message: item.message,
+ isRetryable: false,
+ responseBody: JSON.stringify(input),
+ },
+ })
+ })
+ })
+
+ test("serializes unknown inputs", () => {
+ const result = MessageV2.fromError(123, { providerID: "test" })
+
+ expect(result).toStrictEqual({
+ name: "UnknownError",
+ data: {
+ message: "123",
+ },
+ })
+ })
+})
From 0cd52f830c2a7e7a09612dbdd7a529bfd4b4bde8 Mon Sep 17 00:00:00 2001
From: AdJIa
Date: Mon, 9 Feb 2026 12:42:23 +0800
Subject: [PATCH 063/399] fix: enable thinking for all reasoning models on
alibaba-cn (DashScope) (#12772)
Co-authored-by: lujia
---
packages/opencode/src/provider/transform.ts | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 8aab0d4151d8..9aadb84cfd76 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -643,6 +643,20 @@ export namespace ProviderTransform {
}
}
+ // Enable thinking for reasoning models on alibaba-cn (DashScope).
+ // DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
+ // to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
+ // deepseek-r1, etc. never output thinking/reasoning tokens.
+ // Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
+ if (
+ input.model.providerID === "alibaba-cn" &&
+ input.model.capabilities.reasoning &&
+ input.model.api.npm === "@ai-sdk/openai-compatible" &&
+ !modelId.includes("kimi-k2-thinking")
+ ) {
+ result["enable_thinking"] = true
+ }
+
if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
if (!input.model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
From d40dffb854b74af8fc2695018d8523ef152278de Mon Sep 17 00:00:00 2001
From: fgonzalezurriola <140219655+fgonzalezurriola@users.noreply.github.com>
Date: Mon, 9 Feb 2026 01:43:52 -0300
Subject: [PATCH 064/399] fix(provider): remove obsolete copilot model
enablement instructions (#12739)
---
packages/opencode/src/provider/transform.ts | 6 ------
packages/web/src/content/docs/providers.mdx | 2 --
2 files changed, 8 deletions(-)
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 9aadb84cfd76..4e9467523195 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -830,12 +830,6 @@ export namespace ProviderTransform {
if (providerID.includes("github-copilot") && error.statusCode === 403) {
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
}
- if (providerID.includes("github-copilot") && message.includes("The requested model is not supported")) {
- return (
- message +
- "\n\nMake sure the model is enabled in your copilot settings: https://github.com/settings/copilot/features"
- )
- }
return message
}
diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx
index 6852672149f7..e7befcf026cf 100644
--- a/packages/web/src/content/docs/providers.mdx
+++ b/packages/web/src/content/docs/providers.mdx
@@ -791,8 +791,6 @@ To use your GitHub Copilot subscription with opencode:
:::note
Some models might need a [Pro+
subscription](https://github.com/features/copilot/plans) to use.
-
-Some models need to be manually enabled in your [GitHub Copilot settings](https://docs.github.com/en/copilot/how-tos/use-ai-models/configure-access-to-ai-models#setup-for-individual-use).
:::
1. Run the `/connect` command and search for GitHub Copilot.
From 99ea1351ce701e9b186ab9f76e84a7848869a829 Mon Sep 17 00:00:00 2001
From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Date: Sun, 8 Feb 2026 23:54:01 -0600
Subject: [PATCH 065/399] tweak: add new ContextOverflowError type (#12777)
---
packages/opencode/src/provider/error.ts | 191 ++++++++++++++++++
packages/opencode/src/provider/transform.ts | 11 +-
packages/opencode/src/session/message-v2.ts | 155 +++++---------
packages/opencode/src/session/retry.ts | 3 +
.../opencode/test/session/message-v2.test.ts | 90 ++++++++-
packages/sdk/js/src/v2/gen/types.gen.ts | 24 ++-
6 files changed, 349 insertions(+), 125 deletions(-)
create mode 100644 packages/opencode/src/provider/error.ts
diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts
new file mode 100644
index 000000000000..2693df04fd54
--- /dev/null
+++ b/packages/opencode/src/provider/error.ts
@@ -0,0 +1,191 @@
+import { APICallError } from "ai"
+import { STATUS_CODES } from "http"
+import { iife } from "@/util/iife"
+
+export namespace ProviderError {
+ // Adapted from overflow detection patterns in:
+ // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
+ const OVERFLOW_PATTERNS = [
+ /prompt is too long/i, // Anthropic
+ /input is too long for requested model/i, // Amazon Bedrock
+ /exceeds the context window/i, // OpenAI (Completions + Responses API message text)
+ /input token count.*exceeds the maximum/i, // Google (Gemini)
+ /maximum prompt length is \d+/i, // xAI (Grok)
+ /reduce the length of the messages/i, // Groq
+ /maximum context length is \d+ tokens/i, // OpenRouter
+ /exceeds the limit of \d+/i, // GitHub Copilot
+ /exceeds the available context size/i, // llama.cpp server
+ /greater than the context length/i, // LM Studio
+ /context window exceeds limit/i, // MiniMax
+ /exceeded model token limit/i, // Kimi For Coding
+ /context[_ ]length[_ ]exceeded/i, // Generic fallback
+ /too many tokens/i, // Generic fallback
+ /token limit exceeded/i, // Generic fallback
+ ]
+
+ function isOpenAiErrorRetryable(e: APICallError) {
+ const status = e.statusCode
+ if (!status) return e.isRetryable
+ // openai sometimes returns 404 for models that are actually available
+ return status === 404 || e.isRetryable
+ }
+
+ // Providers not reliably handled in this function:
+ // - z.ai: can accept overflow silently (needs token-count/context-window checks)
+ function isOverflow(message: string) {
+ if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
+
+ // Providers/status patterns handled outside of regex list:
+ // - Cerebras: often returns "400 (no body)" / "413 (no body)"
+ // - Mistral: often returns "400 (no body)" / "413 (no body)"
+ return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
+ }
+
+ function error(providerID: string, error: APICallError) {
+ if (providerID.includes("github-copilot") && error.statusCode === 403) {
+ return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
+ }
+
+ return error.message
+ }
+
+ function message(providerID: string, e: APICallError) {
+ return iife(() => {
+ const msg = e.message
+ if (msg === "") {
+ if (e.responseBody) return e.responseBody
+ if (e.statusCode) {
+ const err = STATUS_CODES[e.statusCode]
+ if (err) return err
+ }
+ return "Unknown error"
+ }
+
+ const transformed = error(providerID, e)
+ if (transformed !== msg) {
+ return transformed
+ }
+ if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
+ return msg
+ }
+
+ try {
+ const body = JSON.parse(e.responseBody)
+ // try to extract common error message fields
+ const errMsg = body.message || body.error || body.error?.message
+ if (errMsg && typeof errMsg === "string") {
+ return `${msg}: ${errMsg}`
+ }
+ } catch {}
+
+ return `${msg}: ${e.responseBody}`
+ }).trim()
+ }
+
+ function json(input: unknown) {
+ if (typeof input === "string") {
+ try {
+ const result = JSON.parse(input)
+ if (result && typeof result === "object") return result
+ return undefined
+ } catch {
+ return undefined
+ }
+ }
+ if (typeof input === "object" && input !== null) {
+ return input
+ }
+ return undefined
+ }
+
+ export type ParsedStreamError =
+ | {
+ type: "context_overflow"
+ message: string
+ responseBody: string
+ }
+ | {
+ type: "api_error"
+ message: string
+ isRetryable: false
+ responseBody: string
+ }
+
+ export function parseStreamError(input: unknown): ParsedStreamError | undefined {
+ const body = json(input)
+ if (!body) return
+
+ const responseBody = JSON.stringify(body)
+ if (body.type !== "error") return
+
+ switch (body?.error?.code) {
+ case "context_length_exceeded":
+ return {
+ type: "context_overflow",
+ message: "Input exceeds context window of this model",
+ responseBody,
+ }
+ case "insufficient_quota":
+ return {
+ type: "api_error",
+ message: "Quota exceeded. Check your plan and billing details.",
+ isRetryable: false,
+ responseBody,
+ }
+ case "usage_not_included":
+ return {
+ type: "api_error",
+ message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
+ isRetryable: false,
+ responseBody,
+ }
+ case "invalid_prompt":
+ return {
+ type: "api_error",
+ message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
+ isRetryable: false,
+ responseBody,
+ }
+ }
+ }
+
+ export type ParsedAPICallError =
+ | {
+ type: "context_overflow"
+ message: string
+ responseBody?: string
+ }
+ | {
+ type: "api_error"
+ message: string
+ statusCode?: number
+ isRetryable: boolean
+ responseHeaders?: Record
+ responseBody?: string
+ metadata?: Record
+ }
+
+ export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError {
+ const m = message(input.providerID, input.error)
+ if (isOverflow(m)) {
+ return {
+ type: "context_overflow",
+ message: m,
+ responseBody: input.error.responseBody,
+ }
+ }
+
+ const metadata = input.error.url ? { url: input.error.url } : undefined
+ return {
+ type: "api_error",
+ message: m,
+ statusCode: input.error.statusCode,
+ isRetryable: input.providerID.startsWith("openai")
+ ? isOpenAiErrorRetryable(input.error)
+ : input.error.isRetryable,
+ responseHeaders: input.error.responseHeaders,
+ responseBody: input.error.responseBody,
+ metadata,
+ }
+ }
+}
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 4e9467523195..01291491d323 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -1,4 +1,4 @@
-import type { APICallError, ModelMessage } from "ai"
+import type { ModelMessage } from "ai"
import { mergeDeep, unique } from "remeda"
import type { JSONSchema7 } from "@ai-sdk/provider"
import type { JSONSchema } from "zod/v4/core"
@@ -824,13 +824,4 @@ export namespace ProviderTransform {
return schema as JSONSchema7
}
-
- export function error(providerID: string, error: APICallError) {
- let message = error.message
- if (providerID.includes("github-copilot") && error.statusCode === 403) {
- return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
- }
-
- return message
- }
}
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 3119c2bce319..e45bfc772869 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -7,8 +7,7 @@ import { LSP } from "../lsp"
import { Snapshot } from "@/snapshot"
import { fn } from "@/util/fn"
import { Storage } from "@/storage/storage"
-import { ProviderTransform } from "@/provider/transform"
-import { STATUS_CODES } from "http"
+import { ProviderError } from "@/provider/error"
import { iife } from "@/util/iife"
import { type SystemError } from "bun"
import type { Provider } from "@/provider/provider"
@@ -35,6 +34,10 @@ export namespace MessageV2 {
}),
)
export type APIError = z.infer
+ export const ContextOverflowError = NamedError.create(
+ "ContextOverflowError",
+ z.object({ message: z.string(), responseBody: z.string().optional() }),
+ )
const PartBase = z.object({
id: z.string(),
@@ -361,6 +364,7 @@ export namespace MessageV2 {
NamedError.Unknown.Schema,
OutputLengthError.Schema,
AbortedError.Schema,
+ ContextOverflowError.Schema,
APIError.Schema,
])
.optional(),
@@ -711,13 +715,6 @@ export namespace MessageV2 {
return result
}
- const isOpenAiErrorRetryable = (e: APICallError) => {
- const status = e.statusCode
- if (!status) return e.isRetryable
- // openai sometimes returns 404 for models that are actually available
- return status === 404 || e.isRetryable
- }
-
export function fromError(e: unknown, ctx: { providerID: string }) {
switch (true) {
case e instanceof DOMException && e.name === "AbortError":
@@ -751,45 +748,28 @@ export namespace MessageV2 {
{ cause: e },
).toObject()
case APICallError.isInstance(e):
- const message = iife(() => {
- let msg = e.message
- if (msg === "") {
- if (e.responseBody) return e.responseBody
- if (e.statusCode) {
- const err = STATUS_CODES[e.statusCode]
- if (err) return err
- }
- return "Unknown error"
- }
- const transformed = ProviderTransform.error(ctx.providerID, e)
- if (transformed !== msg) {
- return transformed
- }
- if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
- return msg
- }
-
- try {
- const body = JSON.parse(e.responseBody)
- // try to extract common error message fields
- const errMsg = body.message || body.error || body.error?.message
- if (errMsg && typeof errMsg === "string") {
- return `${msg}: ${errMsg}`
- }
- } catch {}
-
- return `${msg}: ${e.responseBody}`
- }).trim()
+ const parsed = ProviderError.parseAPICallError({
+ providerID: ctx.providerID,
+ error: e,
+ })
+ if (parsed.type === "context_overflow") {
+ return new MessageV2.ContextOverflowError(
+ {
+ message: parsed.message,
+ responseBody: parsed.responseBody,
+ },
+ { cause: e },
+ ).toObject()
+ }
- const metadata = e.url ? { url: e.url } : undefined
return new MessageV2.APIError(
{
- message,
- statusCode: e.statusCode,
- isRetryable: ctx.providerID.startsWith("openai") ? isOpenAiErrorRetryable(e) : e.isRetryable,
- responseHeaders: e.responseHeaders,
- responseBody: e.responseBody,
- metadata,
+ message: parsed.message,
+ statusCode: parsed.statusCode,
+ isRetryable: parsed.isRetryable,
+ responseHeaders: parsed.responseHeaders,
+ responseBody: parsed.responseBody,
+ metadata: parsed.metadata,
},
{ cause: e },
).toObject()
@@ -797,72 +777,27 @@ export namespace MessageV2 {
return new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
default:
try {
- const json = iife(() => {
- if (typeof e === "string") {
- try {
- return JSON.parse(e)
- } catch {
- return undefined
- }
- }
-
- if (typeof e === "object" && e !== null) {
- return e
- }
- return undefined
- })
- if (json) {
- const responseBody = JSON.stringify(json)
- // Handle Responses API mid stream style errors
- if (json?.type === "error") {
- switch (json?.error?.code) {
- case "context_length_exceeded":
- return new MessageV2.APIError(
- {
- message: "Input exceeds context window of this model",
- isRetryable: false,
- responseBody,
- },
- {
- cause: e,
- },
- ).toObject()
- case "insufficient_quota":
- return new MessageV2.APIError(
- {
- message: "Quota exceeded. Check your plan and billing details.",
- isRetryable: false,
- responseBody,
- },
- {
- cause: e,
- },
- ).toObject()
- case "usage_not_included":
- return new MessageV2.APIError(
- {
- message:
- "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
- isRetryable: false,
- responseBody,
- },
- {
- cause: e,
- },
- ).toObject()
- case "invalid_prompt":
- return new MessageV2.APIError(
- {
- message: json?.error?.message || "Invalid prompt.",
- isRetryable: false,
- responseBody,
- },
- {
- cause: e,
- },
- ).toObject()
- }
+ const parsed = ProviderError.parseStreamError(e)
+ if (parsed) {
+ if (parsed.type === "context_overflow") {
+ return new MessageV2.ContextOverflowError(
+ {
+ message: parsed.message,
+ responseBody: parsed.responseBody,
+ },
+ { cause: e },
+ ).toObject()
}
+ return new MessageV2.APIError(
+ {
+ message: parsed.message,
+ isRetryable: parsed.isRetryable,
+ responseBody: parsed.responseBody,
+ },
+ {
+ cause: e,
+ },
+ ).toObject()
}
} catch {}
return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts
index a71a6a38241f..0d9a865b1f3f 100644
--- a/packages/opencode/src/session/retry.ts
+++ b/packages/opencode/src/session/retry.ts
@@ -59,6 +59,9 @@ export namespace SessionRetry {
}
export function retryable(error: ReturnType) {
+ // DO NOT retry context overflow errors
+ if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
+
if (MessageV2.APIError.isInstance(error)) {
if (!error.data.isRetryable) return undefined
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts
index 39c58bb6ec75..c043754bdb4e 100644
--- a/packages/opencode/test/session/message-v2.test.ts
+++ b/packages/opencode/test/session/message-v2.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test"
+import { APICallError } from "ai"
import { MessageV2 } from "../../src/session/message-v2"
import type { Provider } from "../../src/provider/provider"
@@ -786,12 +787,26 @@ describe("session.message-v2.toModelMessage", () => {
})
describe("session.message-v2.fromError", () => {
- test("serializes response error codes", () => {
- const cases = [
- {
+ test("serializes context_length_exceeded as ContextOverflowError", () => {
+ const input = {
+ type: "error",
+ error: {
code: "context_length_exceeded",
+ },
+ }
+ const result = MessageV2.fromError(input, { providerID: "test" })
+
+ expect(result).toStrictEqual({
+ name: "ContextOverflowError",
+ data: {
message: "Input exceeds context window of this model",
+ responseBody: JSON.stringify(input),
},
+ })
+ })
+
+ test("serializes response error codes", () => {
+ const cases = [
{
code: "insufficient_quota",
message: "Quota exceeded. Check your plan and billing details.",
@@ -827,6 +842,75 @@ describe("session.message-v2.fromError", () => {
})
})
+ test("maps github-copilot 403 to reauth guidance", () => {
+ const error = new APICallError({
+ message: "forbidden",
+ url: "https://api.githubcopilot.com/v1/chat/completions",
+ requestBodyValues: {},
+ statusCode: 403,
+ responseHeaders: { "content-type": "application/json" },
+ responseBody: '{"error":"forbidden"}',
+ isRetryable: false,
+ })
+
+ const result = MessageV2.fromError(error, { providerID: "github-copilot" })
+
+ expect(result).toStrictEqual({
+ name: "APIError",
+ data: {
+ message:
+ "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode.",
+ statusCode: 403,
+ isRetryable: false,
+ responseHeaders: { "content-type": "application/json" },
+ responseBody: '{"error":"forbidden"}',
+ metadata: {
+ url: "https://api.githubcopilot.com/v1/chat/completions",
+ },
+ },
+ })
+ })
+
+ test("detects context overflow from APICallError provider messages", () => {
+ const cases = [
+ "prompt is too long: 213462 tokens > 200000 maximum",
+ "Your input exceeds the context window of this model",
+ "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
+ "Please reduce the length of the messages or completion",
+ "400 status code (no body)",
+ "413 status code (no body)",
+ ]
+
+ cases.forEach((message) => {
+ const error = new APICallError({
+ message,
+ url: "https://example.com",
+ requestBodyValues: {},
+ statusCode: 400,
+ responseHeaders: { "content-type": "application/json" },
+ isRetryable: false,
+ })
+ const result = MessageV2.fromError(error, { providerID: "test" })
+ expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
+ })
+ })
+
+ test("does not classify 429 no body as context overflow", () => {
+ const result = MessageV2.fromError(
+ new APICallError({
+ message: "429 status code (no body)",
+ url: "https://example.com",
+ requestBodyValues: {},
+ statusCode: 429,
+ responseHeaders: { "content-type": "application/json" },
+ isRetryable: false,
+ }),
+ { providerID: "test" },
+ )
+ expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(false)
+ expect(MessageV2.APIError.isInstance(result)).toBe(true)
+ })
+
test("serializes unknown inputs", () => {
const result = MessageV2.fromError(123, { providerID: "test" })
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index d72c37a28b5a..9543e5b5796d 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -152,6 +152,14 @@ export type MessageAbortedError = {
}
}
+export type ContextOverflowError = {
+ name: "ContextOverflowError"
+ data: {
+ message: string
+ responseBody?: string
+ }
+}
+
export type ApiError = {
name: "APIError"
data: {
@@ -176,7 +184,13 @@ export type AssistantMessage = {
created: number
completed?: number
}
- error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError
+ error?:
+ | ProviderAuthError
+ | UnknownError
+ | MessageOutputLengthError
+ | MessageAbortedError
+ | ContextOverflowError
+ | ApiError
parentID: string
modelID: string
providerID: string
@@ -820,7 +834,13 @@ export type EventSessionError = {
type: "session.error"
properties: {
sessionID?: string
- error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError
+ error?:
+ | ProviderAuthError
+ | UnknownError
+ | MessageOutputLengthError
+ | MessageAbortedError
+ | ContextOverflowError
+ | ApiError
}
}
From b12eab782fab6039337ec2345cc708ea4957ca5e Mon Sep 17 00:00:00 2001
From: "opencode-agent[bot]"
Date: Mon, 9 Feb 2026 05:54:56 +0000
Subject: [PATCH 066/399] chore: generate
---
packages/sdk/openapi.json | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index f50cc06c1010..18022a3384d9 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -6242,6 +6242,28 @@
},
"required": ["name", "data"]
},
+ "ContextOverflowError": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "const": "ContextOverflowError"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "responseBody": {
+ "type": "string"
+ }
+ },
+ "required": ["message"]
+ }
+ },
+ "required": ["name", "data"]
+ },
"APIError": {
"type": "object",
"properties": {
@@ -6327,6 +6349,9 @@
{
"$ref": "#/components/schemas/MessageAbortedError"
},
+ {
+ "$ref": "#/components/schemas/ContextOverflowError"
+ },
{
"$ref": "#/components/schemas/APIError"
}
@@ -8094,6 +8119,9 @@
{
"$ref": "#/components/schemas/MessageAbortedError"
},
+ {
+ "$ref": "#/components/schemas/ContextOverflowError"
+ },
{
"$ref": "#/components/schemas/APIError"
}
From 687210a55d6158a1f749b9f08333bd479853958f Mon Sep 17 00:00:00 2001
From: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Date: Mon, 9 Feb 2026 08:18:06 +0100
Subject: [PATCH 067/399] feat(desktop): add isSidecar prop to AppInterface and
logic to persist sidecar server urls (#12366)
Co-authored-by: Brendan Allan
---
packages/app/src/app.tsx | 5 +++--
packages/app/src/context/server.tsx | 22 +++++++++++++++++++---
packages/desktop/src/index.tsx | 3 ++-
3 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 8a111472baf0..937aab011982 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -84,7 +84,8 @@ function ServerKey(props: ParentProps) {
)
}
-export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element }) {
+
+export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
const platform = usePlatform()
const stored = (() => {
@@ -106,7 +107,7 @@ export function AppInterface(props: { defaultUrl?: string; children?: JSX.Elemen
}
return (
-
+
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 72693e6ef647..e01a933815db 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -28,7 +28,7 @@ function projectsKey(url: string) {
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server",
- init: (props: { defaultUrl: string }) => {
+ init: (props: { defaultUrl: string, isSidecar?: boolean }) => {
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
@@ -59,7 +59,13 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const fallback = normalizeServerUrl(props.defaultUrl)
if (fallback && url === fallback) {
- setState("active", url)
+ batch(() => {
+ if (!store.list.includes(url)) {
+ // Add the fallback url to the list if it's not already in the list
+ setStore("list", store.list.length, url)
+ }
+ setState("active", url)
+ })
return
}
@@ -89,7 +95,17 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
if (state.active) return
const url = normalizeServerUrl(props.defaultUrl)
if (!url) return
- setState("active", url)
+ batch(() => {
+
+ // Add the new sidecar url
+ if(props.isSidecar && props.defaultUrl) {
+ add(props.defaultUrl)
+ }
+
+ setState("active", url)
+ })
+
+ console.log(store.list)
})
const isReady = createMemo(() => ready() && !!state.active)
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index dd78224e345c..c0da8d254611 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -404,6 +404,7 @@ render(() => {
window.__OPENCODE__ ??= {}
window.__OPENCODE__.serverPassword = data().password ?? undefined
+
function Inner() {
const cmd = useCommand()
@@ -413,7 +414,7 @@ render(() => {
}
return (
-
+
)
From 019cfd4a52280ec45f230f89fa58e1a24b1d92e2 Mon Sep 17 00:00:00 2001
From: "opencode-agent[bot]"
Date: Mon, 9 Feb 2026 07:18:48 +0000
Subject: [PATCH 068/399] chore: generate
---
packages/app/src/app.tsx | 1 -
packages/app/src/context/server.tsx | 5 ++---
packages/desktop/src/index.tsx | 1 -
3 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 937aab011982..5bbe86e2093d 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -84,7 +84,6 @@ function ServerKey(props: ParentProps) {
)
}
-
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
const platform = usePlatform()
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index e01a933815db..4de498b5fcff 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -28,7 +28,7 @@ function projectsKey(url: string) {
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server",
- init: (props: { defaultUrl: string, isSidecar?: boolean }) => {
+ init: (props: { defaultUrl: string; isSidecar?: boolean }) => {
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
@@ -96,9 +96,8 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const url = normalizeServerUrl(props.defaultUrl)
if (!url) return
batch(() => {
-
// Add the new sidecar url
- if(props.isSidecar && props.defaultUrl) {
+ if (props.isSidecar && props.defaultUrl) {
add(props.defaultUrl)
}
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index c0da8d254611..cf007bdd3cd5 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -404,7 +404,6 @@ render(() => {
window.__OPENCODE__ ??= {}
window.__OPENCODE__.serverPassword = data().password ?? undefined
-
function Inner() {
const cmd = useCommand()
From d4a68b0f4e3a1f815815f5dce56999fc8cbcc548 Mon Sep 17 00:00:00 2001
From: Silvio Ney
Date: Mon, 9 Feb 2026 07:30:15 +0000
Subject: [PATCH 069/399] feat: exclude devtools from production builds
(#12290)
Co-authored-by: Brendan Allan
---
packages/desktop/src-tauri/Cargo.toml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml
index 62fc7979c707..2d6f13eca07e 100644
--- a/packages/desktop/src-tauri/Cargo.toml
+++ b/packages/desktop/src-tauri/Cargo.toml
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
-tauri = { version = "2.9.5", features = ["macos-private-api", "devtools"] }
+tauri = { version = "2.9.5", features = ["macos-private-api"] }
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2.4.6"
tauri-plugin-shell = "2"
@@ -56,6 +56,7 @@ webkit2gtk = "=2.0.2"
objc2 = "0.6"
objc2-web-kit = "0.3"
+
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61", features = [
"Win32_Foundation",
From 520110e8642065709b282e1eb407f962fdedcb4d Mon Sep 17 00:00:00 2001
From: Brendan Allan
Date: Mon, 9 Feb 2026 15:34:52 +0800
Subject: [PATCH 070/399] desktop: track currentSidecarUrl
---
packages/app/src/context/server.tsx | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 4de498b5fcff..3d77cdece7b0 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -35,6 +35,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
Persist.global("server", ["server.v3"]),
createStore({
list: [] as string[],
+ currentSidecarUrl: "",
projects: {} as Record,
lastProject: {} as Record,
}),
@@ -96,6 +97,11 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const url = normalizeServerUrl(props.defaultUrl)
if (!url) return
batch(() => {
+ // Remove the previous startup sidecar url
+ if(store.currentSidecarUrl) {
+ remove(store.currentSidecarUrl)
+ }
+
// Add the new sidecar url
if (props.isSidecar && props.defaultUrl) {
add(props.defaultUrl)
From 40b111d92c44501adb0d71d9ac37944fbc9154fd Mon Sep 17 00:00:00 2001
From: "opencode-agent[bot]"
Date: Mon, 9 Feb 2026 07:36:24 +0000
Subject: [PATCH 071/399] chore: generate
---
packages/app/src/context/server.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 3d77cdece7b0..5e4bfb1bcea9 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -98,7 +98,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
if (!url) return
batch(() => {
// Remove the previous startup sidecar url
- if(store.currentSidecarUrl) {
+ if (store.currentSidecarUrl) {
remove(store.currentSidecarUrl)
}
From b0ceec9b19419dcc1953af906b99848b9a4955dc Mon Sep 17 00:00:00 2001
From: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Date: Mon, 9 Feb 2026 08:56:26 +0100
Subject: [PATCH 072/399] feat(desktop): persist currentSidecarUrl in state
when isSidecar prop is true (#12792)
---
packages/app/src/context/server.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 5e4bfb1bcea9..351407d91ba2 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -105,12 +105,11 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
// Add the new sidecar url
if (props.isSidecar && props.defaultUrl) {
add(props.defaultUrl)
+ setStore("currentSidecarUrl", props.defaultUrl)
}
setState("active", url)
})
-
- console.log(store.list)
})
const isReady = createMemo(() => ready() && !!state.active)
From 94feb811ca32f4e01a1bada9cfbc022e8d5ca9e3 Mon Sep 17 00:00:00 2001
From: Brendan Allan
Date: Mon, 9 Feb 2026 16:51:04 +0800
Subject: [PATCH 073/399] app: include sandboxes in project unseen/error notifs
---
packages/app/src/pages/layout/sidebar-items.tsx | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx
index facfbddc7f39..b184c8bff8b5 100644
--- a/packages/app/src/pages/layout/sidebar-items.tsx
+++ b/packages/app/src/pages/layout/sidebar-items.tsx
@@ -21,8 +21,11 @@ const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
const notification = useNotification()
- const unseenCount = createMemo(() => notification.project.unseenCount(props.project.worktree))
- const hasError = createMemo(() => notification.project.unseenHasError(props.project.worktree))
+ const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
+ const unseenCount = createMemo(() =>
+ dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
+ )
+ const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
return (
From 93a11ddedf697c9b673dd59628cee3db48ac67d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Israel=20Ara=C3=BAjo=20de=20Oliveira?=
Date: Mon, 9 Feb 2026 06:00:35 -0300
Subject: [PATCH 074/399] feat(desktop): add native Wayland toggle on Linux
(#11971)
Co-authored-by: Brendan Allan
---
.../app/src/components/settings-general.tsx | 44 ++++++++++++++++-
packages/app/src/context/platform.tsx | 8 ++++
packages/app/src/i18n/en.ts | 6 +++
packages/app/src/index.ts | 2 +-
packages/desktop/src-tauri/src/lib.rs | 41 ++++++++++++++++
.../desktop/src-tauri/src/linux_display.rs | 47 +++++++++++++++++++
packages/desktop/src-tauri/src/main.rs | 17 +++++--
packages/desktop/src/bindings.ts | 4 ++
packages/desktop/src/index.tsx | 19 +++++++-
9 files changed, 179 insertions(+), 9 deletions(-)
create mode 100644 packages/desktop/src-tauri/src/linux_display.rs
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index b31cfb6cc794..db057a4c41f7 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -1,8 +1,10 @@
-import { Component, createMemo, type JSX } from "solid-js"
+import { Component, Show, createEffect, createMemo, createResource, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
+import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
@@ -40,6 +42,8 @@ export const SettingsGeneral: Component = () => {
checking: false,
})
+ const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
+
const check = () => {
if (!platform.checkUpdate) return
setStore("checking", true)
@@ -410,13 +414,49 @@ export const SettingsGeneral: Component = () => {