mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-16 11:20:42 +00:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4243dbc8c | ||
|
|
d471d2c231 | ||
|
|
ce3389197a | ||
|
|
c543d18eb7 | ||
|
|
be1d4b6925 | ||
|
|
da9d49a44f | ||
|
|
2e873bc40b | ||
|
|
4f0ee179fa | ||
|
|
0cd2e829af | ||
|
|
9f5ce4674a | ||
|
|
2573244805 | ||
|
|
dbf5e655df | ||
|
|
78558202c0 | ||
|
|
25facb9dfe | ||
|
|
4003f64688 | ||
|
|
7dbe56078b | ||
|
|
bc65cbe961 | ||
|
|
56782638cb | ||
|
|
9fe5766435 | ||
|
|
77b8f1dfc7 | ||
|
|
3a0a231d84 | ||
|
|
5c4079a926 | ||
|
|
e004ec3f57 | ||
|
|
c3ceabf01c |
No files matched your search
@@ -1,62 +0,0 @@
|
||||
name: models-snapshot
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0,12 * * *"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
refresh:
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: dev
|
||||
|
||||
# The refresh script only needs `fetch`; skip the monorepo install.
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: package.json
|
||||
|
||||
- name: Setup git committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Refresh snapshot
|
||||
run: bun packages/core/script/update-models-snapshot.ts
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
FILE="packages/core/src/models-dev/snapshot.txt"
|
||||
|
||||
if [ -z "$(git status --short -- "$FILE")" ]; then
|
||||
echo "No changes to commit"
|
||||
echo "### models.dev snapshot" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add "$FILE"
|
||||
git commit -m "chore(core): refresh bundled models.dev snapshot"
|
||||
|
||||
git pull --rebase --autostash origin dev
|
||||
git push origin HEAD:dev --no-verify
|
||||
|
||||
echo "### models.dev snapshot" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Status: committed $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -6,7 +6,7 @@ permissions:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [dev]
|
||||
branches: [dev, beta]
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
|
||||
@@ -141,8 +141,7 @@ jobs:
|
||||
sign-cli-macos:
|
||||
needs: build-cli
|
||||
runs-on: macos-26
|
||||
# Beta publishes only the desktop app, which signs the CLI it bundles.
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
@@ -197,7 +196,7 @@ jobs:
|
||||
needs: version
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' && !(github.ref_name == 'dev' && (inputs.bump || inputs.version))
|
||||
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'dev' && (inputs.bump || inputs.version))
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
@@ -219,7 +218,7 @@ jobs:
|
||||
needs:
|
||||
- version
|
||||
- build-node-app-archive
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta' && !(github.ref_name == 'dev' && (inputs.bump || inputs.version))
|
||||
if: github.repository == 'anomalyco/opencode' && !(github.ref_name == 'dev' && (inputs.bump || inputs.version))
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -277,7 +276,7 @@ jobs:
|
||||
needs:
|
||||
- sign-cli-macos
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'dev'
|
||||
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'beta')
|
||||
env:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -592,7 +591,6 @@ jobs:
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
---
|
||||
name: app-e2e-tests
|
||||
description: Use when writing, changing, reviewing, or debugging Playwright e2e tests in packages/app/e2e (regression keepers and user stories). Covers the mocked opencode server harness and the Bun static server, per-test isolation, page-load and route cost, the suite-wide service-worker block, races with first mount, hover, measurement, clock, dialogs and shortcuts, projects, shards and worker scheduling, rehearsing Windows timing locally, and diagnosing a CI flake from its artifacts. Not for Storybook component tests or e2e/performance benchmarks.
|
||||
---
|
||||
|
||||
# App e2e tests
|
||||
|
||||
Read `packages/app/e2e/AGENTS.md` and the Tests section of `packages/app/AGENTS.md` first. They define what to test, which locators to use, and how to assert. `packages/app/README.md` (E2E Testing) documents run modes and env vars. This skill covers what makes these tests slow or flaky in CI, which runs the suite as two shards per OS on 4-vCPU runners (`.github/workflows/test.yml:207-229`). Most races below were found on Windows.
|
||||
|
||||
All paths below are relative to `packages/app` unless they start with `packages/`, `services/` or `.github/`.
|
||||
|
||||
## What a test runs against
|
||||
|
||||
- **No opencode backend.** Each shard leg runs `bun --cwd packages/app test:e2e:built --shard=<n>/2` (`test.yml:274-279`). It builds once and starts one Bun static server, `e2e/utils/serve.ts`, on `127.0.0.1:${PLAYWRIGHT_PORT ?? 3000}`. All 5 workers share it (`playwright.config.ts:23-27`).
|
||||
- `serve.ts` keeps vite preview's SPA fallback rules. Do not move built e2e back to vite preview. On case-insensitive file systems it synchronously reads the whole `dist/_assets` directory (about 3,100 files) for every asset request, so every worker's page loads wait in line. The `e2e/performance` configs still use it to keep their baselines comparable.
|
||||
- **The API is an in-memory mock per page,** running in the test worker. `mockOpenCodeServer` (`e2e/utils/mock-server.ts:202`):
|
||||
- answers `page.route("**/api/**")` with an Effect HttpApi handler;
|
||||
- fakes `/api/event` SSE with an init-script `fetch` override;
|
||||
- serves PTY sockets through `routeWebSocket`.
|
||||
- **Unmocked routes fail the test** with a 501 and `Unmocked API request` (`mock-server.ts:403-411`). Add the route to `mock-server.ts` once instead of routing around it in a spec.
|
||||
- **Build pages with the helpers:**
|
||||
- `mockWorkspace`, `openSession`, `openDraft`, `openWorktreeDraft`, `mockRemoteServer`, `openSettings` (`e2e/utils/workspace.ts`);
|
||||
- `setupTimeline` (`e2e/utils/timeline.ts:209`) for streamed timelines, `mockServers` (`mock-server.ts:495`) for several origins;
|
||||
- `seed` (`e2e/utils/app.ts:61`) for storage, `holdRoute` (`app.ts:207`) to hold requests until you release them.
|
||||
- **Deliver events** with the mock's `push`, which buffers until the app connects, or with `installSseTransport` (`e2e/utils/sse-transport.ts:60`), whose `send` resolves on delivery. After `page.reload()`, await `transport.waitForConnection()` before sending (`e2e/regression/timeline-stream.spec.ts:1700-1702`).
|
||||
- **Windows-only failures are races, not Windows code paths.** Mock directories are `C:/OpenCode/<name>` on every OS (`workspace.ts:33`), and nothing touches the disk. Lazy chunks, stylesheets and responses finish out of order more often on the Windows runners.
|
||||
|
||||
## Isolation
|
||||
|
||||
- **No shared state between tests.** Each test gets a fresh context, mock handler and init scripts. Outside `e2e/service-worker`, which runs under its own config and shares builds through a worker fixture (`e2e/service-worker/cache.spec.ts:21`, `:74`), app e2e has no `beforeAll`, serial mode, `test.extend` or worker fixtures. Keep it that way so `-g` and `PLAYWRIGHT_FULLY_PARALLEL=1` keep working.
|
||||
- **Handlers mutate the objects you pass.** Creating a session pushes into `sessions` (`mock-server.ts:839`). Renaming writes `session.title` (`:1340`), and VCS init writes `project.vcs` (`:1019`).
|
||||
- Call `session()`/`project()` inside the test.
|
||||
- `mockWorkspace` and `openSession` copy what you pass (`workspace.ts:36-48`).
|
||||
- `mockOpenCodeServer` and `mockServers` use your objects as they are. Module-level fixtures like `multi-server.spec.ts:10-19` are therefore shared by every test in the file.
|
||||
- **Timeline builders keep state per test attempt** (`timeline.ts:165-198`). Call stateful builders such as `assistantMessage` or `partUpdated` inside the test. Only pure ones such as `toolPart` belong at module scope.
|
||||
- **Seed before the first `goto`.** `seed` writes storage once per tab behind a `sessionStorage` marker, so reloads keep what the app stored (`app.ts:59-90`). It cannot reset state mid-test.
|
||||
|
||||
## Keep it fast
|
||||
|
||||
- **Navigate once.** Every `goto` or `reload` is a cold load of the production bundle: about 150 requests per session open, about 125 of them static files. Each test has a new context, and any `page.route` disables the HTTP cache. Reach later states through the UI or through pushed events. Reload only when surviving a reload is the contract.
|
||||
- **Service workers are already blocked suite-wide** (`serviceWorkers: "block"`, `playwright.config.ts:66-67`). Do not add a per-file block; the 11 in `e2e/regression` are redundant.
|
||||
- Production registers `/sw.js` on load (`src/entry.tsx:54-55`) and precaches about 1,600 files (`globPatterns: ["**/*"]`, `vite.pwa.ts:34`). `e2e/service-worker/` covers it under its own config. The main config ignores that directory (`playwright.config.ts:33-36`), and CI runs it after shard 2 (`test.yml:281-285`).
|
||||
- Opt back in with `test.use({ serviceWorkers: "allow" })` only when the worker is the contract. It never controls the first load (`clientsClaim: false`, `vite.pwa.ts:26-28`). After a reload it can control the page, and page routes may then miss its requests.
|
||||
- Every built-mode page load logs Playwright's block warning. Console collectors must ignore it, as `captureConsoleWarnings` does (`e2e/utils/command-palette.ts:15-26`).
|
||||
- **Match new routes with a RegExp or a query-aware glob, not a function.**
|
||||
- A single function matcher makes Playwright intercept `**/*` for the page. Every static asset then round-trips to the test worker (250 continued requests in one Windows test).
|
||||
- Existing `holdRoute` calls such as `review.spec.ts:51` and `mockMissingLocation` (`mock-server.ts:480`) still use functions and pay this. Converting one route gains nothing while another function route is registered on the page.
|
||||
- Globs match the whole URL, so `**/api/vcs/init` misses `/api/vcs/init?location...`. Write `/\/api\/vcs\/init\?/` or `"**/api/session?*"` (`command-palette.spec.ts:10`, `:28`).
|
||||
- **Use the stress fixture only when scale is the contract.** `fixture`, `mockStressTimeline` and `installStressSessionTabs` (`e2e/utils/session-fixture.ts:336`, `:436`, `:448`) serve a long timeline with 300-line diffs.
|
||||
- Its cold mount reveals rows only after worker-rendered markdown arrives. Mounting one of its diffs blocked the renderer for 4-11 s on CI.
|
||||
- The 14 regression files that use it held about half of the e2e flakes CI found.
|
||||
- Prefer `openSession` or `setupTimeline` with the few messages the test needs.
|
||||
- **Assert inside loops only when each step is the contract.** A web-first assertion that fails its first check retries after 100, 250, 500, then 1,000 ms. The 10-step loop at `mobile-timeline-scroll.spec.ts:688-693` takes about 8 s, mostly backoff.
|
||||
- **Real-time waits cost their full length on every run and retry** (`settings-wsl.spec.ts:268-276` waits out a 10 s health check). Prefer `page.clock`:
|
||||
- install it before the `goto` or `reload` whose timers you need (`timeline-stream.spec.ts:1700`);
|
||||
- keep fast-forwards under the client's 45 s event-stream idle timeout (`packages/client/src/solid/connection.ts:35`).
|
||||
- **Leave benchmark pacing out of regression tests.** The `sendAll` delays and `messageDelay` exist for benchmarks (`timeline.ts:306`, `mock-server.ts:82`). Hold message pages with `beforeMessagesResponse` instead.
|
||||
|
||||
## Wait for state, not time: races CI found
|
||||
|
||||
Most of these rarely reproduce on an idle local machine.
|
||||
|
||||
- **First mount happens in stages.** `openSession` and `expectSessionTitle` return once the composer is editable and a title is visible. Later the loading header's `<h1>` is replaced, and a cold timeline keeps its rows hidden while it scrolls itself to the bottom. Before you measure or hover in the session view, wait for something that only the final mount renders, such as the last `[data-timeline-row="UserMessage"]` or the row you click next.
|
||||
- **Hovering during a scroll loses the tooltip.** A tooltip closes when its trigger scrolls, and Playwright's mouse never moves again. `session-summary.spec.ts:21-26` waits for the last row before it hovers; without that wait it needed a retry in 6 of 36 Windows runs.
|
||||
- **One-shot measurements can hit a detached node.** `locator.evaluate` resolves the element, then evaluates in a second round trip. If the node is replaced in between, `getBoundingClientRect` returns an all-zero rect and `boundingBox` returns `null`. The subagent title-anchor test failed with "Expected: 0" in 11 runs until it waited for a timeline-only button (`subagent-child-navigation.spec.ts:60-64`). Take reference measurements once the final structure is in place. Polled reads re-resolve and are safe.
|
||||
- **`goto` returns late, so a modal may already be open.**
|
||||
- `goto` waits for `load`, which can fire after the app is already interactive.
|
||||
- An open dialog sets `aria-hidden` on the app root, which hides everything behind it from role locators.
|
||||
- What's New opens 500 ms after the changelog response, and it raced the Settings check in 11 of 36 Windows runs. Hold the request that opens a modal until readiness is asserted, as `startUpgraded` does (`whats-new.spec.ts:227-234`).
|
||||
- **`pauseAt(new Date())` right after `install()` can throw** "Cannot fast-forward to the past", because the installed clock starts slightly ahead of the runner's clock. Install at an explicit time and pause at an offset from it (`tabs.spec.ts:443-445`).
|
||||
- **A shortcut that does nothing is usually a product guard, not timing.** The global handler ignores keys while `dialog.active` (`src/shell/commands/command.tsx:489`). `active` is the top dialog that has not started closing (`packages/ui/src/context/dialog.tsx:52`, `:310-311`). A dialog that has started closing does not block keys during its 100 ms exit (`dialog.tsx:87`), but any open dialog does, including one you did not expect. Check that guard before you add a wait.
|
||||
- **Shortcut tests fail on local macOS; that is not a regression.**
|
||||
- The Desktop Chrome profile reports a Windows UA and `navigator.platform = "Win32"`, so the app (`IS_MAC`, `command.tsx:14`) expects Ctrl. On macOS, `ControlOrMeta` sends Meta.
|
||||
- Every test that opens the palette through `openCommandPalette` (`e2e/utils/command-palette.ts:50`) fails locally and passes in CI, as do `tabs.spec.ts:835` and `timeline-cache.spec.ts:163`. Verify on Linux or in CI, or press `Control` in a throwaway copy. Do not edit tests for macOS.
|
||||
- A new test can pick the modifier from the page, as `session-summary.spec.ts:29-30` does.
|
||||
- **Font probes mislead.** `document.fonts.check()` can stay false after the font has loaded: when a lazy `@layer` stylesheet activates late, Chromium rebuilds font faces as unloaded. The code-font test failed outright in 9 of 36 Windows runs that way. Polling a probe that lays out text can make itself true. Await `document.fonts.ready`, lay out new text, and then check once (`timeline-cache.spec.ts:281-293`).
|
||||
- **Use the harness's signals.** `holdRoute(...).arrived` resolves when the request reaches the route, and `transport.send` resolves after delivery. `setupTimeline(...).waitForPart` resolves once a part renders (`timeline.ts:328`).
|
||||
- **Decide which side is wrong.** Three of eight investigated e2e flakes were product bugs, each since fixed in the product:
|
||||
- a stale scroll offset restored when a scroll settles (`src/session/timeline/observe-element-offset.ts`);
|
||||
- a video double-click that also entered native fullscreen (`packages/gui-extensions/src/file/artifact-view.tsx:617`);
|
||||
- shortcuts dropped while a dialog closed.
|
||||
|
||||
If a user could hit the interleaving, fix the product and keep the assertion.
|
||||
|
||||
## Scheduling
|
||||
|
||||
- **Two projects split the suite** (`playwright.config.ts:69-86`). `chromium-touch-scroll` runs only `mobile-timeline-scroll.spec.ts`, which the `touchScroll` regex (`:29`) matches, with `fullyParallel: true`.
|
||||
- Projects queue in declaration order, so its 37 tests start after every `chromium` file and spread across all workers. As one serial group, the file took 3-5 minutes in CI.
|
||||
- A rename that the regex misses quietly puts the file back in `chromium` as that serial group.
|
||||
- `--project chromium`, UI mode and the VS Code extension's default project skip the file.
|
||||
- **In `chromium`, files are the scheduling unit.** `fullyParallel` is off unless `PLAYWRIGHT_FULLY_PARALLEL=1` (`:42`), so a file's tests run in order on one worker. A long file sets a floor and leaves an idle tail. New regressions go into the area's keeper file (`e2e/AGENTS.md`), so keep each case cheap.
|
||||
- **Shards split whole files by test count, not duration** (`test.yml:219-222`). The touch project queues last, so its tests always fall in shard 2. To reproduce a leg's schedule, pass the same `--shard`.
|
||||
- **Worker-scoped options fork a worker.** `launchOptions`, `channel`, `headless`, `video`, `trace`, `screenshot` and `defaultBrowserType` give a file its own worker hash.
|
||||
- Playwright groups a project's files by worker hash in order of first appearance, so such a file runs after every default file in its project.
|
||||
- Use test-scoped options instead: `viewport`, `deviceScaleFactor`, `hasTouch`, `isMobile`, `userAgent`, `colorScheme`, `serviceWorkers`, `locale`.
|
||||
- Use `launchOptions` only when a Chromium flag is the contract, with a comment (`patch-border.spec.ts:5-9`).
|
||||
- **Never spread `devices[...]` into `test.use`.** iPhone entries set `defaultBrowserType: "webkit"`, which forks a worker and needs a browser that CI never installs (`test.yml:269-272`). Copy the fields you need, as `mobile-timeline-scroll.spec.ts:23-28` does.
|
||||
- **Retries are not free.** CI retries twice (`playwright.config.ts:44`). Each failure restarts the worker's browser and costs 10-30 s of cleanup on Windows. The first retry then reruns with a trace and a video (`:62-65`).
|
||||
- **Timeouts are not a fix.** The limits are 60 s per test and 10 s per assertion (`playwright.config.ts:38-41`). `APP_READY_TIMEOUT` (30 s, `e2e/utils/waits.ts:4`) covers only first readiness.
|
||||
- When one wait is genuinely long, scope the timeout to that assertion and say what takes the time. `settings-wsl.spec.ts:268-276` does this with `{ timeout: 20_000 }` for a 10 s health check.
|
||||
- `test.setTimeout` does not lengthen assertions. `timeline-history.spec.ts:73` sets 240 s without a comment and still fails at the 10 s poll at `:127`.
|
||||
|
||||
## Before you push
|
||||
|
||||
Run from `packages/app`:
|
||||
|
||||
- [ ] Run in built mode, as CI does: `bun run test:e2e:built -- e2e/regression/<file>.spec.ts`. Dev mode serves unbundled modules, with the API on another origin (`playwright.config.ts:19-21`).
|
||||
- [ ] Use a free port when another worktree or agent may hold 3000: `PLAYWRIGHT_PORT=4310 bun run test:e2e:built -- ...`. Built runs never reuse a server, and `serve.ts` fails on a busy port. Stay below 49152 on Windows: it reserves ports from there up and hands them out for outgoing connections (`services/www/src/docs/content/cli/web.mdx:43`, `packages/client/test/fixture/service-fixture.ts:108-109`).
|
||||
- [ ] To repeat without rebuilding:
|
||||
- build once with `VITE_OPENCODE_TEST_FIXTURES=1 bun run build`;
|
||||
- keep `bun e2e/utils/serve.ts --host 127.0.0.1 --port 4310` running;
|
||||
- run `PLAYWRIGHT_BUILD=1 PLAYWRIGHT_BASE_URL=http://127.0.0.1:4310 bun run test:e2e -- ...`.
|
||||
- [ ] Repeat new or changed tests at CI concurrency without retries: `--repeat-each=10 --workers=5 --retries=0 -g "<title>"`. Run the file once with `PLAYWRIGHT_FULLY_PARALLEL=1` to prove that order does not matter.
|
||||
- [ ] Rehearse Windows timing by delaying assets, not only the CPU. CPU throttling at 4x and 6x did not reproduce the tooltip or title-anchor races, but delayed responses did.
|
||||
- In a throwaway copy, hold the suspect lazy chunk, stylesheet or API response with `holdRoute`, and release it 0.6-1.5 s after `arrived`.
|
||||
- Route worker scripts with `page.context().route`.
|
||||
- A race fix must fail before the fix and pass after it (`packages/app/AGENTS.md`, Tests).
|
||||
- [ ] Check the cost on a noisy machine. Other agents add load, and local A/Bs at a load average of 20-35 were inconclusive.
|
||||
- Compare the medians of interleaved before/after runs at `--workers=5`, or the step durations in the report (`bun run test:e2e:report`).
|
||||
- Count every `goto` and `reload` you added.
|
||||
- [ ] `bun run typecheck:e2e` reports 4 existing errors: `timeline-stream.spec.ts:1822`, `:1825`, and two `e2e/performance` probes. Add none. If you changed `playwright.config.ts`, update `test-browser/playwright-config.test.ts`.
|
||||
|
||||
## When CI flakes
|
||||
|
||||
- **Find it.** e2e runs on pull requests that affect `@opencode/app` or `@opencode/storybook`, including their workspace dependencies, and on every manual dispatch (`.github/workflows/test.yml:42-54`). It never runs on dev (`:28`).
|
||||
- Each OS has the legs `app-1` and `app-2` (the shards), `session-ui-components` and `app-components`.
|
||||
- The required `e2e (linux)` and `e2e (windows)` checks only aggregate those legs (`:316-335`).
|
||||
- A pass on retry leaves a leg green. Check each Windows shard report's Flaky list, and make sure your new tests are not on it.
|
||||
- **Download:** `gh run download <run-id> -n playwright-windows-app-2-1 -D <dir>`. Artifacts are named `playwright-<os>-<leg>-<run attempt>` (`test.yml:301-314`). App e2e results are in `<dir>/app/e2e/`.
|
||||
- **In CI, the failing first attempt has no trace and no video** (`playwright.config.ts:62-65`).
|
||||
- `test-results/<test>-<project>/` holds `error-context.md` (the aria snapshot at the failure) and `test-failed-1.png`.
|
||||
- `<test>-<project>-retry1/` holds `trace.zip` and `video.webm` whether the retry passed or failed, plus failure files only if it failed. A second retry records neither.
|
||||
- **Read `error-context.md` first.** An open dialog or an element with no accessible name explains most "element(s) not found" errors.
|
||||
- **Get timings from the retry trace:** `bunx playwright show-trace <dir>/app/e2e/test-results/<test>-<project>-retry1/trace.zip`. To see which side won, compare when a screencast frame changed with when the response landed.
|
||||
- **Get the transient state from a failing video,** sampled frame by frame (`ffmpeg -i video.webm -vf fps=4 frame-%03d.png`). That is how the tooltip race showed the tooltip closing in the same frame the timeline rows appeared.
|
||||
- In CI, retry1's video shows the failure only if the retry failed too.
|
||||
- Locally, a failing test keeps its video (`retain-on-failure`), so you can record the race with `--repeat-each` and delayed assets.
|
||||
- **Open the report:** `bunx playwright show-report <dir>/app/e2e/playwright-report`. It shows step durations for every attempt, including passing ones.
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
name: component-tests
|
||||
description: Use when writing, changing, debugging, or reviewing Storybook-based Playwright component tests. That covers specs in packages/app/component-tests or packages/session-ui/component-tests, `*.fixture.tsx` files there or beside source (such as packages/gui-extensions/src/browser/panel.fixture.tsx), the stories they mount, and the shared harness in packages/storybook (playwright/config.ts, playwright/story.ts, .storybook/main.ts, preview.tsx, mocks). Also use when a component test is flaky, passed on retry, or fails with "Execution context was destroyed", "Failed to fetch dynamically imported module", or `#storybook-root` hidden. Covers mount args, source modules imported by URL, Vite dependency pre-bundling and reloads, cold vs warm Storybook, mobile drawers, geometry, and checks before pushing.
|
||||
---
|
||||
|
||||
# Component tests
|
||||
|
||||
Each package suite starts one Storybook/Vite dev server, and every worker shares it (`packages/storybook/playwright/config.ts:17-24`; CI uses 2 workers, `config.ts:15`). With `fullyParallel` (`config.ts:12`), the tests of one file run on several workers in any order. There is no backend: server, SDK and sync modules are in-page mocks. Every test gets a fresh context, and `mount` is a full iframe navigation (`packages/storybook/playwright/story.ts:37`). Slow or flaky component tests in CI have come from five sources: the shared server's dependency optimizer, module resolution that depended on the install layout, a cold first story, a product race (the mobile drawer), or an action that ran before the UI reached the state it needed.
|
||||
|
||||
Read these first rather than repeating them:
|
||||
|
||||
- `packages/storybook/playwright/README.md` covers layout, run commands and "Adding a test".
|
||||
- `packages/app/AGENTS.md` "Tests" decides whether a test should exist, when theme or direction cases are allowed, and that a regression test must fail before the fix.
|
||||
- `packages/app/e2e/AGENTS.md` "Test Hygiene" applies here unchanged.
|
||||
|
||||
## Stories and `mount`
|
||||
|
||||
- **Location.** Put the story next to the production component under `src/` of `ui`, `session-ui`, `app` or `gui-extensions`. Storybook indexes only the `stories` globs in `packages/storybook/.storybook/main.ts:44-49`.
|
||||
- **Story ID.** It is the meta `id` if one is set (`packages/session-ui/src/components/session-review.stories.tsx:25`). Otherwise it is the kebab-cased `title`, then `--`, then the kebab-cased export. Check it at `http://127.0.0.1:6006/?path=/story/<id>`. A wrong ID never renders, so `mount` waits out its full 30 s root wait (`story.ts:39`) and then fails.
|
||||
- **URL args can be dropped.** `mount(id, { args, globals })` writes them into the URL (`story.ts:20-36`). Storybook drops an arg when the story declares no `args`/`argTypes` entry for its key. It also drops one whose value is not `[A-Za-z0-9 _-]*`, a number, a hex color or `rgb()`/`hsl()`, which rules out paths, URLs and file names with dots. The only trace is the browser console warning "Omitted potentially unsafe URL args.", and the test then passes on the default scenario.
|
||||
- Give every arg a typed default. With `args: { split: false }` (`session-review.stories.tsx:129`), `split:false` arrives as a boolean.
|
||||
- Pick punctuated values inside the story, from a `scenario`-style arg.
|
||||
- **Globals** are `theme`, `direction` and `locale` (`packages/storybook/.storybook/preview.tsx:106-122`). `mount` also sets `a11y.manual` (`story.ts:29-36`); caller globals override it.
|
||||
- **Portaled UI.** `mount` returns `#storybook-root`. Dialogs, menus and drawers render outside it, so locate them from `page` (`packages/app/component-tests/composer.spec.ts:101-107`).
|
||||
- **Server state** comes from the `resolve.alias` list in `viteFinal` (`main.ts:72-94`), which points app server, SDK and sync modules at `packages/storybook/.storybook/mocks/app/**`. Extend a mock rather than adding a server. Restore mock state in `onCleanup` (`mockProviderAuth`, `packages/app/src/providers/connect/dialog.stories.tsx:23`) so that switching stories in the Storybook UI does not inherit it. Tests reset it on every mount.
|
||||
- **Requests the mocks do not answer.** Route them with `page.route`. That includes server endpoints such as `/api/fs/read` (`packages/session-ui/component-tests/read-image.spec.ts:20`) and external URLs such as favicons (`packages/session-ui/component-tests/markdown.spec.ts:217`).
|
||||
|
||||
## Fixtures imported by URL
|
||||
|
||||
Use these when the contract is not visible in a story, such as construction counts, unmount races or cache state.
|
||||
|
||||
- **Fixture file.** Put a `*.fixture.tsx` file in `component-tests/` or beside the source it drives (`packages/gui-extensions/src/browser/panel.fixture.tsx`). It exports a `mountX()` that renders into a host with a `data-testid` (`packages/session-ui/component-tests/basic-tool.fixture.tsx:7-46`). Never end the name in `.spec` or `.test`.
|
||||
- **Mount it in three steps.**
|
||||
1. Mount a light story first, so that the app styles load.
|
||||
2. Import the fixture and call it inside `page.evaluate`.
|
||||
3. Locate it with `page.getByTestId` (`packages/session-ui/component-tests/basic-tool.spec.ts:30-36`).
|
||||
|
||||
It renders outside the preview decorators, so it must supply its own providers (`packages/app/component-tests/browser-pane-restore.spec.ts:69-83`).
|
||||
|
||||
- **Module URLs.** Build each one with `sourceURL(new URL("<relative path with extension>", import.meta.url))` (`story.ts:10`), written as a string literal in the spec (`browser-pane-restore.spec.ts:6-24`). Any other spelling either fails to load or loads a second module instance whose contexts the page never provides; a hand-written `/@fs//...` is the documented second-instance case (`story.ts:6-9`). The literal also matters for pre-bundling (next section).
|
||||
- **Aliases.** A module imported by URL is the real file. App code that reaches the same module through a `main.ts` alias gets the mock (`browser-pane-restore.spec.ts:95-96`). Decide which of the two the contract needs.
|
||||
- **Spec imports.** Import source modules into the spec with `import type` only (`browser-pane-restore.spec.ts:2-4`). A value import runs browser code in the Node runner.
|
||||
- **Async order.** Let the test decide the order through deferreds the fixture exposes (`mountExtensionHost`, `packages/app/component-tests/extension-host.fixture.tsx:135`). Await the outcome they lead to (`extension-host.spec.ts:46-77`), or poll in-page state with `until` (`extension-host.fixture.tsx:124-132`). Do not copy the fixed in-page `setTimeout` waits that other cases in `extension-host.spec.ts` (`:38`, `:88-90`, `:108-119`) use to prove a negative.
|
||||
|
||||
## Vite pre-bundling and reloads
|
||||
|
||||
`viteFinal` drops `storybook:optimize-deps-plugin` (`main.ts:105-108`) and sets `optimizeDeps` itself (`main.ts:56-67`). Its `entries` are every `*.stories.tsx` and `*.worker.ts` under the four `src/` trees, every module a component spec names by a `./` or `../` string literal ending in `.ts`/`.tsx` (collected at `main.ts:15-29`), and `preview.tsx`; `include` adds the Storybook preview runtime. Vite pre-bundles their bare imports before the first page loads. A dependency the scan misses is found on first import instead. If that changes chunks already served, Vite logs `✨ optimized dependencies changed. reloading` and sends `full-reload` with path `*` to every page in every worker, and requests with a stale `?v=` hash get a 504 "Outdated Optimize Dep".
|
||||
|
||||
- **The bar is zero reloads.** A cold run of either suite prints neither `new dependencies optimized` nor `optimized dependencies changed`; neither did the Linux and Windows CI legs. Dropping `preview.tsx` and `include` from `main.ts` brings one reload back, so these lines are the signal to watch.
|
||||
- **Keep new code reachable by the scan.** A module path built at runtime, in a template literal, or without its extension is not collected, and neither is a spec named other than `*.spec.ts`. Name stories `*.stories.tsx`: Storybook indexes other extensions, but the scan reads only that one. Name workers `*.worker.ts`, since the scan does not follow `new Worker(new URL(…))`. Add an `optimizeDeps.include` entry only for a dependency the scan cannot reach. Vite names it in `✨ new dependencies optimized: …`, and `DEBUG=vite:deps` replaces that line with scan and discovery detail.
|
||||
- **Symptoms.** "page.evaluate: Execution context was destroyed", "Failed to fetch dynamically imported module …/@fs/…", or fixture state that is gone right after `mount` ("Registrations: 1" not found, an empty tab strip).
|
||||
- **Only `mount()` survives a reload,** because it navigates back to the same URL. A reload loses everything set up after it: `page.evaluate` imports, mounted fixtures, state from earlier clicks, and the elements your locators pointed at. Another worker's discovery can trigger it at any moment.
|
||||
- **Fix reloads in the scan, never with retries, timeouts or import order.** The import-everything-before-mount `Promise.all` in `browser-pane-restore.spec.ts:43-58` (comment at `:56`) only narrowed the window. Before the entries existed, that file's `:123` and `:150` were retried in 31 CI jobs each, and failed attempts cost about 39-47 s (Linux) and 34 s (Windows) of worker time per run.
|
||||
- **Clear the cache whenever entries matter.** Vite's `getConfigHash` covers `optimizeDeps.include`, `resolve` and plugin names, but not `entries`, and a valid `_metadata.json` skips the scan. A warm cache therefore never scans a new story or fixture, so it shows neither the reloads nor the absence of reloads that CI's cold scan would. Delete `packages/storybook/node_modules/.cache` before judging.
|
||||
- **Resolve through package resolution, not paths into `node_modules`.** `main.ts:69-71` dedupes `@tanstack/solid-query` instead of aliasing it into `app/node_modules`, a path that exists only under bun's isolated linker. While Windows CI installed hoisted, that alias made every app story fail there, and 23 of 30 runs hit the step's 15-minute cap. CI now runs the same `bun install --frozen-lockfile` on every OS (`.github/actions/setup-bun/action.yml:66-68`), so such an alias would pass CI and still break a hoisted local install.
|
||||
|
||||
## Cold vs warm, and what a mount costs
|
||||
|
||||
- **CI is always cold.** Each suite runs in its own leg on a fresh runner (`test.yml:228-229`), so every leg starts Storybook with no cache and runs the full scan.
|
||||
- **Do not count the first test.** In one CI run of this harness, the first test per worker took 5.7-7.0 s on Linux and 14-15 s on Windows. Warm tests had a median of 1.5 s (app) and 2.0 s (session-ui) on Linux and 2.4-2.6 s on Windows. A warm `mount` (navigation plus root wait) had a median of 1.0 s (app) and 1.5 s (session-ui) on Linux and 1.7-1.9 s on Windows, which is most of a typical test. Judge cost by warm attempts.
|
||||
- **Local reuse.** Playwright reuses whatever answers on the port (`config.ts:20`; default 6006, `config.ts:3`). That hides cold cost and reloads, and with several worktrees another checkout's Storybook can serve your tests its own code. Set `PLAYWRIGHT_STORYBOOK_PORT` or `PLAYWRIGHT_STORYBOOK_URL` (`config.ts:3-4`). On Windows pick a port below 49152: the dynamic range starts there, and Hyper-V and WSL reserve blocks inside it (`netsh int ipv4 show excludedportrange protocol=tcp`). Two Storybooks in one checkout share, and race on, `packages/storybook/node_modules/.cache/storybook/<version>/<hash>/sb-vite`, so a separate port is not enough: use a separate worktree.
|
||||
- **Keep mounts few.** Put one scenario in each test, and resize in place with `page.setViewportSize` (`composer.spec.ts:28-29`) instead of mounting again.
|
||||
- `a11y.manual` makes addon-a11y skip its axe run after each render, so changing `a11y.test` in `preview.tsx:133-135` is not enforced by these tests. Assert accessibility in the spec when it is the contract.
|
||||
- CI records video only on the first retry, next to the trace; local runs keep `retain-on-failure` (`config.ts:27-30`).
|
||||
|
||||
## Mobile, touch and drawers
|
||||
|
||||
- **Touch.** Use `story.use({ hasTouch: true, isMobile: true, viewport: { width: 390, height: 844 } })` inside a `story.describe` (`packages/session-ui/component-tests/session-timeline-mobile.spec.ts:3-4`). These options are test-scoped, so the shared workers stay. Prove that emulation took effect with `matchMedia("(hover: none)")` (`composer.spec.ts:244`), and interact with `tap()` or `page.touchscreen.tap` (`session-timeline-mobile.spec.ts:41-42`).
|
||||
- **No `launchOptions`.** They are worker-scoped, so the file gets its own worker hash and its own workers. App e2e had to move `packages/app/e2e/regression/mobile-timeline-scroll.spec.ts:18` into its own `fullyParallel` project (`packages/app/playwright.config.ts:69-86`) because it otherwise formed one long serial group running alone on a single worker.
|
||||
- **Drawers.** Below 768 px, pickers open in a bottom drawer (`composer.spec.ts:100-107`).
|
||||
- @corvu/drawer 0.2.4 lost a close requested in the first frames after opening: focus stayed trapped in an off-screen sheet and `body` kept `pointer-events: none`. Before the fix, this caused most first-attempt failures of `composer.spec.ts:5`, in 17 of 31 Linux runs.
|
||||
- `patches/@corvu%2Fdrawer@0.2.4.patch` (root `package.json` `patchedDependencies`) fixes it, so tests need no wait before pressing Escape or tapping outside.
|
||||
- `composer.spec.ts:375-429` guards the patch. It dismisses in-page, before and as `data-opening` appears, because Playwright input cannot target those frames. Upgrading @corvu/drawer drops the patch, so rerun those tests.
|
||||
- Before measuring or screenshotting a drawer, wait until it is fully in the viewport and has no `data-transitioning` (`packages/app/e2e/regression/mobile-model-pickers.spec.ts:25-26`).
|
||||
- A stuck overlay is a product bug. Fix the product and guard it in-page, as the patch did, rather than waiting it out.
|
||||
|
||||
## Geometry, platforms, known failures
|
||||
|
||||
- **Fonts.** Inter's `@font-face` has no `font-display` (`packages/app/src/index.css:13-18`), so a story can lay out with fallback metrics and then shift. Windows CI saw 6.5 px in the composer overflow measurement (`composer.spec.ts:93-95`), which now waits first (`:25-26`). Before measuring, run `await page.evaluate(() => document.fonts.ready.then(() => undefined))` (`markdown.spec.ts:247`). The `.then` keeps the `FontFaceSet` from being returned to Node.
|
||||
- **Page scroll.** Outside fullscreen, the preview frame is `calc(100vh - 2rem)` to fit Storybook's 1rem padding (`preview.tsx:82-84`). With `100vh`, every such story scrolled by 32 px, and any scroll-into-view shifted boxes (`composer.spec.ts:116` saw y go from 300 to 268). Do not give a story root `100vh`. A story taller than the viewport still scrolls, so measure relative to a container captured at the same moment, or use `parameters: { layout: "fullscreen" }`.
|
||||
- **Shortcuts.** Write them as `ControlOrMeta+…` (`browser-pane-restore.spec.ts:134`). Derive expected labels from `navigator.platform` (`packages/app/component-tests/session-question-dock.spec.ts:121`).
|
||||
- **Known local failure.** On macOS, `browser-pane-restore.spec.ts:123` fails after `ControlOrMeta+w` (Meta+W, `:134-135`), while CI (Control+W) passes. It is not your regression.
|
||||
- **`story.fail`.** It waits out the full 10 s expect timeout on every run: `packages/session-ui/component-tests/file-tool.spec.ts:49-50` took 12.6 s on Linux CI and 14.8 s on Windows CI. Fix the product instead, or make the expected failure an immediate check once the state has settled.
|
||||
|
||||
## Before you push
|
||||
|
||||
From the owning package (`packages/session-ui` or `packages/app`):
|
||||
|
||||
1. **Repeat** at CI's worker count. Locally retries are already 0 (`config.ts:14`). Every attempt must pass: `bun run test:components -- component-tests/<file>.spec.ts --workers=2 --repeat-each=10`.
|
||||
2. **Read durations** in `bunx playwright show-report component-tests/playwright-report`. Compare warm tests with the CI medians above. Anything much slower needs a reason in the work the test does.
|
||||
3. **Rehearse load.** Late font swaps and focus restored after a menu closes (`composer.spec.ts:114-115`) race the test under load. In a throwaway copy of the spec, throttle before `mount` with `const cdp = await page.context().newCDPSession(page)` and then `await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 })`. Alternatively, repeat while the other suite runs.
|
||||
4. **Run each suite cold** whenever you add a story, fixture, dependency, worker or lazy import, or change `packages/storybook`. From the repo root, once per suite, as CI runs each in its own leg. Playwright pipes Storybook's output into its own (`config.ts:22-23`), and a free port makes it start a fresh server:
|
||||
|
||||
```sh
|
||||
rm -rf packages/storybook/node_modules/.cache
|
||||
PLAYWRIGHT_STORYBOOK_PORT=6216 bun run --cwd packages/app test:components -- --workers=2 2>&1 | tee /tmp/app-components.log
|
||||
grep -E "new dependencies optimized|optimized dependencies changed" /tmp/app-components.log
|
||||
```
|
||||
|
||||
The `grep` must print nothing. Count optimizer events, not durations: under load, the same config ran anywhere from 127 to 189 s.
|
||||
|
||||
5. **Windows.** Check platform keys and labels and font-dependent geometry. The Windows component legs are the authoritative check.
|
||||
- Do not skip on Windows, and do not raise `config.ts` timeouts, retries or the `mount` wait.
|
||||
- If one test does genuinely expensive work, call `story.setTimeout(...)` in that test, with a comment that names the work.
|
||||
6. **Confirm CI ran the component legs.** E2E never runs on pushes to dev (`test.yml:28`), so no post-merge run catches a regression.
|
||||
- The `affected` gate filters on `@opencode/app` and `@opencode/storybook` (`test.yml:52-53`). `ui`, `session-ui` and `gui-extensions` count through app's dependencies. `workflow_dispatch` forces the legs on (`test.yml:48-51`).
|
||||
- `test.yml` is listed in `globalDependencies` (`turbo.json:8`), so editing it marks every package affected.
|
||||
- The required `e2e (linux)` and `e2e (windows)` checks also pass when no leg was needed (`test.yml:316-335`). Look for `e2e (<os>, session-ui-components)` and `e2e (<os>, app-components)`.
|
||||
- If CI skipped them, report your local cold run in the PR.
|
||||
|
||||
## When CI flakes
|
||||
|
||||
- **Download the artifact.** Run `gh run download <run-id> -n playwright-<linux|windows>-<session-ui|app>-components-<attempt> -D <dir>` (`test.yml:301-314`). Results are in `<dir>/session-ui/component-tests/` or `<dir>/app/component-tests/`. Only the first retry has a trace and, in CI, a video: `test-results/<test>-components-retry1/`.
|
||||
- **Find reloads.** Storybook's output reaches the job log with a `[WebServer]` prefix, so grep the log for `optimized dependencies changed`. In the retry trace, a reload shows up as any of:
|
||||
- a second `iframe.html` load and a second `[vite] connecting...`;
|
||||
- the deps `?v=` hash changing between requests;
|
||||
- 504 "Outdated Optimize Dep" on `deps/*.js`.
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
name: tui-tests
|
||||
description: Use when writing, fixing, or reviewing tests that render OpenTUI (testRender, createTestRenderer, createAppFixture) in packages/tui or packages/cli. Covers frame waits (waitForFrame, waitFor, waitForVisualIdle, flush), started vs non-started renderers, injected clocks, mock keyboard and mouse input, terminal sizes, cleanup and shared paths, and the 30s budget. Also use when a TUI test fails with "Timed out waiting for frame predicate after N passes", or when a tui test file is slow. For cli subprocess and service tests, use the unit-tests skill.
|
||||
---
|
||||
|
||||
# TUI tests
|
||||
|
||||
packages/tui runs about 170 test files in one `bun test` process. CI runs it next to up to two other suites on 4-vCPU runners (`bun turbo test --affected --concurrency=3` in `.github/workflows/test.yml`), where it has taken 136-146s on Linux and 147-171s on Windows, most of it idle on frame pacing, production timers and sleeps. Most flakes come from waiting on the wrong signal and then running out of frames on a busy runner.
|
||||
|
||||
Read the root `AGENTS.md` Testing section (no `globalThis` mocks, run from the package directory) and its TUI Stories section. To drive the real TUI, use the `opencode-dev` skill. For CLI subprocesses, service deadlines, Effect `TestClock` and turbo test tasks, use the `unit-tests` skill.
|
||||
|
||||
## Frame budgets are not time
|
||||
|
||||
- `waitForFrame`, `waitFor` and `flush` give up after `maxPasses` (default 20). `waitForVisualIdle` gives up after `maxFrames` (default 20). One pass is one rendered frame (`DEFAULT_MAX_PASSES` in `@opentui/core/testing`). "Timed out waiting for frame predicate after 20 passes" comes from this budget, not from bun's timeout.
|
||||
- **Started renderer** (`setup.renderer.start()`, as `createAppFixture` does at `packages/tui/test/fixture/app.ts:28`): frames tick at OpenTUI's default 30 fps, so 20 passes are about 0.66s. Production renders at `targetFps: 60` (`packages/tui/src/app.tsx:241`), so test frames are twice as slow as the real app.
|
||||
- **Not started** (the default for `testRender` and `createTestRenderer`): `waitFor` and `waitForFrame` fail within milliseconds when no render is scheduled, and the error shows `isRunning: false`. In the same state `flush()` and `waitForVisualIdle()` return immediately without error; they do not wait for fetches or timers. Await the signal, then `renderOnce()`.
|
||||
- Wait on frames only for something a frame produces. For anything else, wait on the signal itself:
|
||||
- **Focus set on a timer.** `DialogSelect` focuses its filter with `setTimeout(..., 1)` (`packages/tui/src/ui/dialog-select.tsx:702`), which takes about 15ms on Windows. `typeText` sends keys to whatever is focused, so early keys are dropped and Enter then picks the default option. Put the focus in the predicate, as both dialog waits in `packages/tui/test/command-selection.test.tsx` do (`:68-70` for the model, `:75-80` for the variant): `setup.renderer.currentFocusedRenderable instanceof InputRenderable`. Before the variant wait had it, the test flaked in 5 CI runs in 20 hours, including a dev push, and a bigger budget did not help: it still failed with `maxPasses: 150`, because the keys were lost, not late. If a wait fails with the focus in the predicate, look for a state race, not the budget.
|
||||
- **Markdown and code text.** `<markdown>` (`packages/tui/src/routes/session/message-parts.tsx:182`) draws no text until the shared tree-sitter worker replies, while frames keep ticking, so a plain `waitForFrame` can spend all 20 passes on a loaded runner with the text still pending. Use `waitForHighlightedFrame(setup, predicate, options?)` from `packages/tui/test/fixture/frame.ts`. When the predicate fails, it awaits every in-flight `CodeRenderable.highlightingDone` inside the frame predicate, and awaiting there does not use up passes. It accepts any setup with `renderer` and `waitForFrame`; app-lifecycle, session-group-navigation and session-home wait on transcript text this way. If the code under test accepts a client, inject `new MockTreeSitterClient({ autoResolveTimeout: 0 })` instead (`packages/tui/test/mini/scrollback.surface.test.ts:87`).
|
||||
- **Requests and other state.** Await the deferred response your fetch handler returns. With no signal to await, poll with a wall-clock deadline instead of raising `maxPasses`: the file-local `waitForFrame(setup, matches)` at `command-selection.test.tsx:117` checks `captureCharFrame()` every 10ms for up to 2s. `wait` in `packages/tui/test/cli/tui/data.test.tsx:37` and `waitForModel` in `packages/tui/test/fixture/local.tsx:109` do the same for state.
|
||||
- `waitForVisualIdle` returns on the first frame with `cellsUpdated === 0` by default. A frame waiting on a highlight is also quiet, so wait for the text before you measure layout or assert on it: `session-home.test.tsx:227` waits for "History message 0001" with `waitForHighlightedFrame` after `waitForVisualIdle()` instead of reading the frame directly.
|
||||
- Settle steps cost frames. Each `waitForVisualIdle({ quietFrames: 3 })` takes at least 3 frames, about 100ms at 30 fps. `session-group-navigation.test.tsx` has 14 of them. When you know what you are waiting for, wait for that frame instead.
|
||||
- Do not use `maxPasses` as a clock. Several waits do, and each assumes 33ms frames:
|
||||
- `app-lifecycle.test.tsx:792`: 120 passes to cover the 1s reconnect delay after `setup.events.disconnect()` at `:784`;
|
||||
- `app-lifecycle.test.tsx:1721-1723`: 200 passes per retry-countdown second;
|
||||
- `component/dialog-shell-output.test.tsx:144`: 100 passes for a 1s poll, plus `Bun.sleep(1100)` at `:163` and `:182`;
|
||||
- `session-panel.test.tsx:590`: 120 passes in `ready()` for the initial load.
|
||||
|
||||
With test frame rates scaled 4x, dialog-shell-output (3 tests) and session-activity-anchor (2 tests) failed. The same budgets are why tests cannot yet run at production fps. A larger budget is fine only for a long run of frame-driven work, such as scrolling 50 rows.
|
||||
|
||||
## Pick the renderer mode and clock
|
||||
|
||||
- **Component logic:** `testRender(() => <X />, { width, height })` without `start()`. Change state, `await app.renderOnce()`, then assert on `captureCharFrame()`. One frame, deterministic.
|
||||
- **Time-driven renderables:** pass `clock: new ManualClock()`, call `renderer.pause()`, then advance the clock and `renderOnce()` (`fixture()` in `packages/tui/test/component/retry-provider.test.tsx:8`). `pause()` also stops `requestLive()` (spinners, animations) from starting the frame loop.
|
||||
- **Clock order:** inject a clock first (OpenTUI `ManualClock`; Effect `TestClock` for Effect code, see `unit-tests`). Use `jest.useFakeTimers()` only when nothing is injectable: it patches the global timers of a process that runs every tui file, so restore them with `jest.useRealTimers()` in `finally` (`packages/tui/test/util/delayed-presence.test.ts:24-26`).
|
||||
- **Whole app:** `createAppFixture` (`packages/tui/test/fixture/app.ts`) gives a started renderer, an in-process `Bun.serve` and `kittyKeyboard: true`. Call `await setup.ready` before the first wait. Passing `config` replaces the default `{ animations: false }`, so include `animations: false` yourself. The first use in a process pays `await import("../../src/app")`: about 1s idle, 12-25s under heavy load. Use it only when the behavior spans routes or data.
|
||||
- **Server behavior:** use the existing `createFetch` and `createEventStream` fixture (`packages/tui/test/fixture/tui-client.ts:15`, `:69`) rather than spawning a server. The persistent PTY, for example, is faked as `/api/experimental/persistent-pty/...` routes plus a `Bun.serve` websocket (`packages/tui/test/session-wheel-focus.test.tsx:52`); real PTY behavior is tested in `packages/server/test/persistent-pty.test.ts`. Every fixture server listens on `port: 0` (`fixture/app.ts:32`) and is reached through `server.url`; never hard-code a port. Windows hands out ephemeral ports from 49152 up, so a fixed port in that range can already be taken by another connection.
|
||||
|
||||
## Keyboard and mouse
|
||||
|
||||
- Use `setup.mockInput`: `pressKey`, `typeText`, `pressEnter`, `pressEscape`, `pressArrow`, `pasteBracketedText`.
|
||||
- Create renderers with `kittyKeyboard: true`. Without it, `mockInput` silently sends something else (OpenTUI 0.5.17):
|
||||
|
||||
| Input | Without kitty mode | With kitty mode |
|
||||
| -------------------------------------------- | ----------------------------------------- | --------------- |
|
||||
| `pressKey("u", { ctrl: true, shift: true })` | `ctrl+u` | `ctrl+shift+u` |
|
||||
| `pressKey("2", { ctrl: true })` | `2` | `ctrl+2` |
|
||||
| `pressEnter({ shift: true })` | `return` | `shift+return` |
|
||||
| `pressEscape()` then `pressKey("a")` | `meta+a` (a lone Escape is held for 20ms) | `escape`, `a` |
|
||||
|
||||
- A second click on the same renderable within 500ms (renderer clock) and within 1 cell becomes a word or line selection. Do not sleep past that window: `Bun.sleep(550)` at `packages/tui/test/session-activity-anchor.test.tsx:120` costs 4.4s per file. Call `setup.renderer.clearSelection()` first, which resets the click history, or click at least 2 cells away. In a `ManualClock` renderer driven by `renderOnce()`, `clock.advance(501)` also works.
|
||||
|
||||
## Terminal sizes
|
||||
|
||||
- Always pass `width` and `height`. Otherwise the renderer uses `process.stdout.columns`: your terminal locally, 80 in CI.
|
||||
- Cover layout at a narrow and a wide width, for example `test.each([100, 44])` (`packages/tui/test/app-lifecycle.test.tsx:13`). After `setup.resize(w, h)`, `await setup.renderOnce()` or `flush()` before asserting. Run any real-time sequence at one width only.
|
||||
- Measure work by counting, not timing: mounted rows (`packages/tui/test/session-mount-budget.test.tsx`), rendered diff lines (`packages/tui/test/cli/tui/diff-viewer.test.tsx:2198`). `performance.now()` thresholds fail on slow Windows runners.
|
||||
|
||||
## Timers, cleanup and shared state
|
||||
|
||||
- A TUI test that takes over a second is usually waiting on a production constant; make it injectable instead of waiting it out. Two such constants: the shell output poll `setTimeout(poll, more ? 0 : 1_000)` (`packages/tui/src/component/dialog-shell-output.tsx:73`; 6 tests, 18.8s, 95% idle) and `reconnectDelay = 1_000` (`packages/client/src/solid/connection.ts:33`; about 1s per reconnect test). connection.ts already takes `idleTimeout` and `flushInterval` options, so add a sibling.
|
||||
- Clean up with `await using` or `finally`: destroy the renderer and stop the fixture server. Cancel timers first, or they throw "TextBuffer is destroyed" into the next test.
|
||||
- Remove the tmpdir last. Destroy the renderer and await the app task first, as `createAppFixture`'s `[Symbol.asyncDispose]` does (`fixture/app.ts:63`). Removing it under a pending write logs "Failed to persist" and flock "lock is compromised".
|
||||
- `TestTuiContexts` (`packages/tui/test/fixture/tui-environment.tsx:32-35`) defaults `home` and `state` to fixed `/tmp/opencode/...` paths. When the tree mounts a provider that writes under `paths.state` (`StorageProvider` in `src/context/storage.tsx:145`, `LocalProvider`, or the prompt history, frecency and stash providers), pass `paths={{ state, home }}` built from `tmpdir()` (`test/fixture/fixture.ts:8`). Process-shared paths are what keep tui from parallel workers: `--parallel=2 --no-isolate` halved local wall time (162.9s to 80.7s). Isolated mode re-imports `src/app` in each of the 18 test files that load it, at 6x the CPU.
|
||||
- Bun does not cancel a timed-out test body. It keeps running and can fail the next test or rerun as "Unhandled error between tests". Triage the first failure in a file; the ones after it may be leftovers.
|
||||
- Import another package's test helpers by relative path only from a package in tui's workspace dependency closure. `@opencode/tui#test` depends on `transit` in `turbo.json`, which folds tui's whole workspace dependency closure into the test hash, so `data.test.tsx:7` reaching `../../../../core/test` is covered because tui depends on `@opencode/core`. A package outside that closure is not hashed, and a change there can replay a stale pass (see `unit-tests`, Turbo test tasks).
|
||||
|
||||
## Timeouts and failures
|
||||
|
||||
- The tui and cli test scripts run `bun test --timeout 30000`; Bun's default is 5s. In tui, that budget exists for the cold `src/app` import, not for slow waits.
|
||||
- A frame-budget failure is not a timeout problem. Raising the test timeout does nothing for it, and raising `maxPasses` only hides the wrong wait.
|
||||
- A per-test timeout below 30s, like the 15000 at `session-group-navigation.test.tsx:249`, must still cover the cold import when the file runs alone. New per-test timeouts name their work in a comment (see `unit-tests`, Know the budget).
|
||||
- A frame-budget failure that reproduces on every run of one branch is a regression in that change, not a flake. dialog-shell-output failed "after 100 passes" in 21 of 29 tui runs on one PR stack and in 0 of about 102 elsewhere. Do not answer it with a bigger budget.
|
||||
- Do not add `skipIf(process.platform === "win32")` to hide a timing flake; fix the wait. Windows timers have about 15ms resolution, so a 1ms `setTimeout` race that never shows on macOS fails there.
|
||||
|
||||
## Before you push
|
||||
|
||||
- From `packages/tui`: `bun test --timeout 30000 test/<file>`. This matches CI's budget and prints per-test milliseconds, which the package script's `--only-failures` hides.
|
||||
- `bun test --timeout 30000 test/<file> --rerun-each 10`: all 10 must pass. The first iteration pays the cold import, so a timeout on iteration 1 under heavy load is that import, not your wait. Repeat while one other suite runs (for example `bun run test` in `packages/core`), not on an overloaded machine. Before its focus wait, the command-selection race failed 5 of 8 local runs under load.
|
||||
- Force the race you suspect: temporarily slow the timer or reply (for example the focus `setTimeout` to 150ms). The test must still pass. Revert afterwards.
|
||||
- Negative control: make the predicate impossible. The test must fail fast with a frame dump, not hang until the timeout.
|
||||
- Windows-sensitive behavior to check for: timers under 16ms, renaming a file another process holds, process kill, and paths shown in frames. On pushes and pull requests the unit job runs `--affected`, so `@opencode/tui:test` is scheduled only when tui, a workspace package it depends on, or a `globalDependencies` file in `turbo.json` changed. In the `unit (windows)` log, confirm the lines prefixed `@opencode/tui:test:` are there with `cache miss, executing` and a pass, and compare its `Ran N tests across M files. [Xs]` line with a recent run that included tui. Per-file timing is only available locally.
|
||||
@@ -1,140 +0,0 @@
|
||||
---
|
||||
name: unit-tests
|
||||
description: Use when writing, reviewing, speeding up, or fixing a flaky `bun test` unit test in any package (core, cli, server, ai, codemode, util, client, sdk, plugin, http-recorder, merman, app src and test-browser, desktop, gui-extensions, ui, session-ui, latex, ...), except OpenTUI renderer tests (see tui-tests). Also use when a test "timed out after 5000ms" on Windows CI, fails only on Windows, when the unit job is slow, when adding cli subprocess or service tests, or when adding a package's test task to turbo.json. Covers controlling time instead of sleeping, per-test timeouts, process and git spawn cost on Windows, Windows-only failures, temp dirs and process state, leaks between files, and reading CI timeouts.
|
||||
---
|
||||
|
||||
# Fast, Reliable Unit Tests
|
||||
|
||||
CI runs `bun turbo test --affected --concurrency=3` on 4-vCPU Linux and Windows runners (unit job in `.github/workflows/test.yml`), so each suite shares the CPU with two others, and Windows is the slower leg. Most slow or flaky unit tests here wait on real time, spawn too many processes, or assert wall-clock speed.
|
||||
|
||||
Read these first; this skill does not repeat them:
|
||||
|
||||
- Root `AGENTS.md`: the Testing and Style Guide sections.
|
||||
- `packages/app/AGENTS.md` Tests (`packages/desktop` and `packages/gui-extensions` follow it): one owner per contract, and no production seams for tests.
|
||||
- `packages/ai/AGENTS.md`: cassettes. The `effect` skill: `it.effect` and `it.live`.
|
||||
|
||||
Sibling skills: `tui-tests` (OpenTUI renderer tests in tui and cli), `app-e2e-tests` (Playwright in `packages/app/e2e`), `component-tests` (Storybook Playwright). This skill owns cli subprocess and service tests.
|
||||
|
||||
## Know the budget
|
||||
|
||||
- Bun's default per-test timeout is 5000ms. Check the package's `test` script:
|
||||
- 30s: ai, cli, desktop, http-recorder, latex, merman, simulation, theme and tui pass `--timeout 30000`; core's `script/test.ts` passes `--timeout=30000`.
|
||||
- 5s: client, httpapi-codegen and sdk pass `--timeout 5000`; app, codemode, function, gui-extensions, plugin, plugin-browser, script, server, session-ui, ui and util use the default.
|
||||
- Size Windows risk by counting spawns, not by local wall time.
|
||||
- npm.test.ts "installs a Git package from an npm ::path: subdirectory at a branch" takes 0.4s locally and hit 5000ms on Windows CI. Each Npm.add Git case makes about 75 git process events: 3 `clone --recurse-submodules`, which run the git-submodule shell script.
|
||||
- Assume roughly 40ms per Git-for-Windows spawn (estimated from an earlier git.test run on Windows CI: ~24s for 564 spawns). Some core file groups run 5-9x slower on Windows than locally.
|
||||
- Those `test.each` cases (`npm.test.ts:223`, `:251`) hit 5000ms in 13 of 45 Windows core runs; that is why core's runner now defaults to 30s (comment at `packages/core/script/test.ts:38`).
|
||||
- A timeout loosens the budget only when it exceeds the package default.
|
||||
- In a 5s package, work that legitimately needs longer gets a per-test timeout and a comment naming the work: `packages/server/test/fetch.test.ts:406-407` ("Real retries wait 18 \* 200 ms ...", `{ timeout: 10_000 }`). `test.each(table)(name, fn, 30_000)` takes it as the third argument. Use file-wide `setDefaultTimeout` only when every test in the file does such work.
|
||||
- In core, the four file-level `setDefaultTimeout(30_000)` calls only restate the runner default, and a per-test `{ timeout: 15_000 }` (still in `tool-shell.test.ts`, `session-generate.test.ts` and others) is stricter than it. Do not add values at or below 30s there.
|
||||
- Never absorb a sleep, poll or production interval with a larger timeout, `--retry`, or `skipIf(win32)`; remove the wait. Skip on Windows only when the platform lacks the capability, with a comment (`npm.test.ts:313`, symlinks).
|
||||
|
||||
## Control time; do not wait for it
|
||||
|
||||
- **Effect code.** `it.effect` from `packages/core/test/lib/effect.ts` runs on `TestClock`; `testEffect(layer)` adds your layer. server and cli import it from `../../core/test/lib/effect`; ai has `packages/ai/test/lib/effect.ts`.
|
||||
- **Fork, then adjust.** Fork with `Effect.forkChild({ startImmediately: true })` (`packages/ai/test/http-timeout.test.ts:58`), or await a Deferred the code completes and then `Effect.yieldNow` (`:80-81`). Otherwise `TestClock.adjust` can run before the sleep is registered, and the join hangs until Bun's timeout.
|
||||
- **Debounced streams and watchers.** `advance(condition)` and `drain` in `packages/core/test/lib/clock.ts` alternate one real 1ms macrotask with 500ms adjusts, because debounces register sleeps across fiber hops. Use `drain` (4 such steps) before a "nothing happened" assertion instead of `Bun.sleep(25)`. `advance`'s 100-step bound is right under TestClock, but a condition waiting on slow real work (a child, a large write) can run out of steps under load.
|
||||
- **Real I/O between virtual steps.** `TestClock.withLive(Effect.sleep("10 millis"))`. `registered()` at `packages/cli/test/service.test.ts:740` tests the 5s registration watch with real file I/O under `it.effect`. This departs from the effect skill's "filesystem → `it.live`" on purpose: the timer is the contract.
|
||||
- **Real events need `it.live`.** Under `it.effect` the clock moves only when the test adjusts it, so an Effect timeout on a path driven by real processes, sockets or file events never fires and a missing event hangs instead of failing. The AppProcess tests (`packages/core/test/process/process.test.ts:13-14`) use `it.live` because the spawner's 1-second post-exit capture deadline (`packages/util/src/cross-spawn-spawner.ts:322`) never fired under TestClock.
|
||||
- **Give a racing component its own clock.** When a real-time deadline races slow setup, provide that component a `Clock` that only the test moves past the deadline. `packages/core/test/browser-idle.test.ts:24-38` wraps the live clock for `LocationActivity` (`:49-51`) and adds the TTL at `:84`, once the browser state is attached. Before, it waited out a real 2s TTL and failed on Windows CI with "RPC is unavailable: experimental.browser" when plugin activation was slow. Shortening a real TTL makes that race flakier.
|
||||
- **Product Effect code must stay on the caller's clock**, which is what makes both patterns above possible. Read time through `Clock`, wait with `Effect.sleep` or `Schedule`, and run nested effects with the caller's context.
|
||||
- A bare `Effect.runPromise` runs on the default real clock. McpStdio does this (`packages/core/src/mcp/stdio.ts:66`, `:82`, `:109`), so `packages/core/test/mcp.test.ts:832` waits the real 2s `CLOSE_GRACE` on every OS.
|
||||
- Capture `yield* Effect.context()` and use `Effect.runPromiseWith(context)`, as `packages/cli/src/acp/agent.ts:27` does.
|
||||
- **Code outside Effect** (app, desktop, ui, session-ui): `jest.useFakeTimers()` and `setSystemTime` from `bun:test`, restored in `afterEach` (`packages/desktop/src/main/extension/lifecycle.test.ts:27-32`, `packages/desktop/src/main/storage/drafts.test.ts:15-17`).
|
||||
- **Advance fake timers to the next timer** (`jest.advanceTimersToNextTimer()`, `jest.runOnlyPendingTimers()`), not in small fixed steps. The 10ms-step `advance` at `lifecycle.test.ts:37` turns the 1,000-seed fuzz test (`:607`) into about 430k event-loop turns: 2.8-3.8s on Windows, 0.56s on Linux.
|
||||
|
||||
## Real-time waits: deadlines, not attempt counts
|
||||
|
||||
- Bound a real-time poll in milliseconds, generously, and return as soon as it holds: `until` at `packages/cli/test/service-lifecycle.test.ts:267` (30s), `eventually` at `packages/core/test/integration.test.ts:37` (10s).
|
||||
- Attempt-count loops hide a small budget that shrinks under load. Do not copy `waitUntil` at `packages/core/test/config/reload.test.ts:371` (100 × 10ms; the skills test at `:57` timed out after 1.2s on a Linux dev push) or `packages/core/test/plugin/provider-azure.test.ts:219` (3000 × 1ms).
|
||||
- Poll for every state you assert afterwards: the reload poll at `reload.test.ts:301-317` includes the skills that `:319-320` assert.
|
||||
- Let the test, not a timer, release a child's next step. The login command in `integration.test.ts:230-254` prints its secret only after the test, having seen the pending status, writes a release file; it used to print after a fixed `Bun.sleep(50)`.
|
||||
- Small timer gaps race too: `packages/core/test/process/process.test.ts:59-67` ordered stdout and stderr with 10ms and 20ms child timers and failed on Windows CI (`out 2` read before `err 1`); it now uses 100ms and 200ms.
|
||||
|
||||
## When the wait happens in another process
|
||||
|
||||
TestClock cannot reach a spawned server or worker. These production waits already cost real time:
|
||||
|
||||
- cli (about 60s of its roughly 2-minute suite): the 15s bind-retry deadline (`Date.now() + 15_000` in `packages/cli/src/server-process.ts`), the 5s registration watch (`packages/cli/src/services/service-registration.ts:63`), and fixed sleeps (`Bun.sleep(8_000)` at `service.test.ts:620`, `Bun.sleep(5_500)` at `service-lifecycle.test.ts:237`).
|
||||
- core (about 8s locally): McpStdio `CLOSE_GRACE` 2s + `FORCE_KILL_AFTER` 2s (`stdio.ts:10-13`), Shell `forceKillAfter` 3s (`packages/core/src/shell.ts:306`, `:401`), and the 1s capture deadline in cross-spawn-spawner.
|
||||
|
||||
To avoid adding more:
|
||||
|
||||
- Test the timing logic in-process under TestClock (the `registered()` tests at `service.test.ts:270-299`). Keep real processes for exit codes, signals, stdio and port binding, with one test per escalation path. Do not add tests that wait out these constants.
|
||||
- Shorten a production duration through an option, not by waiting it out. Precedent: real product options (`Flock.Options` in `packages/util/src/flock.ts:41-47`, `idleTimeout`/`flushInterval` in `packages/client/src/solid/connection.ts`) and client's hidden seam `withEnsureTiming` (`packages/client/src/service-timing.ts:31`, used by `packages/client/test/fixture/service-timing.ts`).
|
||||
- For a spawned process the value must reach the child, as `timeoutMs` does through the worker payload at `packages/core/test/util/flock.test.ts:50-58`.
|
||||
- server-process.ts has no override today, so its 15s tests cannot be shortened from the test side.
|
||||
- app, desktop and gui-extensions forbid such seams (their AGENTS.md Tests sections); core, cli, util, client and tui have no such ban.
|
||||
- Retry timing belongs in core under TestClock. `packages/server/test/fetch.test.ts:363-408` spends about 5.8s of server's 16s in real 200ms bind retries (`packages/core/src/plugin/provider/openai.ts:21`). The fixed ports 1455/1457 are the contract there; new server tests cover routing and error mapping only. Elsewhere bind port 0 (`packages/app/src/runtime/server/global-sync/bootstrap.test.ts:81`).
|
||||
|
||||
## Do not assert wall-clock speed
|
||||
|
||||
- `expect(performance.now() - startedAt).toBeLessThan(500)` failed on Windows at 510ms (`packages/ui/src/context/marked-parser.test.ts:84`); `packages/core/test/shell-scan/adversarial.test.ts:77` has the same shape. merman's layout audit no longer asserts its elapsed time.
|
||||
- Count operations, or pick an input where a regression is catastrophic and assert only the result, so it trips the test timeout: `packages/codemode/test/openapi.test.ts:638` (depth 30 is 2^30 work without memoization). A ratio of two timings still fails on a load spike; if you keep one, use a generous factor and say it is timing-sensitive.
|
||||
|
||||
## Spawn less
|
||||
|
||||
- **Process start-up.** Each `bun run src/index.ts` CLI boot costs about 1.3s of CPU (the entry loads TUI `.tsx` through the preload in `packages/cli/bunfig.toml`). Test parsing and validation in-process; keep one or two real subprocess tests per file. Each cold bun child that imports the MCP SDK cost 1-3s; the MCP lifecycle tests, which spawned several, hit 5000ms on Windows until they moved to the in-process `resourceServer()` (`packages/core/test/mcp.test.ts:90`).
|
||||
- **Spawned CLIs** use `isolatedEnv(root)` from `packages/cli/test/fixture/environment.ts`: isolated HOME, XDG and database, shared transpiler cache. `packages/cli/test/upgrade.test.ts:98` and `debug-paths.test.ts:24` set a private `XDG_CACHE_HOME` without `BUN_RUNTIME_TRANSPILER_CACHE_PATH`, so every spawn transpiles again; do not copy them.
|
||||
- **Git.** Before the shared fixture below, a PATH shim counted about 2,850 git spawns per core run, and git.test took about 24s on Windows CI and 7s locally.
|
||||
- Create repos with `packages/core/test/fixture/git.ts`. `initGit(directory, ...initArgs)` (`:24-27`) runs one `git init` and appends the fsmonitor, gpgsign and identity settings to `.git/config` in one write; `initRepo` (`:64-67`) adds an empty root commit (2 spawns; with one `git config` per key it took 6), and `gitRemote` builds on `initGit`.
|
||||
- Do not write per-file `git init` plus `git config` copies. server still has two (`packages/server/test/vcs.test.ts:215-218`, `packages/server/test/worktree.test.ts:131-133`); server already imports core fixtures such as `tmpdir`.
|
||||
- Use `-c user.name=… -c user.email=…` only for commits the test makes itself (`npm.test.ts:53`). Repos where production code commits later (snapshot, worktree, vcs) need repo-local identity.
|
||||
- Keep identity repo-local. The unit job's "Configure git identity" step sets a global identity, but core's `script/test.ts` and cli's `isolatedEnv` replace HOME, so it is invisible there and visible to server and tui. That HOME isolation keeps core's missing-identity coverage: do not depend on CI's identity, and do not add a global gitconfig in core.
|
||||
- **Independent children** start together (`Promise.all`, `packages/ai/test/exports.test.ts:63`). `packages/app/test-browser/playwright-config.test.ts` runs sequential `Bun.spawnSync` (3.5s on Windows, 1.4s on Linux); `packages/desktop/scripts/windows-installer.test.ts` makes 6 sequential cold `pwsh` starts for ACL reads, which one `pwsh` call could batch.
|
||||
- **Size contention and thresholds to the claim.** `flock.test.ts:46` starts 16 bun workers; use only as many as still exceed the 4 CI vCPUs. A hard-coded threshold forces huge fixtures: `packages/core/test/git.test.ts:354` writes 2,100 files to cross `looseObjectLimit = 2048` (`packages/core/src/git.ts:50`), 2.7s locally and slower on Windows. Make the threshold an option with the production default, cross a small value in the test, and assert the default once.
|
||||
|
||||
## Windows-only failures
|
||||
|
||||
- Renaming onto a file another process holds open fails. cli `service.test.ts:462` failed on a dev push with `FileSystem.rename (...service-local.json.<uuid>.tmp)` from the temp-then-rename at `service-registration.ts:39`. Close handles the test holds; when two product processes race, fix the product, not the test.
|
||||
- SIGTERM cannot be caught: `process.kill` ends the target at once with exit code 1, so a shutdown handler runs only on POSIX. Assert per platform with `catchesSigterm`, `stopSignal` and `stoppedExitCode` (`packages/client/test/fixture/service-fixture.ts:7-11`).
|
||||
- Do not prove a stopped server released its port by binding it again. Windows hands out ephemeral ports from 49152 up, and any process, including suites running alongside, may take the port for an outgoing connection; a re-bind failed there with "Failed to start server. Is port 65518 in use?". Assert that nothing accepts connections (`expectNotListening`, `service-fixture.ts:108-117`).
|
||||
- `os.tmpdir()` reads `TEMP` on Windows and `TMPDIR` on POSIX; set both for a child (`packages/util/test/global.test.ts:22-24`). Compare canonical paths with `fs.realpathSync.native`, which expands 8.3 short names such as `RUNNER~1` (`:84-86`).
|
||||
- Reset a test socket once the request has arrived (`data`), not on `open`. Resetting on accept races the client's write, and on Windows the lost race is a write or connect error instead of a reset: `packages/util/src/retry.test.ts:25-30` recorded 2 of its 3 expected failures there. `packages/app/src/runtime/server/global-sync/bootstrap.test.ts:83-88` still resets on `open` and failed 3 of about 19 loaded local runs with `write EPIPE`, which `transientMessages` in `packages/util/src/retry.ts` does not list.
|
||||
|
||||
## Temp dirs, files, process state
|
||||
|
||||
- core's `script/test.ts` points HOME, XDG\_\* and TMPDIR (plus USERPROFILE, TMP and TEMP on Windows) at a fresh directory per run. Per-test dirs: `tmpdir()` (`await using`), `tmpdirScoped` or `withTempDir` from `packages/core/test/fixture/tmpdir.ts`.
|
||||
- Close watchers, databases and children before the dir is removed. On Windows an open handle fails removal with `EBUSY`; `remove` in `tmpdir.ts` then forces a GC and retries every 100ms, up to 30 times.
|
||||
- Restore process state: env with `withEnv` (`packages/core/test/fixture/env.ts`) in Effect tests and in `afterEach` elsewhere, real timers, and `setSystemTime()`.
|
||||
- `CI=true` in the unit job, and Turbo passes it through. Code that reads `CI` takes another path there, so a test of the non-CI path unsets it itself: `packages/http-recorder/test/websocket.test.ts:132` wraps its record-mode case in `withEnvironment("CI", undefined, ...)`, because in CI that case replayed and waited forever for "open".
|
||||
|
||||
## Keep files independent without `--isolate`
|
||||
|
||||
- Except app, each suite runs all its files in one process with one module registry (core: 262 files), so state leaks between files.
|
||||
- Do not use `mock.module`; it leaks across files.
|
||||
- It is why app's scripts pass `--isolate`: app unit went from 2.4s to 9.9s locally, and unit plus browser from 5.3s to 12-20s on CI.
|
||||
- `packages/cli/test/mini-config.test.ts:25` and `packages/core/test/plugin/provider-gitlab.test.ts:74` already leak in non-isolated suites; do not copy them.
|
||||
- In Effect packages, provide the dependency through a test layer. In app, desktop and gui-extensions, drive the real module and entry point, and inject only through a parameter the product already has.
|
||||
- Never add `--isolate` or bare `--parallel` (which implies `--isolate`) to a test script: ai went from 7.6s to 26.3s with `--parallel=2`.
|
||||
- `--parallel=N --no-isolate` keeps the shared registry and measured 0.47-0.68x serial wall time at about the same CPU in core. It also exposed load-sensitive tests (browser-idle and integration's `eventually`, both since fixed).
|
||||
- Adopting it is a runner decision for maintainers once CI passes on both OSes, not a fix for one slow test.
|
||||
- Runtime imports run in every file that loads the module. `packages/desktop/src/main/storage/store.ts:3` imports `electron` only for `getStore`, so `src/main/extension/storage.test.ts` and `src/main/storage/store.test.ts` download the ~100MB Electron binary on a fresh install. On a first local run, storage.test.ts took 5.3s of file time for 0.1s of test bodies. Use `import type` for types (`packages/desktop/src/main/ipc-transport.test.ts:4`).
|
||||
|
||||
## Turbo test tasks
|
||||
|
||||
- `bun turbo test` runs only packages with an `@opencode/<pkg>#test` entry in `turbo.json`; all 24 packages with a `test` script have one. A new suite copies its neighbors (`"dependsOn": ["transit"], "outputs": []`); check with `bun turbo test --dry=json`. Suites without an entry rotted: client, util, sdk, http-recorder and merman needed fixes before CI could run them.
|
||||
- `transit` runs nothing and depends on `^transit`, so a test hash covers the package's whole workspace closure. The global dependencies add root `package.json` (whose `packageManager` pins Bun), `bunfig.toml`, `patches/**`, `.npmrc`, `.github/actions/setup-bun/**` and `test.yml`.
|
||||
- Tests do not wait for builds: workspace packages export their sources (`"./*": "./src/*.ts"` in `packages/core/package.json`), so a test must not read a dependency's `dist`. The build job's "Build workspace libraries" step checks the library builds on Linux.
|
||||
- A package that compiles `src` with tsc keeps colocated tests out of the build (`"exclude": ["src/**/*.test.ts"]` in `packages/util/tsconfig.build.json`). Otherwise `bun test` also runs the compiled copies, as Windows CI did with `packages/util/dist/retry.test.js`.
|
||||
|
||||
## When CI flakes
|
||||
|
||||
- Most suites print only failures (`--only-failures`): `(fail) <name> [5000ms]` and `^ this test timed out after 5000ms`.
|
||||
- When `killed 1 dangling process` and `# Unhandled error between tests` follow (for example `NpmInstallFailedError` with `SIGTERM` from a `git clone`), the test was still doing real work, not hanging.
|
||||
- The unhandled error is the abandoned Effect failing after Bun killed its child, not a second bug. Count the work (see Before you push) before choosing a fix.
|
||||
- Check whether the test also fails on dev pushes: `gh run list --workflow test.yml --branch dev --event push --status failure`. In one census before core's 30s default, 4 of 25 dev pushes went red, all on Windows unit tests (3 of them Npm.add 5s timeouts).
|
||||
- Read whole job logs with `gh run view --log --job <id>`; `--log-failed` omits green jobs, whose `Ran N tests across M files. [Xs]` lines you need for comparison.
|
||||
- Assert a child's stderr before its exit codes. `packages/core/test/util/effect-flock.test.ts:327-328` checks codes first, so its one CI failure never showed the cause.
|
||||
|
||||
## Before you push
|
||||
|
||||
Run from the package directory with the flags from its `test` script (app needs `--conditions` and `--preload`). In core use `bun run script/test.ts <file> …`, which forwards flags, isolates HOME and applies the 30s timeout.
|
||||
|
||||
- [ ] `bun test <file> --rerun-each 10` passes every time. Add `--randomize` for order dependence; reproduce a failure with `--seed=<n>`.
|
||||
- [ ] Per-test durations: `--reporter=junit --reporter-outfile=<abs path>` (works through `script/test.ts`). Any test over about 1s does real work you can name.
|
||||
- [ ] Windows: count spawns. `GIT_TRACE=<abs file>` logs roughly one `trace: built-in:` or `trace: exec:` line per git process; a logging `git` shim first on PATH also works. At about 40ms per spawn, compare with the package timeout.
|
||||
- [ ] Rerun the file while the full package suite runs in another terminal; CI runs three suites on four vCPUs.
|
||||
- [ ] Grep the diff for `Bun.sleep(`, `setTimeout(`, `Date.now() +`, `performance.now()`, `Effect.runPromise(` in product code, `mock.module`, `git config`, and literal port numbers.
|
||||
- [ ] After CI: with `--only-failures`, the `unit (windows)` log hides passing durations. Compare your package's `Ran N tests across M files. [Xs]` line with a recent dev run's.
|
||||
- [ ] New package suite: `bun turbo test --dry=json` lists `@opencode/<pkg>#test`.
|
||||
@@ -166,7 +166,6 @@ const table = sqliteTable("session", {
|
||||
- Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.
|
||||
- Test actual implementation, do not duplicate logic into tests
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package directories such as `packages/core`.
|
||||
- Before writing or changing tests, read the matching skill in `.opencode/skills`: `unit-tests` (bun test), `tui-tests` (OpenTUI renderer tests), `app-e2e-tests` (Playwright in `packages/app/e2e`) or `component-tests` (Storybook Playwright). They cover what has made tests slow or flaky in CI.
|
||||
|
||||
## Checks
|
||||
|
||||
|
||||
@@ -2,63 +2,6 @@
|
||||
|
||||
Release notes for OpenCode V2, newest first.
|
||||
|
||||
## v2.0.30 — 2026-10-15
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Changed Files by Turn**: Enable **Settings → Preferences → Timeline → Changed files** to see each completed turn’s changed files, line counts, and expandable diffs. ([#55064](https://github.com/anomalyco/opencode/pull/55064))
|
||||
- **Docked TUI Diffs**: Run `/diff` inside a session to open the diff viewer beside your work. Press `f` to switch between the side panel and full screen; the layout adapts automatically to narrower terminals. ([#47152](https://github.com/anomalyco/opencode/pull/47152), [#55574](https://github.com/anomalyco/opencode/pull/55574), [#55780](https://github.com/anomalyco/opencode/pull/55780))
|
||||
- **Keyboard Tab Navigation**: Use **Option+↑/↓** to move between session tabs, or add **Shift** to jump between unread tabs. ([#54277](https://github.com/anomalyco/opencode/pull/54277))
|
||||
|
||||
### Core
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Load local glob patterns and remote URLs configured through `instructions`, and refresh them when configuration changes. ([#55539](https://github.com/anomalyco/opencode/pull/55539))
|
||||
- Honor `OPENCODE_DISABLE_CLAUDE_CODE` and `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` when discovering `.claude` skills. ([#55774](https://github.com/anomalyco/opencode/pull/55774))
|
||||
- Publish a configuration schema that accepts both V1 and V2 configuration shapes during migration. ([#55524](https://github.com/anomalyco/opencode/pull/55524))
|
||||
- Remove the retired session-sharing backend and unavailable `/share` command. ([#55716](https://github.com/anomalyco/opencode/pull/55716))
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Use protocol-specific inference endpoints for OpenCode providers and accept `null` namespaces in Responses API function calls. ([#55680](https://github.com/anomalyco/opencode/pull/55680), [#55631](https://github.com/anomalyco/opencode/pull/55631))
|
||||
- Fit compaction output limits to the request actually sent. ([#55613](https://github.com/anomalyco/opencode/pull/55613))
|
||||
- Restore authenticated TinyFish web search by initializing its MCP session. Thanks [@terry-an-investor](https://github.com/terry-an-investor). ([#54730](https://github.com/anomalyco/opencode/pull/54730))
|
||||
- Improve MCP compatibility by skipping unsupported catalog and codemode calls, advertising spec-compliant elicitation capabilities, and accepting `*.localhost` OAuth token endpoints. Thanks [@tsehla42](https://github.com/tsehla42) and [@donbowman](https://github.com/donbowman). ([#55474](https://github.com/anomalyco/opencode/pull/55474), [#55618](https://github.com/anomalyco/opencode/pull/55618), [#53196](https://github.com/anomalyco/opencode/pull/53196), [#55627](https://github.com/anomalyco/opencode/pull/55627))
|
||||
- Treat empty provider and model names as omitted, and substitute an empty value for missing configuration-file references. ([#55465](https://github.com/anomalyco/opencode/pull/55465), [#55388](https://github.com/anomalyco/opencode/pull/55388))
|
||||
- Default the Windows background service to port `45278`. ([#55547](https://github.com/anomalyco/opencode/pull/55547))
|
||||
|
||||
### TUI
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Open `/diff` beside the current session by default, retain state when toggling full screen with `f`, and adapt the file tree and patch layout to terminal width. ([#47152](https://github.com/anomalyco/opencode/pull/47152), [#55574](https://github.com/anomalyco/opencode/pull/55574), [#55780](https://github.com/anomalyco/opencode/pull/55780))
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Show `/fork`’s pending view reliably and keep it aligned with the file list. ([#55746](https://github.com/anomalyco/opencode/pull/55746), [#55751](https://github.com/anomalyco/opencode/pull/55751))
|
||||
- Correct spacing and inactive key-hint styling in the docked diff panel. ([#55452](https://github.com/anomalyco/opencode/pull/55452), [#55467](https://github.com/anomalyco/opencode/pull/55467))
|
||||
|
||||
### Desktop
|
||||
|
||||
#### Improvements
|
||||
|
||||
- Show each completed turn’s changed files, line counts, and expandable diffs when **Settings → Preferences → Timeline → Changed files** is enabled. ([#55064](https://github.com/anomalyco/opencode/pull/55064))
|
||||
- Navigate session tabs with **Option+↑/↓**, or add **Shift** to jump to the previous or next unread tab. ([#54277](https://github.com/anomalyco/opencode/pull/54277))
|
||||
- Display delegations from the iOS app as **Voice** messages. ([#54426](https://github.com/anomalyco/opencode/pull/54426))
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Open PowerShell in the selected project directory on Windows. ([#55710](https://github.com/anomalyco/opencode/pull/55710))
|
||||
- Keep project settings accessible when the project folder is missing. ([#55553](https://github.com/anomalyco/opencode/pull/55553))
|
||||
- Show the requested URL while `webfetch` is running. ([#55590](https://github.com/anomalyco/opencode/pull/55590))
|
||||
|
||||
### Extensions
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
- Include MCP tool input as `metadata.input` in permission requests, allowing permission hooks to inspect the proposed call. Thanks [@lu-zero](https://github.com/lu-zero). ([#51062](https://github.com/anomalyco/opencode/pull/51062))
|
||||
|
||||
## v2.0.29 — 2026-10-14
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode.cjs",
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
@@ -180,7 +180,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -205,7 +205,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -218,7 +218,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -246,7 +246,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/console-resource": "workspace:*",
|
||||
},
|
||||
@@ -273,7 +273,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
@@ -339,7 +339,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "5.0.0",
|
||||
@@ -383,9 +383,46 @@
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
},
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
"@opencode/session-ui": "workspace:*",
|
||||
"@opencode/ui": "workspace:*",
|
||||
"@opencode/util": "workspace:*",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"effect": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"hono-openapi": "catalog:",
|
||||
"js-base64": "3.7.7",
|
||||
"luxon": "catalog:",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -401,7 +438,7 @@
|
||||
},
|
||||
"packages/gui-extensions": {
|
||||
"name": "@opencode/gui-extensions",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@betteroffice/docx": "0.4.3",
|
||||
"@betteroffice/fonts": "0.4.0",
|
||||
@@ -450,7 +487,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
},
|
||||
@@ -469,7 +506,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -482,7 +519,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -496,7 +533,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -511,7 +548,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -548,7 +585,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -563,7 +600,7 @@
|
||||
},
|
||||
"packages/plugins/figma": {
|
||||
"name": "@opencode/plugin-figma",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"gray-matter": "4.0.3",
|
||||
},
|
||||
@@ -593,7 +630,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -608,7 +645,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -632,7 +669,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -653,7 +690,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -676,7 +713,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -711,7 +748,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -731,7 +768,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -758,7 +795,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@planetscale/database": "1.19.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -776,7 +813,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode/stats-core": "workspace:*",
|
||||
@@ -822,7 +859,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -836,7 +873,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -870,7 +907,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@kitlangton/solid-motion": "0.2.2",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -906,7 +943,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -944,7 +981,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -998,7 +1035,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
@@ -1040,6 +1077,7 @@
|
||||
"ghostty-web@github:anomalyco/ghostty-web#83c0a07": "patches/ghostty-web@0.3.0.patch",
|
||||
"@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch",
|
||||
"pacote@21.5.1": "patches/pacote@21.5.1.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"solid-js@1.9.15": "patches/solid-js@1.9.15.patch",
|
||||
@@ -1787,6 +1825,8 @@
|
||||
|
||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
||||
|
||||
"@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="],
|
||||
|
||||
"@ibm/plex": ["@ibm/plex@6.4.1", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="],
|
||||
|
||||
"@ibm/telemetry-js": ["@ibm/telemetry-js@1.11.0", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="],
|
||||
@@ -2011,6 +2051,8 @@
|
||||
|
||||
"@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-beta-18050", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.111" } }, "sha512-/D6VXaWlytTXR3IOiMLIKuPcfp7FQNUzRPm9z3K7UBFd1Bw4q/WZksaf5RVcBGz+0YRxYMc1V4D7MFlceSgtyg=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.21", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-k6iHQ5C8wOPglk+LgFyYnst168cGMQYumgpbVoeXJ+iC1AtvwD5zmjuF8CxMze/y9G1K2bOeO6p9yRvA7eHZLA=="],
|
||||
|
||||
"@opencode/ai": ["@opencode/ai@workspace:packages/ai"],
|
||||
|
||||
"@opencode/app": ["@opencode/app@workspace:packages/app"],
|
||||
@@ -2033,6 +2075,8 @@
|
||||
|
||||
"@opencode/desktop": ["@opencode/desktop@workspace:packages/desktop"],
|
||||
|
||||
"@opencode/enterprise": ["@opencode/enterprise@workspace:packages/enterprise"],
|
||||
|
||||
"@opencode/files": ["@opencode/files@workspace:services/files"],
|
||||
|
||||
"@opencode/function": ["@opencode/function@workspace:packages/function"],
|
||||
@@ -2739,6 +2783,10 @@
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.24", "", {}, "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw=="],
|
||||
|
||||
"@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="],
|
||||
|
||||
"@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@storybook/addon-a11y": ["@storybook/addon-a11y@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.4.4" } }, "sha512-/eUCx/6Ozq5grauwm/NqKtlW0oJ26b6GNesXrMuFID8WLg/qLEKf79Awfz9XrmyWxe7loD40K952r7AA5Oc23A=="],
|
||||
@@ -2943,6 +2991,8 @@
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||
|
||||
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
|
||||
@@ -3943,6 +3993,8 @@
|
||||
|
||||
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
|
||||
|
||||
"hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="],
|
||||
|
||||
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
|
||||
|
||||
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
|
||||
@@ -4529,6 +4581,8 @@
|
||||
|
||||
"openai": ["openai@6.49.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
||||
|
||||
"opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
|
||||
@@ -4689,6 +4743,8 @@
|
||||
|
||||
"qr-scanner": ["qr-scanner@1.4.2", "", { "dependencies": { "@types/offscreencanvas": "^2019.6.4" } }, "sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
|
||||
@@ -5715,6 +5771,12 @@
|
||||
|
||||
"@opencode/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||
|
||||
"@opencode/enterprise/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
|
||||
"@opencode/enterprise/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"@opencode/gui-extensions/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"@opencode/session-ui/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
@@ -6435,6 +6497,10 @@
|
||||
|
||||
"@opencode/console-app/wrangler/workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
|
||||
|
||||
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
|
||||
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
|
||||
@@ -7193,6 +7259,32 @@
|
||||
|
||||
"@opencode/console-app/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251118.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2LV99AHSlpr8WcCb/BYbU2QsYkXLUL1izN6YKWkN9Eibv80JKX0RtgmD3dfmajE5sNvClavxZejgzVvHD9N9Ag=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
|
||||
|
||||
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
|
||||
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
|
||||
@@ -7615,6 +7707,38 @@
|
||||
|
||||
"@opencode/console-app/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
|
||||
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||
@@ -7693,6 +7817,8 @@
|
||||
|
||||
"@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@opencode/storybook/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { SECRET } from "./secret"
|
||||
import { shortDomain } from "./stage"
|
||||
|
||||
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||
|
||||
new sst.cloudflare.x.SolidStart("Teams", {
|
||||
domain: shortDomain,
|
||||
path: "packages/enterprise",
|
||||
buildCommand: "bun run build:cloudflare",
|
||||
link: [SECRET.SupportApiKey],
|
||||
environment: {
|
||||
OPENCODE_STORAGE_ADAPTER: "r2",
|
||||
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
|
||||
OPENCODE_STORAGE_ACCESS_KEY_ID: SECRET.R2AccessKey.value,
|
||||
OPENCODE_STORAGE_SECRET_ACCESS_KEY: SECRET.R2SecretKey.value,
|
||||
OPENCODE_STORAGE_BUCKET: storage.name,
|
||||
},
|
||||
})
|
||||
@@ -5,6 +5,9 @@ sst.Linkable.wrap(random.RandomPassword, (resource) => ({
|
||||
}))
|
||||
|
||||
export const SECRET = {
|
||||
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||
HoneycombApiKey: new sst.Secret("HONEYCOMB_API_KEY"),
|
||||
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
|
||||
SupportApiKey: new sst.Secret("SUPPORT_API_KEY"),
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { shortDomain } from "./stage"
|
||||
|
||||
// Retains existing share data until it is deleted separately.
|
||||
new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||
|
||||
// The Share worker takes over the hostname of the retired Teams worker. Cloudflare rejects a second custom domain for
|
||||
// the same hostname, so adopt the Teams domain and replace it, deleting the old one first.
|
||||
$transform(cloudflare.WorkersCustomDomain, (_args, opts, name) => {
|
||||
if (name !== "ShareDomain") return
|
||||
opts.aliases = [
|
||||
`urn:pulumi:${$app.stage}::${$app.name}::sst:cloudflare:SolidStart$sst:cloudflare:Worker$cloudflare:index/workersCustomDomain:WorkersCustomDomain::TeamsWorkerDomain`,
|
||||
]
|
||||
opts.deleteBeforeReplace = true
|
||||
opts.replaceOnChanges = ["*"]
|
||||
})
|
||||
|
||||
new sst.cloudflare.Worker("Share", {
|
||||
domain: shortDomain,
|
||||
handler: "packages/function/src/share.ts",
|
||||
})
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-foIqeQoc/G6w/9bd7OqC6mIxyQlCgbkPQWl3aHtb1l8=",
|
||||
"aarch64-linux": "sha256-wiiiSjLyLhn0OFnh1Ug2CdAfwtpkncaUPLUI0TIucdA=",
|
||||
"aarch64-darwin": "sha256-zQ26uuI0p+2tf7KYzcKLnPF+ERTumGz4uqezO9pObNg="
|
||||
"x86_64-linux": "sha256-wmSDJ8NZOJyfSaBD8hnAAtbYrp3qnMS4eJzgFdyrhFc=",
|
||||
"aarch64-linux": "sha256-6WE4BHW11/gAQ75ktPbWU2Aw7uqivueMrElDAqu7QdE=",
|
||||
"aarch64-darwin": "sha256-6aAic4o1Zx6QiMk8IFU6lV7typClc3yXoWH7Qiq+hFs="
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -516,7 +516,7 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request:
|
||||
)
|
||||
// Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in
|
||||
// tools → system → messages order to favour the highest-impact prefixes.
|
||||
const breakpoints = BedrockCache.breakpoints(request.model)
|
||||
const breakpoints = BedrockCache.breakpoints(request.model.id)
|
||||
const toolConfig = (() => {
|
||||
if (flattened.tools.length === 0) return undefined
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import type { CacheHint, LLMRequest } from "../../schema/index.js"
|
||||
import type { CacheHint } from "../../schema/index.js"
|
||||
import { newBreakpoints, ttlBucket } from "./cache.js"
|
||||
|
||||
// Bedrock cache markers are positional: emit a `cachePoint` block immediately
|
||||
@@ -28,17 +28,14 @@ const CLAUDE_5M = [
|
||||
// Callers share the four-breakpoint budget across system, messages, and tools.
|
||||
export const BEDROCK_BREAKPOINT_CAP = 4
|
||||
|
||||
export const breakpoints = (model: LLMRequest["model"]) => {
|
||||
export const breakpoints = (modelID: string) => {
|
||||
// Substring matching also handles regional prefixes and model-bearing ARNs.
|
||||
const short = CLAUDE_5M.some((id) => model.id.includes(id))
|
||||
const short = CLAUDE_5M.some((id) => modelID.includes(id))
|
||||
return {
|
||||
...newBreakpoints(BEDROCK_BREAKPOINT_CAP),
|
||||
// An explicit Anthropic cache format opts opaque IDs such as application inference profile ARNs in.
|
||||
// Assume modern Claude releases retain caching support; older generations need an explicit exception.
|
||||
// Other model families use implicit caching where available.
|
||||
supported:
|
||||
model.compatibility?.cacheFormat === "anthropic" ||
|
||||
(model.id.includes("anthropic.claude-") && (short || !LEGACY_CLAUDE.some((id) => model.id.includes(id)))),
|
||||
supported: modelID.includes("anthropic.claude-") && (short || !LEGACY_CLAUDE.some((id) => modelID.includes(id))),
|
||||
ttl1h: !short,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { SystemOne } from "../experimental/system-one.js"
|
||||
|
||||
export const id = ProviderID.make("opencode")
|
||||
const baseURL = "https://opencode.ai/inference/systemone/v1"
|
||||
const baseURL = "https://opencode.ai/inference/v1"
|
||||
|
||||
export type Options = ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://opencode.ai/inference/systemone/v1/systemone",
|
||||
"url": "https://opencode.ai/inference/v1/systemone",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { CacheHint, LanguageModel, LLM, Message, ToolCallPart } from "../../src/index.js"
|
||||
import { CacheHint, LLM, Message, ToolCallPart } from "../../src/index.js"
|
||||
import { AmazonBedrock } from "../../src/providers.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
@@ -116,53 +116,4 @@ describe("Bedrock Converse cache policy", () => {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const arn = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123"
|
||||
|
||||
for (const ttlSeconds of [undefined, 3600]) {
|
||||
it.effect(
|
||||
`emits checkpoints for an opaque ARN declaring the Anthropic cache format (TTL: ${ttlSeconds ?? "default"})`,
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: LanguageModel.update(bedrock.model(arn), { compatibility: { cacheFormat: "anthropic" } }),
|
||||
system: [
|
||||
{ type: "text", text: "Agent" },
|
||||
{ type: "text", text: "Project" },
|
||||
],
|
||||
tools: [{ name: "lookup", description: "Lookup", inputSchema: { type: "object" } }],
|
||||
prompt: "Question",
|
||||
cache:
|
||||
ttlSeconds === undefined ? undefined : { tools: true, system: true, messages: { tail: 1 }, ttlSeconds },
|
||||
}),
|
||||
)
|
||||
const marker = { cachePoint: ttlSeconds === undefined ? { type: "default" } : { type: "default", ttl: "1h" } }
|
||||
|
||||
expect(prepared.body).toMatchObject({
|
||||
modelId: arn,
|
||||
toolConfig: { tools: [{ toolSpec: { name: "lookup" } }, marker] },
|
||||
system: [{ text: "Agent" }, marker, { text: "Project" }, marker],
|
||||
messages: [{ role: "user", content: [{ text: "Question" }, marker] }],
|
||||
})
|
||||
if (ttlSeconds === undefined) expect(JSON.stringify(prepared.body)).not.toContain('"ttl"')
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
for (const id of [arn, "amazon.nova-lite-v1:0"]) {
|
||||
it.effect(`ignores the Alibaba cache format for ${id}`, () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: LanguageModel.update(bedrock.model(id), { compatibility: { cacheFormat: "alibaba" } }),
|
||||
system: "System prefix",
|
||||
prompt: "Question",
|
||||
}),
|
||||
)
|
||||
|
||||
expect(JSON.stringify(prepared.body)).not.toContain("cachePoint")
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -71,16 +71,17 @@ Environment options:
|
||||
|
||||
## Deployment
|
||||
|
||||
The `deploy` GitHub Actions workflow uses SST to deploy the web app to `beta.opencode.ai` on every push to the
|
||||
`beta` branch in `anomalyco/opencode`. The `dev` and `production` stages do not deploy the web app.
|
||||
The `deploy` GitHub Actions workflow uses SST to deploy the web app from these branches in `anomalyco/opencode`:
|
||||
|
||||
`beta` is the team's dogfooding branch: `dev` merged in by hand, plus commits pushed directly to it. Do not rebase or
|
||||
force-push it. Each push also runs the `publish` workflow, which ships the same commit as the OpenCode Beta desktop app
|
||||
(see `packages/desktop/README.md`).
|
||||
| Branch | Site |
|
||||
| ------------ | --------------------- |
|
||||
| `dev` | `app.dev.opencode.ai` |
|
||||
| `production` | `app.opencode.ai` |
|
||||
| `beta` | `beta.opencode.ai` |
|
||||
|
||||
The beta SST stage deploys only the web app, using the `WebApp` StaticSite definition in `infra/webapp.ts`. It sets
|
||||
the build channel and Sentry environment to `beta` without deploying the API, console, database, or billing
|
||||
infrastructure.
|
||||
Changes merged into `v2` reach the beta site when they are promoted to `beta`. The beta SST stage deploys
|
||||
only the web app, using the same `WebApp` StaticSite definition as production. It sets the build channel
|
||||
and Sentry environment to `beta` without deploying the API, console, database, or billing infrastructure.
|
||||
|
||||
`VITE_OPENCODE_SERVER_MODE` controls which server the web build provides at startup:
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ test("terminal stacks under review by default and spans the bottom when configur
|
||||
),
|
||||
]
|
||||
|
||||
const { session } = await openSession(page, { name: "SideStack", pty: {}, vcsDiff: diffs })
|
||||
await openSession(page, { name: "SideStack", pty: {}, vcsDiff: diffs })
|
||||
const toggle = page.getByRole("button", { name: "Toggle review" })
|
||||
const reviewPanel = page.locator("#review-panel")
|
||||
const terminalPanel = page.locator("#terminal-panel")
|
||||
@@ -163,30 +163,13 @@ test("terminal stacks under review by default and spans the bottom when configur
|
||||
expect(Math.abs(closing.terminalBottom - stacked.terminalBottom)).toBeLessThanOrEqual(1)
|
||||
expect(closing.anchor).toBeLessThanOrEqual(8)
|
||||
expect(closing.terminalGap).toBeLessThanOrEqual(1)
|
||||
expect(closing.reviewOverlap).toBeLessThanOrEqual(1)
|
||||
|
||||
await toggle.click()
|
||||
await expect(reviewPanel).toBeVisible()
|
||||
await expectMarked(review)
|
||||
await expectTreeSentinel(page)
|
||||
|
||||
// Panels restored by a reload skip their entrance animation, and must still move together when the terminal hides.
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
(key) => JSON.parse(localStorage.getItem("opencode.window.browser.dat:tabs.panes") ?? "{}")[key],
|
||||
tabKey(session.id),
|
||||
),
|
||||
)
|
||||
.toMatchObject({ review: true, terminal: true })
|
||||
await page.reload()
|
||||
await expect(reviewPanel).toBeVisible()
|
||||
await expect(terminal).toBeVisible()
|
||||
await expectTreeSentinel(page)
|
||||
await mark(terminal)
|
||||
|
||||
// Hiding the terminal mid-motion: both surfaces fill their regions, the review never runs under the gap, and the
|
||||
// clipped terminal keeps its size.
|
||||
// Hiding the terminal mid-motion: both surfaces fill their regions and the clipped terminal keeps its size.
|
||||
const open = await stackGeometry(page)
|
||||
await holdTransitions(page)
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
@@ -197,7 +180,6 @@ test("terminal stacks under review by default and spans the bottom when configur
|
||||
expect(hiding.terminalRegion).toBeLessThan(open.terminalRegion - 1)
|
||||
expect(hiding.terminalRegion).toBeGreaterThan(1)
|
||||
expect(hiding.reviewGap).toBeLessThanOrEqual(1)
|
||||
expect(hiding.reviewOverlap).toBeLessThanOrEqual(1)
|
||||
expect(hiding.terminalGap).toBeLessThanOrEqual(1)
|
||||
expect(Math.abs(hiding.content.width - open.content.width)).toBeLessThanOrEqual(1)
|
||||
expect(Math.abs(hiding.content.height - open.content.height)).toBeLessThanOrEqual(1)
|
||||
@@ -311,23 +293,6 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
return actions.y + actions.height / 2 - (button.y + button.height / 2)
|
||||
})
|
||||
.toBe(0)
|
||||
|
||||
// Toggling the terminal under the review leaves the review header and the toggle in place.
|
||||
const header = async () => {
|
||||
const box = await page.locator('[data-slot="session-side-panel-actions"]').boundingBox()
|
||||
|
||||
return box && { y: box.y, height: box.height }
|
||||
}
|
||||
|
||||
const stacked = await header()
|
||||
|
||||
for (const visible of [false, true]) {
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(terminal).toBeVisible({ visible })
|
||||
await expect.poll(header).toEqual(stacked)
|
||||
await expect.poll(() => toggle.boundingBox()).toEqual(position)
|
||||
}
|
||||
|
||||
await toggle.press("Enter")
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(toggle).toBeFocused()
|
||||
@@ -356,7 +321,6 @@ async function expectTreeSentinel(page: Page) {
|
||||
// Pauses each CSS transition as it starts, so a real toggle can be measured mid-motion.
|
||||
function holdTransitions(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
// SAFETY: only these two helpers write this optional global, in the declared shape.
|
||||
const host = window as Window & { e2eHold?: { active: boolean } }
|
||||
|
||||
if (!host.e2eHold) {
|
||||
@@ -377,9 +341,7 @@ function holdTransitions(page: Page) {
|
||||
|
||||
function releaseTransitions(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
// SAFETY: `holdTransitions` installed this global, in this shape, before any release.
|
||||
const host = window as Window & { e2eHold?: { active: boolean } }
|
||||
host.e2eHold!.active = false
|
||||
;(window as Window & { e2eHold?: { active: boolean } }).e2eHold!.active = false
|
||||
document
|
||||
.getAnimations()
|
||||
.filter((item) => item instanceof CSSTransition && item.playState === "paused")
|
||||
@@ -397,7 +359,7 @@ async function stackGeometry(page: Page, progress?: number) {
|
||||
(item) =>
|
||||
item instanceof CSSTransition && item.playState === "paused" && item.transitionProperty === "height",
|
||||
)
|
||||
.map((item) => (item.effect instanceof KeyframeEffect ? item.effect.target?.getAttribute("data-slot") : null)),
|
||||
.map((item) => ((item.effect as KeyframeEffect).target as Element).getAttribute("data-slot")),
|
||||
)
|
||||
|
||||
if (progress !== undefined) {
|
||||
@@ -409,10 +371,9 @@ async function stackGeometry(page: Page, progress?: number) {
|
||||
.getAnimations()
|
||||
.filter((item): item is CSSTransition => item instanceof CSSTransition && item.playState === "paused")
|
||||
|
||||
const slot = (item: CSSTransition) =>
|
||||
item.effect instanceof KeyframeEffect ? item.effect.target?.getAttribute("data-slot") : null
|
||||
|
||||
const region = transitions.find((item) => slot(item) === "session-side-region")
|
||||
const region = transitions.find(
|
||||
(item) => ((item.effect as KeyframeEffect).target as Element).getAttribute("data-slot") === "session-side-region",
|
||||
)
|
||||
|
||||
if (progress !== undefined) {
|
||||
const time = Number(region!.effect!.getTiming().duration) * progress
|
||||
@@ -421,20 +382,17 @@ async function stackGeometry(page: Page, progress?: number) {
|
||||
|
||||
const box = (selector: string) => document.querySelector(selector)?.getBoundingClientRect()
|
||||
const reviewRegion = box('[data-slot="session-side-region"]')!
|
||||
const gap = box('[data-slot="session-side-panel-gap"]')!
|
||||
const terminalRegion = box('[data-slot="session-side-terminal-region"]')!
|
||||
const review = box("#review-panel")
|
||||
const terminal = box("#terminal-panel")!
|
||||
const content = box('[data-slot="terminal-panel-content"]')!
|
||||
|
||||
return {
|
||||
moving: transitions.map(slot),
|
||||
moving: transitions.map((item) => ((item.effect as KeyframeEffect).target as Element).getAttribute("data-slot")),
|
||||
terminalRegion: terminalRegion.height,
|
||||
terminalBottom: terminal.bottom,
|
||||
anchor: Math.abs(terminal.top - content.top),
|
||||
reviewGap: review ? Math.abs(reviewRegion.height - review.height) : 0,
|
||||
// The gap paints over the review region, so any overlap hides the review's bottom corners.
|
||||
reviewOverlap: reviewRegion.bottom - gap.top,
|
||||
terminalGap: Math.abs(terminalRegion.height - terminal.height),
|
||||
content: { width: content.width, height: content.height },
|
||||
}
|
||||
@@ -455,8 +413,8 @@ async function expectTerminalControlsAligned(terminal: Locator, toggle: Locator)
|
||||
.poll(async () => {
|
||||
const centers = await Promise.all(
|
||||
[terminal.getByRole("button", { name: "New terminal", exact: true }), toggle].map((button) =>
|
||||
button.locator("svg").evaluate((svg) => {
|
||||
if (!(svg instanceof SVGGraphicsElement)) return Number.NaN
|
||||
button.locator("svg").evaluate((element) => {
|
||||
const svg = element as SVGSVGElement
|
||||
const path = svg.getBBox()
|
||||
|
||||
return new DOMPoint(path.x + path.width / 2, path.y + path.height / 2).matrixTransform(svg.getScreenCTM()!)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ComposerDropzone(props: {
|
||||
<div
|
||||
data-slot="session-dropzone-blur"
|
||||
data-visible={props.active}
|
||||
class="pointer-events-none absolute inset-0 z-[79] rounded-[inherit] opacity-0 transition-[opacity,backdrop-filter] duration-200 ease-[cubic-bezier(0.215,0.61,0.355,1)] data-[visible=true]:opacity-100 data-[visible=true]:backdrop-blur-[1.5px] data-[visible=true]:will-change-[opacity,backdrop-filter] motion-reduce:transition-none"
|
||||
class="pointer-events-none absolute inset-0 z-[79] rounded-[inherit] opacity-[0.001] backdrop-blur-[1.5px] transition-opacity duration-200 ease-[cubic-bezier(0.215,0.61,0.355,1)] will-change-[opacity,backdrop-filter] data-[visible=true]:opacity-100 motion-reduce:transition-none"
|
||||
style={{
|
||||
"-webkit-mask-image":
|
||||
"linear-gradient(to right, transparent 0%, black 25%, black 75%, transparent 100%), linear-gradient(to bottom, transparent 0%, black 28%, black 72%, transparent 100%)",
|
||||
|
||||
@@ -107,11 +107,6 @@
|
||||
animation: side-region-presence-out 240ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
/* A collapsing frame's elevation outlives its height as a flat line over the panel beside it, so it fades out. */
|
||||
[data-slot="session-side-region-presence"][data-opened="false"] #review-panel {
|
||||
animation: side-panel-frame-out 240ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
[data-slot="terminal-panel-presence"][data-opened="true"] {
|
||||
animation: terminal-panel-presence-in 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
@@ -142,7 +137,6 @@
|
||||
[data-slot="session-side-panel-presence"],
|
||||
[data-slot="session-review-content"],
|
||||
[data-slot="session-side-region-presence"],
|
||||
[data-slot="session-side-region-presence"] #review-panel,
|
||||
[data-component="terminal-panel"] {
|
||||
animation: none !important;
|
||||
}
|
||||
@@ -179,7 +173,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Like `side-panel-frame-out`, the collapsing terminal frame drops its elevation. */
|
||||
@keyframes side-terminal-panel-presence-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
@@ -187,13 +180,6 @@
|
||||
to {
|
||||
opacity: 0.999999;
|
||||
visibility: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes side-panel-frame-out {
|
||||
to {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,12 @@ export function SideRegion(props: {
|
||||
props.region.select(value)
|
||||
}}
|
||||
>
|
||||
<div class="session-review-v2-tabs-bar sticky top-0 shrink-0 flex items-center">
|
||||
{/* Tabs and actions share the review toggle's row: the session header's 48px, or the 51px
|
||||
above a side dock's divider. */}
|
||||
<div
|
||||
class="session-review-v2-tabs-bar sticky top-0 shrink-0 flex items-center"
|
||||
style={{ "--tabs-bar-height": props.stacked ? "51px" : "48px" }}
|
||||
>
|
||||
<Tabs.List
|
||||
ref={(el: HTMLDivElement) => {
|
||||
tabList = el
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMemo, type Accessor } from "solid-js"
|
||||
import { createEffect, createMemo, type Accessor } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLayout } from "@/shell/state/layout"
|
||||
@@ -74,6 +74,14 @@ export function createSessionScreenLayout(
|
||||
stacked: tabsOpen() && dockSideOpen(),
|
||||
}))
|
||||
|
||||
const [motion, setMotion] = createStore({ gap: panelLayout().stacked, closing: false })
|
||||
createEffect((previous) => {
|
||||
const stacked = panelLayout().stacked
|
||||
|
||||
if (previous !== stacked) setMotion({ gap: stacked, closing: !stacked })
|
||||
|
||||
return stacked
|
||||
}, panelLayout().stacked)
|
||||
const sideRegionOpen = createMemo(() => tabsOpen() || fileTreeOpen())
|
||||
|
||||
const dockRegion = createMemo(() =>
|
||||
@@ -116,7 +124,10 @@ export function createSessionScreenLayout(
|
||||
},
|
||||
side: {
|
||||
contentWidth: sideContentWidth,
|
||||
gap: { height: createMemo(() => (panelLayout().stacked ? "8px" : "0px")) },
|
||||
gap: {
|
||||
closing: () => motion.closing,
|
||||
height: createMemo(() => (motion.gap ? "8px" : "0px")),
|
||||
},
|
||||
layout: panelLayout,
|
||||
region: {
|
||||
height: createMemo(() => {
|
||||
|
||||
@@ -172,9 +172,6 @@ function SessionScreenContent(props: {
|
||||
if (bottomDockVisible()) setStore("bottomDockCached", true)
|
||||
|
||||
if (!sideVisible()) setStore("sideHeightMotion", false)
|
||||
|
||||
// A side panel restored without its entrance animation never fires the animationend that enables height motion.
|
||||
if (sideVisible() && !sidePresence.animate()) setStore("sideHeightMotion", true)
|
||||
})
|
||||
createEffect(() => {
|
||||
if (!isDesktop() || screen.dock.bottom()) setStore("sideDockPresent", false)
|
||||
@@ -371,10 +368,11 @@ function SessionScreenContent(props: {
|
||||
<>
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 px-[var(--shell-inline-inset,8px)] pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]">
|
||||
<div ref={screen.panel.ref} class="relative flex-1 min-h-0 flex flex-col md:flex-row gap-2">
|
||||
{/* Keep the control outside panel animations. */}
|
||||
{/* Keep the control outside panel animations; a side dock's 52px header includes a 1px divider. */}
|
||||
<Show when={isDesktop() && messagesReady() && session.identity.params.id}>
|
||||
<div
|
||||
class="absolute end-3 top-0 z-30 h-12 flex items-center"
|
||||
class="absolute end-3 top-0 z-30 flex items-center"
|
||||
classList={{ "h-[51px]": sideDockVisible(), "h-12": !sideDockVisible() }}
|
||||
data-slot="session-review-toggle"
|
||||
onPointerDown={hideTimelineScrollbar}
|
||||
onClick={hideTimelineScrollbar}
|
||||
@@ -489,13 +487,13 @@ function SessionScreenContent(props: {
|
||||
</Show>
|
||||
</div>
|
||||
<div class="absolute start-0 bottom-0 flex flex-col" style={{ width: screen.side.contentWidth() }}>
|
||||
{/* The gap moves with both regions, so together they always fill the column and the review's
|
||||
bottom corners are never covered. */}
|
||||
<div
|
||||
data-slot="session-side-panel-gap"
|
||||
classList={{
|
||||
"relative z-0 shrink-0 overflow-visible bg-v2-background-bg-deep transition-[height] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none": true,
|
||||
"transition-none": screen.size.active() || !store.sideHeightMotion || !regionAnimating(),
|
||||
"relative z-0 shrink-0 overflow-visible bg-v2-background-bg-deep transition-[height] duration-[40ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none": true,
|
||||
"delay-0": !screen.side.gap.closing(),
|
||||
"delay-[200ms]": screen.side.gap.closing(),
|
||||
"transition-none": !regionAnimating(),
|
||||
}}
|
||||
style={{ height: screen.side.gap.height() }}
|
||||
onPointerDown={() => screen.size.start()}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -76,11 +76,9 @@ const authenticate = Effect.fn("cli.mcp.auth.run")(function* (name?: string) {
|
||||
|
||||
const exampleConfig = `
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"my-server": {
|
||||
"type": "remote",
|
||||
"url": "https://example.com/mcp"
|
||||
}
|
||||
"my-server": {
|
||||
"type": "remote",
|
||||
"url": "https://example.com/mcp"
|
||||
}
|
||||
}`
|
||||
|
||||
|
||||
@@ -71,8 +71,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
? Service.incumbent({ ...serviceOptions, url: serviceURL(hostname, port) })
|
||||
: undefined
|
||||
if (findIncumbent !== undefined && (yield* findIncumbent) !== undefined) return
|
||||
// Desktop can launch the service directly without injecting its saved environment.
|
||||
Object.assign(process.env, config.env)
|
||||
// Keep a package-manager or curl install replaceable while the service runs; Desktop updates its own copy.
|
||||
if (options.mode === "service" && process.platform === "win32" && RetainedImage.installed(global.home))
|
||||
yield* RetainedImage.retain(global.cache, "service")
|
||||
@@ -119,11 +117,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
project: !truthy(
|
||||
process.env.OPENCODE_CONFIG_PROJECT_DISABLE ?? process.env.OPENCODE_DISABLE_PROJECT_CONFIG,
|
||||
),
|
||||
// .claude is only read for skills, so both flags gate the same roots.
|
||||
claude: !(
|
||||
truthy(process.env.OPENCODE_DISABLE_CLAUDE_CODE) ||
|
||||
truthy(process.env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS)
|
||||
),
|
||||
file: process.env.OPENCODE_CONFIG,
|
||||
content: process.env.OPENCODE_CONFIG_CONTENT,
|
||||
},
|
||||
|
||||
@@ -192,62 +192,6 @@ test("service config manages environment variables", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("managed service loads configured environment", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-env-"))
|
||||
const registration = path.join(root, "state", "opencode", ServiceConfig.filename())
|
||||
await fs.mkdir(path.join(root, "config"), { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(root, "config", ServiceConfig.filename()),
|
||||
JSON.stringify({ env: { OPENCODE_SERVICE_ENV_TEST: "configured", OPENCODE_SERVICE_ENV_EMPTY_TEST: "" } }),
|
||||
)
|
||||
await fs.writeFile(
|
||||
path.join(root, "config", "opencode.json"),
|
||||
JSON.stringify({
|
||||
mcp: {
|
||||
servers: {
|
||||
env: {
|
||||
type: "local",
|
||||
command: ["true"],
|
||||
environment: {
|
||||
configured: "{env:OPENCODE_SERVICE_ENV_TEST}",
|
||||
empty: "{env:OPENCODE_SERVICE_ENV_EMPTY_TEST}",
|
||||
inherited: "{env:OPENCODE_SERVICE_ENV_INHERITED_TEST}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
const owner = Bun.spawn(
|
||||
[process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service", "--port", "0"],
|
||||
{
|
||||
env: isolatedEnv(root, {
|
||||
OPENCODE_SERVICE_ENV_TEST: "inherited",
|
||||
OPENCODE_SERVICE_ENV_EMPTY_TEST: "inherited",
|
||||
OPENCODE_SERVICE_ENV_INHERITED_TEST: "inherited",
|
||||
}),
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
},
|
||||
)
|
||||
try {
|
||||
const info = await waitForInfo(registration)
|
||||
const url = new URL("/api/config", info.url)
|
||||
url.searchParams.set("location[directory]", root)
|
||||
const response = await fetch(url, { headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) } })
|
||||
expect(response.status).toBe(200)
|
||||
expect((await response.json())[0].info.mcp.servers.env.environment).toEqual({
|
||||
configured: "configured",
|
||||
empty: "",
|
||||
inherited: "inherited",
|
||||
})
|
||||
} finally {
|
||||
owner.kill("SIGTERM")
|
||||
await owner.exited
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("service filenames share release channels and identify preview channels", () => {
|
||||
expect(ServiceConfig.filename("latest")).toBe("service.json")
|
||||
expect(ServiceConfig.filename("dev")).toBe("service.json")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1760,6 +1760,7 @@ export type TuiCommandExecute = {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.background"
|
||||
| "session.compact"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,6 +3,7 @@ export const goModels = [
|
||||
{ id: "kimi-k3", name: "Kimi K3", requests: 110, allowance: 15, featured: true },
|
||||
{ id: "qwen3.8-max", name: "Qwen3.8 Max", requests: 160, allowance: 15 },
|
||||
{ id: "grok-4.7", name: "Grok 4.7", requests: 169, allowance: 15 },
|
||||
{ id: "grok-4.6", name: "Grok 4.6", requests: 169, allowance: 15 },
|
||||
{ id: "glm-5.3", name: "GLM-5.3", requests: 220, allowance: 15 },
|
||||
{
|
||||
id: "gemini-4-argon-contributor",
|
||||
@@ -23,6 +24,7 @@ export const goModels = [
|
||||
{ id: "minimax-m3", name: "MiniMax M3", requests: 3200, allowance: 60, featured: true },
|
||||
{ id: "mimo-v2.6-pro", name: "MiMo-V2.6-Pro", requests: 3250, allowance: 15 },
|
||||
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", requests: 3250, allowance: 15 },
|
||||
{ id: "minimax-m2.7", name: "MiniMax M2.7", requests: 3400, allowance: 60 },
|
||||
{ id: "claude-haiku-5-5", name: "Claude Haiku 5.5", requests: 3850, allowance: 15, featured: true, fresh: true },
|
||||
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", requests: 4300, allowance: 60, featured: true },
|
||||
{ id: "gpt-6-luna", name: "GPT 6 Luna", requests: 4230, allowance: 15, featured: true },
|
||||
@@ -69,11 +71,10 @@ export const goModels = [
|
||||
limitedTime: true,
|
||||
},
|
||||
{
|
||||
id: "step-5-preview",
|
||||
name: "Step 5 Preview",
|
||||
requests: 1720,
|
||||
allowance: 60,
|
||||
bonus: 4,
|
||||
id: "step-5-preview-free",
|
||||
name: "Step 5 Preview Free",
|
||||
requests: Infinity,
|
||||
allowance: Infinity,
|
||||
featured: true,
|
||||
fresh: true,
|
||||
limitedTime: true,
|
||||
@@ -94,18 +95,19 @@ const plusLimits: Record<string, [number, number]> = {
|
||||
"kimi-k3": [440, 60],
|
||||
"qwen3.8-max": [640, 60],
|
||||
"grok-4.7": [676, 60],
|
||||
"grok-4.6": [676, 60],
|
||||
"glm-5.3": [1760, 120],
|
||||
"gemini-4-argon-contributor": [1760, 240],
|
||||
"glm-5.2": [2640, 180],
|
||||
"deepseek-v4-pro": [4200, 60],
|
||||
"kimi-k2.7-code": [4050, 180],
|
||||
"hy4-preview": [5400, 120],
|
||||
"step-5-preview": [6880, 240],
|
||||
"gpt-5.6-luna": [8200, 60],
|
||||
"space-bunny": [12500, 120],
|
||||
"minimax-m3": [9600, 180],
|
||||
"mimo-v2.6-pro": [13000, 60],
|
||||
"mimo-v2.5-pro": [13000, 60],
|
||||
"minimax-m2.7": [13600, 240],
|
||||
"qwen3.7-plus": [12900, 180],
|
||||
"gpt-6-luna": [16920, 60],
|
||||
"claude-haiku-5-5": [15380, 60],
|
||||
@@ -119,6 +121,7 @@ const plusLimits: Record<string, [number, number]> = {
|
||||
"muse-spark-1.3-contributor": [90600, 120],
|
||||
"muse-spark-1.2-contributor": [90600, 120],
|
||||
"longcat-2.5-preview-free": [Infinity, Infinity],
|
||||
"step-5-preview-free": [Infinity, Infinity],
|
||||
"cat-jam-free": [Infinity, Infinity],
|
||||
}
|
||||
|
||||
|
||||
@@ -138,9 +138,11 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode مجاني 100% للاستخدام. كما يأتي مع مجموعة من النماذج المجانية. قد توجد تكاليف إضافية إذا ربطت مزوّدًا آخر.",
|
||||
"home.faq.q7": "ماذا عن البيانات والخصوصية؟",
|
||||
"home.faq.a7.p1": "لا يتم تخزين بياناتك ومعلوماتك إلا عندما تستخدم نماذجنا المجانية.",
|
||||
"home.faq.a7.p1": "لا يتم تخزين بياناتك ومعلوماتك إلا عندما تستخدم نماذجنا المجانية أو تنشئ روابط قابلة للمشاركة.",
|
||||
"home.faq.a7.p2.beforeModels": "اعرف المزيد عن",
|
||||
"home.faq.a7.p2.modelsLink": "نماذجنا",
|
||||
"home.faq.a7.p2.and": "و",
|
||||
"home.faq.a7.p2.shareLink": "صفحات المشاركة",
|
||||
"home.faq.q8": "هل OpenCode مفتوح المصدر؟",
|
||||
"home.faq.a8.p1": "نعم، OpenCode مفتوح المصدر بالكامل. الكود المصدري متاح علنًا على",
|
||||
"home.faq.a8.p2": "بموجب",
|
||||
@@ -216,7 +218,7 @@ export const dict = {
|
||||
"تتم استضافة جميع نماذج Zen في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||
|
||||
"go.promo.step5": "يتوفر Step 5 Preview الآن في Go باستخدام مضاعف 4 مرات لفترة محدودة.",
|
||||
"go.promo.gemini": "يتوفر Gemini 4 Argon الآن باستخدام مضاعف مرتين لفترة محدودة.",
|
||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.meta.description": "يبلغ سعر Go $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
|
||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||
@@ -367,6 +369,10 @@ export const dict = {
|
||||
"أي تكاليف إضافية ستأتي من اشتراكك لدى مزود النماذج. بينما يعمل OpenCode مع أي مزود نماذج، نوصي باستخدام",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "يتم حفظ بياناتك ومعلوماتك فقط عند إنشاء روابط قابلة للمشاركة في OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "اعرف المزيد عن",
|
||||
"download.faq.a6.shareLink": "صفحات المشاركة",
|
||||
|
||||
"enterprise.title": "OpenCode | حلول المؤسسات لمنظمتك",
|
||||
"enterprise.meta.description": "تواصل مع OpenCode لحلول المؤسسات",
|
||||
"enterprise.hero.title": "كودك ملكك",
|
||||
|
||||
@@ -142,9 +142,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode é 100% gratuito para usar. Ele também vem com um conjunto de modelos gratuitos. Pode haver custos adicionais se você conectar qualquer outro provedor.",
|
||||
"home.faq.q7": "E sobre dados e privacidade?",
|
||||
"home.faq.a7.p1": "Seus dados e informações só são armazenados quando você usa nossos modelos gratuitos.",
|
||||
"home.faq.a7.p1":
|
||||
"Seus dados e informações só são armazenados quando você usa nossos modelos gratuitos ou cria links compartilháveis.",
|
||||
"home.faq.a7.p2.beforeModels": "Saiba mais sobre",
|
||||
"home.faq.a7.p2.modelsLink": "nossos modelos",
|
||||
"home.faq.a7.p2.and": "e",
|
||||
"home.faq.a7.p2.shareLink": "páginas de compartilhamento",
|
||||
"home.faq.q8": "OpenCode é código aberto?",
|
||||
"home.faq.a8.p1": "Sim, OpenCode é totalmente open source. O código-fonte é público no",
|
||||
"home.faq.a8.p2": "sob a",
|
||||
@@ -220,7 +223,7 @@ export const dict = {
|
||||
"Todos os modelos Zen são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelo, com as",
|
||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview já está disponível no Go com 4× mais uso por tempo limitado.",
|
||||
"go.promo.gemini": "Gemini 4 Argon já está disponível com 2× mais uso por tempo limitado.",
|
||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||
"go.meta.description":
|
||||
"O Go custa $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.",
|
||||
@@ -377,6 +380,11 @@ export const dict = {
|
||||
"Quaisquer custos adicionais virão da sua assinatura de um provedor de modelo. Embora o OpenCode funcione com qualquer provedor de modelo, recomendamos o uso do",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1":
|
||||
"Seus dados e informações só são armazenados quando você cria links compartilháveis no OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Saiba mais sobre",
|
||||
"download.faq.a6.shareLink": "páginas de compartilhamento",
|
||||
|
||||
"enterprise.title": "OpenCode | Soluções empresariais para sua organização",
|
||||
"enterprise.meta.description": "Contate a OpenCode para soluções empresariais",
|
||||
"enterprise.hero.title": "Seu código é seu",
|
||||
|
||||
@@ -141,9 +141,11 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode er 100% gratis at bruge. Det kommer også med et sæt gratis modeller. Der kan være ekstra omkostninger, hvis du forbinder en anden udbyder.",
|
||||
"home.faq.q7": "Hvad med data og privatliv?",
|
||||
"home.faq.a7.p1": "Dine data gemmes kun, når du bruger vores gratis modeller.",
|
||||
"home.faq.a7.p1": "Dine data gemmes kun, når du bruger vores gratis modeller eller opretter delbare links.",
|
||||
"home.faq.a7.p2.beforeModels": "Læs mere om",
|
||||
"home.faq.a7.p2.modelsLink": "vores modeller",
|
||||
"home.faq.a7.p2.and": "og",
|
||||
"home.faq.a7.p2.shareLink": "delingssider",
|
||||
"home.faq.q8": "Er OpenCode open source?",
|
||||
"home.faq.a8.p1": "Ja, OpenCode er fuldt open source. Kildekoden er offentlig på",
|
||||
"home.faq.a8.p2": "under",
|
||||
@@ -219,7 +221,7 @@ export const dict = {
|
||||
"Alle Zen-modeller er hostet i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning med",
|
||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview er nu tilgængelig i Go med 4× forbrug i en begrænset periode.",
|
||||
"go.promo.gemini": "Gemini 4 Argon er nu tilgængelig med 2× forbrug i en begrænset periode.",
|
||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||
"go.meta.description":
|
||||
"Go koster $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.",
|
||||
@@ -374,6 +376,10 @@ export const dict = {
|
||||
"Eventuelle ekstra omkostninger kommer fra dit abonnement hos en modeludbyder. Selvom OpenCode fungerer med enhver modeludbyder, anbefaler vi at bruge",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "Dine data og oplysninger gemmes kun når du opretter delbare links i OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Læs mere om",
|
||||
"download.faq.a6.shareLink": "delingssider",
|
||||
|
||||
"enterprise.title": "OpenCode | Enterprise-løsninger til din organisation",
|
||||
"enterprise.meta.description": "Kontakt OpenCode for enterprise-løsninger",
|
||||
"enterprise.hero.title": "Din kode er din egen",
|
||||
|
||||
@@ -141,9 +141,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode ist zu 100% kostenlos. Es enthält auch eine Reihe kostenloser Modelle. Zusätzliche Kosten können entstehen, wenn du andere Anbieter verbindest.",
|
||||
"home.faq.q7": "Was ist mit Daten und Privatsphäre?",
|
||||
"home.faq.a7.p1": "Deine Daten und Informationen werden nur gespeichert, wenn du unsere kostenlosen Modelle nutzt.",
|
||||
"home.faq.a7.p1":
|
||||
"Deine Daten und Informationen werden nur gespeichert, wenn du unsere kostenlosen Modelle nutzt oder teilbare Links erstellst.",
|
||||
"home.faq.a7.p2.beforeModels": "Erfahre mehr über",
|
||||
"home.faq.a7.p2.modelsLink": "unsere Modelle",
|
||||
"home.faq.a7.p2.and": "und",
|
||||
"home.faq.a7.p2.shareLink": "Share-Pages",
|
||||
"home.faq.q8": "Ist OpenCode Open Source?",
|
||||
"home.faq.a8.p1": "Ja, OpenCode ist vollständig Open Source. Der Quellcode ist öffentlich auf",
|
||||
"home.faq.a8.p2": "unter der",
|
||||
@@ -220,7 +223,7 @@ export const dict = {
|
||||
"Alle Zen-Modelle werden in den USA gehostet. Anbieter folgen einer Zero-Retention-Policy und nutzen deine Daten nicht für Modelltraining, mit den",
|
||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview ist jetzt in Go für begrenzte Zeit mit 4× Nutzung verfügbar.",
|
||||
"go.promo.gemini": "Gemini 4 Argon ist jetzt für begrenzte Zeit mit 2× Nutzung verfügbar.",
|
||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||
"go.meta.description":
|
||||
"Go kostet $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.",
|
||||
@@ -377,6 +380,11 @@ export const dict = {
|
||||
"Zusätzliche Kosten entstehen durch dein Abo bei einem Modellanbieter. Während OpenCode mit jedem Modellanbieter funktioniert, empfehlen wir",
|
||||
"download.faq.a5.p2.afterZen": " zu nutzen.",
|
||||
|
||||
"download.faq.a6.p1":
|
||||
"Deine Daten und Informationen werden nur gespeichert, wenn du teilbare Links in OpenCode erstellst.",
|
||||
"download.faq.a6.p2.beforeShare": "Erfahre mehr über",
|
||||
"download.faq.a6.shareLink": "Share-Pages",
|
||||
|
||||
"enterprise.title": "OpenCode | Enterprise-Lösungen für Ihre Organisation",
|
||||
"enterprise.meta.description": "Kontaktieren Sie OpenCode für Enterprise-Lösungen",
|
||||
"enterprise.hero.title": "Ihr Code gehört Ihnen",
|
||||
|
||||
@@ -141,9 +141,11 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode is 100% free to use. It also comes with a set of free models. There might be additional costs if you connect any other provider.",
|
||||
"home.faq.q7": "What about data and privacy?",
|
||||
"home.faq.a7.p1": "Your data and information is only stored when you use our free models.",
|
||||
"home.faq.a7.p1": "Your data and information is only stored when you use our free models or create sharable links.",
|
||||
"home.faq.a7.p2.beforeModels": "Learn more about",
|
||||
"home.faq.a7.p2.modelsLink": "our models",
|
||||
"home.faq.a7.p2.and": "and",
|
||||
"home.faq.a7.p2.shareLink": "share pages",
|
||||
"home.faq.q8": "Is OpenCode open source?",
|
||||
"home.faq.a8.p1": "Yes, OpenCode is fully open source. The source code is public on",
|
||||
"home.faq.a8.p2": "under the",
|
||||
@@ -218,7 +220,7 @@ export const dict = {
|
||||
"All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
||||
"zen.privacy.exceptionsLink": "following exceptions",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview is now available in Go with 4× usage for a limited time.",
|
||||
"go.promo.gemini": "Gemini 4 Argon is now available with 2× usage for a limited time.",
|
||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||
"go.meta.description": "Go costs $10/month, with generous usage limits and reliable access to leading coding models.",
|
||||
"go.hero.title": "Low cost coding models for everyone",
|
||||
@@ -372,6 +374,10 @@ export const dict = {
|
||||
"Any additional costs will come from your subscription to a model provider. While OpenCode works with any model provider, we recommend using",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "Your data and information is only stored when you create sharable links in OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Learn more about",
|
||||
"download.faq.a6.shareLink": "share pages",
|
||||
|
||||
"enterprise.title": "OpenCode | Enterprise solutions for your organisation",
|
||||
"enterprise.meta.description": "Contact OpenCode for enterprise solutions",
|
||||
"enterprise.hero.title": "Your code is yours",
|
||||
|
||||
@@ -142,9 +142,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode es 100% gratuito de usar. También viene con un conjunto de modelos gratuitos. Puede haber costos adicionales si conectas cualquier otro proveedor.",
|
||||
"home.faq.q7": "¿Qué hay sobre datos y privacidad?",
|
||||
"home.faq.a7.p1": "Tus datos e información solo se almacenan cuando usas nuestros modelos gratuitos.",
|
||||
"home.faq.a7.p1":
|
||||
"Tus datos e información solo se almacenan cuando usas nuestros modelos gratuitos o creas enlaces compartibles.",
|
||||
"home.faq.a7.p2.beforeModels": "Más información sobre",
|
||||
"home.faq.a7.p2.modelsLink": "nuestros modelos",
|
||||
"home.faq.a7.p2.and": "y",
|
||||
"home.faq.a7.p2.shareLink": "páginas compartidas",
|
||||
"home.faq.q8": "¿Es OpenCode de código abierto?",
|
||||
"home.faq.a8.p1": "Sí, OpenCode es totalmente de código abierto. El código fuente es público en",
|
||||
"home.faq.a8.p2": "bajo la",
|
||||
@@ -221,7 +224,7 @@ export const dict = {
|
||||
"Todos los modelos Zen están alojados en EE. UU. Los proveedores siguen una política de cero retención y no usan tus datos para entrenamiento de modelos, con las",
|
||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview ya está disponible en Go con 4× de uso por tiempo limitado.",
|
||||
"go.promo.gemini": "Gemini 4 Argon ya está disponible con 2× de uso por tiempo limitado.",
|
||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||
"go.meta.description":
|
||||
"Go cuesta 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.",
|
||||
@@ -378,6 +381,10 @@ export const dict = {
|
||||
"Cualquier costo adicional vendrá de tu suscripción a un proveedor de modelos. Aunque OpenCode funciona con cualquier proveedor de modelos, recomendamos usar",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "Tus datos e información solo se almacenan cuando creas enlaces compartibles en OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Más información sobre",
|
||||
"download.faq.a6.shareLink": "páginas compartidas",
|
||||
|
||||
"enterprise.title": "OpenCode | Soluciones empresariales para tu organización",
|
||||
"enterprise.meta.description": "Contacta a OpenCode para soluciones empresariales",
|
||||
"enterprise.hero.title": "Tu código es tuyo",
|
||||
|
||||
@@ -141,9 +141,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode est 100% gratuit. Il inclut aussi des modèles gratuits. Des coûts supplémentaires peuvent s'appliquer si vous connectez un autre fournisseur.",
|
||||
"home.faq.q7": "Qu'en est-il des données et de la confidentialité ?",
|
||||
"home.faq.a7.p1": "Vos données ne sont stockées que lorsque vous utilisez nos modèles gratuits.",
|
||||
"home.faq.a7.p1":
|
||||
"Vos données ne sont stockées que lorsque vous utilisez nos modèles gratuits ou créez des liens partageables.",
|
||||
"home.faq.a7.p2.beforeModels": "En savoir plus sur",
|
||||
"home.faq.a7.p2.modelsLink": "nos modèles",
|
||||
"home.faq.a7.p2.and": "et",
|
||||
"home.faq.a7.p2.shareLink": "les pages de partage",
|
||||
"home.faq.q8": "OpenCode est-il open source ?",
|
||||
"home.faq.a8.p1": "Oui, OpenCode est entièrement open source. Le code source est public sur",
|
||||
"home.faq.a8.p2": "sous la",
|
||||
@@ -223,7 +226,7 @@ export const dict = {
|
||||
"Tous les modèles Zen sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview est désormais disponible dans Go avec 4× d’utilisation pour une durée limitée.",
|
||||
"go.promo.gemini": "Gemini 4 Argon est désormais disponible avec 2× d’utilisation pour une durée limitée.",
|
||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||
"go.meta.description":
|
||||
"Go coûte 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.",
|
||||
@@ -381,6 +384,11 @@ export const dict = {
|
||||
"Les coûts supplémentaires viendront de votre abonnement à un fournisseur de modèle. Même si OpenCode fonctionne avec n'importe quel fournisseur, nous recommandons d'utiliser",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1":
|
||||
"Vos données et informations ne sont stockées que lorsque vous créez des liens partageables dans OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "En savoir plus sur",
|
||||
"download.faq.a6.shareLink": "les pages de partage",
|
||||
|
||||
"enterprise.title": "OpenCode | Solutions entreprise pour votre organisation",
|
||||
"enterprise.meta.description": "Contactez OpenCode pour des solutions entreprise",
|
||||
"enterprise.hero.title": "Votre code vous appartient",
|
||||
|
||||
@@ -140,9 +140,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode è gratuito al 100%. Include anche un set di modelli gratuiti. Potrebbero esserci costi aggiuntivi se colleghi altri provider.",
|
||||
"home.faq.q7": "E per quanto riguarda dati e privacy?",
|
||||
"home.faq.a7.p1": "I tuoi dati vengono archiviati solo quando usi i nostri modelli gratuiti.",
|
||||
"home.faq.a7.p1":
|
||||
"I tuoi dati vengono archiviati solo quando usi i nostri modelli gratuiti o crei link condivisibili.",
|
||||
"home.faq.a7.p2.beforeModels": "Scopri di più su",
|
||||
"home.faq.a7.p2.modelsLink": "i nostri modelli",
|
||||
"home.faq.a7.p2.and": "e",
|
||||
"home.faq.a7.p2.shareLink": "le pagine di condivisione",
|
||||
"home.faq.q8": "OpenCode è open source?",
|
||||
"home.faq.a8.p1": "Sì, OpenCode è completamente open source. Il codice sorgente è pubblico su",
|
||||
"home.faq.a8.p2": "sotto la",
|
||||
@@ -218,7 +221,7 @@ export const dict = {
|
||||
"Tutti i modelli Zen sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview è ora disponibile in Go con utilizzo 4× per un periodo limitato.",
|
||||
"go.promo.gemini": "Gemini 4 Argon è ora disponibile con utilizzo 2× per un periodo limitato.",
|
||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||
"go.meta.description":
|
||||
"Go costa $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.",
|
||||
@@ -376,6 +379,10 @@ export const dict = {
|
||||
"Eventuali costi aggiuntivi proverranno dal tuo abbonamento a un provider di modelli. Mentre OpenCode funziona con qualsiasi provider di modelli, raccomandiamo di usare",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "I tuoi dati e informazioni sono archiviati solo quando crei link condivisibili in OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Scopri di più sulle",
|
||||
"download.faq.a6.shareLink": "pagine condivise",
|
||||
|
||||
"enterprise.title": "OpenCode | Soluzioni Enterprise per la tua organizzazione",
|
||||
"enterprise.meta.description": "Contatta OpenCode per soluzioni enterprise",
|
||||
"enterprise.hero.title": "Il tuo codice è tuo",
|
||||
|
||||
@@ -140,9 +140,11 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCodeは100%無料で使えます。無料モデルも含まれています。他のプロバイダーに接続する場合は追加費用が発生することがあります。",
|
||||
"home.faq.q7": "データとプライバシーは?",
|
||||
"home.faq.a7.p1": "無料モデルを使う場合にのみ、データが保存されます。",
|
||||
"home.faq.a7.p1": "無料モデルを使う場合や共有リンクを作成する場合にのみ、データが保存されます。",
|
||||
"home.faq.a7.p2.beforeModels": "詳しくは",
|
||||
"home.faq.a7.p2.modelsLink": "モデルのプライバシー",
|
||||
"home.faq.a7.p2.and": "と",
|
||||
"home.faq.a7.p2.shareLink": "共有ページのプライバシー",
|
||||
"home.faq.q8": "OpenCodeはオープンソースですか?",
|
||||
"home.faq.a8.p1": "はい、OpenCodeは完全にオープンソースです。ソースコードは",
|
||||
"home.faq.a8.p2": "の",
|
||||
@@ -218,7 +220,7 @@ export const dict = {
|
||||
"すべてのZenモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
||||
"zen.privacy.exceptionsLink": "以下の例外",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview を Go で期間限定・利用枠4倍で提供中です。",
|
||||
"go.promo.gemini": "Gemini 4 Argon を期間限定で利用枠2倍で提供中です。",
|
||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||
"go.meta.description":
|
||||
"Goは月額$10で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
|
||||
@@ -372,6 +374,10 @@ export const dict = {
|
||||
"追加コストはモデルプロバイダーのサブスクリプションから発生します。OpenCodeはどのモデルプロバイダーでも利用できますが、",
|
||||
"download.faq.a5.p2.afterZen": "の利用をおすすめします。",
|
||||
|
||||
"download.faq.a6.p1": "あなたのデータと情報は、OpenCodeで共有リンクを作成したときにのみ保存されます。",
|
||||
"download.faq.a6.p2.beforeShare": "詳しくは",
|
||||
"download.faq.a6.shareLink": "共有ページ",
|
||||
|
||||
"enterprise.title": "OpenCode | 組織向けエンタープライズソリューション",
|
||||
"enterprise.meta.description": "エンタープライズソリューションについてOpenCodeに問い合わせる",
|
||||
"enterprise.hero.title": "あなたのコードはあなたのもの",
|
||||
|
||||
@@ -139,9 +139,11 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode는 100% 무료로 사용할 수 있습니다. 무료 모델 세트도 포함되어 있습니다. 다른 제공자를 연결할 경우 추가 비용이 발생할 수 있습니다.",
|
||||
"home.faq.q7": "데이터와 프라이버시는 어떤가요?",
|
||||
"home.faq.a7.p1": "데이터와 정보는 무료 모델을 사용할 때만 저장됩니다.",
|
||||
"home.faq.a7.p1": "데이터와 정보는 무료 모델을 사용하거나 공유 링크를 생성할 때만 저장됩니다.",
|
||||
"home.faq.a7.p2.beforeModels": "더 알아보기:",
|
||||
"home.faq.a7.p2.modelsLink": "모델",
|
||||
"home.faq.a7.p2.and": "및",
|
||||
"home.faq.a7.p2.shareLink": "공유 페이지",
|
||||
"home.faq.q8": "OpenCode는 오픈 소스인가요?",
|
||||
"home.faq.a8.p1": "네, OpenCode는 완전히 오픈 소스입니다. 소스 코드는",
|
||||
"home.faq.a8.p2": "에 공개되어 있으며,",
|
||||
@@ -215,7 +217,7 @@ export const dict = {
|
||||
"모든 Zen 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
||||
"zen.privacy.exceptionsLink": "다음 예외",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview를 Go에서 한정 기간 동안 사용량 4배로 이용할 수 있습니다.",
|
||||
"go.promo.gemini": "Gemini 4 Argon을 한정 기간 동안 사용량 2배로 이용할 수 있습니다.",
|
||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||
"go.meta.description": "Go는 월 $10이며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
|
||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||
@@ -365,6 +367,10 @@ export const dict = {
|
||||
"추가 비용은 모델 제공자 구독에서 발생합니다. OpenCode는 모든 모델 제공자와 작동하지만, 우리는",
|
||||
"download.faq.a5.p2.afterZen": " 사용을 권장합니다.",
|
||||
|
||||
"download.faq.a6.p1": "데이터와 정보는 OpenCode에서 공유 링크를 생성할 때만 저장됩니다.",
|
||||
"download.faq.a6.p2.beforeShare": "더 알아보기:",
|
||||
"download.faq.a6.shareLink": "공유 페이지",
|
||||
|
||||
"enterprise.title": "OpenCode | 조직을 위한 엔터프라이즈 솔루션",
|
||||
"enterprise.meta.description": "OpenCode 엔터프라이즈 솔루션 문의",
|
||||
"enterprise.hero.title": "여러분의 코드는 여러분의 것입니다",
|
||||
|
||||
@@ -141,9 +141,11 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode er 100% gratis å bruke. Det kommer også med et sett gratis modeller. Det kan være ekstra kostnader hvis du kobler til en annen leverandør.",
|
||||
"home.faq.q7": "Hva med data og personvern?",
|
||||
"home.faq.a7.p1": "Dataene dine lagres kun når du bruker våre gratis modeller.",
|
||||
"home.faq.a7.p1": "Dataene dine lagres kun når du bruker våre gratis modeller eller lager delbare lenker.",
|
||||
"home.faq.a7.p2.beforeModels": "Les mer om",
|
||||
"home.faq.a7.p2.modelsLink": "våre modeller",
|
||||
"home.faq.a7.p2.and": "og",
|
||||
"home.faq.a7.p2.shareLink": "delingssider",
|
||||
"home.faq.q8": "Er OpenCode åpen kildekode?",
|
||||
"home.faq.a8.p1": "Ja, OpenCode er fullt open source. Kildekoden er offentlig på",
|
||||
"home.faq.a8.p2": "under",
|
||||
@@ -219,7 +221,7 @@ export const dict = {
|
||||
"Alle Zen-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview er nå tilgjengelig i Go med 4× bruk i en begrenset periode.",
|
||||
"go.promo.gemini": "Gemini 4 Argon er nå tilgjengelig med 2× bruk i en begrenset periode.",
|
||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||
"go.meta.description":
|
||||
"Go koster $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.",
|
||||
@@ -375,6 +377,10 @@ export const dict = {
|
||||
"Eventuelle ekstra kostnader kommer fra abonnementet ditt hos en modellleverandør. Selv om OpenCode fungerer med enhver modellleverandør, anbefaler vi å bruke",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "Dataene og informasjonen din lagres bare når du oppretter delbare lenker i OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Les mer om",
|
||||
"download.faq.a6.shareLink": "delingssider",
|
||||
|
||||
"enterprise.title": "OpenCode | Enterprise-løsninger for din organisasjon",
|
||||
"enterprise.meta.description": "Kontakt OpenCode for enterprise-løsninger",
|
||||
"enterprise.hero.title": "Koden din er din",
|
||||
|
||||
@@ -140,9 +140,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode jest w 100% darmowy. Zawiera również zestaw darmowych modeli. Mogą pojawić się dodatkowe koszty, jeśli podłączysz innego dostawcę.",
|
||||
"home.faq.q7": "A co z danymi i prywatnością?",
|
||||
"home.faq.a7.p1": "Twoje dane i informacje są przechowywane tylko wtedy, gdy używasz naszych darmowych modeli.",
|
||||
"home.faq.a7.p1":
|
||||
"Twoje dane i informacje są przechowywane tylko wtedy, gdy używasz naszych darmowych modeli lub tworzysz linki do udostępniania.",
|
||||
"home.faq.a7.p2.beforeModels": "Dowiedz się więcej o",
|
||||
"home.faq.a7.p2.modelsLink": "naszych modelach",
|
||||
"home.faq.a7.p2.and": "i",
|
||||
"home.faq.a7.p2.shareLink": "stronach udostępniania",
|
||||
"home.faq.q8": "Czy OpenCode jest open source?",
|
||||
"home.faq.a8.p1": "Tak, OpenCode jest w pełni open source. Kod źródłowy jest publicznie dostępny na",
|
||||
"home.faq.a8.p2": "na licencji",
|
||||
@@ -219,7 +222,7 @@ export const dict = {
|
||||
"Wszystkie modele Zen są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie wykorzystują Twoich danych do trenowania modeli, z",
|
||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview jest już dostępny w Go z 4× większym limitem przez ograniczony czas.",
|
||||
"go.promo.gemini": "Gemini 4 Argon jest już dostępny z 2× większym limitem przez ograniczony czas.",
|
||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||
"go.meta.description":
|
||||
"Go kosztuje $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.",
|
||||
@@ -376,6 +379,11 @@ export const dict = {
|
||||
"Wszelkie dodatkowe koszty będą pochodzić z Twojej subskrypcji u dostawcy modelu. Chociaż OpenCode współpracuje z dowolnym dostawcą modeli, zalecamy korzystanie z",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1":
|
||||
"Twoje dane i informacje są przechowywane tylko wtedy, gdy tworzysz linki do udostępniania w OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Dowiedz się więcej o",
|
||||
"download.faq.a6.shareLink": "stronach udostępniania",
|
||||
|
||||
"enterprise.title": "OpenCode | Rozwiązania Enterprise dla Twojej organizacji",
|
||||
"enterprise.meta.description": "Skontaktuj się z OpenCode w sprawie rozwiązań dla przedsiębiorstw",
|
||||
"enterprise.hero.title": "Twój kod jest Twój",
|
||||
|
||||
@@ -142,9 +142,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode на 100% бесплатен. Он также включает набор бесплатных моделей. Дополнительные расходы могут возникнуть, если вы подключите другого провайдера.",
|
||||
"home.faq.q7": "Как насчет данных и приватности?",
|
||||
"home.faq.a7.p1": "Ваши данные сохраняются только тогда, когда вы используете наши бесплатные модели.",
|
||||
"home.faq.a7.p1":
|
||||
"Ваши данные сохраняются только тогда, когда вы используете наши бесплатные модели или создаете ссылки для шеринга.",
|
||||
"home.faq.a7.p2.beforeModels": "Подробнее о",
|
||||
"home.faq.a7.p2.modelsLink": "наших моделях",
|
||||
"home.faq.a7.p2.and": "и",
|
||||
"home.faq.a7.p2.shareLink": "страницах шеринга",
|
||||
"home.faq.q8": "OpenCode — это open source?",
|
||||
"home.faq.a8.p1": "Да, OpenCode полностью open source. Исходный код доступен публично на",
|
||||
"home.faq.a8.p2": "под",
|
||||
@@ -222,7 +225,7 @@ export const dict = {
|
||||
"Все модели Zen размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview уже доступна в Go с лимитом ×4 в течение ограниченного времени.",
|
||||
"go.promo.gemini": "Gemini 4 Argon уже доступна с лимитом ×2 в течение ограниченного времени.",
|
||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||
"go.meta.description":
|
||||
"Go стоит $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
|
||||
@@ -380,6 +383,10 @@ export const dict = {
|
||||
"Любые дополнительные расходы будут связаны с вашей подпиской у провайдера моделей. Хотя OpenCode работает с любым провайдером моделей, мы рекомендуем использовать",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "Ваши данные и информация сохраняются только при создании ссылок для шеринга в OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Подробнее о",
|
||||
"download.faq.a6.shareLink": "страницах шеринга",
|
||||
|
||||
"enterprise.title": "OpenCode | Корпоративные решения для вашей организации",
|
||||
"enterprise.meta.description": "Свяжитесь с OpenCode для корпоративных решений",
|
||||
"enterprise.hero.title": "Ваш код принадлежит вам",
|
||||
|
||||
@@ -138,9 +138,11 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode ใช้งานได้ฟรี 100% และมาพร้อมกับชุดโมเดลฟรี อาจมีค่าใช้จ่ายเพิ่มเติมหากคุณเชื่อมต่อกับผู้ให้บริการรายอื่น",
|
||||
"home.faq.q7": "แล้วเรื่องข้อมูลและความเป็นส่วนตัวล่ะ?",
|
||||
"home.faq.a7.p1": "ข้อมูลของคุณจะถูกจัดเก็บเฉพาะเมื่อคุณใช้โมเดลฟรีของเรา",
|
||||
"home.faq.a7.p1": "ข้อมูลของคุณจะถูกจัดเก็บเฉพาะเมื่อคุณใช้โมเดลฟรีของเราหรือสร้างลิงก์ที่แชร์ได้",
|
||||
"home.faq.a7.p2.beforeModels": "เรียนรู้เพิ่มเติมเกี่ยวกับ",
|
||||
"home.faq.a7.p2.modelsLink": "โมเดลของเรา",
|
||||
"home.faq.a7.p2.and": "และ",
|
||||
"home.faq.a7.p2.shareLink": "หน้าแชร์",
|
||||
"home.faq.q8": "OpenCode เป็นโอเพนซอร์สหรือไม่?",
|
||||
"home.faq.a8.p1": "ใช่ OpenCode เป็นโอเพนซอร์สเต็มรูปแบบ ซอร์สโค้ดเปิดเผยต่อสาธารณะบน",
|
||||
"home.faq.a8.p2": "ภายใต้",
|
||||
@@ -217,7 +219,7 @@ export const dict = {
|
||||
"โมเดล Zen ทั้งหมดโฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview พร้อมให้ใช้งานใน Go แล้ว พร้อมการใช้งาน 4 เท่าในช่วงเวลาจำกัด",
|
||||
"go.promo.gemini": "Gemini 4 Argon พร้อมให้ใช้งานแล้ว พร้อมการใช้งาน 2 เท่าในช่วงเวลาจำกัด",
|
||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
"go.meta.description":
|
||||
"Go มีราคา $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
|
||||
@@ -371,6 +373,10 @@ export const dict = {
|
||||
"ค่าใช้จ่ายเพิ่มเติมใดๆ จะมาจากการสมัครสมาชิกของคุณกับผู้ให้บริการโมเดล แม้ว่า OpenCode จะทำงานร่วมกับผู้ให้บริการโมเดลใดก็ได้ แต่เราแนะนำให้ใช้",
|
||||
"download.faq.a5.p2.afterZen": "",
|
||||
|
||||
"download.faq.a6.p1": "ข้อมูลของคุณจะถูกจัดเก็บเฉพาะเมื่อคุณสร้างลิงก์ที่แชร์ได้ใน OpenCode เท่านั้น",
|
||||
"download.faq.a6.p2.beforeShare": "เรียนรู้เพิ่มเติมเกี่ยวกับ",
|
||||
"download.faq.a6.shareLink": "หน้าแชร์",
|
||||
|
||||
"enterprise.title": "OpenCode | โซลูชันระดับองค์กรสำหรับองค์กรของคุณ",
|
||||
"enterprise.meta.description": "ติดต่อ OpenCode สำหรับโซลูชันระดับองค์กร",
|
||||
"enterprise.hero.title": "โค้ดของคุณเป็นของคุณ",
|
||||
|
||||
@@ -139,9 +139,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode %100 ücretsizdir. Ayrıca ücretsiz model setiyle gelir. Başka bir sağlayıcı bağlarsanız ek maliyetler olabilir.",
|
||||
"home.faq.q7": "Veri ve gizlilik ne olacak?",
|
||||
"home.faq.a7.p1": "Verileriniz yalnızca ücretsiz modellerimizi kullandığınızda saklanır.",
|
||||
"home.faq.a7.p1":
|
||||
"Verileriniz yalnızca ücretsiz modellerimizi kullandığınızda veya paylaşılabilir bağlantılar oluşturduğunuzda saklanır.",
|
||||
"home.faq.a7.p2.beforeModels": "Daha fazla bilgi:",
|
||||
"home.faq.a7.p2.modelsLink": "modellerimiz",
|
||||
"home.faq.a7.p2.and": "ve",
|
||||
"home.faq.a7.p2.shareLink": "paylaşım sayfaları",
|
||||
"home.faq.q8": "OpenCode açık kaynak mı?",
|
||||
"home.faq.a8.p1": "Evet, OpenCode tamamen açık kaynaktır. Kaynak kodu",
|
||||
"home.faq.a8.p2": "'da",
|
||||
@@ -220,7 +223,7 @@ export const dict = {
|
||||
"Tüm Zen modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview artık Go'da sınırlı bir süre için 4× kullanımla sunuluyor.",
|
||||
"go.promo.gemini": "Gemini 4 Argon artık sınırlı bir süre için 2× kullanımla sunuluyor.",
|
||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||
"go.meta.description":
|
||||
"Go ayda 10$'dır; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.",
|
||||
@@ -378,6 +381,11 @@ export const dict = {
|
||||
"Ek maliyetler, bir model sağlayıcısına olan aboneliğinizden gelir. OpenCode herhangi bir model sağlayıcısıyla çalışır, ancak",
|
||||
"download.faq.a5.p2.afterZen": " kullanmanızı öneririz.",
|
||||
|
||||
"download.faq.a6.p1":
|
||||
"Verileriniz ve bilginiz yalnızca OpenCode'da paylaşılabilir bağlantılar oluşturduğunuzda saklanır.",
|
||||
"download.faq.a6.p2.beforeShare": "Daha fazla bilgi:",
|
||||
"download.faq.a6.shareLink": "paylaşım sayfaları",
|
||||
|
||||
"enterprise.title": "OpenCode | Kurumunuz için kurumsal çözümler",
|
||||
"enterprise.meta.description": "Kurumsal çözümler için OpenCode ile iletişime geçin",
|
||||
"enterprise.hero.title": "Kodunuz size aittir",
|
||||
|
||||
@@ -140,9 +140,12 @@ export const dict = {
|
||||
"home.faq.a6":
|
||||
"OpenCode є 100% безкоштовним. Він також має набір безкоштовних моделей. Додаткові витрати можливі, якщо ви підключите іншого провайдера.",
|
||||
"home.faq.q7": "А як щодо даних та конфіденційності?",
|
||||
"home.faq.a7.p1": "Ваші дані зберігаються лише тоді, коли ви використовуєте безкоштовні моделі.",
|
||||
"home.faq.a7.p1":
|
||||
"Ваші дані зберігаються лише тоді, коли ви використовуєте безкоштовні моделі або створюєте посилання для обміну.",
|
||||
"home.faq.a7.p2.beforeModels": "Дізнайтеся більше про",
|
||||
"home.faq.a7.p2.modelsLink": "наші моделі",
|
||||
"home.faq.a7.p2.and": "та",
|
||||
"home.faq.a7.p2.shareLink": "сторінки обміну",
|
||||
"home.faq.q8": "Чи є OpenCode відкритим?",
|
||||
"home.faq.a8.p1": "Так, OpenCode повністю відкритий. Вихідний код доступний публічно на",
|
||||
"home.faq.a8.p2": "під ліцензією",
|
||||
@@ -218,7 +221,7 @@ export const dict = {
|
||||
"Усі моделі Zen розміщені в США. Провайдери дотримуються політики нульового зберігання та не використовують ваші дані для навчання моделей, за",
|
||||
"zen.privacy.exceptionsLink": "такими винятками",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview уже доступна в Go з лімітом ×4 протягом обмеженого часу.",
|
||||
"go.promo.gemini": "Gemini 4 Argon уже доступна з лімітом ×2 протягом обмеженого часу.",
|
||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||
"go.meta.description":
|
||||
"Go коштує $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
|
||||
@@ -374,6 +377,10 @@ export const dict = {
|
||||
"Будь-які додаткові витрати будуть з вашої підписки у провайдера моделі. Ми рекомендуємо",
|
||||
"download.faq.a5.p2.afterZen": ".",
|
||||
|
||||
"download.faq.a6.p1": "Ваші дані зберігаються лише при створенні посилань для обміну в OpenCode.",
|
||||
"download.faq.a6.p2.beforeShare": "Дізнайтеся більше про",
|
||||
"download.faq.a6.shareLink": "сторінки обміну",
|
||||
|
||||
"enterprise.title": "OpenCode | Enterprise-рішення для вашої організації",
|
||||
"enterprise.meta.description": "Зв'яжіться з OpenCode для Enterprise-рішень",
|
||||
"enterprise.hero.title": "Ваш код належить вам",
|
||||
@@ -434,4 +441,5 @@ export const dict = {
|
||||
"changelog.hero.subtitle": "Нові оновлення та покращення OpenCode",
|
||||
"changelog.empty": "Записів у журналі змін не знайдено.",
|
||||
"changelog.viewJson": "Переглянути JSON",
|
||||
|
||||
}
|
||||
@@ -135,9 +135,11 @@ export const dict = {
|
||||
"home.faq.q6": "OpenCode 多少钱?",
|
||||
"home.faq.a6": "OpenCode 是 100% 免费使用的。它还自带一组免费模型。如果您连接其他提供商,可能会产生额外费用。",
|
||||
"home.faq.q7": "数据和隐私如何?",
|
||||
"home.faq.a7.p1": "只有当您使用我们的免费模型时,您的数据和信息才会被存储。",
|
||||
"home.faq.a7.p1": "只有当您使用我们的免费模型或创建分享链接时,您的数据和信息才会被存储。",
|
||||
"home.faq.a7.p2.beforeModels": "了解更多关于",
|
||||
"home.faq.a7.p2.modelsLink": "我们的模型",
|
||||
"home.faq.a7.p2.and": "和",
|
||||
"home.faq.a7.p2.shareLink": "分享页面",
|
||||
"home.faq.q8": "OpenCode 是开源的吗?",
|
||||
"home.faq.a8.p1": "是的,OpenCode 是完全开源的。源代码公开在",
|
||||
"home.faq.a8.p2": "遵循",
|
||||
@@ -208,7 +210,7 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview 现已上线 Go,限时 4 倍用量。",
|
||||
"go.promo.gemini": "Gemini 4 Argon 现已上线,限时 2 倍用量。",
|
||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||
"go.meta.description": "Go 每月 $10,提供充裕的使用限额,并可可靠访问领先的编程模型。",
|
||||
"go.hero.title": "人人可用的低成本编程模型",
|
||||
@@ -356,6 +358,10 @@ export const dict = {
|
||||
"任何额外费用都来自您对模型提供商的订阅。虽然 OpenCode 支持任何模型提供商,但我们建议使用",
|
||||
"download.faq.a5.p2.afterZen": "。",
|
||||
|
||||
"download.faq.a6.p1": "只有当您在 OpenCode 中创建分享链接时,您的数据和信息才会被存储。",
|
||||
"download.faq.a6.p2.beforeShare": "了解更多关于",
|
||||
"download.faq.a6.shareLink": "分享页面",
|
||||
|
||||
"enterprise.title": "OpenCode | 面向组织的 OpenCode 企业版解决方案",
|
||||
"enterprise.meta.description": "联系 OpenCode 获取企业版解决方案",
|
||||
"enterprise.hero.title": "您的代码属于您",
|
||||
|
||||
@@ -136,9 +136,11 @@ export const dict = {
|
||||
"home.faq.q6": "OpenCode 價格如何?",
|
||||
"home.faq.a6": "OpenCode 100% 免費使用。它也自帶一組免費模型。如果你連接其他供應商,可能會產生額外費用。",
|
||||
"home.faq.q7": "資料與隱私怎麼辦?",
|
||||
"home.faq.a7.p1": "僅當你使用我們的免費模型時,才會儲存你的資料與資訊。",
|
||||
"home.faq.a7.p1": "僅當你使用我們的免費模型或建立可分享連結時,才會儲存你的資料與資訊。",
|
||||
"home.faq.a7.p2.beforeModels": "了解更多關於",
|
||||
"home.faq.a7.p2.modelsLink": "我們的模型",
|
||||
"home.faq.a7.p2.and": "與",
|
||||
"home.faq.a7.p2.shareLink": "分享頁面",
|
||||
"home.faq.q8": "OpenCode 是開源的嗎?",
|
||||
"home.faq.a8.p1": "是的,OpenCode 完全開源。原始碼公開在",
|
||||
"home.faq.a8.p2": "並採用",
|
||||
@@ -207,7 +209,7 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均在美國託管。供應商遵循零留存政策,不會將你的資料用於模型訓練,並且有",
|
||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||
|
||||
"go.promo.step5": "Step 5 Preview 現已上線 Go,限時 4 倍用量。",
|
||||
"go.promo.gemini": "Gemini 4 Argon 現已上線,限時 2 倍用量。",
|
||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||
"go.meta.description": "Go 每月 $10,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
|
||||
"go.hero.title": "低成本全民編碼模型",
|
||||
@@ -354,6 +356,10 @@ export const dict = {
|
||||
"download.faq.a5.p2.beforeZen": "額外費用來自你對模型供應商的訂閱。雖然 OpenCode 支援任何模型供應商,但我們建議使用",
|
||||
"download.faq.a5.p2.afterZen": "。",
|
||||
|
||||
"download.faq.a6.p1": "你的資料與資訊只會在你於 OpenCode 中建立可分享連結時被儲存。",
|
||||
"download.faq.a6.p2.beforeShare": "了解更多關於",
|
||||
"download.faq.a6.shareLink": "分享頁面",
|
||||
|
||||
"enterprise.title": "OpenCode | 面向組織的企業解決方案",
|
||||
"enterprise.meta.description": "聯絡 OpenCode 取得企業解決方案",
|
||||
"enterprise.hero.title": "你的程式碼屬於你",
|
||||
|
||||
@@ -134,6 +134,34 @@ const DOCS_LOCALE = {
|
||||
"zh-tw": "zht",
|
||||
} as const satisfies Record<string, Locale>
|
||||
|
||||
// Heading IDs from the localized Go documentation.
|
||||
const GO_USAGE_LIMITS = {
|
||||
en: "usage-limits",
|
||||
zh: "使用限制",
|
||||
zht: "使用限制",
|
||||
ko: "사용-한도",
|
||||
de: "nutzungslimits",
|
||||
es: "límites-de-uso",
|
||||
fr: "limites-dutilisation",
|
||||
it: "limiti-di-utilizzo",
|
||||
da: "forbrugsgrænser",
|
||||
ja: "利用制限",
|
||||
pl: "limity-użycia",
|
||||
ru: "лимиты-использования",
|
||||
uk: "usage-limits",
|
||||
ar: "حدود-الاستخدام",
|
||||
no: "bruksgrenser",
|
||||
br: "limites-de-uso",
|
||||
th: "usage-limits",
|
||||
tr: "kullanım-limitleri",
|
||||
} satisfies Record<Locale, string>
|
||||
|
||||
export function goUsageLimits(locale: Locale) {
|
||||
// No Ukrainian Go docs yet; the explicit English path overrides the locale cookie.
|
||||
if (locale === "uk") return "/docs/en/go/#usage-limits"
|
||||
return docs(locale, `/docs/go/#${GO_USAGE_LIMITS[locale]}`)
|
||||
}
|
||||
|
||||
function suffix(pathname: string) {
|
||||
const index = pathname.search(/[?#]/)
|
||||
if (index === -1) {
|
||||
|
||||
@@ -421,8 +421,8 @@ export default function Download() {
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("home.faq.q7")}>
|
||||
{i18n.t("home.faq.a7.p1")} {i18n.t("home.faq.a7.p2.beforeModels")}{" "}
|
||||
<a href="/v2/docs/console/models/#privacy">{i18n.t("home.faq.a7.p2.modelsLink")}</a>.
|
||||
{i18n.t("download.faq.a6.p1")} {i18n.t("download.faq.a6.p2.beforeShare")}{" "}
|
||||
<a href={language.route("/docs/share/#privacy")}>{i18n.t("download.faq.a6.shareLink")}</a>.
|
||||
</Faq>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -29,13 +29,15 @@ import {
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { LocaleLinks } from "~/component/locale-links"
|
||||
import { goUsageLimits } from "~/lib/language"
|
||||
|
||||
const models = [
|
||||
{ name: "Space Bunny", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "LongCat 2.5 Preview Free", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "Step 5 Preview", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "Step 5 Preview Free", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "Cat Jam Free", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "Grok 4.7", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "Grok 4.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "GPT 6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "GPT 5.6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
{ name: "Claude Haiku 5.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
|
||||
@@ -54,6 +56,7 @@ const models = [
|
||||
{ name: "Qwen3.8 Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "Qwen3.7 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "MiniMax M3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "MiniMax M2.7", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
{ name: "Muse Spark 1.3 Contributor", training: "go.faq.a5.used", retention: "go.faq.a5.notZdr" },
|
||||
{ name: "Muse Spark 1.2 Contributor", training: "go.faq.a5.used", retention: "go.faq.a5.notZdr" },
|
||||
{ name: "DeepSeek V4.1 Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
|
||||
@@ -97,7 +100,7 @@ export default function Home() {
|
||||
<div data-component="desktop-app-banner">
|
||||
<span data-slot="badge">{i18n.t("home.banner.badge")}</span>
|
||||
<div data-slot="content">
|
||||
<span data-slot="text">{i18n.t("go.promo.step5")}</span>
|
||||
<span data-slot="text">{i18n.t("go.promo.gemini")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={searchParams.ref}>
|
||||
@@ -182,7 +185,7 @@ export default function Home() {
|
||||
</section>
|
||||
|
||||
<section data-component="comparison">
|
||||
<GoPlanChart href="/v2/docs/console/go/#usage-limits" />
|
||||
<GoPlanChart href={goUsageLimits(language.locale())} />
|
||||
</section>
|
||||
|
||||
<section data-component="problem">
|
||||
@@ -221,7 +224,7 @@ export default function Home() {
|
||||
<span>[1]</span>
|
||||
<div>
|
||||
<strong>{i18n.t("go.how.step1.title")}</strong> - {i18n.t("go.how.step1.beforeLink")}{" "}
|
||||
<a href="/v2/docs/console/go/#how-it-works" title={i18n.t("go.how.step1.link")}>
|
||||
<a href={language.route("/docs/go/#how-it-works")} title={i18n.t("go.how.step1.link")}>
|
||||
{i18n.t("go.how.step1.link")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -230,7 +233,7 @@ export default function Home() {
|
||||
<span>[2]</span>
|
||||
<div>
|
||||
<strong>{i18n.t("go.how.step2.title")}</strong> -{" "}
|
||||
<a href="/v2/docs/console/go/">{i18n.t("go.how.step2.link")}</a>{" "}
|
||||
<a href={language.route("/docs/go/#pricing")}>{i18n.t("go.how.step2.link")}</a>{" "}
|
||||
{i18n.t("go.how.step2.afterLink")} · {i18n.t("go.plans.plus.description")}
|
||||
</div>
|
||||
</li>
|
||||
@@ -270,7 +273,7 @@ export default function Home() {
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q4")}>
|
||||
{i18n.t("go.faq.a4.p1.beforePricing")}{" "}
|
||||
<a href="/v2/docs/console/go/">{i18n.t("go.faq.a4.p1.pricingLink")}</a>{" "}
|
||||
<a href={language.route("/docs/go/#pricing")}>{i18n.t("go.faq.a4.p1.pricingLink")}</a>{" "}
|
||||
{i18n.t("go.faq.a4.p1.afterPricing")} {i18n.t("go.plans.plus.description")}{" "}
|
||||
{i18n.t("go.faq.a4.p2.beforeAccount")} <a href={subscribeUrl}>{i18n.t("go.faq.a4.p2.accountLink")}</a>
|
||||
. {i18n.t("go.faq.a4.p3")}
|
||||
@@ -302,7 +305,7 @@ export default function Home() {
|
||||
</div>
|
||||
<div data-slot="faq-retention-notes">
|
||||
<p>
|
||||
<strong>Grok 4.7:</strong> {i18n.t("go.faq.a5.grokRetention")}{" "}
|
||||
<strong>Grok 4.7/4.6:</strong> {i18n.t("go.faq.a5.grokRetention")}{" "}
|
||||
<a href="https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr">
|
||||
{i18n.t("go.faq.a5.learnMore")}
|
||||
</a>
|
||||
|
||||
@@ -702,7 +702,9 @@ export default function Home() {
|
||||
<li>
|
||||
<Faq question={i18n.t("home.faq.q7")}>
|
||||
{i18n.t("home.faq.a7.p1")} {i18n.t("home.faq.a7.p2.beforeModels")}{" "}
|
||||
<a href="/v2/docs/console/models/#privacy">{i18n.t("home.faq.a7.p2.modelsLink")}</a>.
|
||||
<a href={language.route("/docs/zen/#privacy")}>{i18n.t("home.faq.a7.p2.modelsLink")}</a>{" "}
|
||||
{i18n.t("home.faq.a7.p2.and")}{" "}
|
||||
<a href={language.route("/docs/share/#privacy")}>{i18n.t("home.faq.a7.p2.shareLink")}</a>.
|
||||
</Faq>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function Home() {
|
||||
</li>
|
||||
<li>
|
||||
<strong>{i18n.t("temp.zen")}</strong> {i18n.t("temp.feature.zen.beforeLink")}{" "}
|
||||
<a href="/v2/docs/console/models/">{i18n.t("temp.feature.zen.link")}</a>{" "}
|
||||
<a href={language.route("/docs/zen")}>{i18n.t("temp.feature.zen.link")}</a>{" "}
|
||||
{i18n.t("temp.feature.zen.afterLink")} <label>{i18n.t("home.banner.badge")}</label>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function Home() {
|
||||
<span>[1]</span>
|
||||
<div>
|
||||
<strong>{i18n.t("zen.how.step1.title")}</strong> - {i18n.t("zen.how.step1.beforeLink")}{" "}
|
||||
<a href="/v2/docs/console/models/#how-it-works" title={i18n.t("zen.how.step1.link")}>
|
||||
<a href={language.route("/docs/zen/#how-it-works")} title={i18n.t("zen.how.step1.link")}>
|
||||
{i18n.t("zen.how.step1.link")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -177,7 +177,7 @@ export default function Home() {
|
||||
<span>[2]</span>
|
||||
<div>
|
||||
<strong>{i18n.t("zen.how.step2.title")}</strong> -{" "}
|
||||
<a href="/v2/docs/console/models/#pricing">{i18n.t("zen.how.step2.link")}</a>{" "}
|
||||
<a href={language.route("/docs/zen/#pricing")}>{i18n.t("zen.how.step2.link")}</a>{" "}
|
||||
{i18n.t("zen.how.step2.afterLink")}
|
||||
</div>
|
||||
</li>
|
||||
@@ -197,7 +197,7 @@ export default function Home() {
|
||||
<span>[*]</span>
|
||||
<p>
|
||||
{i18n.t("zen.privacy.beforeExceptions")}{" "}
|
||||
<a href="/v2/docs/console/models/#privacy">{i18n.t("zen.privacy.exceptionsLink")}</a>.
|
||||
<a href={language.route("/docs/zen/#privacy")}>{i18n.t("zen.privacy.exceptionsLink")}</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,7 +293,7 @@ export default function Home() {
|
||||
<li>
|
||||
<Faq question={i18n.t("zen.faq.q4")}>
|
||||
{i18n.t("zen.faq.a4.p1.beforePricing")}{" "}
|
||||
<a href="/v2/docs/console/models/#pricing">{i18n.t("zen.faq.a4.p1.pricingLink")}</a>{" "}
|
||||
<a href={language.route("/docs/zen/#pricing")}>{i18n.t("zen.faq.a4.p1.pricingLink")}</a>{" "}
|
||||
{i18n.t("zen.faq.a4.p1.afterPricing")} {i18n.t("zen.faq.a4.p2.beforeAccount")}{" "}
|
||||
<a href="/auth">{i18n.t("zen.faq.a4.p2.accountLink")}</a>. {i18n.t("zen.faq.a4.p3")}
|
||||
</Faq>
|
||||
@@ -301,7 +301,7 @@ export default function Home() {
|
||||
<li>
|
||||
<Faq question={i18n.t("zen.faq.q5")}>
|
||||
{i18n.t("zen.faq.a5.beforeExceptions")}{" "}
|
||||
<a href="/v2/docs/console/models/#privacy">{i18n.t("zen.faq.a5.exceptionsLink")}</a>.
|
||||
<a href={language.route("/docs/zen/#privacy")}>{i18n.t("zen.faq.a5.exceptionsLink")}</a>.
|
||||
</Faq>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"name": "@opencode/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -32,12 +32,15 @@ export function rewrite<ID extends string>(
|
||||
const replacement = resolve(input.specifier, { ...input, settings }) ?? plain
|
||||
const translated = options(replacement, input.modelID, settings)
|
||||
if (target.package !== undefined || replacement !== input.specifier) target.package = replacement
|
||||
// Without `provider`, the native compatible package reports every AI SDK-mapped host as `openai-compatible`.
|
||||
const identity =
|
||||
replacement === "@opencode/ai/providers/openai-compatible" && !Provider.builtinPackages.has(input.specifier)
|
||||
? { provider: input.canonical ?? input.providerID }
|
||||
: {}
|
||||
target.settings = target.settings === undefined ? undefined : { ...translated.settings, ...identity }
|
||||
target.settings =
|
||||
target.settings === undefined
|
||||
? undefined
|
||||
: {
|
||||
...translated.settings,
|
||||
...(!NATIVE.has(input.specifier) && replacement === "@opencode/ai/providers/openai-compatible"
|
||||
? { provider: input.canonical ?? input.providerID }
|
||||
: {}),
|
||||
}
|
||||
target.headers = Provider.mergeHeaders(translated.headers, target.headers)
|
||||
target.body = Provider.mergeOverlay(translated.body, target.body)
|
||||
target.variants = target.variants?.map((variant) => {
|
||||
@@ -119,6 +122,15 @@ const HOSTS: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
||||
"zhipuai-coding-plan": protocols("zai-coding-plan"),
|
||||
}
|
||||
|
||||
const NATIVE = new Set([
|
||||
...Object.values(PACKAGES),
|
||||
...Object.values(HOSTS).flatMap((host) => Object.values(host)),
|
||||
"@opencode/ai/providers/azure/chat",
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle",
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/chat",
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/responses",
|
||||
])
|
||||
|
||||
export function native(npm: string, context: Context & { readonly settings?: Provider.Settings }): string | undefined {
|
||||
const host = HOSTS[context.providerID]?.[npm]
|
||||
if (host) return host
|
||||
@@ -130,10 +142,8 @@ export function native(npm: string, context: Context & { readonly settings?: Pro
|
||||
const azureChat = (context: Context & { readonly settings?: Provider.Settings }) =>
|
||||
context.shape === "completions" || context.settings?.useCompletionUrls === true
|
||||
|
||||
// Mantle serves Claude only through Anthropic Messages and GPT-OSS only through Chat.
|
||||
const mantle = (modelID: string | undefined) => {
|
||||
if (modelID === undefined) return "@opencode/ai/providers/amazon-bedrock/mantle"
|
||||
if (modelID.includes("anthropic.")) return "@opencode/ai/providers/amazon-bedrock/mantle/messages"
|
||||
return `@opencode/ai/providers/amazon-bedrock/mantle/${modelID.includes("gpt-oss") ? "chat" : "responses"}`
|
||||
}
|
||||
|
||||
@@ -141,14 +151,9 @@ function resolve(specifier: string, context: Context & { readonly settings?: Pro
|
||||
const npm = Provider.packageName(specifier)
|
||||
if (Provider.isAISDK(specifier) || npm in PACKAGES || npm in (HOSTS[context.providerID] ?? {}))
|
||||
return native(npm, context)
|
||||
if (npm === "@opencode/ai/providers/amazon-bedrock/mantle" || npm === "@opencode/ai/providers/amazon-bedrock-mantle")
|
||||
return mantle(context.modelID)
|
||||
if (
|
||||
(npm === "@opencode/ai/providers/azure" || npm === "@opencode/ai/providers/azure/responses") &&
|
||||
azureChat(context)
|
||||
)
|
||||
return "@opencode/ai/providers/azure/chat"
|
||||
return Provider.builtinPackages.has(npm) ? npm : undefined
|
||||
if (npm === "@opencode/ai/providers/amazon-bedrock/mantle") return mantle(context.modelID)
|
||||
if (npm === "@opencode/ai/providers/azure/responses" && azureChat(context)) return "@opencode/ai/providers/azure/chat"
|
||||
return NATIVE.has(npm) ? npm : undefined
|
||||
}
|
||||
|
||||
type Overlay = {
|
||||
|
||||
@@ -50,8 +50,6 @@ export const Options = Schema.Struct({
|
||||
// wellknown, file, and content entries still load.
|
||||
global: Schema.optional(Schema.Boolean),
|
||||
system: Schema.optional(Schema.Union([Schema.Boolean, Schema.String])),
|
||||
// false skips ~/.claude and project .claude dirs.
|
||||
claude: Schema.optional(Schema.Boolean),
|
||||
file: Schema.optional(Schema.String),
|
||||
content: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
@@ -126,15 +126,12 @@ export const discover = Effect.fn("ConfigDiscovery.discover")(function* (options
|
||||
visible.filter((item) => path.basename(item) === ".opencode").toReversed(),
|
||||
(directory) => fs.isDir(directory).pipe(Effect.map((present) => ({ path: directory, present }))),
|
||||
),
|
||||
claude:
|
||||
options?.claude === false
|
||||
? []
|
||||
: [
|
||||
...new Set([
|
||||
...(globalEnabled ? [globalClaudeDirectory] : []),
|
||||
...visible.filter((item) => path.basename(item) === ".claude").toReversed(),
|
||||
]),
|
||||
],
|
||||
claude: [
|
||||
...new Set([
|
||||
...(globalEnabled ? [globalClaudeDirectory] : []),
|
||||
...visible.filter((item) => path.basename(item) === ".claude").toReversed(),
|
||||
]),
|
||||
],
|
||||
agents: [
|
||||
...new Set([
|
||||
...(globalEnabled ? [globalAgentsDirectory] : []),
|
||||
|
||||
@@ -257,7 +257,6 @@ function normalizeMcp(input: Record<string, unknown>, encoded: Record<string, un
|
||||
["experimental", "mcp_timeout"],
|
||||
diagnostics,
|
||||
)
|
||||
// V1 never applied this to connecting, unlike a per-server timeout.
|
||||
if (value !== undefined) {
|
||||
timeout.catalog = value
|
||||
timeout.execution = value
|
||||
@@ -273,18 +272,13 @@ function normalizeMcp(input: Record<string, unknown>, encoded: Record<string, un
|
||||
return
|
||||
}
|
||||
if (name === "servers" && !isDirectLegacyMcp(value)) {
|
||||
Object.entries(
|
||||
decodeMap(expandServerTimeouts(value), ConfigMCP.Server, path, diagnostics, decodeEncoded),
|
||||
).forEach(([key, server]) => setOwn(nativeServers, key, server))
|
||||
Object.entries(decodeMap(value, ConfigMCP.Server, path, diagnostics, decodeEncoded)).forEach(
|
||||
([key, server]) => setOwn(nativeServers, key, server),
|
||||
)
|
||||
return
|
||||
}
|
||||
if (name === "timeout" && !isDirectLegacyMcp(value)) {
|
||||
normalizeMcpTimeout(
|
||||
typeof value === "number" ? ConfigMigrateV1.migrateMcpTimeout(value) : value,
|
||||
timeout,
|
||||
path,
|
||||
diagnostics,
|
||||
)
|
||||
normalizeMcpTimeout(value, timeout, path, diagnostics)
|
||||
return
|
||||
}
|
||||
const server = decodeValue(ConfigMCPV1.Info, value, path, diagnostics)
|
||||
@@ -304,19 +298,6 @@ function normalizeMcp(input: Record<string, unknown>, encoded: Record<string, un
|
||||
}
|
||||
}
|
||||
|
||||
// Native servers accept the V1 bare-number timeout so one V1-shaped field does not drop the whole server.
|
||||
function expandServerTimeouts(value: unknown) {
|
||||
if (!isRecord(value)) return value
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([name, server]) => [
|
||||
name,
|
||||
isRecord(server) && typeof server.timeout === "number"
|
||||
? { ...server, timeout: ConfigMigrateV1.migrateMcpTimeout(server.timeout) }
|
||||
: server,
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeMcpTimeout(
|
||||
value: unknown,
|
||||
timeout: Record<string, unknown>,
|
||||
|
||||
@@ -90,14 +90,6 @@ export const Plugin = define({
|
||||
}
|
||||
next.set(item, { providerID, models: definitions })
|
||||
}
|
||||
for (const [item, definition] of next) {
|
||||
for (const [id, config] of Object.entries(item.models ?? {})) {
|
||||
if (config.limit === undefined) continue
|
||||
providers.models.update(definition.providerID, id, (model) => {
|
||||
model.limit = { ...model.limit, ...config.limit }
|
||||
})
|
||||
}
|
||||
}
|
||||
sources.defaultModel = Config.latest(loaded.entries, "model")
|
||||
sources.models = next
|
||||
})
|
||||
@@ -158,13 +150,7 @@ export const Plugin = define({
|
||||
if (config.disabled !== undefined) model.enabled = !config.disabled
|
||||
if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
|
||||
})
|
||||
// Inherited variants are spelled for the base model's package, so a package override needs fresh ones.
|
||||
// A base without variants has no reasoning support to respell.
|
||||
const repackaged =
|
||||
source?.base !== undefined &&
|
||||
source.base.variants.length > 0 &&
|
||||
models.get(providerID, id)?.package !== source.base.package
|
||||
if (config.variants === undefined && (!source?.base || repackaged))
|
||||
if (config.variants === undefined && !source?.base)
|
||||
models.update(providerID, id, (model) => {
|
||||
model.variants = [
|
||||
...Variant.resolve({
|
||||
|
||||
@@ -3,8 +3,8 @@ export * as ConfigVariable from "./variable.js"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { type ParseError, parse } from "jsonc-parser"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { InvalidError } from "../v1/config/error.js"
|
||||
|
||||
type ParseSource =
|
||||
| {
|
||||
@@ -24,17 +24,12 @@ type SubstituteInput = ParseSource & {
|
||||
|
||||
/** Apply {env:VAR} and {file:path} substitutions to config text. */
|
||||
export const substitute = Effect.fn("ConfigVariable.substitute")(function* (input: SubstituteInput) {
|
||||
const env = (escape: boolean) =>
|
||||
input.text.replace(/\{env:([^}]+)\}/g, (_, name: string) => {
|
||||
const value = (input.env?.[name] ?? process.env[name]) || ""
|
||||
return escape ? JSON.stringify(value).slice(1, -1) : value
|
||||
})
|
||||
// Escape only when raw values break the JSON (e.g. Windows paths), so configs that parse today keep their meaning.
|
||||
const raw = yield* substituteFiles(input, env(false))
|
||||
const errors: ParseError[] = []
|
||||
if (input.text.includes("{env:")) parse(raw, errors, { allowTrailingComma: true })
|
||||
if (!errors.length) return raw
|
||||
return yield* substituteFiles(input, env(true))
|
||||
const text = input.text.replace(
|
||||
/\{env:([^}]+)\}/g,
|
||||
(_, varName: string) => (input.env?.[varName] ?? process.env[varName]) || "",
|
||||
)
|
||||
if (!text.includes("{file:")) return text
|
||||
return yield* substituteFiles(input, text)
|
||||
})
|
||||
|
||||
const substituteFiles = Effect.fnUntraced(function* (input: SubstituteInput, text: string) {
|
||||
@@ -60,20 +55,24 @@ const substituteFiles = Effect.fnUntraced(function* (input: SubstituteInput, tex
|
||||
const filePath = token.slice("{file:".length, -1)
|
||||
const expandedPath = filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath
|
||||
const resolvedPath = path.isAbsolute(expandedPath) ? expandedPath : path.resolve(configDir, expandedPath)
|
||||
const fileContent = yield* fs
|
||||
.readFileStringSafe(resolvedPath)
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new InvalidError({ path: configSource, message: `bad file reference: "${token}"` }, { cause: error }),
|
||||
),
|
||||
)
|
||||
// Like a missing {env:VAR}, a missing or unreadable file substitutes empty so one stale secret
|
||||
// reference cannot take down every Location that loads this config.
|
||||
const fileContent = yield* fs.readFileString(resolvedPath).pipe(
|
||||
Effect.catch((cause) =>
|
||||
Effect.logWarning("config file reference is missing or unreadable", {
|
||||
source: configSource,
|
||||
reference: token,
|
||||
path: resolvedPath,
|
||||
cause,
|
||||
}).pipe(Effect.as("")),
|
||||
),
|
||||
)
|
||||
if (fileContent === undefined)
|
||||
yield* Effect.logWarning("config file reference is missing or unreadable", {
|
||||
source: configSource,
|
||||
reference: token,
|
||||
path: resolvedPath,
|
||||
})
|
||||
|
||||
out += JSON.stringify(fileContent.trim()).slice(1, -1)
|
||||
out += JSON.stringify((fileContent ?? "").trim()).slice(1, -1)
|
||||
cursor = index + token.length
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ const layer = Layer.effect(
|
||||
const all = Array.from(byProvider.values())
|
||||
.flatMap((models) => Array.from(models.values()))
|
||||
.sort((left, right) => right.time.released - left.time.released)
|
||||
cached = freeze({ data, all, available: preferFree(all.filter((model) => model.enabled)), byProvider }, true)
|
||||
cached = freeze({ data, all, available: all.filter((model) => model.enabled), byProvider }, true)
|
||||
return cached
|
||||
}
|
||||
})
|
||||
@@ -286,24 +286,6 @@ const layer = Layer.effect(
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Provider.node, Bus.node, Location.node] })
|
||||
|
||||
// Signed out, only free OpenCode models are enabled. Clients default to the first available model, so keep
|
||||
// these free models ahead of each new free launch instead of letting release order pick the default.
|
||||
const PREFERRED_FREE = [
|
||||
"big-pickle",
|
||||
"deepseek-v4.1-flash-free",
|
||||
"glm-5.3-flash-free",
|
||||
"muse-spark-1.3-contributor-free",
|
||||
]
|
||||
|
||||
function preferFree(models: Info[]) {
|
||||
const freeOnly = models.every(
|
||||
(model) => model.providerID === Provider.ID.opencode && !model.cost.some((cost) => cost.input > 0),
|
||||
)
|
||||
if (!freeOnly) return models
|
||||
const picks = PREFERRED_FREE.flatMap((id) => models.find((model) => model.id === id) ?? [])
|
||||
return [...picks, ...models.filter((model) => !picks.includes(model))]
|
||||
}
|
||||
|
||||
export function supportsText(model: Pick<Info, "capabilities">) {
|
||||
return (
|
||||
(model.capabilities.input.length === 0 || model.capabilities.input.some((item) => item.startsWith("text"))) &&
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"deepinfra":{"id":"deepinfra","env":["DEEPINFRA_API_KEY"],"npm":"@ai-sdk/deepinfra","name":"Deep Infra","doc":"https://deepinfra.com/models","models":{"anthropic/claude-fable-5":{"id":"anthropic/claude-fable-5","name":"Claude Fable 5","description":"Claude model for creative writing, analysis, and controlled agent workflows","family":"claude-fable","attachment":true,"reasoning":true,"reasoning_options":[{"type":"effort","values":["low","medium","high","xhigh","max"]}],"tool_call":true,"structured_output":true,"temperature":false,"knowledge":"2026-01-31","release_date":"2026-06-09","last_updated":"2026-06-09","modalities":{"input":["text","image","pdf"],"output":["text"]},"open_weights":false,"limit":{"context":1000000,"output":128000},"cost":{"input":10,"output":50},"canonical_model_id":"anthropic/claude-fable-5"},"tencent/Hy3":{"id":"tencent/Hy3","name":"Hy3","description":"Tencent Hy reasoning model for coding, instruction following, and agent tasks","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-07-06","last_updated":"2026-07-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":262144,"input":192000,"output":128000},"status":"deprecated","cost":{"input":0.13,"output":0.53,"cache_read":0.033},"canonical_model_id":"tencent/hy3"},"tencent/Hy4-preview":{"id":"tencent/Hy4-preview","name":"Hy4 preview","description":"A next-generation productivity model with significantly enhanced Agent and complex task execution capabilities.","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","high"]}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-08-28","last_updated":"2026-08-28","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":64000},"cost":{"input":0.834,"output":2.501,"cache_read":0.042},"canonical_model_id":"tencent/hy4-preview"},"meta-llama/Llama-3.3-70B-Instruct-Turbo":{"id":"meta-llama/Llama-3.3-70B-Instruct-Turbo","name":"Llama 3.3 70B Turbo","description":"Compact Llama instruction model for fast chat and local deployment","family":"llama","attachment":false,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2024-12-06","last_updated":"2024-12-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":131072,"output":16384},"cost":{"input":0.1,"output":0.32}},"meta-llama/Llama-4-Scout-17B-16E-Instruct":{"id":"meta-llama/Llama-4-Scout-17B-16E-Instruct","name":"Llama 4 Scout 17B","description":"Open multimodal Llama model for long-context analysis and efficient agents","family":"llama","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":327680,"output":16384},"cost":{"input":0.1,"output":0.3}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"id":"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8","name":"Llama 4 Maverick 17B FP8","description":"Open multimodal Llama model for strong reasoning and fast responses","family":"llama","attachment":true,"reasoning":false,"tool_call":false,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":0.2,"output":0.8}},"XiaomiMiMo/MiMo-V2.6-Pro":{"id":"XiaomiMiMo/MiMo-V2.6-Pro","name":"MiMo-V2.6-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.43,"output":0.87,"cache_read":0.0036},"canonical_model_id":"xiaomi/mimo-v2.6-pro"},"XiaomiMiMo/MiMo-V2.5-Pro":{"id":"XiaomiMiMo/MiMo-V2.5-Pro","name":"MiMo-V2.5-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"interleaved":{"field":"reasoning_content"},"structured_output":true,"temperature":true,"knowledge":"2024-12","release_date":"2026-04-22","last_updated":"2026-04-22","modalities":{"input":["text","audio"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":1,"output":3,"cache_read":0.2},"canonical_model_id":"xiaomi/mimo-v2.5-pro"},"XiaomiMiMo/MiMo-V2.6-Flash":{"id":"XiaomiMiMo/MiMo-V2.6-Flash","name":"MLine truncated
|
||||
{"deepinfra":{"id":"deepinfra","env":["DEEPINFRA_API_KEY"],"npm":"@ai-sdk/deepinfra","name":"Deep Infra","doc":"https://deepinfra.com/models","models":{"tencent/Hy3":{"id":"tencent/Hy3","name":"Hy3","description":"Tencent Hy reasoning model for coding, instruction following, and agent tasks","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-07-06","last_updated":"2026-07-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":262144,"input":192000,"output":128000},"status":"deprecated","cost":{"input":0.13,"output":0.53,"cache_read":0.033},"canonical_model_id":"tencent/hy3"},"tencent/Hy4-preview":{"id":"tencent/Hy4-preview","name":"Hy4 preview","description":"A next-generation productivity model with significantly enhanced Agent and complex task execution capabilities.","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","high"]}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-08-28","last_updated":"2026-08-28","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":64000},"cost":{"input":0.834,"output":2.501,"cache_read":0.042},"canonical_model_id":"tencent/hy4-preview"},"meta-llama/Llama-3.3-70B-Instruct-Turbo":{"id":"meta-llama/Llama-3.3-70B-Instruct-Turbo","name":"Llama 3.3 70B Turbo","description":"Compact Llama instruction model for fast chat and local deployment","family":"llama","attachment":false,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2024-12-06","last_updated":"2024-12-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":131072,"output":16384},"cost":{"input":0.1,"output":0.32}},"meta-llama/Llama-4-Scout-17B-16E-Instruct":{"id":"meta-llama/Llama-4-Scout-17B-16E-Instruct","name":"Llama 4 Scout 17B","description":"Open multimodal Llama model for long-context analysis and efficient agents","family":"llama","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":327680,"output":16384},"cost":{"input":0.1,"output":0.3}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"id":"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8","name":"Llama 4 Maverick 17B FP8","description":"Open multimodal Llama model for strong reasoning and fast responses","family":"llama","attachment":true,"reasoning":false,"tool_call":false,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":0.2,"output":0.8}},"XiaomiMiMo/MiMo-V2.6-Pro":{"id":"XiaomiMiMo/MiMo-V2.6-Pro","name":"MiMo-V2.6-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.43,"output":0.87,"cache_read":0.0036},"canonical_model_id":"xiaomi/mimo-v2.6-pro"},"XiaomiMiMo/MiMo-V2.5-Pro":{"id":"XiaomiMiMo/MiMo-V2.5-Pro","name":"MiMo-V2.5-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"interleaved":{"field":"reasoning_content"},"structured_output":true,"temperature":true,"knowledge":"2024-12","release_date":"2026-04-22","last_updated":"2026-04-22","modalities":{"input":["text","audio"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":1,"output":3,"cache_read":0.2},"canonical_model_id":"xiaomi/mimo-v2.5-pro"},"XiaomiMiMo/MiMo-V2.6-Flash":{"id":"XiaomiMiMo/MiMo-V2.6-Flash","name":"MiMo-V2.6-Flash","description":"MiMo Flash model for multimodal coding agents and long-context automation","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028},"canonical_model_id":"xiaomi/mimo-v2.6-flash"},"XiaomiMiMo/MiMo-V2.5":{"id":"XiaomiMiMo/MiMo-V2.5","name":"MiMo-V2.5","description":"Open MiMo model for multimodal codLine truncated
|
||||
@@ -7,13 +7,12 @@ import { Model } from "../model.js"
|
||||
import { ModelsDev } from "../models-dev.js"
|
||||
|
||||
// These catalog entries require inference profiles on Bedrock Runtime.
|
||||
// Opus/Sonnet 4.6 and Opus 5.5 support in-region calls in eu-west-2 and must remain available.
|
||||
// Opus/Sonnet 4.6 support in-region calls in eu-west-2 and must remain available.
|
||||
const BEDROCK_PROFILE_ONLY_IDS = [
|
||||
"amazon.nova-2-lite-v1:0",
|
||||
"anthropic.claude-fable-5",
|
||||
"anthropic.claude-fable-5-1",
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
"anthropic.claude-haiku-5-5",
|
||||
"anthropic.claude-opus-4-1-20250805-v1:0",
|
||||
"anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
"anthropic.claude-opus-4-7",
|
||||
@@ -21,13 +20,8 @@ const BEDROCK_PROFILE_ONLY_IDS = [
|
||||
"anthropic.claude-opus-5",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"anthropic.claude-sonnet-5",
|
||||
"anthropic.claude-sonnet-5-5",
|
||||
"deepseek.r1-v1:0",
|
||||
"meta.llama4-maverick-17b-instruct-v1:0",
|
||||
"meta.llama4-scout-17b-instruct-v1:0",
|
||||
"mistral.pixtral-large-2502-v1:0",
|
||||
"writer.palmyra-x4-v1:0",
|
||||
"writer.palmyra-x5-v1:0",
|
||||
]
|
||||
|
||||
export const ModelsDevPlugin = define({
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
export * as WebSearchTinyFish from "./tinyfish.js"
|
||||
|
||||
import { define } from "@opencode/plugin/effect/plugin"
|
||||
import { Duration, Effect, Option, Schema, Scope } from "effect"
|
||||
import { HttpClient, HttpClientRequest } from "effect/http"
|
||||
import { Effect, Option, Schema, Scope } from "effect"
|
||||
import { HttpClient } from "effect/http"
|
||||
import { App } from "../../app.js"
|
||||
import { collectBoundedResponseBody } from "../../tool/http-body.js"
|
||||
import { WebSearchMcp } from "./mcp.js"
|
||||
|
||||
export const endpoint = "https://agent.tinyfish.ai/mcp"
|
||||
@@ -30,22 +29,6 @@ const SearchResponse = Schema.fromJsonString(
|
||||
)
|
||||
const decodeSearchResponse = Schema.decodeUnknownOption(SearchResponse)
|
||||
|
||||
const InitializeRequest = Schema.Struct({
|
||||
jsonrpc: Schema.Literal("2.0"),
|
||||
id: Schema.Literal(1),
|
||||
method: Schema.Literal("initialize"),
|
||||
params: Schema.Struct({
|
||||
protocolVersion: Schema.String,
|
||||
capabilities: Schema.Struct({}),
|
||||
clientInfo: Schema.Struct({ name: Schema.String, version: Schema.String }),
|
||||
}),
|
||||
})
|
||||
|
||||
const InitializedNotification = Schema.Struct({
|
||||
jsonrpc: Schema.Literal("2.0"),
|
||||
method: Schema.Literal("notifications/initialized"),
|
||||
})
|
||||
|
||||
export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
||||
id: "opencode.websearch.tinyfish",
|
||||
effect: Effect.fn("WebSearchTinyFish.Plugin")(function* (ctx) {
|
||||
@@ -69,21 +52,18 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
||||
Effect.gen(function* () {
|
||||
const connection = yield* ctx.integration.connection.active("tinyfish")
|
||||
const credential = connection ? yield* ctx.integration.connection.resolve(connection) : undefined
|
||||
const headers = {
|
||||
"User-Agent": App.useragent(ctx.app),
|
||||
...(credential?.type === "key"
|
||||
? { "X-API-Key": credential.key }
|
||||
: { "X-TinyFish-Access-Mode": "keyless" }),
|
||||
}
|
||||
// Keyed requests are rejected without the `Mcp-Session-Id` returned by `initialize`; keyless ones are not.
|
||||
const session = credential?.type === "key" ? yield* initialize(http, headers) : undefined
|
||||
const result = yield* WebSearchMcp.call(
|
||||
http,
|
||||
endpoint,
|
||||
"search",
|
||||
{ input: McpInput, output: McpOutput },
|
||||
{ query: input.query },
|
||||
{ ...headers, ...(session === undefined ? {} : { "Mcp-Session-Id": session }) },
|
||||
{
|
||||
"User-Agent": App.useragent(ctx.app),
|
||||
...(credential?.type === "key"
|
||||
? { "X-API-Key": credential.key }
|
||||
: { "X-TinyFish-Access-Mode": "keyless" }),
|
||||
},
|
||||
)
|
||||
const content = result?.content.find((item) => item.text)
|
||||
const response = content ? Option.getOrUndefined(decodeSearchResponse(content.text)) : undefined
|
||||
@@ -100,47 +80,3 @@ export const Plugin = define<HttpClient.HttpClient | Scope.Scope>({
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
const initialize = (http: HttpClient.HttpClient, headers: Record<string, string>) =>
|
||||
Effect.gen(function* () {
|
||||
const client = HttpClient.withScope(HttpClient.filterStatusOk(http))
|
||||
const request = yield* HttpClientRequest.post(endpoint).pipe(
|
||||
HttpClientRequest.accept("application/json, text/event-stream"),
|
||||
HttpClientRequest.setHeaders(headers),
|
||||
HttpClientRequest.schemaBodyJson(InitializeRequest)({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: "2025-06-18",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "opencode", version: "1.0" },
|
||||
},
|
||||
}),
|
||||
)
|
||||
const response = yield* client.execute(request)
|
||||
yield* collectBoundedResponseBody(
|
||||
response,
|
||||
WebSearchMcp.MAX_RESPONSE_BYTES,
|
||||
() => new Error(`initialize response exceeded ${WebSearchMcp.MAX_RESPONSE_BYTES} bytes`),
|
||||
)
|
||||
const session = response.headers["mcp-session-id"]
|
||||
if (session === undefined)
|
||||
return yield* Effect.fail(new Error("TinyFish MCP initialize did not return an Mcp-Session-Id header"))
|
||||
const notification = yield* HttpClientRequest.post(endpoint).pipe(
|
||||
HttpClientRequest.accept("application/json, text/event-stream"),
|
||||
HttpClientRequest.setHeaders({ ...headers, "Mcp-Session-Id": session }),
|
||||
HttpClientRequest.schemaBodyJson(InitializedNotification)({
|
||||
jsonrpc: "2.0",
|
||||
method: "notifications/initialized",
|
||||
}),
|
||||
)
|
||||
yield* client.execute(notification)
|
||||
return session
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.timeoutOrElse({
|
||||
duration: Duration.seconds(25),
|
||||
orElse: () => Effect.fail(new Error("TinyFish initialize request timed out")),
|
||||
}),
|
||||
)
|
||||
@@ -46,25 +46,19 @@ export type ProviderPackage = ProviderPackageDefinition
|
||||
|
||||
const packages = new Map<string, Promise<unknown>>()
|
||||
const builtins = new Map<string, () => Promise<unknown>>([
|
||||
["@opencode/ai/providers/alibaba", () => import("@opencode/ai/providers/alibaba")],
|
||||
["@opencode/ai/providers/alibaba/chat", () => import("@opencode/ai/providers/alibaba/chat")],
|
||||
["@opencode/ai/providers/alibaba/messages", () => import("@opencode/ai/providers/alibaba/messages")],
|
||||
["@opencode/ai/providers/alibaba/responses", () => import("@opencode/ai/providers/alibaba/responses")],
|
||||
["@opencode/ai/providers/amazon-bedrock", () => import("@opencode/ai/providers/amazon-bedrock")],
|
||||
["@opencode/ai/providers/amazon-bedrock/mantle", () => import("@opencode/ai/providers/amazon-bedrock/mantle")],
|
||||
[
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/chat",
|
||||
() => import("@opencode/ai/providers/amazon-bedrock/mantle/chat"),
|
||||
],
|
||||
[
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/messages",
|
||||
() => import("@opencode/ai/providers/amazon-bedrock/mantle/messages"),
|
||||
],
|
||||
[
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/responses",
|
||||
() => import("@opencode/ai/providers/amazon-bedrock/mantle/responses"),
|
||||
],
|
||||
["@opencode/ai/providers/amazon-bedrock-mantle", () => import("@opencode/ai/providers/amazon-bedrock-mantle")],
|
||||
["@opencode/ai/providers/alibaba/chat", () => import("@opencode/ai/providers/alibaba/chat")],
|
||||
["@opencode/ai/providers/alibaba/messages", () => import("@opencode/ai/providers/alibaba/messages")],
|
||||
["@opencode/ai/providers/alibaba/responses", () => import("@opencode/ai/providers/alibaba/responses")],
|
||||
["@opencode/ai/providers/anthropic", () => import("@opencode/ai/providers/anthropic")],
|
||||
["@opencode/ai/providers/anthropic-compatible", () => import("@opencode/ai/providers/anthropic-compatible")],
|
||||
["@opencode/ai/providers/azure", () => import("@opencode/ai/providers/azure")],
|
||||
@@ -83,34 +77,23 @@ const builtins = new Map<string, () => Promise<unknown>>([
|
||||
["@opencode/ai/providers/google", () => import("@opencode/ai/providers/google")],
|
||||
["@opencode/ai/providers/google/interactions", () => import("@opencode/ai/providers/google/interactions")],
|
||||
["@opencode/ai/providers/google-vertex", () => import("@opencode/ai/providers/google-vertex")],
|
||||
["@opencode/ai/providers/google-vertex/chat", () => import("@opencode/ai/providers/google-vertex/chat")],
|
||||
["@opencode/ai/providers/google-vertex/gemini", () => import("@opencode/ai/providers/google-vertex/gemini")],
|
||||
[
|
||||
"@opencode/ai/providers/google-vertex/interactions",
|
||||
() => import("@opencode/ai/providers/google-vertex/interactions"),
|
||||
],
|
||||
["@opencode/ai/providers/google-vertex/chat", () => import("@opencode/ai/providers/google-vertex/chat")],
|
||||
["@opencode/ai/providers/google-vertex/responses", () => import("@opencode/ai/providers/google-vertex/responses")],
|
||||
["@opencode/ai/providers/google-vertex/messages", () => import("@opencode/ai/providers/google-vertex/messages")],
|
||||
["@opencode/ai/providers/google-vertex/mistral", () => import("@opencode/ai/providers/google-vertex/mistral")],
|
||||
["@opencode/ai/providers/google-vertex/responses", () => import("@opencode/ai/providers/google-vertex/responses")],
|
||||
["@opencode/ai/providers/google-vertex-chat", () => import("@opencode/ai/providers/google-vertex-chat")],
|
||||
[
|
||||
"@opencode/ai/providers/google-vertex-interactions",
|
||||
() => import("@opencode/ai/providers/google-vertex-interactions"),
|
||||
],
|
||||
["@opencode/ai/providers/google-vertex-messages", () => import("@opencode/ai/providers/google-vertex-messages")],
|
||||
["@opencode/ai/providers/google-vertex-mistral", () => import("@opencode/ai/providers/google-vertex-mistral")],
|
||||
["@opencode/ai/providers/google-vertex-responses", () => import("@opencode/ai/providers/google-vertex-responses")],
|
||||
["@opencode/ai/providers/groq", () => import("@opencode/ai/providers/groq")],
|
||||
["@opencode/ai/providers/meta", () => import("@opencode/ai/providers/meta")],
|
||||
["@opencode/ai/providers/meta/chat", () => import("@opencode/ai/providers/meta/chat")],
|
||||
["@opencode/ai/providers/meta/messages", () => import("@opencode/ai/providers/meta/messages")],
|
||||
["@opencode/ai/providers/meta/responses", () => import("@opencode/ai/providers/meta/responses")],
|
||||
["@opencode/ai/providers/minimax", () => import("@opencode/ai/providers/minimax")],
|
||||
["@opencode/ai/providers/minimax/chat", () => import("@opencode/ai/providers/minimax/chat")],
|
||||
["@opencode/ai/providers/minimax/messages", () => import("@opencode/ai/providers/minimax/messages")],
|
||||
["@opencode/ai/providers/minimax/responses", () => import("@opencode/ai/providers/minimax/responses")],
|
||||
["@opencode/ai/providers/mistral", () => import("@opencode/ai/providers/mistral")],
|
||||
["@opencode/ai/providers/moonshot", () => import("@opencode/ai/providers/moonshot")],
|
||||
["@opencode/ai/providers/moonshot/chat", () => import("@opencode/ai/providers/moonshot/chat")],
|
||||
["@opencode/ai/providers/moonshot/messages", () => import("@opencode/ai/providers/moonshot/messages")],
|
||||
["@opencode/ai/providers/moonshot/responses", () => import("@opencode/ai/providers/moonshot/responses")],
|
||||
@@ -130,19 +113,8 @@ const builtins = new Map<string, () => Promise<unknown>>([
|
||||
["@opencode/ai/providers/togetherai", () => import("@opencode/ai/providers/togetherai")],
|
||||
["@opencode/ai/providers/venice", () => import("@opencode/ai/providers/venice")],
|
||||
["@opencode/ai/providers/vercel-ai-gateway", () => import("@opencode/ai/providers/vercel-ai-gateway")],
|
||||
["@opencode/ai/providers/vercel-ai-gateway/chat", () => import("@opencode/ai/providers/vercel-ai-gateway/chat")],
|
||||
[
|
||||
"@opencode/ai/providers/vercel-ai-gateway/messages",
|
||||
() => import("@opencode/ai/providers/vercel-ai-gateway/messages"),
|
||||
],
|
||||
[
|
||||
"@opencode/ai/providers/vercel-ai-gateway/responses",
|
||||
() => import("@opencode/ai/providers/vercel-ai-gateway/responses"),
|
||||
],
|
||||
["@opencode/ai/providers/xai", () => import("@opencode/ai/providers/xai")],
|
||||
["@opencode/ai/providers/zai", () => import("@opencode/ai/providers/zai")],
|
||||
["@opencode/ai/providers/zai/chat", () => import("@opencode/ai/providers/zai/chat")],
|
||||
["@opencode/ai/providers/zai-coding-plan", () => import("@opencode/ai/providers/zai-coding-plan")],
|
||||
["@opencode/ai/providers/zai-coding-plan/chat", () => import("@opencode/ai/providers/zai-coding-plan/chat")],
|
||||
["@opencode/ai/providers/zai-coding-plan/messages", () => import("@opencode/ai/providers/zai-coding-plan/messages")],
|
||||
[
|
||||
@@ -151,9 +123,6 @@ const builtins = new Map<string, () => Promise<unknown>>([
|
||||
],
|
||||
])
|
||||
|
||||
/** Native `@opencode/ai/providers/*` entrypoints bundled with Core and loadable by name from config. */
|
||||
export const builtinPackages: ReadonlySet<string> = new Set(builtins.keys())
|
||||
|
||||
export const loadPackage = Effect.fn("Provider.loadPackage")(function* (input: string, npm?: Npm.Interface) {
|
||||
const specifier = packageName(input)
|
||||
const builtin = builtins.get(specifier)
|
||||
|
||||
@@ -299,10 +299,7 @@ function sanitizeMessage(message: SessionMessage.Info): SessionMessage.Info {
|
||||
summary: redact("compaction-summary", message.id, message.summary),
|
||||
recent: redact("compaction-recent", message.id, message.recent),
|
||||
...(message.status === "completed"
|
||||
? {
|
||||
providerState: metadata("compaction-provider-state", message.id, message.providerState),
|
||||
providerContext: undefined,
|
||||
}
|
||||
? { providerState: metadata("compaction-provider-state", message.id, message.providerState) }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +76,6 @@ export function modelSelection(input?: string, variant?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// V1 had a single MCP timeout, so a bare number bounds every phase.
|
||||
export function migrateMcpTimeout(timeout: number) {
|
||||
return { startup: timeout, catalog: timeout, execution: timeout }
|
||||
}
|
||||
|
||||
export function migrateMcp(info: ConfigMCPV1.Info) {
|
||||
const disabled = info.enabled === undefined ? undefined : !info.enabled
|
||||
if (info.type === "local")
|
||||
@@ -90,7 +85,7 @@ export function migrateMcp(info: ConfigMCPV1.Info) {
|
||||
cwd: info.cwd,
|
||||
environment: info.environment,
|
||||
disabled,
|
||||
timeout: info.timeout === undefined ? undefined : migrateMcpTimeout(info.timeout),
|
||||
timeout: info.timeout === undefined ? undefined : { catalog: info.timeout, execution: info.timeout },
|
||||
}
|
||||
return {
|
||||
type: info.type,
|
||||
@@ -104,7 +99,7 @@ export function migrateMcp(info: ConfigMCPV1.Info) {
|
||||
redirect_uri: info.oauth.redirectUri,
|
||||
},
|
||||
disabled,
|
||||
timeout: info.timeout === undefined ? undefined : migrateMcpTimeout(info.timeout),
|
||||
timeout: info.timeout === undefined ? undefined : { catalog: info.timeout, execution: info.timeout },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,18 +99,11 @@ const responsesEffort = (effort: string): Overlay => ({
|
||||
settings: { reasoningEffort: effort, reasoningSummary: "auto", include: ENCRYPTED_REASONING },
|
||||
})
|
||||
|
||||
const openResponses: Protocol = (_, support) => {
|
||||
const xaiResponses: Protocol = (_, support) => {
|
||||
if (support.type !== "effort") return []
|
||||
return efforts(support.values ?? EFFORTS, responsesEffort)
|
||||
}
|
||||
|
||||
const googleInteractions: Protocol = (_, support) => {
|
||||
if (support.type !== "effort") return []
|
||||
return efforts(support.values ?? EFFORTS, (effort) => ({
|
||||
settings: { thinkingLevel: effort, thinkingSummaries: "auto" },
|
||||
}))
|
||||
}
|
||||
|
||||
const cloudflareAIGateway: Protocol = (model, support) => {
|
||||
const id = modelID(model)
|
||||
if (id.startsWith("openai/")) return openaiResponses(model, support)
|
||||
@@ -473,17 +466,8 @@ const cohere: Protocol = (model, support) => {
|
||||
const vercelGateway: Protocol = (model, support) => {
|
||||
const id = modelID(model)
|
||||
if (id.startsWith("openai/gpt-")) return openaiResponses(model, support)
|
||||
if (id.startsWith("spacexai/grok-")) return openResponses(model, support)
|
||||
return vercelMessages(model, support)
|
||||
}
|
||||
|
||||
const vercelResponses: Protocol = (model, support) =>
|
||||
modelID(model).startsWith("openai/gpt-") ? openaiResponses(model, support) : openResponses(model, support)
|
||||
|
||||
const vercelMessages: Protocol = (model, support) =>
|
||||
modelID(model).startsWith("anthropic/") ? anthropicMessages(model, support) : vercelChat(model, support)
|
||||
|
||||
const vercelChat: Protocol = (model, support) => {
|
||||
if (id.startsWith("spacexai/grok-")) return xaiResponses(model, support)
|
||||
if (id.startsWith("anthropic/")) return anthropicMessages(model, support)
|
||||
switch (support.type) {
|
||||
case "effort":
|
||||
return openaiChat(model, support)
|
||||
@@ -494,7 +478,7 @@ const vercelChat: Protocol = (model, support) => {
|
||||
model,
|
||||
support,
|
||||
(tokens) => ({ settings: { thinking: { type: "enabled", budgetTokens: tokens } } }),
|
||||
modelID(model).startsWith("alibaba/") ? ALIBABA_THINKING_BUDGET_MAX : model.limit.output,
|
||||
id.startsWith("alibaba/") ? ALIBABA_THINKING_BUDGET_MAX : model.limit.output,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -592,32 +576,18 @@ const gitlab: Protocol = (model, support) => {
|
||||
|
||||
const PROTOCOLS: Readonly<Record<string, Protocol>> = {
|
||||
"@opencode/ai/providers/openai": openaiResponses,
|
||||
"@opencode/ai/providers/openai/responses": openaiResponses,
|
||||
"@opencode/ai/providers/azure": openaiResponses,
|
||||
"@opencode/ai/providers/azure/responses": openaiResponses,
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle": openaiResponses,
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/chat": openaiResponses,
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/responses": openaiResponses,
|
||||
"@opencode/ai/providers/amazon-bedrock-mantle": openaiResponses,
|
||||
"@opencode/ai/providers/alibaba/responses": openaiResponses,
|
||||
"@opencode/ai/providers/meta": openaiResponses,
|
||||
"@opencode/ai/providers/meta/responses": openaiResponses,
|
||||
"@opencode/ai/providers/minimax/responses": openaiResponses,
|
||||
"@opencode/ai/providers/moonshot/responses": openaiResponses,
|
||||
"@opencode/ai/providers/zai-coding-plan/responses": openaiResponses,
|
||||
|
||||
"@opencode/ai/providers/openai-compatible/responses": openResponses,
|
||||
"@opencode/ai/providers/openai-compatible-responses": openResponses,
|
||||
"@opencode/ai/providers/google-vertex/responses": openResponses,
|
||||
"@opencode/ai/providers/google-vertex-responses": openResponses,
|
||||
"@opencode/ai/providers/xai": openResponses,
|
||||
|
||||
"@opencode/ai/providers/openai-compatible": openaiCompatible,
|
||||
"@opencode/ai/providers/openai/chat": openaiChat,
|
||||
"@opencode/ai/providers/azure/chat": openaiChat,
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/chat": openaiChat,
|
||||
"@opencode/ai/providers/google-vertex/chat": vertexChat,
|
||||
"@opencode/ai/providers/google-vertex-chat": vertexChat,
|
||||
"@opencode/ai/providers/alibaba": alibabaChat,
|
||||
"@opencode/ai/providers/alibaba/chat": alibabaChat,
|
||||
"@opencode/ai/providers/baseten": basetenChat,
|
||||
"@opencode/ai/providers/cerebras": openaiChat,
|
||||
@@ -631,35 +601,24 @@ const PROTOCOLS: Readonly<Record<string, Protocol>> = {
|
||||
"@opencode/ai/providers/meta/chat": openaiChat,
|
||||
"@opencode/ai/providers/minimax/chat": openaiChat,
|
||||
"@opencode/ai/providers/mistral": openaiChat,
|
||||
"@opencode/ai/providers/google-vertex/mistral": openaiChat,
|
||||
"@opencode/ai/providers/google-vertex-mistral": openaiChat,
|
||||
"@opencode/ai/providers/moonshot": moonshotChat,
|
||||
"@opencode/ai/providers/moonshot/chat": moonshotChat,
|
||||
"@opencode/ai/providers/togetherai": openaiChat,
|
||||
"@opencode/ai/providers/venice": veniceChat,
|
||||
"@opencode/ai/providers/zai": zaiChat,
|
||||
"@opencode/ai/providers/xai": xaiResponses,
|
||||
"@opencode/ai/providers/zai/chat": zaiChat,
|
||||
"@opencode/ai/providers/zai-coding-plan": zaiChat,
|
||||
"@opencode/ai/providers/zai-coding-plan/chat": zaiChat,
|
||||
|
||||
"@opencode/ai/providers/anthropic": anthropicMessages,
|
||||
"@opencode/ai/providers/anthropic-compatible": anthropicMessages,
|
||||
"@opencode/ai/providers/amazon-bedrock/mantle/messages": anthropicMessages,
|
||||
"@opencode/ai/providers/google-vertex/messages": anthropicMessages,
|
||||
"@opencode/ai/providers/google-vertex-messages": anthropicMessages,
|
||||
"@opencode/ai/providers/google-vertex/mistral": openaiChat,
|
||||
"@opencode/ai/providers/alibaba/messages": alibabaMessages,
|
||||
"@opencode/ai/providers/meta/messages": anthropicMessages,
|
||||
"@opencode/ai/providers/minimax": minimaxMessages,
|
||||
"@opencode/ai/providers/minimax/messages": minimaxMessages,
|
||||
"@opencode/ai/providers/moonshot/messages": moonshotMessages,
|
||||
"@opencode/ai/providers/zai-coding-plan/messages": zaiMessages,
|
||||
|
||||
"@opencode/ai/providers/google": gemini,
|
||||
"@opencode/ai/providers/google-vertex": gemini,
|
||||
"@opencode/ai/providers/google-vertex/gemini": gemini,
|
||||
"@opencode/ai/providers/google/interactions": googleInteractions,
|
||||
"@opencode/ai/providers/google-vertex/interactions": googleInteractions,
|
||||
"@opencode/ai/providers/google-vertex-interactions": googleInteractions,
|
||||
|
||||
"@opencode/ai/providers/amazon-bedrock": bedrockConverse,
|
||||
"@opencode/ai/providers/cohere": cohere,
|
||||
@@ -668,9 +627,6 @@ const PROTOCOLS: Readonly<Record<string, Protocol>> = {
|
||||
[Provider.aisdk("venice-ai-sdk-provider")]: veniceChat,
|
||||
"@opencode/ai/providers/cloudflare-ai-gateway": cloudflareAIGateway,
|
||||
"@opencode/ai/providers/vercel-ai-gateway": vercelGateway,
|
||||
"@opencode/ai/providers/vercel-ai-gateway/chat": vercelChat,
|
||||
"@opencode/ai/providers/vercel-ai-gateway/messages": vercelMessages,
|
||||
"@opencode/ai/providers/vercel-ai-gateway/responses": vercelResponses,
|
||||
[Provider.aisdk("@ai-sdk/gateway")]: vercelGateway,
|
||||
[Provider.aisdk("@jerome-benoit/sap-ai-provider-v2")]: sapAICore,
|
||||
[Provider.aisdk("gitlab-ai-provider")]: gitlab,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AISDKNative } from "@opencode/core/aisdk-native"
|
||||
import { Provider } from "@opencode/core/provider"
|
||||
|
||||
function map(
|
||||
packageName: string,
|
||||
@@ -226,35 +225,6 @@ describe("AISDKNative", () => {
|
||||
package: "@opencode/ai/providers/amazon-bedrock/mantle/chat",
|
||||
settings: { region: "us-east-1" },
|
||||
})
|
||||
expect(
|
||||
map("@ai-sdk/amazon-bedrock/mantle", { region: "us-east-1", profile: "sso" }, "anthropic.claude-opus-4-8"),
|
||||
).toEqual({
|
||||
package: "@opencode/ai/providers/amazon-bedrock/mantle/messages",
|
||||
settings: { region: "us-east-1", profile: "sso" },
|
||||
})
|
||||
})
|
||||
|
||||
test("translates settings for every bundled native package named in config", () => {
|
||||
const rewrite = (specifier: string, settings: Record<string, unknown>, modelID = "test-model") => {
|
||||
const target: { package?: string; settings?: Record<string, unknown>; headers?: Record<string, string> } = {
|
||||
package: specifier,
|
||||
settings,
|
||||
}
|
||||
AISDKNative.rewrite(target, { specifier, providerID: "test-provider", modelID })
|
||||
return target
|
||||
}
|
||||
|
||||
for (const specifier of Provider.builtinPackages)
|
||||
expect(rewrite(specifier, { headers: { "x-test": "value" } }).headers).toEqual({ "x-test": "value" })
|
||||
expect(
|
||||
rewrite("@opencode/ai/providers/amazon-bedrock/mantle", { bearerToken: "token" }, "anthropic.claude-opus-4-8"),
|
||||
).toMatchObject({
|
||||
package: "@opencode/ai/providers/amazon-bedrock/mantle/messages",
|
||||
settings: { apiKey: "token" },
|
||||
})
|
||||
expect(rewrite("@opencode/ai/providers/azure", { useCompletionUrls: true }).package).toBe(
|
||||
"@opencode/ai/providers/azure/chat",
|
||||
)
|
||||
})
|
||||
|
||||
test("maps Azure deployments and settings to native routes", () => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Model } from "@opencode/core/model"
|
||||
import { ModelResolver } from "@opencode/core/model-resolver"
|
||||
import { Provider } from "@opencode/core/provider"
|
||||
import { AbsolutePath } from "@opencode/core/schema"
|
||||
import { Money } from "@opencode/schema/money"
|
||||
import { location } from "./fixture/location"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
@@ -570,55 +569,6 @@ describe("Provider and Model", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("defaults to big-pickle when only free OpenCode models are available", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
yield* providers.transform((editor) => {
|
||||
editor.update(Provider.ID.opencode, () => {})
|
||||
;["muse-spark-1.3-contributor-free", "new-free", "deepseek-v4.1-flash-free", "big-pickle"].forEach(
|
||||
(id, index) =>
|
||||
editor.models.update(Provider.ID.opencode, Model.ID.make(id), (model) => {
|
||||
model.time.released = 4000 - index * 1000
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
expect((yield* models.available()).map((model) => model.id)).toEqual([
|
||||
Model.ID.make("big-pickle"),
|
||||
Model.ID.make("deepseek-v4.1-flash-free"),
|
||||
Model.ID.make("muse-spark-1.3-contributor-free"),
|
||||
Model.ID.make("new-free"),
|
||||
])
|
||||
expect((yield* models.default())?.id).toBe(Model.ID.make("big-pickle"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps the newest default when a paid model is available", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
yield* providers.transform((editor) => {
|
||||
editor.update(Provider.ID.opencode, () => {})
|
||||
editor.models.update(Provider.ID.opencode, Model.ID.make("big-pickle"), (model) => {
|
||||
model.time.released = 1000
|
||||
})
|
||||
editor.models.update(Provider.ID.opencode, Model.ID.make("paid"), (model) => {
|
||||
model.time.released = 2000
|
||||
model.cost = [
|
||||
{
|
||||
input: Money.USDPerMillionTokens.make(1),
|
||||
output: Money.USDPerMillionTokens.make(1),
|
||||
cache: { read: Money.USDPerMillionTokens.zero, write: Money.USDPerMillionTokens.zero },
|
||||
},
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
expect((yield* models.default())?.id).toBe(Model.ID.make("paid"))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses a transform-provided default model until that transform is replaced", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
|
||||
@@ -14,8 +14,6 @@ import { Credential } from "@opencode/core/credential"
|
||||
import { ConfigV1 } from "../fixture/v1-config/config"
|
||||
import { ConfigPermissionV1 } from "@opencode/core/v1/config/permission"
|
||||
import { ConfigNormalize } from "@opencode/core/config/normalize"
|
||||
import { ConfigVariable } from "@opencode/core/config/variable"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { Watcher } from "@opencode/core/filesystem/watcher"
|
||||
import { Bus } from "@opencode/core/bus"
|
||||
import { Global } from "@opencode/util/global"
|
||||
@@ -134,51 +132,6 @@ describe("Config", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("excludes claude directories when claude is disabled", () =>
|
||||
Effect.acquireDisposable(Effect.promise(() => tmpdir())).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
const global = path.join(tmp.path, "global")
|
||||
const home = path.join(global, "home")
|
||||
const project = path.join(tmp.path, "project")
|
||||
return Effect.promise(() =>
|
||||
Promise.all([
|
||||
fs.mkdir(path.join(home, ".claude"), { recursive: true }),
|
||||
fs.mkdir(path.join(home, ".agents"), { recursive: true }),
|
||||
fs.mkdir(path.join(project, ".claude"), { recursive: true }),
|
||||
fs.mkdir(path.join(project, ".agents"), { recursive: true }),
|
||||
]),
|
||||
).pipe(
|
||||
Effect.andThen(
|
||||
// Fixture control: both global and project claude dirs are found by default.
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const compatibility = yield* config.compatibility!()
|
||||
expect(compatibility.claude.filter((item) => inFixture(tmp.path, item))).toEqual([
|
||||
AbsolutePath.make(path.join(home, ".claude")),
|
||||
AbsolutePath.make(path.join(project, ".claude")),
|
||||
])
|
||||
}).pipe(Effect.provide(testLayer(project, global))),
|
||||
),
|
||||
Effect.andThen(
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const compatibility = yield* config.compatibility!()
|
||||
expect(compatibility.claude).toEqual([])
|
||||
expect(compatibility.agents.filter((item) => inFixture(tmp.path, item))).toEqual([
|
||||
AbsolutePath.make(path.join(home, ".agents")),
|
||||
AbsolutePath.make(path.join(project, ".agents")),
|
||||
])
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
testLayer(project, global, project, undefined, undefined, undefined, undefined, { claude: false }),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("excludes global config reached through the project walk when global is disabled", () =>
|
||||
Effect.acquireDisposable(Effect.promise(() => tmpdir())).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
@@ -1337,10 +1290,8 @@ describe("Config", () => {
|
||||
const previous = {
|
||||
token: process.env.OPENCODE_TEST_MCP_TOKEN,
|
||||
missing: process.env.OPENCODE_TEST_MISSING,
|
||||
flag: process.env.OPENCODE_TEST_FLAG,
|
||||
}
|
||||
process.env.OPENCODE_TEST_FLAG = "false"
|
||||
process.env.OPENCODE_TEST_MCP_TOKEN = String.raw`C:\Users\new\"secret"`
|
||||
process.env.OPENCODE_TEST_MCP_TOKEN = "secret"
|
||||
delete process.env.OPENCODE_TEST_MISSING
|
||||
return previous
|
||||
}),
|
||||
@@ -1357,7 +1308,6 @@ describe("Config", () => {
|
||||
`{
|
||||
// Ignored reference: {file:.}
|
||||
"username": "user-{env:OPENCODE_TEST_MISSING}",
|
||||
"snapshots": {env:OPENCODE_TEST_FLAG},
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"remote": {
|
||||
@@ -1366,8 +1316,7 @@ describe("Config", () => {
|
||||
"headers": {
|
||||
"Authorization": "Bearer {env:OPENCODE_TEST_MCP_TOKEN}",
|
||||
"X-Token": "{file:token.txt}",
|
||||
"X-Missing": "Bearer {file:~/.opencode-test-missing/key}",
|
||||
"X-Folder": "{file:.}"
|
||||
"X-Missing": "Bearer {file:~/.opencode-test-missing/key}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1381,15 +1330,13 @@ describe("Config", () => {
|
||||
const config = yield* Config.Service
|
||||
const document = (yield* config.entries()).find((entry) => entry.type === "document")
|
||||
expect(document?.info.username).toBe("user-")
|
||||
expect(document?.info.snapshots).toBe(false)
|
||||
const remote = document?.info.mcp?.servers?.remote
|
||||
expect(remote?.type).toBe("remote")
|
||||
if (remote?.type !== "remote") return
|
||||
expect(remote.headers).toEqual({
|
||||
Authorization: String.raw`Bearer C:\Users\new\"secret"`,
|
||||
Authorization: "Bearer secret",
|
||||
"X-Token": 'file\n"token"',
|
||||
"X-Missing": "Bearer ",
|
||||
"X-Folder": "",
|
||||
})
|
||||
}).pipe(Effect.provide(testLayer(tmp.path)))
|
||||
}),
|
||||
@@ -1401,25 +1348,10 @@ describe("Config", () => {
|
||||
else process.env.OPENCODE_TEST_MCP_TOKEN = previous.token
|
||||
if (previous.missing === undefined) delete process.env.OPENCODE_TEST_MISSING
|
||||
else process.env.OPENCODE_TEST_MISSING = previous.missing
|
||||
if (previous.flag === undefined) delete process.env.OPENCODE_TEST_FLAG
|
||||
else process.env.OPENCODE_TEST_FLAG = previous.flag
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("keeps env values unescaped when the config already parses", () =>
|
||||
Effect.gen(function* () {
|
||||
const text = yield* ConfigVariable.substitute({
|
||||
type: "virtual",
|
||||
source: "test",
|
||||
dir: "/",
|
||||
text: '{ "quoted": "{env:QUOTED}", "fragment": {env:FRAGMENT} }',
|
||||
env: { QUOTED: String.raw`a\"b`, FRAGMENT: '{"k":"v"}' },
|
||||
})
|
||||
expect(JSON.parse(text)).toEqual({ quoted: 'a"b', fragment: { k: "v" } })
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(FSUtil.node))),
|
||||
)
|
||||
|
||||
it.live("does not load legacy config.json files", () =>
|
||||
Effect.acquireDisposable(Effect.promise(() => tmpdir())).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
@@ -1846,13 +1778,13 @@ describe("Config", () => {
|
||||
type: "local",
|
||||
command: ["node", "server.js"],
|
||||
disabled: true,
|
||||
timeout: { startup: 10000, catalog: 10000, execution: 10000 },
|
||||
timeout: { catalog: 10000, execution: 10000 },
|
||||
},
|
||||
remote: {
|
||||
type: "remote",
|
||||
url: "https://mcp.example.com",
|
||||
oauth: { client_id: "client", callback_port: 19876 },
|
||||
timeout: { startup: 20000, catalog: 20000, execution: 20000 },
|
||||
timeout: { catalog: 20000, execution: 20000 },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -420,35 +420,6 @@ describe("ConfigNormalize", () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("expands numeric MCP timeouts to every phase", () => {
|
||||
const result = normalized({
|
||||
mcp: {
|
||||
timeout: 4000,
|
||||
servers: {
|
||||
local: { type: "local", command: ["local"], timeout: 6000 },
|
||||
remote: { type: "remote", url: "https://remote.example.com", timeout: 8000 },
|
||||
structured: { type: "local", command: ["structured"], timeout: { catalog: 9000 } },
|
||||
invalid: { type: "local", command: ["invalid"], timeout: -1 },
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(result.encoded.mcp).toEqual({
|
||||
timeout: { startup: 4000, catalog: 4000, execution: 4000 },
|
||||
servers: {
|
||||
local: { type: "local", command: ["local"], timeout: { startup: 6000, catalog: 6000, execution: 6000 } },
|
||||
remote: {
|
||||
type: "remote",
|
||||
url: "https://remote.example.com",
|
||||
timeout: { startup: 8000, catalog: 8000, execution: 8000 },
|
||||
},
|
||||
structured: { type: "local", command: ["structured"], timeout: { catalog: 9000 } },
|
||||
},
|
||||
})
|
||||
expect(result.diagnostics.map((item) => [item.kind, item.path])).toEqual([
|
||||
["invalid", ["mcp", "servers", "invalid"]],
|
||||
])
|
||||
})
|
||||
|
||||
test("uses raw MCP discriminators for reserved server names", () => {
|
||||
const result = normalized({
|
||||
mcp: {
|
||||
|
||||
@@ -49,10 +49,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
settings: { compaction: { type: "native" } },
|
||||
models: {
|
||||
native: {},
|
||||
local: {
|
||||
settings: { compaction: { type: "summary" } },
|
||||
package: "@opencode/ai/providers/openai/chat",
|
||||
},
|
||||
local: { settings: { compaction: { type: "summary" } }, package: "@opencode/ai/providers/openai/chat" },
|
||||
unsupported: { package: "@opencode/ai/providers/openai/chat" },
|
||||
},
|
||||
},
|
||||
@@ -699,69 +696,6 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("respells inherited variants when config moves a catalog model to another package", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
const providerID = Provider.ID.make("bedrock")
|
||||
const modelID = Model.ID.make("anthropic.claude-opus-5-5")
|
||||
yield* providers.transform((editor) => {
|
||||
editor.models.update(providerID, modelID, (model) => {
|
||||
model.package = "@opencode/ai/providers/amazon-bedrock"
|
||||
model.variants = [
|
||||
{
|
||||
id: Model.VariantID.make("high"),
|
||||
body: { additionalModelRequestFields: { output_config: { effort: "high" } } },
|
||||
},
|
||||
]
|
||||
})
|
||||
})
|
||||
yield* addPlugin([
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
bedrock: { models: { [modelID]: { package: "@opencode/ai/providers/amazon-bedrock/mantle" } } },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
const model = required(yield* models.get(providerID, modelID))
|
||||
expect(model.package).toBe("@opencode/ai/providers/amazon-bedrock/mantle/messages")
|
||||
expect(model.variants.find((variant) => variant.id === "high")).toEqual({
|
||||
id: Model.VariantID.make("high"),
|
||||
settings: { effort: "high", thinking: { type: "adaptive", display: "summarized" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps a moved catalog model without reasoning variants free of variants", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
const models = yield* Model.Service
|
||||
const providerID = Provider.ID.make("bedrock")
|
||||
const modelID = Model.ID.make("anthropic.claude-plain")
|
||||
yield* providers.transform((editor) => {
|
||||
editor.models.update(providerID, modelID, (model) => {
|
||||
model.package = "@opencode/ai/providers/amazon-bedrock"
|
||||
})
|
||||
})
|
||||
yield* addPlugin([
|
||||
new Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
providers: {
|
||||
bedrock: { models: { [modelID]: { package: "@opencode/ai/providers/amazon-bedrock/mantle" } } },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
expect(required(yield* models.get(providerID, modelID)).variants).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps configured model variant bodies unchanged", () =>
|
||||
Effect.gen(function* () {
|
||||
const models = yield* Model.Service
|
||||
@@ -955,15 +889,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
|
||||
model.variants = [{ id: Model.VariantID.make("fast"), settings: { effort: "high" } }]
|
||||
})
|
||||
})
|
||||
let transformed: Model.Info["limit"] | undefined
|
||||
yield* models.transform((editor) => {
|
||||
const model = editor.get(providerID, modelID)
|
||||
if (!model) return
|
||||
transformed = { ...model.limit }
|
||||
model.limit = { context: 10, output: 1 }
|
||||
})
|
||||
yield* addPlugin(entries)
|
||||
expect(transformed).toEqual({ context: 100, output: 75 })
|
||||
|
||||
const provider = required(yield* providers.get(providerID))
|
||||
const model = required(yield* models.get(providerID, modelID))
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, expect } from "bun:test"
|
||||
import { Config } from "@opencode/schema/config"
|
||||
import { Money } from "@opencode/schema/money"
|
||||
import {
|
||||
Cause,
|
||||
DateTime,
|
||||
Deferred,
|
||||
Duration,
|
||||
@@ -78,10 +79,11 @@ const itWithActivity = testEffect(
|
||||
)
|
||||
|
||||
describe("LocationServiceMap", () => {
|
||||
for (const failure of ["file", "permissions"] as const) {
|
||||
for (const failure of ["file", "permissions", "config reference"] as const) {
|
||||
for (const invalidate of [false, true]) {
|
||||
// The file-path fixture boots on Windows rather than failing during
|
||||
// discovery. Windows does not enforce POSIX directory modes, and root bypasses them.
|
||||
// discovery. The config-reference case covers repair on every OS.
|
||||
// Windows does not enforce POSIX directory modes, and root bypasses them.
|
||||
const test =
|
||||
(failure === "file" && process.platform === "win32") ||
|
||||
(failure === "permissions" && (process.platform === "win32" || process.getuid?.() === 0))
|
||||
@@ -100,8 +102,20 @@ describe("LocationServiceMap", () => {
|
||||
yield* Effect.promise(() => fs.mkdir(directory, { mode: 0o000 }))
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => fs.chmod(directory, 0o755)))
|
||||
}
|
||||
if (failure === "config reference") {
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(directory, "username.txt"), { recursive: true }))
|
||||
yield* Effect.promise(() =>
|
||||
fs.writeFile(path.join(directory, "opencode.json"), JSON.stringify({ username: "{file:username.txt}" })),
|
||||
)
|
||||
}
|
||||
const first = yield* Effect.exit(load)
|
||||
expect(Exit.isFailure(first)).toBe(true)
|
||||
if (failure === "config reference" && Exit.isFailure(first)) {
|
||||
expect(Cause.squash(first.cause)).toMatchObject({
|
||||
name: "ConfigInvalidError",
|
||||
data: { message: expect.stringContaining('bad file reference: "{file:username.txt}"') },
|
||||
})
|
||||
}
|
||||
if (!invalidate) expect(yield* locations.contextEffectOption(ref).pipe(Effect.scoped)).toEqual(Option.none())
|
||||
|
||||
if (failure === "file") {
|
||||
@@ -109,6 +123,10 @@ describe("LocationServiceMap", () => {
|
||||
yield* Effect.promise(() => fs.mkdir(directory))
|
||||
}
|
||||
if (failure === "permissions") yield* Effect.promise(() => fs.chmod(directory, 0o755))
|
||||
if (failure === "config reference") {
|
||||
yield* Effect.promise(() => fs.rm(path.join(directory, "username.txt"), { recursive: true }))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(directory, "username.txt"), "test-user"))
|
||||
}
|
||||
expect((yield* Effect.promise(() => fs.stat(directory))).isDirectory()).toBe(true)
|
||||
if (invalidate) yield* locations.invalidate(ref)
|
||||
const repaired = yield* Effect.exit(load)
|
||||
@@ -119,8 +137,8 @@ describe("LocationServiceMap", () => {
|
||||
}
|
||||
}
|
||||
|
||||
for (const failure of ["file", "missing"] as const) {
|
||||
// A file-path Location boots on Windows; the missing directory covers it there.
|
||||
for (const failure of ["file", "missing", "config reference"] as const) {
|
||||
// A file-path Location boots on Windows; use the missing config reference there.
|
||||
const test = failure === "file" && process.platform === "win32" ? it.live.skip : it.live
|
||||
test(`keeps the repaired graph after concurrent ${failure} failures release`, () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -129,6 +147,12 @@ describe("LocationServiceMap", () => {
|
||||
const ref = Location.Ref.make({ directory: AbsolutePath.make(directory) })
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
if (failure === "file") yield* Effect.promise(() => fs.writeFile(directory, "file"))
|
||||
if (failure === "config reference") {
|
||||
yield* Effect.promise(() => fs.mkdir(path.join(directory, "username.txt"), { recursive: true }))
|
||||
yield* Effect.promise(() =>
|
||||
fs.writeFile(path.join(directory, "opencode.json"), JSON.stringify({ username: "{file:username.txt}" })),
|
||||
)
|
||||
}
|
||||
|
||||
const scopes = yield* Effect.forEach(Array.from({ length: 8 }), () =>
|
||||
Effect.acquireRelease(Scope.make(), (scope) => Scope.close(scope, Exit.void)),
|
||||
@@ -140,7 +164,11 @@ describe("LocationServiceMap", () => {
|
||||
)
|
||||
expect(failures.every(Exit.isFailure)).toBe(true)
|
||||
if (failure === "file") yield* Effect.promise(() => fs.rm(directory))
|
||||
yield* Effect.promise(() => fs.mkdir(directory))
|
||||
if (failure !== "config reference") yield* Effect.promise(() => fs.mkdir(directory))
|
||||
if (failure === "config reference") {
|
||||
yield* Effect.promise(() => fs.rm(path.join(directory, "username.txt"), { recursive: true }))
|
||||
yield* Effect.promise(() => fs.writeFile(path.join(directory, "username.txt"), "test-user"))
|
||||
}
|
||||
const repaired = yield* locations.contextEffect(ref)
|
||||
|
||||
yield* Effect.forEach(scopes, (scope) => Scope.close(scope, Exit.void))
|
||||
|
||||
@@ -20,18 +20,12 @@ export const requests: WebSearchRequest[] = []
|
||||
export const signals: AbortSignal[] = []
|
||||
let responseBody = ""
|
||||
let responseStatus = 200
|
||||
let responseHeaders: Record<string, string> = {}
|
||||
|
||||
export function resetWebSearchFixture(body: string, status = 200) {
|
||||
requests.length = 0
|
||||
signals.length = 0
|
||||
responseBody = body
|
||||
responseStatus = status
|
||||
responseHeaders = {}
|
||||
}
|
||||
|
||||
export function setResponseHeaders(headers: Record<string, string>) {
|
||||
responseHeaders = headers
|
||||
}
|
||||
|
||||
const http = Layer.succeed(
|
||||
@@ -45,10 +39,7 @@ const http = Layer.succeed(
|
||||
headers: request.headers,
|
||||
body: JSON.parse(new TextDecoder().decode(request.body.body)),
|
||||
})
|
||||
return HttpClientResponse.fromWeb(
|
||||
request,
|
||||
new Response(responseBody, { status: responseStatus, headers: responseHeaders }),
|
||||
)
|
||||
return HttpClientResponse.fromWeb(request, new Response(responseBody, { status: responseStatus }))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,13 +8,7 @@ import { WebSearchParallel } from "@opencode/core/plugin/websearch/parallel"
|
||||
import { WebSearchTavily } from "@opencode/core/plugin/websearch/tavily"
|
||||
import { WebSearchTinyFish } from "@opencode/core/plugin/websearch/tinyfish"
|
||||
import { host, integrationHost, webSearchHost } from "./host"
|
||||
import {
|
||||
requests,
|
||||
signals,
|
||||
resetWebSearchFixture,
|
||||
setResponseHeaders,
|
||||
webSearchIntegrationTest,
|
||||
} from "./websearch-fixture"
|
||||
import { requests, signals, resetWebSearchFixture, webSearchIntegrationTest } from "./websearch-fixture"
|
||||
|
||||
beforeEach(() => {
|
||||
resetWebSearchFixture(
|
||||
@@ -359,15 +353,11 @@ describe("built-in web search providers", () => {
|
||||
integrationID: Integration.ID.make("tinyfish"),
|
||||
key: "tinyfish-secret",
|
||||
})
|
||||
setResponseHeaders({ "mcp-session-id": "ses_tinyfish" })
|
||||
yield* websearch.query({ query: "effect typescript", providerID: WebSearch.ID.make("tinyfish") })
|
||||
expect(requests[1]?.body).toMatchObject({ method: "initialize" })
|
||||
expect(requests[2]?.body).toMatchObject({ method: "notifications/initialized" })
|
||||
expect(requests[3]).toMatchObject({
|
||||
headers: { "x-api-key": "tinyfish-secret", "mcp-session-id": "ses_tinyfish" },
|
||||
body: { method: "tools/call" },
|
||||
expect(requests[1]).toMatchObject({
|
||||
headers: { "x-api-key": "tinyfish-secret" },
|
||||
})
|
||||
expect(requests[3]?.headers["x-tinyfish-access-mode"]).toBeUndefined()
|
||||
expect(requests[1]?.headers["x-tinyfish-access-mode"]).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -18,11 +18,6 @@ const NODE = process.execPath
|
||||
const MISSING_CWD = path.join(tmpdir(), `opencode-missing-cwd-${randomUUID()}`)
|
||||
const cmd = (...args: string[]) => ChildProcess.make(NODE, args)
|
||||
|
||||
// The child registers its SIGTERM handler before it announces readiness, and publishes the pid with a rename, so a
|
||||
// test that signals it right after reading "ready" never races the handler or reads a partly written file.
|
||||
const settleOnSigterm = (ready: string, settled: string) =>
|
||||
`const fs=require('fs');process.on('SIGTERM',()=>{fs.writeFileSync(${JSON.stringify(settled)},'settled');process.exit(0)});fs.writeFileSync(${JSON.stringify(ready + ".tmp")},String(process.pid));fs.renameSync(${JSON.stringify(ready + ".tmp")},${JSON.stringify(ready)});setInterval(()=>{},60000)`
|
||||
|
||||
const waitForFile = (file: string) =>
|
||||
Effect.promise(async () => {
|
||||
while (true) {
|
||||
@@ -167,7 +162,7 @@ describe("AppProcess", () => {
|
||||
(directory) => {
|
||||
const ready = path.join(directory, "ready")
|
||||
const settled = path.join(directory, "settled")
|
||||
const script = settleOnSigterm(ready, settled)
|
||||
const script = `const fs=require('fs');fs.writeFileSync(${JSON.stringify(ready)},String(process.pid));process.on('SIGTERM',()=>{fs.writeFileSync(${JSON.stringify(settled)},'settled');process.exit(0)});setInterval(()=>{},60000)`
|
||||
return Effect.gen(function* () {
|
||||
const svc = yield* AppProcess.Service
|
||||
const exit = yield* Effect.exit(svc.run(cmd("-e", script), { timeout: "250 millis" }))
|
||||
@@ -188,7 +183,7 @@ describe("AppProcess", () => {
|
||||
(directory) => {
|
||||
const ready = path.join(directory, "ready")
|
||||
const settled = path.join(directory, "settled")
|
||||
const script = settleOnSigterm(ready, settled)
|
||||
const script = `const fs=require('fs');fs.writeFileSync(${JSON.stringify(ready)},String(process.pid));process.on('SIGTERM',()=>{fs.writeFileSync(${JSON.stringify(settled)},'settled');process.exit(0)});setInterval(()=>{},60000)`
|
||||
return Effect.gen(function* () {
|
||||
const svc = yield* AppProcess.Service
|
||||
const fiber = yield* svc.run(cmd("-e", script)).pipe(Effect.forkChild)
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "node:path"
|
||||
import { Effect } from "effect"
|
||||
import { Provider } from "@opencode/core/provider"
|
||||
|
||||
describe("Provider", () => {
|
||||
test("bundles every native provider entrypoint that exports model()", async () => {
|
||||
const providers = path.join(import.meta.dir, "../../ai/src/providers")
|
||||
const entrypoints = await Promise.all(
|
||||
Array.from(new Bun.Glob("**/*.ts").scanSync(providers)).map(async (file) => {
|
||||
// Bun.Glob yields native separators, and package specifiers always use "/".
|
||||
const specifier = `@opencode/ai/providers/${file.replace(/\.ts$/, "").replaceAll(path.sep, "/")}`
|
||||
const module = await import(specifier)
|
||||
return typeof module.model === "function" ? specifier : undefined
|
||||
}),
|
||||
)
|
||||
const packages = entrypoints.filter((specifier) => specifier !== undefined).toSorted()
|
||||
test("loads bundled native provider entrypoints", async () => {
|
||||
const packages = [
|
||||
"@opencode/ai/providers/baseten",
|
||||
"@opencode/ai/providers/cerebras",
|
||||
"@opencode/ai/providers/cloudflare-ai-gateway",
|
||||
"@opencode/ai/providers/cloudflare-workers-ai",
|
||||
"@opencode/ai/providers/cohere",
|
||||
"@opencode/ai/providers/cohere/chat",
|
||||
"@opencode/ai/providers/deepinfra",
|
||||
"@opencode/ai/providers/deepseek",
|
||||
"@opencode/ai/providers/fireworks",
|
||||
"@opencode/ai/providers/google-vertex",
|
||||
"@opencode/ai/providers/google-vertex/gemini",
|
||||
"@opencode/ai/providers/google-vertex/interactions",
|
||||
"@opencode/ai/providers/google-vertex/chat",
|
||||
"@opencode/ai/providers/google-vertex/responses",
|
||||
"@opencode/ai/providers/google-vertex/messages",
|
||||
"@opencode/ai/providers/groq",
|
||||
"@opencode/ai/providers/mistral",
|
||||
"@opencode/ai/providers/togetherai",
|
||||
"@opencode/ai/providers/vercel-ai-gateway",
|
||||
]
|
||||
|
||||
expect(packages.filter((specifier) => !Provider.builtinPackages.has(specifier))).toEqual([])
|
||||
expect(Array.from(Provider.builtinPackages).toSorted()).toEqual(packages)
|
||||
for (const specifier of packages) {
|
||||
const loaded = await Effect.runPromise(Provider.loadPackage(specifier))
|
||||
expect(loaded.model).toBeFunction()
|
||||
|
||||
@@ -293,7 +293,11 @@ describe("Session.move", () => {
|
||||
replacements: [
|
||||
...bindings,
|
||||
...(broken
|
||||
? [Config.node.replace(Layer.effect(Config.Service, Effect.die("broken config")))]
|
||||
? [
|
||||
Config.node.replace(
|
||||
Config.configured({ project: false, global: false, content: config }),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -164,17 +164,6 @@ test("spells Vercel AI Gateway variants for their selected routes", () => {
|
||||
{ id: "high", settings: { thinking: { type: "enabled", budgetTokens: 19_456 } } },
|
||||
{ id: "max", settings: { thinking: { type: "enabled", budgetTokens: 38_911 } } },
|
||||
])
|
||||
expect(resolve(model(`${pkg}/chat`, "anthropic/claude-sonnet-4.6"), [{ type: "effort", values: ["high"] }])).toEqual([
|
||||
{ id: "high", settings: { reasoningEffort: "high" } },
|
||||
])
|
||||
expect(
|
||||
resolve(model(`${pkg}/messages`, "anthropic/claude-sonnet-4.6"), [{ type: "effort", values: ["high"] }]),
|
||||
).toEqual([{ id: "high", settings: { effort: "high", thinking: { type: "adaptive", display: "summarized" } } }])
|
||||
expect(resolve(model(`${pkg}/responses`, "spacexai/grok-4.7"), [{ type: "effort" }]).map((item) => item.id)).toEqual([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
])
|
||||
})
|
||||
|
||||
test("spells xAI Responses variants with catalog effort levels", () => {
|
||||
@@ -507,40 +496,3 @@ test("spells GitLab Duo variants for gitlab-ai-provider routes", () => {
|
||||
expect(resolve(gitlab("duo-chat-gpt-5-4"), [])).toEqual([])
|
||||
expect(resolve(gitlab("duo-workflow-opus-5-5"), [{ type: "effort" }])).toEqual([])
|
||||
})
|
||||
|
||||
test("spells reasoning variants for every bundled native package", () => {
|
||||
const supports: Variant.Support[] = [{ type: "toggle" }, { type: "effort" }, { type: "budget_tokens" }]
|
||||
const missing = Array.from(Provider.builtinPackages).filter(
|
||||
(packageName) => Variant.resolve(model(packageName, "test-model", 32_000), supports).length === 0,
|
||||
)
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
test("spells Mantle variants for the API each model is routed through", () => {
|
||||
expect(
|
||||
resolve(model("@opencode/ai/providers/amazon-bedrock/mantle/messages", "anthropic.claude-opus-4-8"), [
|
||||
{ type: "effort", values: ["low", "max"] },
|
||||
]),
|
||||
).toEqual([
|
||||
{ id: "low", settings: { effort: "low", thinking: { type: "adaptive", display: "summarized" } } },
|
||||
{ id: "max", settings: { effort: "max", thinking: { type: "adaptive", display: "summarized" } } },
|
||||
])
|
||||
expect(
|
||||
resolve(model("@opencode/ai/providers/amazon-bedrock/mantle/chat", "openai.gpt-oss-120b"), [{ type: "effort" }]),
|
||||
).toEqual([
|
||||
{ id: "low", settings: { reasoningEffort: "low" } },
|
||||
{ id: "medium", settings: { reasoningEffort: "medium" } },
|
||||
{ id: "high", settings: { reasoningEffort: "high" } },
|
||||
])
|
||||
})
|
||||
|
||||
test("spells Google Interactions variants as thinking levels", () => {
|
||||
for (const packageName of [
|
||||
"@opencode/ai/providers/google/interactions",
|
||||
"@opencode/ai/providers/google-vertex/interactions",
|
||||
])
|
||||
expect(resolve(model(packageName, "gemini-3.5-pro"), [{ type: "effort", values: ["low", "high"] }])).toEqual([
|
||||
{ id: "low", settings: { thinkingLevel: "low", thinkingSummaries: "auto" } },
|
||||
{ id: "high", settings: { thinkingLevel: "high", thinkingSummaries: "auto" } },
|
||||
])
|
||||
})
|
||||
@@ -40,13 +40,6 @@ CLI preparation uses these channel rules:
|
||||
default. `bun dev --download-server <version>` instead downloads that CLI version for local development. Neither path
|
||||
requires `OPENCODE_CLI_DIST` or runs the production prebuild.
|
||||
|
||||
## Beta channel
|
||||
|
||||
Every push to the `beta` branch publishes OpenCode Beta (`ai.opencode.desktop.beta`) as version `0.0.0-beta-<run>` to
|
||||
the `beta` update channel. Beta publishes only the desktop app and `beta.opencode.ai`; the CLI and SDK packages ship
|
||||
from `dev`. Install it from `https://opencode.ai/download/beta/<platform>`, for example `darwin-aarch64-dmg` or
|
||||
`windows-x64-nsis`. Installed builds update in place on every platform.
|
||||
|
||||
## Startup benchmark
|
||||
|
||||
`bun run bench:startup` measures a **packaged** build from process spawn to the restored tab being ready and the
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/desktop",
|
||||
"private": true,
|
||||
"version": "2.0.30",
|
||||
"version": "2.0.29",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as DesktopFiles from "./index"
|
||||
|
||||
import { execFile, spawn } from "node:child_process"
|
||||
import { execFile } from "node:child_process"
|
||||
import { clipboard, dialog, nativeImage, shell } from "electron"
|
||||
import { Context, Effect, FileSystem, Layer, Path } from "effect"
|
||||
import type { DirectoryPickerOptions, FilePickerOptions, SaveFilePickerOptions } from "../../shared/ipc-contract"
|
||||
@@ -87,26 +87,6 @@ function make(fs: FileSystem.FileSystem, path: Path.Path) {
|
||||
}),
|
||||
openPath: Effect.fn("DesktopFiles.openPath")(function* (target: string, application?: string) {
|
||||
if (!application) return yield* Effect.promise(() => shell.openPath(target))
|
||||
|
||||
if (process.platform === "win32" && /^powershell(\.exe)?$/i.test(path.basename(application))) {
|
||||
const info = yield* fs.stat(target)
|
||||
const cwd = info.type === "Directory" ? target : path.dirname(target)
|
||||
|
||||
// PowerShell takes its working directory rather than a path argument, and needs its own console window:
|
||||
// `start` opens one, and ignored stdio keeps the console from holding cmd's pipes open until PowerShell exits.
|
||||
return yield* Effect.tryPromise(() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
spawn("cmd.exe", ["/d", "/c", "start", "", application, "-NoExit"], {
|
||||
cwd,
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
})
|
||||
.once("error", reject)
|
||||
.once("exit", (code) => (code === 0 ? resolve() : reject(new Error(`start exited with code ${code}`))))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
yield* Effect.tryPromise(() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const command =
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
dist
|
||||
.wrangler
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,32 @@
|
||||
# SolidStart
|
||||
|
||||
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
||||
|
||||
## Creating a project
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm init solid@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm init solid@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Solid apps are built with _presets_, which optimise your project for deployment to different environments.
|
||||
|
||||
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
|
||||
|
||||
## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.29",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:cloudflare": "OPENCODE_DEPLOYMENT_TARGET=cloudflare vite build",
|
||||
"start": "vite start",
|
||||
"shell-prod": "sst shell --target Teams --stage production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
"@opencode/session-ui": "workspace:*",
|
||||
"@opencode/ui": "workspace:*",
|
||||
"@opencode/util": "workspace:*",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"hono-openapi": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"js-base64": "3.7.7",
|
||||
"luxon": "catalog:",
|
||||
"nitro": "3.0.1-alpha.1",
|
||||
"solid-js": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/apple-touch-icon-v3.png
|
||||
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/apple-touch-icon.png
|
||||
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/favicon-96x96-v3.png
|
||||
@@ -0,0 +1 @@
|
||||
../../ui/src/assets/favicon/favicon-96x96.png
|
||||
Loaded 100 of 470 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user