Compare commits

...
Author SHA1 Message Date
vimtor 3c7b57ca80 fix(tui): capitalize OpenCode web search provider 2026-09-14 18:32:00 +00:00
Aiden Cline 76594882b4 fix(codemode): bound what one built-in can build with string, array, and promise caps (#49019) 2026-09-14 12:54:02 -05:00
Dax Raad 75f046212f refactor(protocol): mark transient session controls 2026-09-14 13:46:04 -04:00
Dax Raad 6bc60caab9 refactor(session): rename interrupt resume option 2026-09-14 13:33:03 -04:00
Aiden Cline 3468a092c9 feat(codemode): expose host classes and functions through extensions (#48941) 2026-09-14 12:29:00 -05:00
Dax Raad 60b88daddf fix(tui): show latest shell output 2026-09-14 13:03:10 -04:00
Dax Raad 8cf4e6b9b4 refactor(protocol): simplify session actions 2026-09-14 13:00:08 -04:00
Shoubhit Dash fee337c462 refactor(tui): anchor turn token usage on idle markers (#49001) 2026-09-14 21:35:01 +05:30
Shoubhit Dash 8378f6caa9 test(core): avoid stdin race in check-ignore flake test (#48995) 2026-09-14 20:31:19 +05:30
Shoubhit Dash 94804423f4 fix(core): match versioned usage event type in stats (#48997) 2026-09-14 20:30:13 +05:30
Dax c62d2e8336 refactor(core): split provider and model registries (#48901) 2026-09-14 10:34:11 -04:00
OpeOginniandShoubhit Dash f64ce6aad4 fix(tui): preserve form drafts across tabs (#48952)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-09-14 19:58:35 +05:30
Shoubhit Dash dac296f963 feat: preserve prompt cache across effort switches (#48513) 2026-09-14 18:35:46 +05:30
OpeOginni 55e1b3eb17 feat(tui): add inline home footer slot (#48798) 2026-09-14 17:34:03 +05:30
opencode-agent[bot]andBrendonovich f1149efee7 fix(session-ui): group file tool diffs (#48932)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-09-14 15:37:56 +08:00
Aiden Cline 0ee9f61e3d fix(tui): allow toggling several MCP servers at once (#48940) 2026-09-14 02:28:25 -05:00
Aiden Cline 41df55ac15 feat(core): carry the reason on MCP needs_auth status (#48939) 2026-09-14 02:09:29 -05:00
opencode-agent[bot] 8fc49da6eb chore: update nix node_modules hashes 2026-09-14 07:02:13 +00:00
Aiden Cline cbe8823671 feat(core): rework MCP client for SDK v2 and the 2026-07-28 revision (#48937) 2026-09-14 01:42:53 -05:00
Mohammad AandAiden Cline 6e43875123 fix(nix): enable Wayland clipboard images (#48928)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-09-14 01:00:04 -05:00
ReStranger ce380fba29 fix(nix): install opencode and keep opencode2 alias (#48662) 2026-09-14 00:59:22 -05:00
Aiden Cline 3045156fa6 fix(codemode): strip __proto__ keys at host object boundaries (#48926) 2026-09-14 00:49:42 -05:00
Luke Parker 0a3dc16961 fix(app): shorten context usage label (#48927) 2026-09-14 05:36:08 +00:00
Aiden Cline 1d4d9b5fa5 fix(ai): preserve OpenAI Chat image URLs (#48862) 2026-09-13 23:43:39 -05:00
Aiden Cline 752d980770 fix(ai): map Vertex service tier to header (#48886) 2026-09-13 23:43:16 -05:00
Aiden Cline 07225172df fix(ai): serialize undefined historical tool input (#48863) 2026-09-13 23:43:07 -05:00
Aiden Cline 223b0271e6 test: align platform and endpoint expectations (#48916) 2026-09-14 04:25:34 +00:00
Luke Parker 93a37958a7 fix(session-ui): remove sticky patch header gap (#48909) 2026-09-14 13:41:07 +10:00
Dax Raad 40f4778296 refactor(protocol): refine audited endpoints 2026-09-13 23:10:37 -04:00
Dax e9bb6d490b refactor(tui): remove terminal pane setting 2026-09-13 23:06:13 -04:00
Aiden Cline 1e697962bd fix: restore required typecheck status (#48913) 2026-09-13 22:03:22 -05:00
Aiden Cline 4418df5d62 test(app): align worker status URL (#48911) 2026-09-13 21:55:47 -05:00
Aiden Cline 052be04466 test: align coverage with v2 refactors (#48900) 2026-09-13 21:44:49 -05:00
Aiden Cline cd3a64b225 test: align status and shell expectations (#48906) 2026-09-13 21:32:42 -05:00
Dax Raad 48c9a0a8de feat(release): add interactive review 2026-09-13 22:22:27 -04:00
Aiden Cline 48875190ef feat(codemode): fail runaway recursion with a RangeError at 10000 nested calls (#48891) 2026-09-13 21:04:36 -05:00
opencode-agent[bot]andHona 476432de1e fix(core): restore Windows Git fast path (#48879)
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
2026-09-14 12:03:03 +10:00
Luke Parker a71bb4d38c fix(app): match notice updates to used label styling (#48895) 2026-09-14 12:02:16 +10:00
Aiden Cline 5d3019e5a1 refactor(codemode): materialize interpreter failures once and locate them at the call boundary (#48770) 2026-09-13 20:19:31 -05:00
Dax Raad aeed4b6375 fix(protocol): return missing integration error 2026-09-13 20:00:46 -04:00
Dax Raad 82f713421f fix(client): replace servers missing status endpoint 2026-09-13 19:59:12 -04:00
Dax Raad 763cac8080 feat(config): add experimental updates 2026-09-13 19:41:06 -04:00
Dax Raad c2cf497e19 refactor(config): remove preferences API 2026-09-13 19:27:56 -04:00
Dax Raad 7784b3ee0d refactor(core): isolate compatibility skills 2026-09-13 19:11:13 -04:00
Dax Raad 199aabe9e2 refactor(protocol): simplify skill and reference contracts 2026-09-13 18:52:55 -04:00
Dax Raad 8905af5074 refactor(protocol): remove plugin activation wait 2026-09-13 18:03:18 -04:00
Dax Raad ce56111a4c refactor(protocol): simplify response locations 2026-09-13 17:52:45 -04:00
Dax Raad 5ab0167288 refactor(schema): remove project initialized time 2026-09-13 17:44:59 -04:00
Dax Raad 487e1bd76e refactor(protocol): remove workspace API 2026-09-13 17:39:23 -04:00
Dax 1eeaaab7a6 refactor: restrict reflective access (#48860) 2026-09-13 17:08:43 -04:00
Aiden Cline b901cb28af fix(ai): omit OpenAI tool choice when tools are empty (#48857) 2026-09-13 16:01:59 -05:00
Dax Raad 13453f2da8 refactor(protocol): consolidate server status 2026-09-13 16:56:22 -04:00
Dax Raad e16c56fe8b refactor(protocol): remove v2 operation prefixes 2026-09-13 16:31:46 -04:00
Dax Raad 42ff564913 refactor(protocol): remove current project endpoint 2026-09-13 16:20:43 -04:00
Aiden Cline 195158c34c fix(ai): normalize Bedrock Mistral tool IDs (#48843) 2026-09-13 12:27:04 -05:00
Aiden Cline 7a31b5c0f7 fix(ai): normalize Bedrock document labels (#48756) 2026-09-12 23:43:25 -05:00
Aiden Cline fb3c10ca66 fix(ai): sanitize replayed Bedrock tool names (#48750) 2026-09-12 23:16:02 -05:00
Aiden Cline c43cfccc4e fix(core): shape compaction and generate requests with built-in context hooks (#48749) 2026-09-12 23:10:09 -05:00
opencode c5aa7d7e34 sync release versions for v2.0.3 2026-09-12 23:48:02 +00:00
Dax Raad 9c8a4ea4ff fix(cli): replace update preflight renderer 2026-09-12 18:56:37 -04:00
Dax Raad c82340a97b fix(tui): probe palette only for system theme 2026-09-12 18:16:21 -04:00
582 changed files with 13296 additions and 37899 deletions
@@ -1,4 +1,4 @@
name: typecheck
name: check
on:
push:
@@ -8,7 +8,8 @@ on:
workflow_dispatch:
jobs:
typecheck:
check:
name: typecheck
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
@@ -17,5 +18,5 @@ jobs:
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Run typecheck
run: bun typecheck
- name: Run checks
run: bun run check
+1
View File
@@ -32,6 +32,7 @@ target
# Local dev files
opencode-dev
UPCOMING_CHANGELOG.md
RELEASE_REVIEW.md
logs/
*.bun-build
tsconfig.tsbuildinfo
+1 -1
View File
@@ -17,4 +17,4 @@ if (process.versions.bun !== expectedBunVersion) {
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
}
'
bun typecheck
bun run check
+14 -37
View File
@@ -1,45 +1,22 @@
{
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
"options": {
"typeAware": true
},
"categories": {
"suspicious": "warn"
"correctness": "off",
"suspicious": "off",
"pedantic": "off",
"perf": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"typescript/no-base-to-string": "warn",
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
"require-yield": "off",
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
"no-unassigned-vars": "off",
// SolidJS tracks reactive deps by reading properties inside createEffect
"no-unused-expressions": "off",
// Intentional control char matching (ANSI escapes, null byte sanitization)
"no-control-regex": "off",
// SST and plugin tools require triple-slash references
"triple-slash-reference": "off",
// Suspicious category: suppress noisy rules
// Effect's nested function* closures inherently shadow outer scope
"no-shadow": "off",
// Namespace-heavy codebase makes this too noisy
"unicorn/consistent-function-scoping": "off",
// Opinionated — .sort()/.reverse() mutation is fine in this codebase
"unicorn/no-array-sort": "off",
"unicorn/no-array-reverse": "off",
// Not relevant — this isn't a DOM event handler codebase
"unicorn/prefer-add-event-listener": "off",
// Bundler handles module resolution
"unicorn/require-module-specifiers": "off",
// postMessage target origin not relevant for this codebase
"unicorn/require-post-message-target-origin": "off",
// Side-effectful constructors are intentional in some places
"no-new": "off",
// Type-aware: catch unhandled promises
"typescript/no-floating-promises": "warn",
// Warn when spreading non-plain objects (Headers, class instances, etc.)
"typescript/no-misused-spread": "warn"
"no-restricted-globals": [
"error",
{
"name": "Reflect",
"message": "Use typed property access or direct invocation. Suppress this rule only for genuine reflection."
}
]
},
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
}
+3 -2
View File
@@ -170,9 +170,10 @@ const table = sqliteTable("session", {
- 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`.
## Type Checking
## Checks
- Always run `bun typecheck` from package directories (for example, `packages/core`), never `tsc` directly.
- Run `bun run check` from the repository root as the canonical full lint and type-check verification.
- During focused iteration, run `bun typecheck` from the affected package directory (for example, `packages/core`). Never run `tsc` directly.
## V2 Session Core
+279
View File
@@ -0,0 +1,279 @@
# V2 HTTP API audit checklist
**Source:** `packages/protocol/openapi.json`
**Current endpoint count:** 139
**Last regenerated:** 2026-09-13
## How to use this checklist
Review endpoints in document order. For each endpoint, select one disposition and capture rationale or follow-up work in Notes. Mark **Reviewed** only after the disposition is agreed.
### Review criteria
- Resource and operation naming
- HTTP method and idempotency
- Request parameters and location scope
- Response shape and error taxonomy
- Authentication and authorization
- Current production consumers
- Stability level: public, experimental, or internal
- Whether the generated client API is intuitive
### Disposition legend
- **Keep:** ship unchanged as a supported V2 API
- **Change:** retain after a defined contract change
- **Remove:** exclude from the official V2 API
- **Experimental-only:** retain outside the stable API commitment
## Progress
- [x] Group 1: Foundation and placement (4)
- [ ] Group 2: Configuration and capability catalogs (16)
- [ ] Group 3: Credentials, integrations, MCP, and web search (22)
- [ ] Group 4: Session lifecycle (12)
- [ ] Group 5: Session execution and inputs (11)
- [ ] Group 6: Session history and recovery (13)
- [ ] Group 7: Inbox, permissions, and forms (19)
- [ ] Group 8: Filesystem, worktrees, and VCS (12)
- [ ] Group 9: PTYs, persistent terminals, and shells (24)
- [ ] Group 10: Events, RPC, and experimental operations (6)
## Resolved during audit
### [x] `POST /api/plugin/await-activation`
- **Decision:** Remove
- **Notes:** Activation timing is an internal server concern. Catalog reads remain non-blocking.
### [x] Location response wrappers
- **Decision:** Reduce generic endpoint response locations to `{ directory }`.
- **Notes:** Full project metadata remains available from `GET /api/location`; no consumers used it from wrapped responses.
### [x] `GET /api/health` and `GET /api/server`
- **Decision:** Merge and rename
- **Replacement:** `GET /api/status` with operation ID `server.status`.
- **Notes:** Returns `version`, `pid`, and connection `urls`; readiness is conveyed by HTTP status.
### [x] `GET /api/project/current`
- **Decision:** Remove
- **Replacement:** `GET /api/location`, using `project` from the response.
- **Notes:** The endpoint duplicated `Location.Info.project`; production callers were migrated.
### [x] `POST /api/workspace` and `DELETE /api/workspace/{workspaceID}`
- **Decision:** Remove
- **Notes:** Provider-backed workspaces are not part of the V2 HTTP contract and can be introduced later. Core and the embedded SDK retain internal workspace support.
## Group 1: Foundation and placement
**Endpoints:** 4
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [x] 001002 | `GET` | `/api/status` | `server.status` | Keep | Replaces the former health and server endpoints. |
| [x] 003 | `GET` | `/api/location` | `location.get` | Keep | Workspace selectors and response fields removed until workspace support ships. |
| [x] 004 | `GET` | `/api/project` | `project.list` | Keep | Removed unused `time.initialized`; the database column remains for migration data. |
| [x] 005 | `PATCH` | `/api/project/{projectID}` | `project.update` | Keep | Request and response accepted as-is. |
## Group 2: Configuration and capability catalogs
**Endpoints:** 16
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [x] 008 | `GET` | `/api/agent` | `agent.list` | Keep | Request and response accepted as-is. |
| [x] 009 | `GET` | `/api/agent/{agentID}` | `agent.get` | Keep | Request, response, and not-found error accepted as-is. |
| [x] 010 | `GET` | `/api/plugin` | `plugin.list` | Keep | Request and response accepted as-is. |
| [x] 012 | `POST` | `/api/plugin/check` | `plugin.check` | Keep | Request and response accepted as-is. |
| [x] 013 | `POST` | `/api/plugin/update` | `plugin.update` | Keep | Request and errors accepted as-is. |
| [x] 014 | `GET` | `/api/model` | `model.list` | Keep | Request and response accepted as-is. |
| [x] 015 | `GET` | `/api/model/default` | `model.default` | Keep | Request and nullable response accepted as-is. |
| [x] 016 | `GET` | `/api/provider` | `provider.list` | Keep | Request and response accepted as-is. |
| [x] 017 | `GET` | `/api/provider/{providerID}` | `provider.get` | Keep | Request, response, and not-found error accepted as-is. |
| [x] 018 | `GET` | `/api/command` | `command.list` | Keep | Request and response accepted as-is. |
| [x] 019 | `GET` | `/api/skill` | `skill.list` | Keep | Renamed `location` to `path`; removed the skill-specific `slash` flag and slash-command behavior. |
| [x] 020 | `GET` | `/api/reference` | `reference.list` | Keep | Removed duplicate `description` and `hidden` fields from nested `source`. |
| [x] 021 | `GET` | `/api/config` | `config.get` | Keep | Compatibility entries removed; response now contains only documents and OpenCode directories. |
| [x] 022 | `GET` | `/api/config/preferences` | `config.preferences` | Remove | Redundant special projection of global config. |
| [x] 023 | `PATCH` | `/api/config/preferences` | `config.updatePreferences` | Remove | Redundant field-specific config mutation API. |
| [ ] 024 | `GET` | `/api/config/shell` | `config.shells` | | |
| [x] 024a | `PATCH` | `/api/experimental/config` | `experimental.config.update` | Change | Experimental global config mutation; initially accepts only `shell`. |
## Group 3: Credentials, integrations, MCP, and web search
**Endpoints:** 22
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [x] 025 | `GET` | `/api/integration` | `integration.list` | Keep | Full integration inventory is consumed by authentication and integration-selection clients. |
| [x] 026 | `GET` | `/api/integration/{integrationID}` | `integration.get` | Change | Missing integration now returns typed `404` instead of optional data. |
| [ ] 027 | `POST` | `/api/experimental/integration/wellknown` | `experimental.integration.wellknown.add` | | |
| [ ] 028 | `POST` | `/api/integration/{integrationID}/connect/key` | `integration.connect.key` | | |
| [ ] 029 | `POST` | `/api/integration/{integrationID}/connect/oauth` | `integration.oauth.connect` | | |
| [ ] 030 | `GET` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.status` | | |
| [ ] 031 | `DELETE` | `/api/integration/{integrationID}/connect/oauth/{attemptID}` | `integration.oauth.cancel` | | |
| [ ] 032 | `POST` | `/api/integration/{integrationID}/connect/oauth/{attemptID}/complete` | `integration.oauth.complete` | | |
| [ ] 033 | `POST` | `/api/integration/{integrationID}/connect/command` | `integration.command.connect` | | |
| [ ] 034 | `GET` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.status` | | |
| [ ] 035 | `DELETE` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.cancel` | | |
| [x] 036 | `GET` | `/api/mcp` | `mcp.list` | Keep | MCP inventory and connection status retained. |
| [x] 037 | `PUT` | `/api/experimental/mcp/{server}` | `experimental.mcp.add` | Experimental-only | Runtime-only MCP override; does not persist configuration. |
| [x] 038 | `DELETE` | `/api/experimental/mcp/{server}` | `experimental.mcp.remove` | Experimental-only | Runtime removal override; missing server returns `404`. |
| [x] 039 | `POST` | `/api/experimental/mcp/{server}/connect` | `experimental.mcp.connect` | Experimental-only | Runtime connection override retained outside the stable API. |
| [x] 040 | `POST` | `/api/experimental/mcp/{server}/disconnect` | `experimental.mcp.disconnect` | Experimental-only | Runtime disconnection override retained outside the stable API. |
| [ ] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | | Deferred for later review. |
| [x] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | Change | Removed redundant location query; credentials and events are global. |
| [x] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | Change | Removed redundant location query; credentials and events are global. |
| [x] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | Change | Removed redundant location query; credentials and events are global. |
| [x] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | Keep | Provider availability remains location-scoped; singular resource path retained. |
| [x] 046 | `POST` | `/api/websearch` | `websearch.query` | Keep | Unknown provider remains an invalid request; published time documented as Unix epoch milliseconds. |
## Group 4: Session lifecycle
**Endpoints:** 12
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [x] 047 | `GET` | `/api/session` | `session.list` | Keep | Existing filtering, ordering, and cursor contract retained for now. |
| [x] 048 | `POST` | `/api/session` | `session.create` | Keep | Existing creation contract retained; model reference includes optional variant. |
| [x] 049 | `GET` | `/api/experimental/session/stats` | `experimental.session.stats` | Experimental-only | Session analytics retained outside the stable API commitment. |
| [x] 050 | `GET` | `/api/session/active` | `session.active` | Keep | Status record retained for future active-state expansion. |
| [x] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | Keep | Specific session read and typed `404` retained. |
| [x] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | Keep | Session and child deletion with typed `404` retained. |
| [x] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | Change | Request now accepts optional branded `before` message ID; omission copies full history. |
| [x] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | Keep | Subsequent-execution agent selection retained. |
| [x] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | Keep | Subsequent-execution model and optional variant selection retained. |
| [x] 056 | `PATCH` | `/api/session/{sessionID}` | `session.rename` | Change | Title-only rename now uses the session resource path. |
| [x] 057 | `POST` | `/api/session/{sessionID}/move` | `session.move` | Change | Removed inaccurate local-change transfer claim; delivery behavior retained. |
| [x] 058 | `POST` | `/api/session/{sessionID}/background` | `session.background` | Keep | Backgroundable foreground tools transition to background observation; idle requests remain no-ops. |
## Group 5: Session execution and inputs
**Endpoints:** 11
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [x] 059 | `POST` | `/api/session/{sessionID}/prompt` | `session.prompt` | Keep | Durable admission, delivery mode, and admit-only resume control retained. |
| [x] 060 | `POST` | `/api/session/{sessionID}/command` | `session.command` | Change | Renamed request field from `command` to `name`; `204` retained. |
| [x] 061 | `POST` | `/api/experimental/session/{sessionID}/skill` | `experimental.session.skill` | Experimental-only | Skill ID is now the `id` field; standalone activation remains experimental. |
| [x] 062 | `POST` | `/api/session/{sessionID}/synthetic` | `session.synthetic` | Keep | Durable synthetic admission and delivery controls retained. |
| [x] 063 | `POST` | `/api/session/{sessionID}/shell` | `session.shell` | Change | Caller ID is now the optimistic shell message ID; server derives its event ID. |
| [x] 064 | `POST` | `/api/session/{sessionID}/compact` | `session.compact` | Keep | Durable compaction admission and delivery controls retained. |
| [x] 065 | `POST` | `/api/experimental/session/{sessionID}/wait` | `experimental.session.wait` | Experimental-only | Race-free idle barrier retained outside the stable API. |
| [x] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | Keep | Transient generation from session context retained. |
| [x] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | Change | Renamed `continue` to `resume` across public and internal interruption APIs. |
| [x] 068 | `PUT` | `/api/experimental/session/{sessionID}/environment` | `experimental.session.environment` | Experimental-only | Process-local environment replacement retained outside the stable API. |
| [x] 069 | `POST` | `/api/session/{sessionID}/view` | `session.view` | Change | Idle watermark now uses the standard epoch-millisecond timestamp schema. |
## Group 6: Session history and recovery
**Endpoints:** 13
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 070 | `POST` | `/api/session/import` | `session.import` | | |
| [ ] 071 | `GET` | `/api/session/{sessionID}/export` | `session.export` | | |
| [ ] 072 | `POST` | `/api/session/{sessionID}/revert/stage` | `session.revert.stage` | | |
| [ ] 073 | `POST` | `/api/session/{sessionID}/revert/clear` | `session.revert.clear` | | |
| [ ] 074 | `POST` | `/api/session/{sessionID}/revert/commit` | `session.revert.commit` | | |
| [ ] 075 | `GET` | `/api/session/{sessionID}/context` | `session.context` | | |
| [ ] 076 | `GET` | `/api/session/{sessionID}/diff` | `session.diff` | | |
| [ ] 077 | `GET` | `/api/session/{sessionID}/instructions/entries` | `session.instructions.entry.list` | | |
| [ ] 078 | `PUT` | `/api/session/{sessionID}/instructions/entries/{key}` | `session.instructions.entry.put` | | |
| [ ] 079 | `DELETE` | `/api/session/{sessionID}/instructions/entries/{key}` | `session.instructions.entry.remove` | | |
| [ ] 080 | `GET` | `/api/experimental/session/{sessionID}/log` | `session.log` | | |
| [ ] 081 | `GET` | `/api/session/{sessionID}/message/{messageID}` | `session.message` | | |
| [ ] 082 | `GET` | `/api/session/{sessionID}/message` | `message.list` | | |
## Group 7: Inbox, permissions, and forms
**Endpoints:** 19
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | | |
| [ ] 084 | `DELETE` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.cancel` | | |
| [ ] 085 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/steer` | `session.inbox.steer` | | |
| [ ] 086 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/queue` | `session.inbox.queue` | | |
| [ ] 087 | `GET` | `/api/form/request` | `form.request.list` | | |
| [ ] 088 | `GET` | `/api/session/{sessionID}/form` | `session.form.list` | | |
| [ ] 089 | `POST` | `/api/session/{sessionID}/form` | `session.form.create` | | |
| [ ] 090 | `GET` | `/api/session/{sessionID}/form/{formID}` | `session.form.get` | | |
| [ ] 091 | `GET` | `/api/session/{sessionID}/form/{formID}/state` | `session.form.state` | | |
| [ ] 092 | `POST` | `/api/session/{sessionID}/form/{formID}/reply` | `session.form.reply` | | |
| [ ] 093 | `POST` | `/api/session/{sessionID}/form/{formID}/cancel` | `session.form.cancel` | | |
| [ ] 094 | `GET` | `/api/permission/request` | `permission.request.list` | | |
| [ ] 095 | `GET` | `/api/permission/saved` | `permission.saved.list` | | |
| [ ] 096 | `DELETE` | `/api/permission/saved/{id}` | `permission.saved.remove` | | |
| [ ] 097 | `POST` | `/api/session/{sessionID}/permission` | `session.permission.create` | | |
| [ ] 098 | `GET` | `/api/session/{sessionID}/permission` | `session.permission.list` | | |
| [ ] 099 | `GET` | `/api/session/{sessionID}/permission/{requestID}` | `session.permission.get` | | |
| [ ] 100 | `POST` | `/api/session/{sessionID}/permission/{requestID}/reply` | `session.permission.reply` | | |
| [ ] 101 | `PUT` | `/api/session/{sessionID}/permission/rules` | `session.permission.rules` | | |
## Group 8: Filesystem, worktrees, and VCS
**Endpoints:** 12
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 102 | `GET` | `/api/fs/read/*` | `fs.read` | | |
| [ ] 103 | `GET` | `/api/fs/list` | `fs.list` | | |
| [ ] 104 | `GET` | `/api/fs/find` | `fs.find` | | |
| [ ] 105 | `GET` | `/api/worktree` | `worktree.list` | | |
| [ ] 106 | `POST` | `/api/worktree` | `worktree.create` | | |
| [ ] 107 | `DELETE` | `/api/worktree` | `worktree.remove` | | |
| [ ] 108 | `POST` | `/api/worktree/refresh` | `worktree.refresh` | | |
| [ ] 109 | `GET` | `/api/vcs` | `vcs.get` | | |
| [ ] 110 | `GET` | `/api/vcs/base` | `vcs.base` | | |
| [ ] 111 | `GET` | `/api/vcs/status` | `vcs.status` | | |
| [ ] 112 | `GET` | `/api/vcs/branches` | `vcs.branches` | | |
| [ ] 113 | `GET` | `/api/vcs/diff` | `vcs.diff` | | |
## Group 9: PTYs, persistent terminals, and shells
**Endpoints:** 24
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 114 | `GET` | `/api/pty` | `pty.list` | | |
| [ ] 115 | `POST` | `/api/pty` | `pty.create` | | |
| [ ] 116 | `GET` | `/api/pty/{ptyID}` | `pty.get` | | |
| [ ] 117 | `PUT` | `/api/pty/{ptyID}` | `pty.update` | | |
| [ ] 118 | `DELETE` | `/api/pty/{ptyID}` | `pty.remove` | | |
| [ ] 119 | `POST` | `/api/pty/{ptyID}/connect-token` | `pty.connect.token` | | |
| [ ] 120 | `GET` | `/api/pty/{ptyID}/connect` | `pty.connect` | | |
| [ ] 121 | `GET` | `/api/experimental/session/{sessionID}/terminal/read` | `server.experimental.persistentPty.read` | | |
| [ ] 122 | `GET` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.list` | | |
| [ ] 123 | `POST` | `/api/experimental/session/{sessionID}/terminal` | `server.experimental.persistentPty.create` | | |
| [ ] 124 | `POST` | `/api/experimental/persistent-pty/shutdown` | `server.experimental.persistentPty.shutdown` | | |
| [ ] 125 | `POST` | `/api/experimental/persistent-pty/handoff` | `server.experimental.persistentPty.handoff` | | |
| [ ] 126 | `GET` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.get` | | |
| [ ] 127 | `PUT` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.update` | | |
| [ ] 128 | `DELETE` | `/api/experimental/persistent-pty/{ptyID}` | `server.experimental.persistentPty.remove` | | |
| [ ] 129 | `GET` | `/api/experimental/persistent-pty/{ptyID}/snapshot` | `server.experimental.persistentPty.snapshot` | | |
| [ ] 130 | `POST` | `/api/experimental/persistent-pty/{ptyID}/connect-token` | `server.experimental.persistentPty.connectToken` | | |
| [ ] 131 | `GET` | `/api/experimental/persistent-pty/{ptyID}/connect` | `persistentPty.connect` | | |
| [ ] 132 | `GET` | `/api/shell` | `shell.list` | | |
| [ ] 133 | `POST` | `/api/shell` | `shell.create` | | |
| [ ] 134 | `GET` | `/api/shell/{id}` | `shell.get` | | |
| [ ] 135 | `DELETE` | `/api/shell/{id}` | `shell.remove` | | |
| [ ] 136 | `PATCH` | `/api/shell/{id}/timeout` | `shell.timeout` | | |
| [ ] 137 | `GET` | `/api/shell/{id}/output` | `shell.output` | | |
## Group 10: Events, RPC, and experimental operations
**Endpoints:** 6
| Done | Method | Path | Operation ID | Decision | Notes |
|---|---|---|---|---|---|
| [ ] 138 | `POST` | `/api/generate` | `generate.text` | | |
| [ ] 139 | `POST` | `/api/rpc/{rpcID}/{method}` | `rpc.call` | | |
| [ ] 140 | `GET` | `/api/event` | `event.subscribe` | | |
| [ ] 141 | `GET` | `/api/debug/location` | `debug.location.list` | | |
| [ ] 142 | `DELETE` | `/api/debug/location` | `debug.location.evict` | | |
| [ ] 143 | `GET` | `/api/experimental/migration/v1` | `experimental.migration.v1.status` | | |
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+51 -137
View File
@@ -14,6 +14,7 @@
"devDependencies": {
"@actions/artifact": "5.0.1",
"@ast-grep/cli": "0.44.0",
"@opencode/client": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@types/react": "19.2.17",
@@ -31,7 +32,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -53,7 +54,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -112,7 +113,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.2",
"version": "2.0.3",
"bin": {
"opencode2": "./bin/opencode2.cjs",
},
@@ -176,7 +177,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -202,7 +203,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -216,7 +217,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -252,7 +253,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -279,7 +280,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
@@ -296,7 +297,7 @@
},
"packages/console/mail": {
"name": "@opencode/console-mail",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -320,7 +321,7 @@
},
"packages/console/support": {
"name": "@opencode/console-support",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -340,7 +341,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.82",
@@ -357,7 +358,7 @@
"@ff-labs/fff-bun": "0.10.5",
"@ff-labs/fff-node": "0.10.5",
"@lydell/node-pty": "catalog:",
"@modelcontextprotocol/sdk": "1.29.0",
"@modelcontextprotocol/client": "2.0.0",
"@opencode-ai/pty": "0.1.13",
"@opencode/ai": "workspace:*",
"@opencode/codemode": "workspace:*",
@@ -394,6 +395,7 @@
"devDependencies": {
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@modelcontextprotocol/server": "2.0.0",
"@opencode/http-recorder": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
@@ -412,7 +414,7 @@
},
"packages/desktop": {
"name": "@opencode/desktop",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "4.1.2",
@@ -464,7 +466,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -501,7 +503,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -517,7 +519,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -536,7 +538,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -549,7 +551,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -563,7 +565,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -578,7 +580,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -617,7 +619,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -647,7 +649,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -662,7 +664,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -686,7 +688,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -707,7 +709,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -729,7 +731,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -764,7 +766,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -784,7 +786,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -818,7 +820,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -837,7 +839,7 @@
},
"packages/stats/server": {
"name": "@opencode/stats-server",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -883,7 +885,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -897,7 +899,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -932,7 +934,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -967,7 +969,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -1000,7 +1002,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1041,7 +1043,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.2",
"version": "2.0.3",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
@@ -1081,8 +1083,8 @@
"patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"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",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
@@ -1877,8 +1879,6 @@
"@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/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="],
"@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=="],
@@ -2037,7 +2037,11 @@
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
"@modelcontextprotocol/client": ["@modelcontextprotocol/client@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "jose": "^6.1.3", "pkce-challenge": "^5.0.0", "zod": "^4.2.0" } }, "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw=="],
"@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA=="],
"@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" } }, "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
@@ -3363,8 +3367,6 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -3523,8 +3525,6 @@
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
@@ -3567,8 +3567,6 @@
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="],
"cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="],
@@ -3691,9 +3689,7 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@@ -3701,16 +3697,12 @@
"cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"core-js": ["core-js@3.50.0", "", {}, "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw=="],
"core-js-compat": ["core-js-compat@3.50.0", "", { "dependencies": { "browserslist": "^4.28.7" } }, "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
@@ -3863,8 +3855,6 @@
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
@@ -3939,8 +3929,6 @@
"editorconfig": ["editorconfig@1.0.7", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
@@ -3979,8 +3967,6 @@
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"engine.io-client": ["engine.io-client@6.6.6", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q=="],
@@ -4033,8 +4019,6 @@
"escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
@@ -4057,8 +4041,6 @@
"eta": ["eta@4.6.0", "", {}, "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
@@ -4077,10 +4059,6 @@
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="],
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
"ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
@@ -4127,8 +4105,6 @@
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
@@ -4153,14 +4129,10 @@
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
@@ -4335,8 +4307,6 @@
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"http-link-header": ["http-link-header@1.1.4", "", {}, "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -4391,8 +4361,6 @@
"ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
@@ -4457,8 +4425,6 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
@@ -4725,12 +4691,8 @@
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="],
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"mermaid": ["mermaid@11.17.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "fastdom": "1.0.12", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg=="],
@@ -4955,8 +4917,6 @@
"oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
@@ -5027,8 +4987,6 @@
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
@@ -5151,8 +5109,6 @@
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
@@ -5179,10 +5135,6 @@
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
"react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
@@ -5337,8 +5289,6 @@
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@@ -5373,8 +5323,6 @@
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
@@ -5385,16 +5333,12 @@
"seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -5519,8 +5463,6 @@
"stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@@ -5657,8 +5599,6 @@
"toad-cache": ["toad-cache@3.7.4", "", {}, "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
"topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="],
@@ -5701,8 +5641,6 @@
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
@@ -5781,8 +5719,6 @@
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"unstorage": ["unstorage@2.0.0-alpha.7", "", { "peerDependencies": { "@azure/app-configuration": "^1.11.0", "@azure/cosmos": "^4.9.1", "@azure/data-tables": "^13.3.2", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.31.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.9.3", "lru-cache": "^11.2.6", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog=="],
@@ -5813,8 +5749,6 @@
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.1.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.51", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-w3OHkuzzKZ3r2TOxER6myBYzZJNoDqol+DUHu3NnfBN/GETnUVxecZJab0CHQQ8GZc0jjzpFymepjcLDPS4SQg=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@@ -6307,9 +6241,13 @@
"@mdx-js/mdx/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"@modelcontextprotocol/sdk/hono": ["hono@4.13.3", "", {}, "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw=="],
"@modelcontextprotocol/client/jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
"@modelcontextprotocol/client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@modelcontextprotocol/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@modelcontextprotocol/server/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@npmcli/arborist/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
@@ -6595,8 +6533,6 @@
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"builder-util/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
@@ -6675,8 +6611,6 @@
"esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"ffi-rs/@yuuang/ffi-rs-darwin-arm64": ["@yuuang/ffi-rs-darwin-arm64@1.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OueBlUFBT9IwD9pQnoYs0UszRBEySskfrEPXXfvKfGjL/DXnfn6kUheQ3oIP6sSmshVGNQUwrTCPo6feAa4QjA=="],
@@ -6811,8 +6745,6 @@
"roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
"safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"safe-push-apply/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -6867,8 +6799,6 @@
"tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
"type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
@@ -7205,32 +7135,24 @@
"@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@17.0.0", "", { "dependencies": { "@octokit/openapi-types": "^28.0.0" } }, "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q=="],
"@octokit/auth-app/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@17.0.0", "", { "dependencies": { "@octokit/openapi-types": "^28.0.0" } }, "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q=="],
"@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="],
"@octokit/auth-oauth-app/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="],
"@octokit/auth-oauth-device/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.1", "", { "dependencies": { "@octokit/types": "^17.0.0" } }, "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA=="],
"@octokit/auth-oauth-user/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
@@ -7241,8 +7163,6 @@
"@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/oauth-methods/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
@@ -8057,20 +7977,14 @@
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.4", "", { "dependencies": { "@octokit/types": "^17.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA=="],
"@octokit/rest/@octokit/core/@octokit/request/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@28.0.0", "", {}, "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ=="],
"@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=="],
+1 -1
View File
@@ -90,7 +90,7 @@ stdenv.mkDerivation (finalAttrs: {
export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli"
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode/", ""))')
mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin"
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode2"
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode"
bun run build
npx electron-builder --dir \
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-euVUyj0CzjCA1nYbN2vKctEPzLkUlNGTK2dMNbackqM=",
"aarch64-linux": "sha256-qQkjqaxpjAae+rohoWI601QnrgKYghJ+ttqeiQBTwCM=",
"aarch64-darwin": "sha256-HYWs31TJlDZsDBNmbPARo16r7zNKy9x840uHGcUMYsk=",
"x86_64-darwin": "sha256-89FOrX813FENk3u8RAHCfyD7voaZWW++Z4Gpa3SkOJs="
"x86_64-linux": "sha256-MLA+R+7GovdBAVtJQ1pCOtXt16s6uC7Aplfjy5k3Q2M=",
"aarch64-linux": "sha256-mCfBpIpdeTDBSvUaKU7zGCAeV5UfkEaf3dYl5Y3okvg=",
"aarch64-darwin": "sha256-VmDVOpRFfR5f9GlboLJWHREcG8ClpClHU27p7tb9BkI=",
"x86_64-darwin": "sha256-wKw5EPQzuc9IptLLgJnBi6XWr3qN8Ze3GCElVFXLs6g="
}
}
+14 -4
View File
@@ -8,6 +8,7 @@
makeBinaryWrapper,
models-dev,
ripgrep,
wayland,
installShellFiles,
versionCheckHook,
writableTmpDirAsHomeHook,
@@ -62,9 +63,10 @@ stdenvNoCC.mkDerivation (finalAttrs: {
installPhase = ''
runHook preInstall
install -Dm755 dist/cli-*/bin/opencode2 $out/bin/opencode2
install -Dm755 dist/cli-*/bin/opencode $out/bin/opencode
wrapProgram $out/bin/opencode2 \
# OpenTUI dlopens Wayland for clipboard images.
wrapProgram $out/bin/opencode \
--prefix PATH : ${
lib.makeBinPath (
[
@@ -73,13 +75,21 @@ stdenvNoCC.mkDerivation (finalAttrs: {
# bun runs sysctl to detect if running on rosetta2
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
)
}
} ${lib.optionalString stdenvNoCC.hostPlatform.isLinux ''
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath [ wayland ]}
''}
ln -s opencode $out/bin/opencode2
runHook postInstall
'';
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
# trick yargs into also generating zsh completions
installShellCompletion --cmd opencode \
--bash <($out/bin/opencode completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode completion)
installShellCompletion --cmd opencode2 \
--bash <($out/bin/opencode2 completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode2 completion)
@@ -101,7 +111,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
description = "The open source coding agent";
homepage = "https://opencode.ai";
license = lib.licenses.mit;
mainProgram = "opencode2";
mainProgram = "opencode";
inherit (node_modules.meta) platforms;
};
})
+5 -3
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.2",
"version": "2.0.3",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
@@ -24,6 +24,7 @@
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
"test:effect-simplification-rules": "ast-grep test -c script/ast-grep/effect-simplifications/sgconfig.yml",
"typecheck": "bun turbo typecheck --concurrency=3",
"check": "bun run lint && bun run typecheck",
"typecheck:profile": "bun script/profile-typecheck.ts",
"typecheck:profile:packages": "bun script/profile-typecheck-packages.ts",
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
@@ -115,6 +116,7 @@
"devDependencies": {
"@actions/artifact": "5.0.1",
"@ast-grep/cli": "0.44.0",
"@opencode/client": "workspace:*",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@tsconfig/bun": "catalog:",
@@ -174,10 +176,10 @@
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch",
"ghostty-web@github:anomalyco/ghostty-web#83c0a07": "patches/ghostty-web@0.3.0.patch",
"vite@8.2.2": "patches/vite@8.2.2.patch"
"vite@8.2.2": "patches/vite@8.2.2.patch",
"@modelcontextprotocol/client@2.0.0": "patches/@modelcontextprotocol%2Fclient@2.0.0.patch"
}
}
+4
View File
@@ -166,6 +166,10 @@ Native chronological system messages are route/model-specific. Open Responses lo
The wrapped-user fallback preserves ordering while visibly lowering authority. Never silently pass a raw chronological `role: "system"` through a route that might reject it. Do not insert raw retrieved documents, tool output, or web content into privileged chronological system updates; keep untrusted content in ordinary user/tool channels.
### Effort Updates
`Message.effort({ effort, previous })` is a chronological "reasoning effort changed here" marker (`undefined` means the model default). Changing a top-level effort invalidates the whole provider prompt cache, so protocols with a native per-message update (`Protocol.supportsEffortUpdates`) keep the top-level effort at the first marker's `previous` and lower each marker in place: Anthropic Messages emits an empty `role: "system"` message with `output_config.effort` plus the `mid-conversation-output-config-2026-07-01` beta, and OpenAI Responses emits `configuration_update` items. `applyEffortUpdates` runs in `prepareRequest` and strips the markers for every other route, so a protocol without support keeps today's plain top-level behaviour. When the last marker disagrees with the effort the request asks for (reverted or forked history), `resolveEffortUpdates` strips the markers and falls back to a plain top-level change.
### Tools
Tool loops are represented in common messages and events:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.2",
"version": "2.0.3",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
+9 -2
View File
@@ -12,6 +12,7 @@
// count against the four-breakpoint budget; auto only fills remaining slots.
import { CacheHint, type CachePolicy, type CachePolicyObject } from "./schema/options.js"
import { LLMRequest, Message, ToolDefinition, type ContentPart, type ToolEntry } from "./schema/messages.js"
import { effortUpdate } from "./effort-updates.js"
const AUTO: CachePolicyObject = {
tools: true,
@@ -121,9 +122,15 @@ const markMessages = (
return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint, budget)
if (strategy === "latest-assistant")
return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint, budget)
const start = Math.max(0, messages.length - strategy.tail)
let start = messages.length
let remaining = strategy.tail
while (remaining > 0 && start > 0) {
start -= 1
if (effortUpdate(messages[start]!) === undefined) remaining -= 1
}
let next = messages
for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint, budget)
for (let i = start; i < messages.length; i++)
if (effortUpdate(messages[i]!) === undefined) next = markMessageAt(next, i, hint, budget)
return next
}
+25
View File
@@ -0,0 +1,25 @@
// Changing the top-level reasoning effort invalidates the provider prompt cache. Protocols with a native
// per-message update keep it frozen and lower `Message.effort(...)` markers instead; other routes strip them.
import { LLMRequest, type EffortPart, type Message } from "./schema/messages.js"
export const effortUpdate = (message: Message): EffortPart | undefined => {
if (message.role !== "system" || message.content.length !== 1) return undefined
const part = message.content[0]
return part.type === "effort" ? part : undefined
}
export const stripEffortUpdates = (request: LLMRequest) => {
const messages = request.messages.filter((message) => effortUpdate(message) === undefined)
return messages.length === request.messages.length ? request : LLMRequest.update(request, { messages })
}
export const applyEffortUpdates = (request: LLMRequest): LLMRequest =>
request.model.route.supportsEffortUpdates?.(request) ? request : stripEffortUpdates(request)
// Reverted or forked history can leave the last marker disagreeing with the requested effort.
export const resolveEffortUpdates = (request: LLMRequest, current: string | undefined) => {
const updates = request.messages.flatMap((message) => effortUpdate(message) ?? [])
if (updates.length === 0) return { request, effort: current }
if (updates.at(-1)?.effort !== current) return { request: stripEffortUpdates(request), effort: current }
return { request, effort: updates[0]?.previous }
}
+50 -18
View File
@@ -27,6 +27,7 @@ import {
} from "../schema/index.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { classifyProviderFailure } from "../provider-error.js"
import { effortUpdate, resolveEffortUpdates } from "../effort-updates.js"
import * as Cache from "./utils/cache.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
@@ -36,6 +37,7 @@ const ADAPTER = "anthropic-messages"
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
export const PATH = "/messages"
export const DEFAULT_MAX_TOKENS = 32_000
const DEFAULT_EFFORT = "high"
const SSE_EVENTS = new Set([
"message",
@@ -286,7 +288,11 @@ type AnthropicToolResultBlock = Schema.Schema.Type<typeof AnthropicToolResultBlo
const AnthropicMessage = Schema.Union([
Schema.Struct({ role: Schema.Literal("user"), content: Schema.Array(AnthropicUserBlock) }),
Schema.Struct({ role: Schema.Literal("assistant"), content: Schema.Array(AnthropicAssistantBlock) }),
Schema.Struct({ role: Schema.Literal("system"), content: Schema.Array(AnthropicTextBlock) }),
Schema.Struct({
role: Schema.Literal("system"),
content: Schema.Array(AnthropicTextBlock),
output_config: Schema.optional(Schema.Struct({ effort: Schema.String })),
}),
]).pipe(Schema.toTaggedUnion("role"))
type AnthropicMessage = Schema.Schema.Type<typeof AnthropicMessage>
@@ -877,6 +883,12 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
for (const [index, message] of request.messages.entries()) {
if (message.role === "system") {
const update = effortUpdate(message)
if (update) {
// Accepted at any position, so the text-update placement rules do not apply.
messages.push({ role: "system", content: [], output_config: { effort: update.effort ?? DEFAULT_EFFORT } })
continue
}
if (splitsLocalToolResults(request.messages, index))
return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result")
if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request, index)) {
@@ -1034,18 +1046,11 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
ProviderShared.isRecord(rawOutputConfig) && ProviderShared.isRecord(rawOutputConfig.format)
? (rawOutputConfig.format as { type: "json_schema"; schema: Record<string, unknown> })
: undefined
const output_config =
outputConfigEffort === undefined && outputConfigFormat === undefined
? undefined
: {
...(outputConfigEffort === undefined ? {} : { effort: outputConfigEffort }),
...(outputConfigFormat === undefined ? {} : { format: outputConfigFormat }),
}
const thinking = yield* resolveThinking(input?.thinking)
return {
thinking: applyThinkingBindingDefault(request.model, thinking),
effort: outputConfigEffort,
output_config,
format: outputConfigFormat,
service_tier,
metadata,
container,
@@ -1054,15 +1059,30 @@ const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (
}
})
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
const claudeVersion = (id: string) => {
const match = /(?:^|[./])claude-(?<family>[a-z]+)-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/.exec(
id.toLowerCase(),
)?.groups
if (!match) return undefined
return { family: match.family, major: Number(match.major), minor: Number(match.minor ?? 0) }
}
const supportsThinkingBlockBinding = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsThinkingBlockBinding
if (override !== undefined) return override
// Accept gateway namespaces and Vertex suffixes without treating a snapshot date as a minor version.
const version = /(?:^|[./])claude-[a-z]+-(?<major>\d+)(?:[.-](?<minor>\d{1,2}))?(?:$|[-:@])/i.exec(model.id)?.groups
if (!version) return false
const major = Number(version.major)
const minor = Number(version.minor ?? 0)
return major > 5 || (major === 5 && minor >= 1)
const version = claudeVersion(model.id)
return version !== undefined && (version.major > 5 || (version.major === 5 && version.minor >= 1))
}
const supportsEffortUpdates = (model: LLMRequest["model"]) => {
const override = model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
const version = claudeVersion(model.id)
if (version === undefined) return false
if (version.family === "opus") return version.major >= 5
if (version.family !== "fable" && version.family !== "mythos") return false
return version.major > 5 || (version.major === 5 && version.minor >= 1)
}
const applyThinkingBindingDefault = (model: LLMRequest["model"], thinking: AnthropicThinking | undefined) => {
@@ -1104,13 +1124,15 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
const management = yield* ProviderShared.validateWith(
Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)),
)(request.providerOptions?.contextManagement)
const options = yield* resolveOptions(request)
const updates = resolveEffortUpdates(request, options.effort)
const generation = request.generation
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
// Allocate the 4-breakpoint budget in invalidation order: tools → system →
// messages. Tools live highest in the cache hierarchy, so when callers
// over-mark we keep their tool hints and shed the message-tail ones first.
const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP)
const flattened = ProviderShared.flattenToolRequest(request)
const flattened = ProviderShared.flattenToolRequest(updates.request)
const tools =
flattened.tools.length === 0
? undefined
@@ -1138,7 +1160,13 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
`Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
)
}
const options = yield* resolveOptions(request)
const output_config =
updates.effort === undefined && options.format === undefined
? undefined
: {
...(updates.effort === undefined ? {} : { effort: updates.effort }),
...(options.format === undefined ? {} : { format: options.format }),
}
const body = {
model: request.model.id,
system,
@@ -1152,7 +1180,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
top_k: generation?.topK,
stop_sequences: generation?.stop,
thinking: options.thinking,
output_config: options.output_config,
output_config,
// top-level passthrough per SDK MessageCreateParamsBase:4638,4643,4649,4654,4670
cache_control: options.cache_control,
container: options.container,
@@ -1677,6 +1705,7 @@ export const protocol = Protocol.make({
}),
step,
},
supportsEffortUpdates: (request) => supportsEffortUpdates(request.model),
})
export const transport = <
@@ -1718,6 +1747,9 @@ function requiredBetaHeaders(body: Pick<AnthropicMessagesBody, "messages" | "con
)
if (requestsCompaction || replaysCompaction) betas.push("compact-2026-01-12")
if (body.messages.some((message) => message.role === "system" && message.output_config !== undefined))
betas.push("mid-conversation-output-config-2026-07-01")
const thinking = body.thinking
if (thinking && thinking.type !== "disabled" && thinking.block_binding)
betas.push("thinking-binding-controls-2026-08-01")
+32 -17
View File
@@ -25,6 +25,7 @@ import { BedrockAuth } from "./utils/bedrock-auth.js"
import { BedrockCache } from "./utils/bedrock-cache.js"
import { BedrockMedia } from "./utils/bedrock-media.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { MistralToolID } from "./utils/mistral-tool-id.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
import { ToolStream } from "./utils/tool-stream.js"
@@ -279,15 +280,19 @@ const removeEmptyToolInputKeys = (input: unknown): unknown => {
)
}
const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
const lowerToolCall = (part: ToolCallPart, normalizeID: (id: string) => string): BedrockToolUseBlock => ({
toolUse: {
toolUseId: part.id,
name: part.name,
toolUseId: normalizeID(part.id),
// Models can emit names that Converse rejects when replayed in history.
name: part.name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "_",
input: removeEmptyToolInputKeys(part.input),
},
})
const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent")(function* (part: ToolResultPart) {
const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent")(function* (
part: ToolResultPart,
documentNames: Set<string>,
) {
if (part.result.type === "text" || part.result.type === "error")
return [{ text: ProviderShared.toolResultText(part) }]
if (part.result.type === "json") return [{ json: part.result.value }]
@@ -298,22 +303,29 @@ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent
content.push({ text: item.text })
continue
}
const media = yield* BedrockMedia.lower({
type: "media",
mediaType: item.mime,
data: item.uri,
filename: item.name,
})
content.push(media)
const media = yield* BedrockMedia.lower(
{
type: "media",
mediaType: item.mime,
data: item.uri,
filename: item.name,
},
documentNames,
)
content.push(...media)
}
return content
})
const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (part: ToolResultPart) {
const lowerToolResult = Effect.fn("BedrockConverse.lowerToolResult")(function* (
part: ToolResultPart,
documentNames: Set<string>,
normalizeID: (id: string) => string,
) {
return {
toolResult: {
toolUseId: part.id,
content: yield* lowerToolResultContent(part),
toolUseId: normalizeID(part.id),
content: yield* lowerToolResultContent(part, documentNames),
status: part.result.type === "error" ? "error" : "success",
},
} satisfies BedrockToolResultBlock
@@ -324,6 +336,9 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
breakpoints: BedrockCache.Breakpoints,
) {
const messages: BedrockMessage[] = []
const documentNames = new Set<string>()
// Mistral can reject replay IDs even when they satisfy Converse's broader ID syntax.
const normalizeID = request.model.id.includes("mistral.") ? MistralToolID.normalizer(request) : (id: string) => id
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
for (const message of request.messages) {
@@ -347,7 +362,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
continue
}
if (part.type === "media") {
content.push(yield* BedrockMedia.lower(part))
content.push(...(yield* BedrockMedia.lower(part, documentNames)))
continue
}
}
@@ -388,7 +403,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
continue
}
if (part.type === "tool-call") {
content.push(lowerToolCall(part))
content.push(lowerToolCall(part, normalizeID))
continue
}
}
@@ -400,7 +415,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["tool-result"]))
return yield* ProviderShared.unsupportedContent("Bedrock Converse", "tool", ["tool-result"])
content.push(yield* lowerToolResult(part))
content.push(yield* lowerToolResult(part, documentNames, normalizeID))
const cachePoint = BedrockCache.block(breakpoints, part.cache)
if (cachePoint) content.push(cachePoint)
}
+5 -33
View File
@@ -21,11 +21,11 @@ import {
import { classifyProviderFailure } from "../provider-error.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { MistralToolID } from "./utils/mistral-tool-id.js"
import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "mistral-chat"
const DONE = "[DONE]" as const
const TOOL_ID = /^[A-Za-z0-9]{9}$/
export const DEFAULT_BASE_URL = "https://api.mistral.ai/v1"
export const PATH = "/chat/completions"
@@ -223,34 +223,6 @@ const MistralEvent = Schema.StructWithRest(
type MistralEvent = Schema.Schema.Type<typeof MistralEvent>
const MistralStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(MistralEvent)])
const hashID = (value: string) => {
const hash = (seed: number) => {
let result = seed
for (const char of value) result = Math.imul(result ^ char.charCodeAt(0), 16777619)
return (result >>> 0).toString(36)
}
return `${hash(2166136261).padStart(7, "0")}${hash(2246822519).padStart(7, "0")}`.slice(-9)
}
const toolIDNormalizer = (request: LLMRequest) => {
const ids = request.messages.flatMap((message) =>
message.content.flatMap((part) => (part.type === "tool-call" || part.type === "tool-result" ? [part.id] : [])),
)
const used = new Set(ids.filter((id) => TOOL_ID.test(id)))
const normalized = new Map<string, string>()
return (id: string) => {
if (TOOL_ID.test(id)) return id
const previous = normalized.get(id)
if (previous) return previous
let attempt = 0
let candidate = hashID(id)
while (used.has(candidate)) candidate = hashID(`${id}:${++attempt}`)
used.add(candidate)
normalized.set(id, candidate)
return candidate
}
}
const lowerMedia = Effect.fn("MistralChat.lowerMedia")(function* (part: MediaPart) {
const media = ProviderShared.normalizeMedia(part)
const url = typeof part.data === "string" && /^(?:https?:|data:)/.test(part.data) ? part.data : media.dataUrl
@@ -359,7 +331,7 @@ const lowerToolResults = Effect.fn("MistralChat.lowerToolResults")(function* (
})
const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request: LLMRequest) {
const normalizeID = toolIDNormalizer(request)
const normalizeID = MistralToolID.normalizer(request)
const messages: MistralMessage[] =
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
for (const message of request.messages) {
@@ -582,13 +554,13 @@ const appendContent = (
}
const normalizeStreamToolID = (state: ParserState, source: string) => {
if (TOOL_ID.test(source))
if (MistralToolID.valid.test(source))
return { id: source, state: { ...state, usedToolIDs: new Set([...state.usedToolIDs, source]) } }
const previous = state.toolIDs.get(source)
if (previous) return { id: previous, state }
let attempt = 0
let id = hashID(source)
while (state.usedToolIDs.has(id)) id = hashID(`${source}:${++attempt}`)
let id = MistralToolID.hash(source)
while (state.usedToolIDs.has(id)) id = MistralToolID.hash(`${source}:${++attempt}`)
return {
id,
state: {
+21 -3
View File
@@ -20,6 +20,7 @@ import {
} from "../schema/index.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { classifyProviderFailure } from "../provider-error.js"
import { effortUpdate } from "../effort-updates.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolSchemaProjection } from "./utils/tool-schema.js"
@@ -164,6 +165,13 @@ export const CompactionItem = Schema.Struct({
encrypted_content: Schema.String,
})
// Kept out of the baseline `InputItem` union: only the OpenAI extension accepts it.
export const ConfigurationUpdate = Schema.Struct({
type: Schema.Literal("configuration_update"),
reasoning: Schema.Struct({ effort: OpenResponsesOptions.ReasoningEffort }),
})
type ConfigurationUpdate = Schema.Schema.Type<typeof ConfigurationUpdate>
export const InputItem = Schema.Union([
CompactionItem,
Schema.Struct({ role: Schema.tag("system"), content: Schema.String }),
@@ -208,6 +216,7 @@ export type HostedToolReplayItem = {
type LoweredInputItem =
| OpenResponsesInputItem
| HostedToolReplayItem
| ConfigurationUpdate
| {
readonly type: "message"
readonly id?: string
@@ -522,7 +531,7 @@ const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenRes
call_id: part.id,
name: part.name,
namespace: part.namespace,
arguments: ProviderShared.encodeJson(part.input),
arguments: ProviderShared.encodeJson(part.input === undefined ? {} : part.input),
}
}
@@ -634,6 +643,8 @@ const lowerToolResultOutput = Effect.fnUntraced(function* (
return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, adapter))
})
const DEFAULT_EFFORT = "medium"
const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
request: LLMRequest,
adapter: ProviderAdapter,
@@ -646,6 +657,14 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
Schema.decodeUnknownEffect(Schema.UndefinedOr(MessageMetadata)),
)(message.providerMetadata?.[providerMetadataKey])
if (message.role === "system") {
const update = effortUpdate(message)
if (update) {
// Consecutive updates are rejected, so a newer one replaces its predecessor.
const last = input.at(-1)
if (last !== undefined && "type" in last && last.type === "configuration_update") input.pop()
input.push({ type: "configuration_update", reasoning: { effort: update.effort ?? DEFAULT_EFFORT } })
continue
}
input.push({
role: "developer",
content: ProviderShared.joinText(yield* ProviderShared.systemUpdateText(adapter.name, message)),
@@ -789,8 +808,7 @@ export const lowerConversation = Effect.fn("OpenResponses.lowerConversation")(fu
}
})
export const lowerGeneration = (request: LLMRequest) => {
const options = OpenResponsesOptions.resolve(request)
export const lowerGeneration = (request: LLMRequest, options = OpenResponsesOptions.resolve(request)) => {
const generation = request.generation
const cacheKey = ProviderShared.promptCacheKey(request)
const parallelToolCalls = resolveParallelToolCalls(request)
+7 -3
View File
@@ -315,7 +315,7 @@ const lowerToolCall = (part: ToolCallPart, options: LoweringOptions): OpenAIChat
type: "function",
function: {
name: part.name,
arguments: ProviderShared.encodeJson(part.input),
arguments: ProviderShared.encodeJson(part.input === undefined ? {} : part.input),
},
})
@@ -323,7 +323,11 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart
const media = ProviderShared.normalizeMedia(part)
if (!media.mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`OpenAI Chat does not support media type ${part.mediaType}`)
return { type: "image_url" as const, image_url: { url: media.dataUrl } }
const url =
typeof part.data === "string" && (part.data.startsWith("https://") || part.data.startsWith("http://"))
? part.data
: media.dataUrl
return { type: "image_url" as const, image_url: { url } }
})
const openAICompatibleReasoningContent = (native: unknown) =>
@@ -766,7 +770,7 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (
supportsStrictMode,
),
),
tool_choice: request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
tool_choice: hasActiveTools && request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined,
stream: true as const,
...(supportsUsageInStreaming ? { stream_options: { include_usage: true } } : {}),
...(zaiToolStream && hasActiveTools ? { tool_stream: true } : {}),
+27 -6
View File
@@ -6,7 +6,9 @@ import { Endpoint } from "../route/endpoint.js"
import { Protocol } from "../route/protocol.js"
import { HttpTransport } from "../route/transport/index.js"
import { LLMRequest, mergeJsonRecords, type JsonSchema, type ToolDefinition, type ToolEntry } from "../schema/index.js"
import { resolveEffortUpdates } from "../effort-updates.js"
import { OpenResponses } from "./open-responses.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { OpenAIImage } from "./utils/openai-image.js"
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
@@ -94,9 +96,15 @@ const OpenAIResponsesToolChoice = Schema.Union([
Schema.Struct({ type: Schema.tag("image_generation") }),
])
const OpenAIResponsesInputItem = Schema.Union([
OpenResponses.InputItem,
OpenAIResponsesHostedToolItem,
OpenResponses.ConfigurationUpdate,
])
const OpenAIResponsesCoreFields = {
...OpenResponses.coreFields,
input: Schema.Array(Schema.Union([OpenResponses.InputItem, OpenAIResponsesHostedToolItem])),
input: Schema.Array(OpenAIResponsesInputItem),
tools: optionalArray(OpenAIResponsesTools),
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
context_management: Schema.optional(
@@ -119,7 +127,7 @@ export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
export const CompactionTrigger = Schema.Struct({ type: Schema.Literal("compaction_trigger") })
const CheckpointBody = Schema.Struct({
...OpenAIResponsesBody.fields,
input: Schema.Array(Schema.Union([OpenResponses.InputItem, OpenAIResponsesHostedToolItem, CompactionTrigger])),
input: Schema.Array(Schema.Union([OpenAIResponsesInputItem, CompactionTrigger])),
store: Schema.Literal(false),
prompt_cache_retention: optionalNull(Schema.String),
prompt_cache_options: optionalNull(
@@ -133,6 +141,14 @@ const adapter = {
restoreHostedToolItem: (item: unknown) => (Schema.is(OpenAIResponsesHostedToolItem)(item) ? item : undefined),
} satisfies OpenResponses.ProviderAdapter
// Only GPT-6 Astra accepts `configuration_update`, and never alongside automatic `context_management` compaction.
const supportsEffortUpdates = (request: LLMRequest) => {
if (request.providerOptions?.contextManagement !== undefined) return false
const override = request.model.compatibility?.supportsEffortUpdates
if (override !== undefined) return override
return /(?:^|\/)gpt-6-astra$/i.test(request.model.id)
}
const nativeImageToolInput = (tool: ToolDefinition) => {
const native = tool.native?.openai
return ProviderShared.isRecord(native) && native.type === "image_generation" ? native : undefined
@@ -189,18 +205,22 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
const management = yield* ProviderShared.validateWith(
Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)),
)(request.providerOptions?.contextManagement)
const options = OpenResponsesOptions.resolve(request)
const updates = resolveEffortUpdates(request, options.reasoningEffort)
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
return yield* decodeBody({
...(yield* OpenResponses.lowerConversation(request, adapter)),
...OpenResponses.lowerGeneration(request),
...(yield* OpenResponses.lowerConversation(updates.request, adapter)),
...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }),
context_management: management?.map((edit) => ({ type: edit.type, compact_threshold: edit.compactThreshold })),
tools:
request.tools.length === 0
? undefined
: yield* Effect.forEach(request.tools, (tool) => lowerToolEntry(tool, toolSchemaCompatibility)),
tool_choice:
OpenResponses.allowedToolChoice(request) ??
(request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined),
request.tools.length === 0
? undefined
: (OpenResponses.allowedToolChoice(request) ??
(request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined)),
})
})
@@ -295,6 +315,7 @@ export const protocol = Protocol.make({
step,
terminal: OpenResponses.terminal,
},
supportsEffortUpdates,
})
const endpoint = Endpoint.path<OpenAIResponsesBody>(PATH, { baseURL: DEFAULT_BASE_URL })
@@ -57,6 +57,25 @@ const documentBlock = (name: string, format: DocumentFormat, bytes: string): Doc
},
})
function documentName(filename: string | undefined, names: Set<string>) {
const base =
(filename ?? "")
.replace(/\.[^.]*$/, "")
.replace(/[^a-zA-Z0-9 ()[\]-]/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 200)
.trim() || "document"
let name = base
// Converse requires labels to be unique across the entire request, including tool results.
for (let index = 2; names.has(name); index++) {
const suffix = ` ${index}`
name = `${base.slice(0, 200 - suffix.length).trimEnd()}${suffix}`
}
names.add(name)
return name
}
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) {
const media = ProviderShared.normalizeMedia(part)
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(
@@ -72,19 +91,26 @@ const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: Media
// document block. Image MIME types not in `IMAGE_FORMATS` (e.g. `image/svg+xml`)
// get an image-specific error so the caller knows it's a format-support issue,
// not a kind-detection issue.
export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart) {
export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart, documentNames: Set<string>) {
const mime = part.mediaType.toLowerCase()
const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS]
if (imageFormat) {
return { image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } } satisfies ImageBlock
return [{ image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } } satisfies ImageBlock]
}
if (mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`)
const documentFormat = DOCUMENT_FORMATS[mime as keyof typeof DOCUMENT_FORMATS]
if (documentFormat) {
if (!part.filename)
return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename")
return documentBlock(part.filename, documentFormat, yield* mediaBase64(part))
const name = documentName(part.filename, documentNames)
const block = documentBlock(name, documentFormat, yield* mediaBase64(part))
return part.filename !== undefined && part.filename !== name
? [
{
text: `Attached file ${ProviderShared.encodeJson(part.filename)} has document label ${ProviderShared.encodeJson(name)}.`,
},
block,
]
: [block]
}
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`)
})
@@ -0,0 +1,34 @@
import type { LLMRequest } from "../../schema/index.js"
export const valid = /^[A-Za-z0-9]{9}$/
export const hash = (value: string) => {
const hash = (seed: number) => {
let result = seed
for (const char of value) result = Math.imul(result ^ char.charCodeAt(0), 16777619)
return (result >>> 0).toString(36)
}
return `${hash(2166136261).padStart(7, "0")}${hash(2246822519).padStart(7, "0")}`.slice(-9)
}
export const normalizer = (request: LLMRequest) => {
const ids = request.messages.flatMap((message) =>
message.content.flatMap((part) => (part.type === "tool-call" || part.type === "tool-result" ? [part.id] : [])),
)
// Reserve valid IDs before projecting any history, including IDs encountered later.
const used = new Set(ids.filter((id) => valid.test(id)))
const normalized = new Map<string, string>()
return (id: string) => {
if (valid.test(id)) return id
const previous = normalized.get(id)
if (previous) return previous
let attempt = 0
let candidate = hash(id)
while (used.has(candidate)) candidate = hash(`${id}:${++attempt}`)
used.add(candidate)
normalized.set(id, candidate)
return candidate
}
}
export * as MistralToolID from "./mistral-tool-id.js"
@@ -1,12 +1,7 @@
import { Option, Schema } from "effect"
import type { LLMRequest } from "../../schema/index.js"
import { ReasoningEffort, ReasoningEfforts, type LLMRequest } from "../../schema/index.js"
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {})
export const ReasoningEffort = Schema.declare<ReasoningEffort>(
(value): value is ReasoningEffort => typeof value === "string",
{ title: "ReasoningEffort" },
)
export { ReasoningEffort, ReasoningEfforts }
export const TextVerbosities = ["low", "medium", "high"] as const
export type TextVerbosity = (typeof TextVerbosities)[number] | (string & {})
@@ -11,6 +11,7 @@ import {
mergeJsonRecords,
} from "../../schema/index.js"
import type { CompactOperation } from "../../route/client.js"
import { stripEffortUpdates } from "../../effort-updates.js"
import { Endpoint } from "../../route/endpoint.js"
import { RequestExecutor } from "../../route/executor.js"
import { HttpTransport } from "../../route/transport/index.js"
@@ -75,7 +76,8 @@ const Response = Schema.Struct({
export const make = (adapter: OpenResponses.ProviderAdapter): CompactOperation =>
Effect.fn("ResponsesCompaction.execute")(function* (request, executor, options) {
const route = request.model.route
const native = yield* OpenResponses.lowerConversation(request, adapter)
// The standalone compaction endpoint rejects histories containing configuration updates.
const native = yield* OpenResponses.lowerConversation(stripEffortUpdates(request), adapter)
const body = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))(
mergeJsonRecords(
{
+5 -1
View File
@@ -37,7 +37,7 @@ export type Settings = ProviderPackage.Settings &
}
const fromRequest = Effect.fn("GoogleVertex.fromRequest")(function* (request: LLMRequest) {
const body = yield* Gemini.protocol.body.from(request)
const { serviceTier: _, ...body } = yield* Gemini.protocol.body.from(request)
// Vertex's native REST schema rejects `id` on FunctionCall/FunctionResponse parts with HTTP 400,
// unlike AI Studio, so history minted there cannot be lowered verbatim.
const contents = body.contents.map((content) => ({
@@ -75,6 +75,10 @@ const route = Route.make({
return `/${model.startsWith("endpoints/") ? model : `models/${model}`}:streamGenerateContent?alt=sse`
}),
auth: Auth.none,
headers: ({ request }): Record<string, string> => {
const serviceTier = request.providerOptions?.serviceTier
return typeof serviceTier === "string" ? { "x-vertex-ai-llm-shared-request-type": serviceTier } : {}
},
framing: Framing.sse,
})
+6 -1
View File
@@ -7,6 +7,7 @@ import { HttpTransport } from "./transport/index.js"
import type { HttpMiddleware, Transport, TransportRuntime, WebSocketChannelExecutor } from "./transport/index.js"
import type { Protocol } from "./protocol.js"
import { applyCachePolicy } from "../cache-policy.js"
import { applyEffortUpdates } from "../effort-updates.js"
import { normalizeToolHistory } from "../tool-history.js"
import { sanitizeSurrogates } from "../utils/sanitize.js"
import * as ProviderShared from "../protocols/shared.js"
@@ -55,6 +56,7 @@ export interface Route<
readonly transport: Transport<Body, Prepared, unknown>
readonly defaults: RouteDefaults
readonly body: RouteBody<Body>
readonly supportsEffortUpdates?: (request: LLMRequest) => boolean
readonly with: {
<Next extends CompactionOperations | undefined>(
patch: RoutePatch<Body, Prepared> & { readonly compact: Next },
@@ -388,6 +390,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
transport: routeInput.transport,
defaults: routeInput.defaults ?? {},
body: protocol.body,
supportsEffortUpdates: protocol.supportsEffortUpdates,
with: (patch: RoutePatch<Body, Prepared>) => {
const { compact, id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch
return build({
@@ -558,7 +561,9 @@ const prepareRequest = (request: LLMRequest) => {
[...new Map(tools.map((tool) => [`${tool.type}:${tool.name}`, tool])).values()].map((tool) =>
tool.type === "tool" ? tool : { ...tool, tools: dedupe(tool.tools) },
)
const resolved = applyCachePolicy(LLMRequest.update(sanitized, { tools: dedupe(sanitized.tools) }))
const resolved = applyCachePolicy(
applyEffortUpdates(LLMRequest.update(sanitized, { tools: dedupe(sanitized.tools) })),
)
const headers = resolved.model.route.headers?.({ request: resolved })
return headers === undefined
? resolved
+2
View File
@@ -41,6 +41,8 @@ export interface Protocol<Body, Frame, Event, State> {
readonly body: ProtocolBody<Body>
/** Response side: streaming state machine. */
readonly stream: ProtocolStream<Frame, Event, State>
/** Whether `body.from` lowers `Message.effort(...)` markers; wrappers around another `body.from` must forward it. */
readonly supportsEffortUpdates?: (request: LLMRequest) => boolean
}
export interface ProtocolBody<Body> {
+13
View File
@@ -9,6 +9,7 @@ import {
LanguageModelSchema,
type LanguageModel,
ProviderOptions,
ReasoningEffort,
} from "./options.js"
import { ProviderID } from "./ids.js"
@@ -217,6 +218,14 @@ export const CompactionPart = Object.assign(compactionPartSchema, {
Schema.decodeUnknownSync(compactionPartSchema)({ type: "compaction", ...input }),
})
/** Reasoning effort changed here, from `previous` to `effort`; `undefined` is the model default. */
export const EffortPart = Schema.Struct({
type: Schema.Literal("effort"),
effort: Schema.optional(ReasoningEffort),
previous: Schema.optional(ReasoningEffort),
}).annotate({ identifier: "LLM.Content.Effort" })
export type EffortPart = Schema.Schema.Type<typeof EffortPart>
export const ContentPart = Schema.Union([
TextPart,
MediaPart,
@@ -224,6 +233,7 @@ export const ContentPart = Schema.Union([
ToolResultPart,
ReasoningPart,
CompactionPart,
EffortPart,
]).pipe(Schema.toTaggedUnion("type"))
export type ContentPart = Schema.Schema.Type<typeof ContentPart>
@@ -265,6 +275,9 @@ export namespace Message {
*/
export const system = (content: SystemContentInput) => make({ role: "system", content })
export const effort = (input: { readonly effort?: ReasoningEffort; readonly previous?: ReasoningEffort }) =>
make({ role: "system", content: [{ type: "effort", effort: input.effort, previous: input.previous }] })
export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
}
+9
View File
@@ -140,6 +140,13 @@ export namespace LanguageModelDefaults {
}
}
export const ReasoningEfforts = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const
export type ReasoningEffort = (typeof ReasoningEfforts)[number] | (string & {})
export const ReasoningEffort = Schema.declare<ReasoningEffort>(
(value): value is ReasoningEffort => typeof value === "string",
{ title: "ReasoningEffort" },
)
export const LanguageModelToolSchemaCompatibility = Schema.Literals(["gemini", "moonshot"])
export type LanguageModelToolSchemaCompatibility = Schema.Schema.Type<typeof LanguageModelToolSchemaCompatibility>
@@ -165,6 +172,8 @@ export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompat
requireSignature: Schema.optional(Schema.Boolean),
/** Supports Anthropic's thinking-prefix mismatch controls. Overrides model-ID detection. */
supportsThinkingBlockBinding: Schema.optional(Schema.Boolean),
/** Supports per-message effort updates. Overrides model-ID detection. */
supportsEffortUpdates: Schema.optional(Schema.Boolean),
}) {}
export namespace LanguageModelCompatibility {
+441
View File
@@ -0,0 +1,441 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMRequest, Message, ToolCallPart } from "../src/index.js"
import { Auth, LLMClient } from "../src/route.js"
import { compileRequest } from "../src/route/client.js"
import { AnthropicMessages } from "../src/protocols/anthropic-messages.js"
import { OpenAIResponses } from "../src/protocols/openai-responses.js"
import { Gemini } from "../src/protocols/gemini.js"
import { GoogleVertexMessages, OpenAI } from "../src/providers.js"
import { applyCachePolicy } from "../src/cache-policy.js"
import { applyEffortUpdates } from "../src/effort-updates.js"
import { it, testEffect } from "./lib/effect.js"
import { dynamicResponse } from "./lib/http.js"
import { sseEvents } from "./lib/sse.js"
const anthropic = (id: string, compatibility?: { readonly supportsEffortUpdates?: boolean }) =>
AnthropicMessages.route
.with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") })
.model({ id, compatibility })
const openai = (id: string, compatibility?: { readonly supportsEffortUpdates?: boolean }) =>
OpenAIResponses.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id, compatibility })
const opus5 = anthropic("claude-opus-5")
const astra = openai("gpt-6-astra")
const lowFromHigh = Message.effort({ effort: "low", previous: "high" })
const conversation = [Message.user("Before."), lowFromHigh, Message.user("After.")]
const systemMessages = (body: AnthropicMessages.AnthropicMessagesBody) =>
body.messages.filter((message) => message.role === "system")
const updates = (body: OpenAIResponses.OpenAIResponsesBody) =>
body.input.filter((item) => "type" in item && item.type === "configuration_update")
describe("applyEffortUpdates", () => {
test("keeps the request identity without markers and for protocols that lower them", () => {
const plain = LLM.request({ model: opus5, prompt: "hi" })
expect(applyEffortUpdates(plain)).toBe(plain)
const supported = LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "low" } })
expect(applyEffortUpdates(supported)).toBe(supported)
})
it.effect("compiles markers away for protocols without per-message effort", () =>
Effect.gen(function* () {
const model = Gemini.route
.with({
endpoint: { baseURL: "https://generativelanguage.test/v1beta/" },
auth: Auth.header("x-goog-api-key", "test"),
})
.model({ id: "gemini-3.5-flash" })
const withMarkers = yield* compileRequest(LLM.request({ model, messages: conversation }))
const withoutMarkers = yield* compileRequest(
LLM.request({ model, messages: [Message.user("Before."), Message.user("After.")] }),
)
expect(withMarkers.body).toEqual(withoutMarkers.body)
}),
)
})
describe("cache policy", () => {
it.effect("walks the tail breakpoint back past a trailing effort marker", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [Message.user("first"), Message.assistant("reply"), Message.user("latest"), lowFromHigh],
providerOptions: { effort: "low" },
cache: "auto",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "first" }] },
{ role: "assistant", content: [{ type: "text", text: "reply" }] },
{ role: "user", content: [{ type: "text", text: "latest", cache_control: { type: "ephemeral" } }] },
{ role: "system", content: [], output_config: { effort: "low" } },
])
}),
)
})
describe("Anthropic Messages effort updates", () => {
it.effect("lowers markers to per-turn system messages and freezes the top-level effort", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "low" }, cache: "none" }),
)
expect(prepared.body.output_config).toEqual({ effort: "high" })
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Before." }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "text", text: "After." }] },
])
}),
)
it.effect("omits the frozen effort for the model default and sends `high` for a switch back to it", () =>
Effect.gen(function* () {
const format = { type: "json_schema" as const, schema: { type: "object" } }
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [
Message.user("One."),
Message.effort({ effort: "low" }),
Message.user("Two."),
Message.effort({ previous: "low" }),
Message.user("Three."),
],
providerOptions: { output_config: { format } },
cache: "none",
}),
)
expect(prepared.body.output_config).toEqual({ format })
expect(systemMessages(prepared.body)).toEqual([
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "system", content: [], output_config: { effort: "high" } },
])
}),
)
it.effect("requests the mid-conversation output config beta only when markers are sent", () =>
Effect.gen(function* () {
for (const [model, expected] of [
[opus5, true],
[anthropic("claude-sonnet-5"), false],
] as const) {
const request = LLM.request({
model,
messages: conversation,
providerOptions: { effort: "low" },
http: { headers: { "anthropic-beta": "existing-beta" } },
})
const compiled = yield* compileRequest(request)
const prepared = yield* AnthropicMessages.route.prepareTransport(compiled.body, request)
const betas = prepared.request.headers["anthropic-beta"]!.split(",")
expect(betas).toContain("existing-beta")
expect(betas.includes("mid-conversation-output-config-2026-07-01")).toBe(expected)
}
}),
)
it.effect("accepts a marker between a tool call and its result", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: opus5,
messages: [
Message.user("Weather?"),
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
lowFromHigh,
Message.tool({ id: "call_1", name: "lookup", result: { temp: 72 } }),
],
providerOptions: { effort: "low" },
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Weather?" }] },
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_1", content: '{"temp":72}' }] },
])
}),
)
it.effect("falls back to a plain top-level effort when history drifted from the current effort", () =>
Effect.gen(function* () {
const drifted = yield* compileRequest(
LLM.request({ model: opus5, messages: conversation, providerOptions: { effort: "medium" }, cache: "none" }),
)
const plain = yield* compileRequest(
LLM.request({
model: opus5,
messages: [Message.user("Before."), Message.user("After.")],
providerOptions: { effort: "medium" },
cache: "none",
}),
)
expect(drifted.body).toEqual(plain.body)
expect(drifted.body.output_config).toEqual({ effort: "medium" })
}),
)
for (const [id, supported] of [
["claude-opus-5", true],
["claude-opus-5-20260901", true],
["anthropic/claude-opus-5", true],
["claude-fable-5-1", true],
["claude-mythos-5-1", true],
["claude-fable-5", false],
["claude-opus-4-8", false],
["claude-sonnet-5", false],
["kimi-k2.5", false],
] as const) {
it.effect(`${supported ? "lowers" : "strips"} markers for ${id}`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: anthropic(id), messages: conversation, providerOptions: { effort: "low" } }),
)
expect(systemMessages(prepared.body)).toHaveLength(supported ? 1 : 0)
expect(prepared.body.output_config).toEqual({ effort: supported ? "high" : "low" })
}),
)
}
it.effect("honors the compatibility override in both directions", () =>
Effect.gen(function* () {
const enabled = yield* compileRequest(
LLM.request({
model: anthropic("claude-sonnet-5", { supportsEffortUpdates: true }),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
const disabled = yield* compileRequest(
LLM.request({
model: anthropic("claude-opus-5", { supportsEffortUpdates: false }),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
expect(systemMessages(enabled.body)).toHaveLength(1)
expect(systemMessages(disabled.body)).toHaveLength(0)
}),
)
it.effect("strips markers on the Vertex Anthropic route, whose protocol wrapper does not forward support", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: GoogleVertexMessages.configure({ accessToken: "test", location: "global", project: "test" }).model(
"claude-opus-5",
),
messages: conversation,
providerOptions: { effort: "low" },
}),
)
expect(systemMessages(prepared.body)).toHaveLength(0)
expect(prepared.body.output_config).toEqual({ effort: "low" })
}),
)
})
describe("OpenAI Responses effort updates", () => {
it.effect("lowers markers to configuration_update items and freezes reasoning.effort", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: astra, messages: conversation, providerOptions: { reasoningEffort: "low" } }),
)
expect(prepared.body.reasoning).toEqual({ effort: "high" })
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "low" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
}),
)
it.effect("coalesces consecutive updates so the newest wins", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: [
Message.user("Before."),
Message.effort({ effort: "low", previous: "medium" }),
Message.effort({ effort: "xhigh", previous: "low" }),
Message.user("After."),
],
providerOptions: { reasoningEffort: "xhigh" },
}),
)
expect(prepared.body.reasoning).toEqual({ effort: "medium" })
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "xhigh" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
}),
)
it.effect("omits reasoning.effort for the model default and sends `medium` for a switch back to it", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: [
Message.user("One."),
Message.effort({ effort: "low" }),
Message.user("Two."),
Message.effort({ previous: "low" }),
Message.user("Three."),
],
}),
)
expect(prepared.body.reasoning).toBeUndefined()
expect(updates(prepared.body)).toEqual([
{ type: "configuration_update", reasoning: { effort: "low" } },
{ type: "configuration_update", reasoning: { effort: "medium" } },
])
}),
)
it.effect("falls back to a plain top-level effort when history drifted from the current effort", () =>
Effect.gen(function* () {
const drifted = yield* compileRequest(
LLM.request({ model: astra, messages: conversation, providerOptions: { reasoningEffort: "xhigh" } }),
)
const plain = yield* compileRequest(
LLM.request({
model: astra,
messages: [Message.user("Before."), Message.user("After.")],
providerOptions: { reasoningEffort: "xhigh" },
}),
)
expect(drifted.body).toEqual(plain.body)
expect(drifted.body.reasoning).toEqual({ effort: "xhigh" })
}),
)
it.effect("strips markers when automatic context management is enabled", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: astra,
messages: conversation,
providerOptions: { reasoningEffort: "low", contextManagement: [{ type: "compaction" }] },
}),
)
expect(updates(prepared.body)).toEqual([])
expect(prepared.body.reasoning).toEqual({ effort: "low" })
expect(prepared.body.context_management).toEqual([{ type: "compaction" }])
}),
)
for (const [id, supported] of [
["gpt-6-astra", true],
["openai/gpt-6-astra", true],
["gpt-6-astra-2026-09-01", false],
["gpt-5.6-sol", false],
] as const) {
it.effect(`${supported ? "lowers" : "strips"} markers for ${id}`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model: openai(id), messages: conversation, providerOptions: { reasoningEffort: "low" } }),
)
expect(updates(prepared.body)).toHaveLength(supported ? 1 : 0)
expect(prepared.body.reasoning).toEqual({ effort: supported ? "high" : "low" })
}),
)
}
it.effect("honors the compatibility override in both directions", () =>
Effect.gen(function* () {
const enabled = yield* compileRequest(
LLM.request({
model: openai("gpt-5.5", { supportsEffortUpdates: true }),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
}),
)
const disabled = yield* compileRequest(
LLM.request({
model: openai("gpt-6-astra", { supportsEffortUpdates: false }),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
}),
)
expect(updates(enabled.body)).toHaveLength(1)
expect(updates(disabled.body)).toHaveLength(0)
}),
)
const checkpoint = { type: "compaction", id: "cmp_1", encrypted_content: "opaque" }
const compactRequest = LLM.request({
model: OpenAI.configure({ apiKey: "fixture" }).responses("gpt-6-astra"),
messages: conversation,
providerOptions: { reasoningEffort: "low" },
})
testEffect(
dynamicResponse(({ text, respond }) =>
Effect.sync(() => {
const body = JSON.parse(text)
expect(body.reasoning).toEqual({ effort: "high" })
expect(body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ type: "configuration_update", reasoning: { effort: "low" } },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
{ type: "compaction_trigger" },
])
return respond(sseEvents({ type: "response.completed", response: { id: "resp_1", output: [checkpoint] } }), {
headers: { "content-type": "text/event-stream" },
})
}),
),
).effect("keeps configuration updates in the checkpoint body", () =>
Effect.gen(function* () {
const result = yield* LLMClient.compact(compactRequest, { mechanism: "trigger" })
expect(result.checkpoint.encrypted).toBe("opaque")
}),
)
testEffect(
dynamicResponse(({ request, text, respond }) =>
Effect.sync(() => {
expect(new URL(request.url).pathname).toEndWith("/responses/compact")
expect(JSON.parse(text).input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
return respond(JSON.stringify({ object: "response.compaction", output: [checkpoint] }))
}),
),
).effect("drops markers from the compaction endpoint body", () =>
Effect.gen(function* () {
const result = yield* LLMClient.compact(compactRequest, { mechanism: "endpoint" })
expect(result.replacement.map((message) => message.content[0]?.type)).toEqual(["compaction"])
}),
)
})
+11
View File
@@ -325,6 +325,7 @@ describe("provider package entrypoints", () => {
test("requires an Anthropic-compatible base URL at runtime", async () => {
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally bypasses static required-option checks.
Reflect.apply(AnthropicCompatible.model, undefined, ["compatible-model", { apiKey: "fixture" }]),
).toThrow(configuration("anthropic-compatible", "Anthropic-compatible providers require a baseURL"))
})
@@ -333,6 +334,7 @@ describe("provider package entrypoints", () => {
const Anthropic = await import("@opencode/ai/providers/anthropic")
const AnthropicCompatible = await import("@opencode/ai/providers/anthropic-compatible")
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
Reflect.apply(AnthropicCompatible.model, undefined, [
"compatible-model",
{
@@ -343,6 +345,7 @@ describe("provider package entrypoints", () => {
]),
).toThrow(configuration("anthropic-compatible", "Anthropic-compatible apiKey cannot be combined with authToken"))
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
Reflect.apply(Anthropic.model, undefined, ["claude-sonnet-4-6", { apiKey: "fixture", authToken: "token" }]),
).toThrow(configuration("anthropic", "Anthropic apiKey cannot be combined with authToken"))
})
@@ -490,11 +493,13 @@ describe("provider package entrypoints", () => {
const GoogleVertexResponses = await import("@opencode/ai/providers/google-vertex/responses")
const Providers = await import("@opencode/ai/providers")
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
Reflect.apply(GoogleVertex.model, undefined, [
"gemini-3.5-flash",
{ accessToken: "token", apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex apiKey cannot be combined with accessToken or auth"))
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes a statically invalid option combination.
const configured = Reflect.apply(GoogleVertex.configure, undefined, [
{ accessToken: "token", auth: {}, project: "vertex-project" },
])
@@ -502,34 +507,40 @@ describe("provider package entrypoints", () => {
configuration("google-vertex", "Google Vertex accessToken cannot be combined with auth"),
)
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
Reflect.apply(GoogleVertexMessages.model, undefined, [
"claude-sonnet-4-6",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Messages does not support API keys"))
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
Reflect.apply(Providers.GoogleVertexMessages.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Messages does not support API keys"))
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
Reflect.apply(GoogleVertexChat.model, undefined, [
"deepseek-ai/deepseek-v3.2-maas",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Chat does not support API keys"))
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
Reflect.apply(Providers.GoogleVertexChat.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Chat does not support API keys"))
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
Reflect.apply(GoogleVertexResponses.model, undefined, [
"xai/grok-4.20-reasoning",
{ apiKey: "fixture", project: "vertex-project" },
]),
).toThrow(configuration("google-vertex", "Google Vertex Responses does not support API keys"))
expect(() =>
// oxlint-disable-next-line no-restricted-globals -- This test intentionally passes an unsupported authentication option.
Reflect.apply(Providers.GoogleVertexResponses.configure, undefined, [
{ apiKey: "fixture", project: "vertex-project" },
]),
@@ -1,7 +1,7 @@
import { EventStreamCodec } from "@smithy/eventstream-codec"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
import { describe, expect } from "bun:test"
import { Effect, Encoding, Ref, Stream } from "effect"
import { Effect, Encoding, Ref, Schema, Stream } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import {
CacheHint,
@@ -11,9 +11,11 @@ import {
LLMEvent,
LLMRequest,
Message,
Tool,
ToolCallPart,
ToolChoice,
ToolDefinition,
ToolRuntime,
} from "../../src/index.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
@@ -394,6 +396,45 @@ describe("Bedrock Converse route", () => {
})
}),
)
;[
{ name: "browser.tabs.open", expected: "browser_tabs_open" },
{ name: "$lookup", expected: "_lookup" },
{ name: "", expected: "_" },
{ name: " ", expected: "___" },
{ name: "a".repeat(65), expected: "a".repeat(64) },
{ name: "lookup_123-ABC", expected: "lookup_123-ABC" },
{ name: "a".repeat(64), expected: "a".repeat(64) },
].forEach((item) => {
it.effect(`replays historical tool name ${JSON.stringify(item.name)} within Bedrock constraints`, () =>
Effect.gen(function* () {
const call = ToolCallPart.make({ id: "call_unknown", name: item.name, input: { query: "weather" } })
const error = `No tool named "${item.name}" is currently available. Please use a tool from the available tool list.`
const request = LLM.request({
model,
cache: "none",
tools: [ToolDefinition.make({ name: "execute", description: "Run code", inputSchema: { type: "object" } })],
messages: [
Message.user("Check the weather"),
Message.assistant([call]),
Message.tool({ id: call.id, name: call.name, result: error, resultType: "error" }),
Message.user("Say OK"),
],
})
const prepared = yield* compileRequest(request)
expect(prepared.body.messages[1].content).toEqual([
{ toolUse: { toolUseId: call.id, name: item.expected, input: call.input } },
])
expect(prepared.body.messages[2].content).toEqual([
{ toolResult: { toolUseId: call.id, content: [{ text: error }], status: "error" } },
{ text: "Say OK" },
])
expect(prepared.body.toolConfig.tools.map((tool) => tool.toolSpec.name)).toEqual(["execute"])
expect(request.messages[1].content[0]).toEqual(call)
expect(call.name).toBe(item.name)
}),
)
})
it.effect("removes empty keys recursively from outbound tool inputs without mutating history", () =>
Effect.gen(function* () {
@@ -797,6 +838,57 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects a provider-emitted dotted name before normalizing its replay", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(
fixedBytes(
eventStreamBody(
[
"contentBlockStart",
{ contentBlockIndex: 0, start: { toolUse: { toolUseId: "call_unknown", name: "browser.tabs.open" } } },
],
["contentBlockDelta", { contentBlockIndex: 0, delta: { toolUse: { input: "{}" } } }],
["contentBlockStop", { contentBlockIndex: 0 }],
["messageStop", { stopReason: "tool_use" }],
),
),
),
)
const call = response.toolCalls[0]
if (!call) throw new Error("Expected a tool call")
expect(call.name).toBe("browser.tabs.open")
const dispatched = yield* ToolRuntime.dispatch(
{
browser_tabs_open: Tool.make({
description: "Open a tab",
parameters: Schema.Struct({}),
success: Schema.String,
execute: () => Effect.die("A normalized replay name must not select an executor"),
}),
},
call,
)
expect(dispatched.result).toEqual({
type: "error",
value:
'No tool named "browser.tabs.open" is currently available. Please use a tool from the available tool list.',
})
const prepared = yield* compileRequest(
LLM.request({
model,
cache: "none",
messages: [response.message, Message.tool({ id: call.id, name: call.name, result: dispatched.result })],
}),
)
expect(prepared.body.messages[0].content).toEqual([
{ toolUse: { toolUseId: call.id, name: "browser_tabs_open", input: {} } },
])
expect(response.toolCalls[0]?.name).toBe("browser.tabs.open")
}),
)
it.effect("ignores tool deltas without an open tool block", () =>
Effect.gen(function* () {
const body = eventStreamBody(
@@ -1673,29 +1765,141 @@ describe("Bedrock Converse route", () => {
role: "user",
content: [
{ text: "Summarize these documents." },
{ document: { format: "pdf", name: "report.pdf", source: { bytes: "UERGREFUQQ==" } } },
{ document: { format: "csv", name: "data.csv", source: { bytes: "Q1NWREFUQQ==" } } },
{ text: 'Attached file "report.pdf" has document label "report".' },
{ document: { format: "pdf", name: "report", source: { bytes: "UERGREFUQQ==" } } },
{ text: 'Attached file "data.csv" has document label "data".' },
{ document: { format: "csv", name: "data", source: { bytes: "Q1NWREFUQQ==" } } },
],
},
],
})
}),
)
;[
{
label: "filename punctuation",
filename: "report_v1.2?.pdf",
expected: "report v1 2",
duplicate: "report v1 2 2",
},
{
label: "repeated whitespace",
filename: " Quarterly\t \n report.txt",
expected: "Quarterly report",
duplicate: "Quarterly report 2",
},
{
label: "allowed characters",
filename: "Report - Final (v2) [2026]",
expected: "Report - Final (v2) [2026]",
duplicate: "Report - Final (v2) [2026] 2",
},
{ label: "accented filename", filename: "résumé.pdf", expected: "r sum", duplicate: "r sum 2" },
{ label: "non-Latin filename", filename: "報告書.pdf", expected: "document", duplicate: "document 2" },
{ label: "missing filename", filename: undefined, expected: "document", duplicate: "document 2" },
{ label: "empty filename", filename: "", expected: "document", duplicate: "document 2" },
{ label: "blank filename", filename: " \t\n", expected: "document", duplicate: "document 2" },
{ label: "extension-only filename", filename: ".pdf", expected: "document", duplicate: "document 2" },
{ label: "symbols-only filename", filename: "@@@.pdf", expected: "document", duplicate: "document 2" },
{
label: "overlong filename",
filename: `${"a".repeat(201)}.txt`,
expected: "a".repeat(200),
duplicate: `${"a".repeat(198)} 2`,
},
{
label: "maximum-length label",
filename: "a".repeat(200),
expected: "a".repeat(200),
duplicate: `${"a".repeat(198)} 2`,
},
{
label: "whitespace at truncation",
filename: `${"a".repeat(199)} b.txt`,
expected: "a".repeat(199),
duplicate: `${"a".repeat(198)} 2`,
},
].forEach((item) => {
it.effect(`normalizes ${item.label} in user and tool-result documents`, () =>
Effect.gen(function* () {
const request = LLM.request({
model,
cache: "none",
messages: [
Message.user([
{ type: "text", text: "Read this document" },
{ type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==", filename: item.filename },
]),
Message.assistant([ToolCallPart.make({ id: "call_read", name: "read", input: {} })]),
Message.tool({
id: "call_read",
name: "read",
result: {
type: "content",
value: [
{ type: "text", text: "Read successfully" },
{
type: "file",
uri: "data:application/pdf;base64,UERGREFUQQ==",
mime: "application/pdf",
name: item.filename,
},
],
},
}),
],
})
const original = JSON.stringify(request.messages)
const first = yield* compileRequest(request)
const second = yield* compileRequest(request)
const expected = { format: "pdf", name: item.expected, source: { bytes: "UERGREFUQQ==" } }
it.effect("requires names for document media", () =>
expect(first.body.messages[0].content.find((part) => "document" in part)?.document).toEqual(expected)
expect(
first.body.messages[2].content[0].toolResult.content.find((part) => "document" in part)?.document,
).toEqual({
...expected,
name: item.duplicate,
})
expect(second.body).toEqual(first.body)
expect(JSON.stringify(request.messages)).toBe(original)
}),
)
})
it.effect("keeps colliding document labels distinct within a request", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [Message.user({ type: "media", mediaType: "application/pdf", data: "UERGREFUQQ==" })],
cache: "none",
messages: [
Message.user([
{ type: "text", text: "Read these documents" },
...["report_v1.txt", "report#v1.txt", "report v1 2.txt", "report v1.txt"].map((filename) => ({
type: "media" as const,
mediaType: "text/plain",
data: "SGVsbG8=",
filename,
})),
]),
],
}),
).pipe(Effect.flip)
expect(error.message).toContain("document media requires a filename")
)
expect(prepared.body.messages[0].content.slice(1)).toEqual([
{ text: 'Attached file "report_v1.txt" has document label "report v1".' },
{ document: { format: "txt", name: "report v1", source: { bytes: "SGVsbG8=" } } },
{ text: 'Attached file "report#v1.txt" has document label "report v1 2".' },
{ document: { format: "txt", name: "report v1 2", source: { bytes: "SGVsbG8=" } } },
{ text: 'Attached file "report v1 2.txt" has document label "report v1 2 2".' },
{ document: { format: "txt", name: "report v1 2 2", source: { bytes: "SGVsbG8=" } } },
{ text: 'Attached file "report v1.txt" has document label "report v1 3".' },
{ document: { format: "txt", name: "report v1 3", source: { bytes: "SGVsbG8=" } } },
])
}),
)
it.effect("passes named document-only messages through for provider validation", () =>
it.effect("annotates renamed document-only messages with their original filename", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
@@ -1715,12 +1919,44 @@ describe("Bedrock Converse route", () => {
expect(prepared.body.messages).toEqual([
{
role: "user",
content: [{ document: { format: "pdf", name: "report.pdf", source: { bytes: "UERGREFUQQ==" } } }],
content: [
{ text: 'Attached file "report.pdf" has document label "report".' },
{ document: { format: "pdf", name: "report", source: { bytes: "UERGREFUQQ==" } } },
],
},
])
}),
)
it.effect("quotes original filenames in annotations and omits redundant mappings", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
cache: "none",
messages: [
Message.user([
{ type: "text", text: "Read these documents" },
...["report", undefined, 'report "final"\n.pdf'].map((filename) => ({
type: "media" as const,
mediaType: "text/plain",
data: "SGVsbG8=",
filename,
})),
]),
],
}),
)
expect(prepared.body.messages[0].content).toEqual([
{ text: "Read these documents" },
{ document: { format: "txt", name: "report", source: { bytes: "SGVsbG8=" } } },
{ document: { format: "txt", name: "document", source: { bytes: "SGVsbG8=" } } },
{ text: 'Attached file "report \\"final\\"\\n.pdf" has document label "report final".' },
{ document: { format: "txt", name: "report final", source: { bytes: "SGVsbG8=" } } },
])
}),
)
it.effect("lowers document media in tool results", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -1740,7 +1976,7 @@ describe("Bedrock Converse route", () => {
type: "file",
uri: "data:application/pdf;base64,UERGREFUQQ==",
mime: "application/pdf",
name: "report",
name: "report.pdf",
},
],
},
@@ -1763,6 +1999,7 @@ describe("Bedrock Converse route", () => {
status: "success",
content: [
{ text: "Read successfully" },
{ text: 'Attached file "report.pdf" has document label "report".' },
{ document: { format: "pdf", name: "report", source: { bytes: "UERGREFUQQ==" } } },
],
},
@@ -0,0 +1,86 @@
import { expect } from "bun:test"
import { Effect, Schema } from "effect"
import { LLM, Message, ToolCallPart } from "../../src/index.js"
import { AmazonBedrock } from "../../src/providers.js"
import { BedrockConverse } from "../../src/protocols/bedrock-converse.js"
import { compileRequest } from "../../src/route/client.js"
import { it } from "../lib/effect.js"
const bedrock = AmazonBedrock.configure({ baseURL: "https://bedrock.test", apiKey: "test-key" })
const history = (ids: string[]) => [
Message.user("Read every label"),
Message.assistant(ids.map((id, index) => ToolCallPart.make({ id, name: "lookup", input: { label: index } }))),
...ids.map((id, index) => Message.tool({ id, name: "lookup", result: `value-${index}` })).toReversed(),
]
for (const model of [
"mistral.mistral-large-2407-v1:0",
"us.mistral.pixtral-large-2502-v1:0",
"arn:aws:bedrock:us-east-1::foundation-model/mistral.pixtral-large-2502-v1:0",
]) {
it.effect(`preserves distinct call/result pairs and history for ${model}`, () =>
Effect.gen(function* () {
const request = LLM.request({ model: bedrock.model(model), messages: history(["a"]), cache: "none" })
const first = Schema.decodeUnknownSync(BedrockConverse.protocol.body.schema)(
(yield* compileRequest(request)).body,
)
const reserved = first.messages.flatMap((message) =>
message.content.flatMap((part) => ("toolUse" in part ? [part.toolUse.toolUseId] : [])),
)[0]
if (!reserved) throw new Error("Expected projected tool call")
const ids = [
"a",
"b",
"tooluse_GQn7COr2AN8bw2oVKYyYzZ",
"tooluse_abcdefghi111111111",
"tooluse_abcdefghi222222222",
"call_other-provider-id",
"Ab12Cd34E",
reserved,
]
const messages = history(ids)
const before = structuredClone(messages)
const input = LLM.request({ model: bedrock.model(model), messages, cache: "none" })
const prepared = yield* compileRequest(input)
const body = Schema.decodeUnknownSync(BedrockConverse.protocol.body.schema)(prepared.body)
const calls = body.messages.flatMap((message) =>
message.content.flatMap((part) => ("toolUse" in part ? [part.toolUse.toolUseId] : [])),
)
const results = body.messages.flatMap((message) =>
message.content.flatMap((part) => ("toolResult" in part ? [part.toolResult.toolUseId] : [])),
)
expect(calls).toHaveLength(ids.length)
expect(new Set(calls).size).toBe(ids.length)
calls.forEach((id) => expect(id).toMatch(/^[A-Za-z0-9]{9}$/))
expect(results).toEqual(calls.toReversed())
expect(calls[0]).not.toBe(reserved)
expect(calls.slice(-2)).toEqual(ids.slice(-2))
expect((yield* compileRequest(input)).body).toEqual(prepared.body)
expect(structuredClone(messages)).toEqual(before)
}),
)
}
it.effect("leaves non-Mistral Bedrock IDs unchanged", () =>
Effect.gen(function* () {
const ids = ["a", "tooluse_abcdefghi111111111", "tooluse_abcdefghi222222222", "Ab12Cd34E"]
const prepared = yield* compileRequest(
LLM.request({
model: bedrock.model("global.anthropic.claude-haiku-4-5-20251001-v1:0"),
messages: history(ids),
cache: "none",
}),
)
const body = Schema.decodeUnknownSync(BedrockConverse.protocol.body.schema)(prepared.body)
expect(
body.messages.flatMap((message) =>
message.content.flatMap((part) => ("toolUse" in part ? [part.toolUse.toolUseId] : [])),
),
).toEqual(ids)
expect(
body.messages.flatMap((message) =>
message.content.flatMap((part) => ("toolResult" in part ? [part.toolResult.toolUseId] : [])),
),
).toEqual(ids.toReversed())
}),
)
@@ -0,0 +1,80 @@
import { expect } from "bun:test"
import { Effect } from "effect"
import { LLM, Message, ToolCallPart } from "../../src/index.js"
import { OpenAIChat } from "../../src/protocols/openai-chat.js"
import { OpenAIResponses } from "../../src/protocols/openai-responses.js"
import { Auth } from "../../src/route/auth.js"
import { compileRequest } from "../../src/route/client.js"
import { it } from "../lib/effect.js"
const tool = { name: "lookup", description: "Look up a value", inputSchema: { type: "object", properties: {} } }
for (const route of [OpenAIChat.route, OpenAIResponses.route]) {
const model = route
.with({ endpoint: { baseURL: "https://api.openai.test/v1" }, auth: Auth.bearer("test") })
.model({ id: "gpt-4.1-mini" })
for (const choice of [
{ type: "auto" },
{ type: "none" },
{ type: "required" },
{ type: "tool", name: "lookup" },
] as const) {
it.effect(`${route.id} omits ${choice.type} tool choice without active tools`, () =>
Effect.gen(function* () {
for (const messages of [
[Message.user("Say OK.")],
[
Message.user("Look up the value."),
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
Message.tool({ id: "call_1", name: "lookup", result: "OK", resultType: "text" }),
],
]) {
const prepared = yield* compileRequest(
LLM.request({ model, messages, tools: [], toolChoice: choice, cache: "none" }),
)
expect(prepared.body.tool_choice).toBeUndefined()
}
}),
)
it.effect(`${route.id} preserves ${choice.type} tool choice with active tools`, () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({ model, prompt: "Look up the value.", tools: [tool], toolChoice: choice }),
)
expect(prepared.body.tool_choice).toEqual(
choice.type !== "tool"
? choice.type
: route.id === "openai-chat"
? { type: "function", function: { name: "lookup" } }
: { type: "function", name: "lookup" },
)
}),
)
}
}
it.effect("OpenAI Responses omits allowed tool choice without tool definitions", () =>
Effect.gen(function* () {
const model = OpenAIResponses.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1" }, auth: Auth.bearer("test") })
.model({ id: "gpt-4.1-mini" })
for (const tools of [[], [tool]]) {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Look up the value.",
tools,
toolChoice: "required",
providerOptions: { allowedTools: { mode: "required", toolNames: ["lookup"] } },
}),
)
expect(prepared.body.tool_choice).toEqual(
tools.length === 0
? undefined
: { type: "allowed_tools", mode: "required", tools: [{ type: "function", name: "lookup" }] },
)
}
}),
)
@@ -75,6 +75,45 @@ describe("Google Vertex providers", () => {
}),
)
it.effect("maps service tiers to the Vertex shared PayGo header", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: GoogleVertex.configure({
accessToken: "vertex-token",
location: "global",
project: "vertex-project",
providerOptions: { serviceTier: "flex" },
}).model("gemini-2.5-flash"),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(request.headers.get("x-vertex-ai-llm-shared-request-type")).toBe("flex")
expect(yield* Effect.promise(() => request.json())).not.toHaveProperty("serviceTier")
return input.respond(
sseEvents({
candidates: [
{
content: { role: "model", parts: [{ text: "Hello." }] },
finishReason: "STOP",
},
],
}),
{ headers: { "content-type": "text/event-stream" } },
)
}),
),
),
)
expect(response.text).toBe("Hello.")
}),
)
it.effect("strips function call ids Vertex does not accept from lowered bodies", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -0,0 +1,73 @@
import { expect } from "bun:test"
import { Effect, Schema } from "effect"
import { LLM, Message, ToolCallPart } from "../../src/index.js"
import { OpenAIChat } from "../../src/protocols/openai-chat.js"
import { OpenAIResponses } from "../../src/protocols/openai-responses.js"
import { Auth } from "../../src/route/auth.js"
import { compileRequest } from "../../src/route/client.js"
import { it } from "../lib/effect.js"
for (const route of [OpenAIChat.route, OpenAIResponses.route]) {
const model = route
.with({ endpoint: { baseURL: "https://api.openai.test/v1" }, auth: Auth.bearer("test") })
.model({ id: "gpt-4.1-mini" })
const chat = route.id === "openai-chat"
it.effect(`${route.id} serializes schema-valid undefined historical tool input as an empty object`, () =>
Effect.gen(function* () {
const message = Schema.decodeUnknownSync(Message)({
role: "assistant",
content: [{ type: "tool-call", id: "call_1", name: "lookup", input: undefined }],
})
expect(Schema.is(Message)(message)).toBe(true)
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user("Look up the value."),
message,
Message.tool({ id: "call_1", name: "lookup", result: "Missing input", resultType: "error" }),
],
}),
)
if (chat)
expect(prepared.body.messages).toContainEqual({
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", type: "function", function: { name: "lookup", arguments: "{}" } }],
})
if (!chat)
expect(prepared.body.input).toContainEqual({
type: "function_call",
call_id: "call_1",
name: "lookup",
arguments: "{}",
})
expect(message.content[0]).toEqual({ type: "tool-call", id: "call_1", name: "lookup", input: undefined })
}),
)
it.effect(`${route.id} preserves defined historical tool inputs`, () =>
Effect.gen(function* () {
for (const [input, encoded] of [
[null, "null"],
[[], "[]"],
[42, "42"],
["invalid", '"invalid"'],
[{ value: "original" }, '{"value":"original"}'],
] as const) {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input })]),
Message.tool({ id: "call_1", name: "lookup", result: "Invalid input", resultType: "error" }),
],
}),
)
if (chat) expect(prepared.body.messages[0].tool_calls[0].function.arguments).toBe(encoded)
if (!chat) expect(prepared.body.input[0].arguments).toBe(encoded)
}
}),
)
}
@@ -698,6 +698,54 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("preserves HTTP and HTTPS image URLs in user content", () =>
Effect.gen(function* () {
const urls = ["https://example.com/image.png?size=64#preview", "http://example.com/image.jpg"]
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: urls.map((data) => ({ type: "media" as const, mediaType: "image/png", data })),
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: urls.map((url) => ({ type: "image_url", image_url: { url } })) },
])
}),
)
it.effect("preserves remote image URLs from tool results", () =>
Effect.gen(function* () {
const url = "https://example.com/tool-image.png?version=2"
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user("Describe the image."),
Message.assistant([ToolCallPart.make({ id: "call_image", name: "read_image", input: {} })]),
Message.tool({
id: "call_image",
name: "read_image",
resultType: "content",
result: [
{ type: "text", text: "Image attached." },
{ type: "file", mime: "image/png", uri: url },
],
}),
],
}),
)
expect(prepared.body.messages).toContainEqual({
role: "tool",
tool_call_id: "call_image",
content: "Image attached.",
})
expect(prepared.body.messages.at(-1)).toEqual({
role: "user",
content: [{ type: "image_url", image_url: { url } }],
})
}),
)
it.effect("rejects non-image media that cannot be lowered", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
+8 -6
View File
@@ -120,16 +120,16 @@ describe("LLMClient tools", () => {
const second = bodies[1]
if (!second || typeof second !== "object") throw new Error("Expected second request body")
const messages = Reflect.get(second, "messages")
const tools = Reflect.get(second, "tools")
const messages = "messages" in second ? second.messages : undefined
const tools = "tools" in second ? second.tools : undefined
expect(Reflect.get(second, "max_completion_tokens")).toBe(50)
expect(Reflect.get(second, "tool_choice")).toBe("auto")
expect("max_completion_tokens" in second ? second.max_completion_tokens : undefined).toBe(50)
expect("tool_choice" in second ? second.tool_choice : undefined).toBe("auto")
expect(tools).toHaveLength(1)
expect(
Array.isArray(messages)
? messages.map((message) =>
message && typeof message === "object" ? Reflect.get(message, "role") : undefined,
message && typeof message === "object" && "role" in message ? message.role : undefined,
)
: undefined,
).toEqual(["user", "assistant", "tool"])
@@ -398,7 +398,9 @@ describe("LLMClient tools", () => {
required: ["temperature", "condition"],
additionalProperties: false,
})
expect(Reflect.get(Reflect.get(typed?.outputSchema ?? {}, "properties") as object, "temperature")).toBeDefined()
const properties =
typed?.outputSchema && "properties" in typed.outputSchema ? typed.outputSchema.properties : undefined
expect(properties && "temperature" in properties ? properties.temperature : undefined).toBeDefined()
expect(dynamic?.outputSchema).toEqual(schema)
})
@@ -63,20 +63,20 @@ for (const size of ["typical", "large"]) {
const source = page.locator(`[data-timeline-part-id="${sourcePart}"] [data-component="markdown"]`)
await expect(source).toHaveAttribute("data-markdown-ready", "")
await page.locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(fixture.targetID)}"]`).click()
await page.waitForFunction(() => Reflect.get(window, "markdownGate").held)
await page.waitForFunction(() => window.markdownGate.held)
await expect(page.locator(`[data-timeline-part-id="${targetPart}"]`)).toBeAttached()
const cdp = await page.context().newCDPSession(page)
await cdp.send("Performance.enable")
const before = await cdp.send("Performance.getMetrics")
await page.evaluate(() => Reflect.get(window, "markdownGate").arm())
await page.evaluate(() => window.markdownGate.arm())
await page.locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(fixture.sourceID)}"]`).click()
await expect(source).toHaveAttribute("data-markdown-ready", "")
await expect(source.getByRole("heading", { name: "Current destination" })).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${targetPart}"]`)).toHaveCount(0)
await page.waitForFunction(() => Reflect.get(window, "markdownGate").settled > 0)
await page.waitForFunction(() => window.markdownGate.settled > 0)
const after = await cdp.send("Performance.getMetrics")
const stats = await page.evaluate(() => {
const value = Reflect.get(window, "markdownGate")
const value = window.markdownGate
return {
admitted: value.admitted,
responses: value.responses,
@@ -4,6 +4,23 @@ import type {
MarkdownWorkerResponse,
} from "../../../../session-ui/src/components/markdown-worker-protocol"
declare global {
interface Window {
markdownGate: {
admitted: number
responses: number
held: boolean
started: number
ready: number
released: number
settled: number
sanitizeCalls: number
sanitizeChars: number
arm: () => void
}
}
}
export async function installMarkdownGate(
page: Page,
input: { answer: string; sourcePart: string; targetPart: string; href: string },
@@ -51,7 +51,7 @@ Terminal.prototype.open = function (element) {
context.drawImage = function (...args: unknown[]) {
probe.draws++
if (hidden) probe.hiddenDraws++
Reflect.apply(draw, this, args)
draw.apply(this, args)
}
}
}
@@ -74,7 +74,7 @@ for (const scenario of ["visible-output", "hidden-output", "full-scrollback-tear
window.terminalProbe.draws++
if (!this.canvas.checkVisibility()) window.terminalProbe.hiddenDraws++
}
Reflect.apply(fill, this, args)
fill.apply(this, args)
}
})
}
@@ -4,7 +4,7 @@ import { expectSessionTitle } from "../../utils/waits"
import { fixture, pageMessages } from "./session-timeline-stress.fixture"
import { installStressSessionTabs, installTimelineSettings, stressSessionHref } from "./timeline-test-helpers"
import { waitForStableTimeline } from "./session-tab-switch-probe"
import type { CatalogUpdated } from "@opencode/client/promise"
import type { ModelUpdated } from "@opencode/client/promise"
benchmark("measures retained renderer memory with a large model catalog", async ({ page, report }) => {
benchmark.setTimeout(120_000)
@@ -77,17 +77,17 @@ benchmark("measures retained renderer memory with a large model catalog", async
selected.name = "Updated catalog model"
await page.evaluate(
(event) => {
const host = window as Window & { __mockServerStream?: { push: (events: CatalogUpdated[]) => void } }
const host = window as Window & { __mockServerStream?: { push: (events: ModelUpdated[]) => void } }
if (!host.__mockServerStream) throw new Error("Missing fixture event stream")
host.__mockServerStream.push([event])
},
{
id: "evt_catalog_refresh",
id: "evt_model_refresh",
created: Date.now(),
type: "catalog.updated",
type: "model.updated",
location: { directory: fixture.directory },
data: {},
} satisfies CatalogUpdated,
} satisfies ModelUpdated,
)
await expect(page.locator('[data-action="composer-model"]')).toContainText(selected.name)
report(
@@ -136,7 +136,7 @@ export async function installTimelineStreamProbe(
}
if (state.scroll.lastCallFrame === state.scroll.frame) state.scroll.sameFrameCalls += 1
state.scroll.lastCallFrame = state.scroll.frame
Reflect.apply(scrollTo, this, typeof first === "number" ? [first, second] : [first])
scrollTo.apply(this, typeof first === "number" ? [first, second] : [first])
}
Element.prototype.scrollTo = measuredScrollTo
Object.defineProperty(Element.prototype, "scrollTop", {
@@ -13,8 +13,8 @@ test("loads home and the directory picker without newer browser APIs", async ({
})
await page.addInitScript((directory) => {
// Safari 16.6 has neither API. Remove them before the web entry runs.
Reflect.deleteProperty(Map, "groupBy")
Reflect.deleteProperty(Promise, "withResolvers")
delete (Map as Partial<typeof Map>).groupBy
delete (Promise as Partial<typeof Promise>).withResolvers
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
@@ -1,195 +0,0 @@
import { expect, test, type Page } from "@playwright/test"
import { base64Encode } from "@opencode/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
const directory = "/repo/slash-skills"
const projectID = "proj_slash_skills"
const sessionID = "ses_slash_skills"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
async function setup(page: Page, queued = false) {
const prompts: Record<string, unknown>[] = []
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "slash-skills",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [
{
id: sessionID,
projectID,
directory,
title: "Slash skills",
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
],
pageMessages: () => ({ items: [] }),
sessionStatus: queued ? { [sessionID]: { type: "running" } } : {},
inbox: queued
? [
{
id: "inb_slash_skill",
sessionID,
timeCreated: 1700000000000,
type: "user",
payload: { text: "Explain caching" },
delivery: "queue",
},
]
: [],
findFiles: ({ query }) =>
query.includes("cache")
? [
{
name: "cache.ts",
path: "src/cache.ts",
absolute: `${directory}/src/cache.ts`,
type: "file",
ignored: false,
},
]
: [],
onPrompt: ({ body }) => prompts.push(body),
})
await page.route("**/api/skill?*", (route) =>
route.fulfill({
json: {
location: { directory, project: { id: projectID, directory, canonical: directory } },
data: [
{ id: "show-me", slash: true, autoinvoke: false },
{ id: "hidden", slash: false },
{ id: "implicit" },
{ id: "review", slash: true },
{ id: "model", slash: true },
].map((skill) => ({
name: skill.id === "show-me" ? "Show Me" : skill.id,
description: "Explain the current topic visually",
location: `/skills/${skill.id}/SKILL.md`,
content: "Explain visually",
...skill,
})),
},
headers: { "access-control-allow-origin": "*" },
}),
)
await page.route("**/api/command?*", (route) =>
route.fulfill({
json: {
location: { directory, project: { id: projectID, directory, canonical: directory } },
data: [{ name: "review", description: "Review changes" }],
},
headers: { "access-control-allow-origin": "*" },
}),
)
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
const composer = page.locator('[data-component="composer"]')
const editor = composer.locator('[data-component="composer-editor"]')
await expect(editor).toBeEditable()
return { prompts, composer, editor }
}
for (const selection of ["keyboard", "pointer"]) {
test(`selects and submits a manual-only slash skill with ${selection}`, async ({ page }) => {
const { prompts, editor } = await setup(page)
await editor.fill("/show")
const skill = page.locator('[data-suggestion-id="skill:show-me"]')
await expect(skill).toContainText("/show-me")
await page.screenshot({ path: test.info().outputPath("slash-menu.png") })
if (selection === "keyboard") await editor.press("Enter")
if (selection === "pointer") await skill.click()
await expect(editor).toHaveText("/show-me ")
await expect(editor).toBeFocused()
await editor.pressSequentially("explain caching")
await editor.press("Enter")
await expect.poll(() => prompts.length).toBe(1)
expect(prompts[0]).toMatchObject({
text: "/show-me explain caching",
skills: [{ id: "show-me", mention: { start: 0, end: 8, text: "/show-me" } }],
})
await expect(editor).toBeEmpty()
})
}
test("respects slash flags and command precedence without hiding context skills", async ({ page }) => {
const { editor } = await setup(page)
await editor.fill("/")
await expect(page.locator('[data-suggestion-id="skill:show-me"]')).toBeVisible()
await expect(page.locator('[data-suggestion-id="custom.review"]')).toBeVisible()
await expect(page.locator('[data-suggestion-id="model.choose"]')).toBeVisible()
for (const id of ["hidden", "implicit", "review", "model"]) {
await expect(page.locator(`[data-suggestion-id="skill:${id}"]`)).toHaveCount(0)
}
await editor.press("Escape")
await expect(page.locator('[data-suggestion-id="skill:show-me"]')).toHaveCount(0)
await expect(editor).toBeFocused()
await editor.fill("@hidden")
const hidden = page.locator('[data-suggestion-id="skill:hidden"]')
await expect(hidden).toContainText("@hidden")
await hidden.click()
await expect(editor).toHaveText("@hidden ")
await expect(editor).toBeFocused()
})
test("preserves structured attachments when adding a slash skill from the command menu", async ({ page }) => {
const { prompts, composer, editor } = await setup(page)
await editor.fill("explain @cache")
const file = page.locator('[data-suggestion-id="file:src/cache.ts"]')
await expect(file).toBeVisible()
await file.click()
await expect(editor).toHaveText("explain @src/cache.ts ")
await composer.getByRole("button", { name: "Add images and files" }).click()
await page.getByRole("menuitem", { name: "Commands" }).click()
const skill = page.locator('[data-suggestion-id="skill:show-me"]')
await expect(skill).toBeVisible()
await skill.click()
await expect(editor).toHaveText("/show-me explain @src/cache.ts ")
await expect(editor).toBeFocused()
await editor.press("Enter")
await expect.poll(() => prompts.length).toBe(1)
expect(prompts[0]).toMatchObject({
skills: [{ id: "show-me", mention: { start: 0, end: 8, text: "/show-me" } }],
files: [{ name: "cache.ts", mention: { start: 17, end: 30, text: "@src/cache.ts" } }],
})
})
for (const select of [true, false]) {
test(`keeps slash skills in queued edits (${select ? "selected" : "typed"})`, async ({ page }) => {
const { prompts, editor } = await setup(page, true)
const row = page.locator('[data-component="session-queue-row"]')
await row.getByText("Explain caching", { exact: true }).click()
await expect(editor).toHaveText("Explain caching")
if (select) {
await editor.fill("/show")
const skill = page.locator('[data-suggestion-id="skill:show-me"]')
await expect(skill).toBeVisible()
await skill.click()
await expect(editor).toHaveText("/show-me ")
await editor.pressSequentially("explain caching")
}
if (!select) await editor.fill("/show-me explain caching")
await editor.press("Enter")
await expect.poll(() => prompts.length).toBe(1)
expect(prompts[0]).toMatchObject({
delivery: "queue",
text: "/show-me explain caching",
skills: [{ id: "show-me", mention: { start: 0, end: 8, text: "/show-me" } }],
})
})
}
@@ -62,7 +62,7 @@ async function mockServers(page: Page, requests: string[]) {
const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/status") return json(route, { version: "test", pid: 1, urls: [url.origin] })
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} })
if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) })
@@ -76,7 +76,7 @@ async function mockServers(page: Page, requests: string[]) {
if (url.pathname === "/api/mcp") return json(route, { location: { directory: current.directory }, data: [] })
if (url.pathname === "/api/mcp/resource")
return json(route, { location: { directory: current.directory }, data: { resources: [], templates: [] } })
if (url.pathname === "/api/project" || url.pathname === "/api/project/current") {
if (url.pathname === "/api/project") {
const project = {
id: current.projectID,
canonical: current.directory,
@@ -84,7 +84,7 @@ async function mockServers(page: Page, requests: string[]) {
time: { created: 1, updated: 1 },
sandboxes: [],
}
return json(route, url.pathname === "/api/project" ? [project] : { id: project.id, directory: current.directory })
return json(route, [project])
}
if (url.pathname === "/api/location")
return json(route, {
@@ -11,8 +11,9 @@ test("renames, exports, and deletes a home session from its context menu", async
project: fixture.project,
pageMessages,
})
await page.route("**/api/session/*/rename", async (route) => {
const sessionID = new URL(route.request().url()).pathname.split("/").at(-2)
await page.route("**/api/session/*", async (route) => {
if (route.request().method() !== "PATCH") return route.fallback()
const sessionID = new URL(route.request().url()).pathname.split("/").at(-1)
const session = sessions.find((item) => item.id === sessionID)
const payload: unknown = route.request().postDataJSON()
if (!payload || typeof payload !== "object" || !("title" in payload) || typeof payload.title !== "string")
@@ -30,16 +30,16 @@ for (const shared of [true, false]) {
sessions: [{ id: sessionID, projectID, directory: workspace, title }],
pageMessages: () => ({ items: [] }),
})
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
requests.push({ path: url.pathname, directory: target })
if (url.pathname === "/api/mcp/figma-desktop/connect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
connected.add(target)
return route.fulfill({ status: 204 })
}
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
connected.delete(target)
return route.fulfill({ status: 204 })
}
@@ -72,7 +72,7 @@ for (const shared of [true, false]) {
await expect(toggle).toBeChecked()
await expect(toggle).toBeEnabled()
expect(connected).toEqual(new Set([workspace]))
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/connect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/mcp/resource", directory: workspace })
expect(requests.every((request) => request.directory === workspace)).toBe(true)
await testInfo.attach("workspace-connected", { body: await page.screenshot(), contentType: "image/png" })
@@ -82,7 +82,7 @@ for (const shared of [true, false]) {
await expect(toggle).not.toBeChecked()
await expect(toggle).toBeEnabled()
expect(connected.size).toBe(0)
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/disconnect", directory: workspace })
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/disconnect", directory: workspace })
expect(requests.every((request) => request.directory === workspace)).toBe(true)
})
}
@@ -109,16 +109,16 @@ for (const surface of ["popover", "dialog"] as const) {
sessions: [{ id: sessionID, projectID, directory: workspace, title }],
pageMessages: () => ({ items: [] }),
})
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
requests.push({ path: url.pathname, directory: target })
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
state.status = "disabled"
return route.fulfill({ status: 204 })
}
if (url.pathname === "/api/mcp/figma-desktop/connect") {
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
state.status = state.fail ? "failed" : "connected"
// Connection failures are reported by the refreshed status, not the HTTP response.
return route.fulfill({ status: 204 })
@@ -167,7 +167,7 @@ for (const surface of ["popover", "dialog"] as const) {
await expect(toggle).toBeChecked({ checked: surface === "popover" })
await expect(toggle).toBeEnabled()
expect(requests.filter((request) => request.path.endsWith("/connect"))).toEqual([
{ path: "/api/mcp/figma-desktop/connect", directory: workspace },
{ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace },
])
expect(requests.every((request) => request.directory === workspace)).toBe(true)
await expect(toast).toHaveCSS("opacity", "1")
@@ -343,7 +343,7 @@ async function openDraft(page: Page, worktree = "main", options: { git?: boolean
(route) => route.fulfill({ json: { location: { directory }, data: { branch: {} } } }),
)
}
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const target = url.searchParams.get("location[directory]") ?? directory
@@ -404,7 +404,7 @@ async function openDraft(page: Page, worktree = "main", options: { git?: boolean
{
id: "summary-skill",
name: "summary-skill",
location: "/skills/summary/SKILL.md",
path: "/skills/summary/SKILL.md",
content: "Summary skill",
},
],
@@ -423,7 +423,7 @@ test("executes a selected slash command after creating its worktree", async ({ p
.toEqual([
{
sessionID: pending.sessionID,
body: { command: "review", text: "latest commit", files: [], agents: [], skills: [], delivery: "steer" },
body: { name: "review", text: "latest commit", files: [], agents: [], skills: [], delivery: "steer" },
},
])
await expect(pending.shimmer).toHaveCount(0)
@@ -0,0 +1,212 @@
import { expect, test } from "@playwright/test"
import { timelinePresets } from "@opencode/session-ui/timeline/detail"
import { createTwoFilesPatch } from "diff"
import {
assistantMessage,
setupTimeline,
textPart,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
const before = Array.from({ length: 80 }, (_, index) => `export const value${index} = ${index}\n`).join("")
const after = before.replaceAll(" = ", " = 1 + ")
const files = ["src/a.ts", "src/b.ts"].map((file) => ({
file,
status: "modified",
additions: 80,
deletions: 80,
patch: createTwoFilesPatch(file, file, before, after),
}))
const scenarios = [
{
name: "grouped patch",
placement: "grouped",
tools: [toolPart("prt_sticky_patch", "patch", "completed", {}, { metadata: { files } })],
files: ["a", "b"],
title: false,
},
{
name: "standalone patch",
placement: "separate",
tools: [toolPart("prt_sticky_patch", "patch", "completed", {}, { metadata: { files } })],
files: ["a", "b"],
title: false,
},
{
name: "grouped edit",
placement: "grouped",
tools: [
toolPart(
"prt_sticky_edit",
"edit",
"completed",
{ path: "src/a.ts", oldString: before, newString: after },
{ metadata: { files: [files[0]] } },
),
],
files: ["a"],
title: false,
},
{
name: "running edit input fallback",
placement: "grouped",
tools: [toolPart("prt_sticky_edit", "edit", "running", { path: "src/a.ts", oldString: before, newString: after })],
files: ["a"],
title: false,
},
{
name: "grouped write",
placement: "grouped",
tools: [toolPart("prt_sticky_write", "write", "completed", { path: "src/a.ts", content: after })],
files: ["a"],
title: false,
},
{
name: "running write input fallback",
placement: "grouped",
tools: [toolPart("prt_sticky_write", "write", "running", { path: "src/a.ts", content: after })],
files: ["a"],
title: false,
},
{
name: "merged edit write and patch",
placement: "separate",
tools: [
toolPart("prt_sticky_edit", "edit", "completed", {}, { metadata: { files: [files[0]] } }),
toolPart("prt_sticky_write", "write", "completed", { path: "src/b.ts", content: after }),
toolPart(
"prt_sticky_patch",
"patch",
"completed",
{},
{ metadata: { files: [{ ...files[0], file: "src/c.ts" }] } },
),
],
files: ["a", "b", "c"],
title: false,
},
{
name: "created and deleted patch files",
placement: "grouped",
tools: [
toolPart(
"prt_sticky_patch",
"patch",
"completed",
{},
{
metadata: {
files: [
{
file: "src/a.ts",
status: "added",
additions: 80,
deletions: 0,
patch: createTwoFilesPatch("src/a.ts", "src/a.ts", "", after),
},
{
file: "src/b.ts",
status: "deleted",
additions: 0,
deletions: 80,
patch: createTwoFilesPatch("src/b.ts", "src/b.ts", before, ""),
},
],
},
},
),
],
files: ["a", "b"],
title: false,
},
] as const
for (const scenario of scenarios) {
for (const width of [1400, 390]) {
for (const direction of ["ltr", "rtl"]) {
test(`${scenario.name}: file headers stay flush at ${width}px in ${direction}`, async ({ page }, info) => {
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([...scenario.tools, textPart("prt_after_patch", "Following explanation.\n\n".repeat(60))]),
],
settings: {
timelineDetail: {
...timelinePresets[2].value,
edit: { placement: scenario.placement, details: "collapsed" },
},
},
reducedMotion: true,
viewport: { width, height: 900 },
})
await page.evaluate((direction) => (document.documentElement.dir = direction), direction)
if (scenario.placement === "grouped") {
await page.locator('[data-component="context-tool-group-trigger"]').click()
}
const patch = page.locator('[data-scope="apply-patch"]')
await expect(patch).toHaveCount(1)
const scroller = page.locator('[data-slot="session-timeline-scroll"] .scroll-view__viewport')
const toolTitle = scroller.locator('[data-slot="collapsible-trigger"][data-locked]')
await expect(toolTitle).toHaveCount(scenario.title ? 1 : 0)
await expect(scroller.locator("[data-session-title]")).toHaveCount(width === 1400 ? 1 : 0)
for (const file of scenario.files) {
const name = new RegExp(`${file}\\.ts`)
const trigger = patch.getByRole("button", { name })
const header = patch.getByRole("heading", { name })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
const content = patch.getByRole("region", { name })
await expect
.poll(() => content.evaluate((element) => element.getBoundingClientRect().height))
.toBeGreaterThan(900)
// Leave follow-latest mode before positioning the viewport inside this file.
await scroller.hover()
await page.mouse.wheel(0, -100)
await content.evaluate((element) => {
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!
viewport.scrollTop += element.getBoundingClientRect().top - viewport.getBoundingClientRect().top + 160
})
await expect
.poll(() =>
content.evaluate((element) => {
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!
return element.getBoundingClientRect().top - viewport.getBoundingClientRect().top
}),
)
.toBeLessThan(0)
await expect
.poll(() =>
header.evaluate((element) => {
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!
const title = viewport.querySelector("[data-session-title]")?.firstElementChild
const toolTitle = element
.closest('[data-component="edit-tool"]')
?.querySelector('[data-slot="collapsible-trigger"][data-locked]')
const top = viewport.getBoundingClientRect().top + (title?.getBoundingClientRect().height ?? 0)
const rect = element.getBoundingClientRect()
const trigger = element.querySelector("button")!
return {
gap: Math.abs(rect.top - top - (toolTitle?.getBoundingClientRect().height ?? 0)),
titleGap: toolTitle ? Math.abs(toolTitle.getBoundingClientRect().top - top) : 0,
clickable: trigger.contains(
document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2),
),
}
}),
)
.toEqual({ gap: 0, titleGap: 0, clickable: true })
await page.screenshot({ path: info.outputPath(`${file}.png`) })
}
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await expect(
patch.getByRole("heading", { name: new RegExp(`${scenario.files.at(-1)}\\.ts`) }),
).not.toBeInViewport()
})
}
}
}
@@ -20,7 +20,6 @@ test("session settings use the remote server context", async ({ page }) => {
// one toggle sweeps every connected server, not just the focused one.
await mockServers(page, permissionRequests, permissionResponses, {
pending: { [serverA]: [pendingPermission("permission-pending-a", sessionA.id)] },
preferencesUnavailable: true,
})
await configureServers(page)
@@ -326,7 +325,6 @@ type MockServerOptions = {
listFailures?: Record<string, number>
// Records /api/session/:id GETs so tests can assert session resyncs.
sessionGets?: string[]
preferencesUnavailable?: boolean
}
async function mockServers(
@@ -398,8 +396,6 @@ async function mockServers(
},
])
}
if (url.pathname === "/api/project/current")
return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory, canonical: directory })
if (url.pathname === "/api/session")
return json(route, { data: sessions.map((session) => currentSession(session)), cursor: {} })
if (url.pathname === "/api/session/active")
@@ -418,11 +414,8 @@ async function mockServers(
directory,
project: { id: remote ? sessionB.projectID : "project-server-a", directory, canonical: directory },
})
if (url.pathname === "/api/config/preferences") return json(route, {}, options.preferencesUnavailable ? 404 : 200)
if (url.pathname === "/api/config/shell")
return json(route, options.preferencesUnavailable ? {} : [], options.preferencesUnavailable ? 404 : 200)
if (url.pathname === "/api/websearch/provider")
return json(route, { location: { directory }, data: [] }, options.preferencesUnavailable ? 404 : 200)
if (url.pathname === "/api/config/shell") return json(route, [])
if (url.pathname === "/api/websearch/provider") return json(route, { location: { directory }, data: [] })
if (url.pathname === "/api/worktree") return json(route, [{ directory }])
if (url.pathname === "/api/vcs")
return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } })
@@ -16,8 +16,8 @@ test("server dialog keeps focus above fullscreen settings", async ({ page }) =>
body: 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n',
})
}
if (url.pathname === "/api/global/health" || url.pathname === "/api/health") {
return json(route, { healthy: true, version: "2.0.0" })
if (url.pathname === "/api/status") {
return json(route, { version: "2.0.0", pid: 1, urls: [url.origin] })
}
return json(route, {})
})
@@ -11,9 +11,9 @@ test.beforeEach(async ({ page }) => {
project: fixture.project,
pageMessages,
})
await page.route("**/api/session/*/rename", async (route) => {
if (route.request().method() !== "POST") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-2)
await page.route("**/api/session/*", async (route) => {
if (route.request().method() !== "PATCH") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-1)
const session = sessions.find((item) => item.id === id)
const payload: unknown = route.request().postDataJSON()
if (
@@ -59,8 +59,10 @@ test("cancels the session heading with Escape", async ({ page }) => {
})
test("keeps the draft when saving the session heading fails", async ({ page }) => {
await page.route("**/api/session/*/rename", (route) =>
route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } }),
await page.route("**/api/session/*", (route) =>
route.request().method() === "PATCH"
? route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } })
: route.fallback(),
)
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
@@ -6,7 +6,7 @@ test("every MCP row hit area toggles exactly once and keeps the submenu open", a
await mockStressTimeline(page)
const state = { enabled: true }
const writes: string[] = []
await page.route("**/api/mcp**", (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
const directory = url.searchParams.get("location[directory]")
@@ -62,7 +62,7 @@ test("every MCP row hit area toggles exactly once and keeps the submenu open", a
await expect(submenu).toBeVisible()
if (target === "keyboard") await expect(toggle).toBeFocused()
expect(writes).toHaveLength(index + 1)
expect(writes[index]).toBe(`/api/mcp/figma/${enabled ? "connect" : "disconnect"}`)
expect(writes[index]).toBe(`/api/experimental/mcp/figma/${enabled ? "connect" : "disconnect"}`)
}
})
@@ -72,7 +72,7 @@ test("MCP authentication starts before a slow resource catalog finishes", async
const attempts: string[] = []
const resources = Promise.withResolvers<void>()
await context.route("https://auth.example.test/**", (route) => route.fulfill({ body: "Sign in" }))
await page.route("**/api/mcp**", async (route) => {
await page.route(/\/api\/(?:experimental\/)?mcp(?:[/?]|$)/, async (route) => {
if (route.request().method() === "OPTIONS") return route.fallback()
const url = new URL(route.request().url())
if (url.pathname.endsWith("/connect")) {
@@ -137,7 +137,9 @@ test("MCP authentication starts before a slow resource catalog finishes", async
test("multiple desktop connections show the session's server name", async ({ page }) => {
await mockStressTimeline(page)
await page.route("http://secondary.test/**", (route) => route.fulfill({ json: { healthy: true, version: "2.0.0" } }))
await page.route("http://secondary.test/**", (route) =>
route.fulfill({ json: { version: "2.0.0", pid: 1, urls: ["http://secondary.test"] } }),
)
await page.addInitScript(
({ directory, server }) => {
const current = { type: "http", http: { url: server }, displayName: "Design server" }
@@ -42,7 +42,7 @@ for (const service of services) {
state: { status: "active" },
},
]
: [{ id: service.item, name: service.item, location: "/skills/summary/SKILL.md", content: "Summary" }]
: [{ id: service.item, name: service.item, path: "/skills/summary/SKILL.md", content: "Summary" }]
return route.fulfill({ json: { location: { directory: fixture.directory }, data: empty ? [] : items } })
},
)
@@ -235,11 +235,11 @@ test("catalog submenus show project plugins and skills, refresh on reopen, and d
json: {
location: { directory: fixture.directory },
data: [
{ id: "find-skills", name: "find-skills", location: "/skills/find/SKILL.md", content: "Find skills" },
{ id: "find-skills", name: "find-skills", path: "/skills/find/SKILL.md", content: "Find skills" },
{
id: "review-animations",
name: "review-animations",
location: "/skills/review/SKILL.md",
path: "/skills/review/SKILL.md",
content: "Review animations",
},
],
@@ -109,11 +109,11 @@ test("passes through non-event fetches", async ({ page }) => {
const timeline = await setupTimeline(page)
const health = await page.evaluate(async () => {
const response = await fetch("/api/health")
const response = await fetch("/api/status")
return response.json()
})
expect(health).toEqual({ healthy: true, version: "2.0.0", pid: 1 })
expect(health).toEqual({ version: "2.0.0", pid: 1, urls: [] })
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
})
@@ -24,9 +24,14 @@ test.beforeEach(async ({ page }) => {
sandboxes,
},
provider: { all: [], connected: [], default: {} },
preferences: { shell: "zsh", websearch: { provider: "exa" } },
shells: [{ path: "/bin/zsh", name: "zsh", acceptable: true }],
websearchProviders: [{ id: "exa", name: "Exa" }],
configEntries: [
{ type: "document", path: "/home/test/.config/opencode/opencode.jsonc", info: { shell: "/bin/zsh" } },
{ type: "directory", path: "/home/test/.config/opencode" },
],
shells: [
{ path: "/bin/zsh", name: "zsh", acceptable: true },
{ path: "/bin/bash", name: "bash", acceptable: true },
],
sessions: sandboxes.map((directory, index) => ({
id: `ses_settings_${index + 1}`,
title: `Workspace ${index + 1} session`,
@@ -78,38 +83,20 @@ test("single-server settings expose scoped pages without a server picker", async
await settings.getByRole("tab", { name: "Server", exact: true }).click()
await expect(settings.getByRole("button", { name: "Add server", exact: true })).toBeVisible()
await expect(settings.getByRole("heading", { name: "Connection", exact: true })).toBeVisible()
await expect(settings.getByRole("heading", { name: "Preferences", exact: true })).toBeVisible()
await expect(settings.getByText("Terminal shell", { exact: true })).toBeVisible()
await expect(settings.getByText("Third-party search", { exact: true })).toBeVisible()
await expect(settings.getByText("zsh", { exact: true })).toBeVisible()
await expect(settings.getByText("Exa", { exact: true })).toBeVisible()
await settings.getByText("Exa", { exact: true }).click()
const updated = page.waitForRequest(
(request) => request.method() === "PATCH" && new URL(request.url()).pathname === "/api/config/preferences",
)
await page.getByRole("option", { name: "Any", exact: true }).click()
expect((await updated).postDataJSON()).toEqual({ websearch: { provider: "random" } })
})
test("server details tolerate unavailable preference endpoints", async ({ page }) => {
await page.route(
(url) =>
url.pathname === "/api/config/preferences" ||
url.pathname === "/api/config/shell" ||
url.pathname === "/api/websearch/provider",
(route) => route.fulfill({ status: 404, json: {} }),
)
const settings = page.getByTestId("settings-screen")
await settings.getByRole("tab", { name: "Server", exact: true }).click()
const connection = settings.locator('[data-component="settings-server-connection"]')
await expect(connection.getByRole("heading", { name: "Connection", exact: true })).toBeVisible()
await expect(connection.locator('[data-component="settings-list"]')).toHaveCSS("padding-left", "16px")
await expect(connection.locator(".settings-servers-row")).toHaveCSS("padding-top", "20px")
await expect(connection.locator(".settings-servers-lead")).toHaveCSS("column-gap", "4px")
await expect(connection.locator(".settings-servers-copy")).toHaveCSS("row-gap", "6px")
await expect(page.getByText("Server request failed", { exact: true })).toHaveCount(0)
await expect(settings.getByRole("heading", { name: "Preferences", exact: true })).toBeVisible()
await expect(settings.getByText("Terminal shell", { exact: true })).toBeVisible()
await settings.getByText("zsh", { exact: true }).click()
const updated = page.waitForRequest(
(request) => request.method() === "PATCH" && new URL(request.url()).pathname === "/api/experimental/config",
)
await page.getByRole("option", { name: "bash", exact: true }).click()
expect((await updated).postDataJSON()).toEqual({ shell: "bash" })
})
test("project settings open as a nested autosaving view", async ({ page }) => {
@@ -592,7 +592,7 @@ async function mockServer(page: Page) {
if (url.pathname === "/api/mcp") return json(route, { location: { directory: sessionA.directory }, data: [] })
if (url.pathname === "/api/mcp/resource")
return json(route, { location: { directory: sessionA.directory }, data: { resources: [], templates: [] } })
if (url.pathname === "/api/project" || url.pathname === "/api/project/current") {
if (url.pathname === "/api/project") {
const project = {
id: sessionA.projectID,
canonical: sessionA.directory,
@@ -600,10 +600,7 @@ async function mockServer(page: Page) {
time: { created: 1, updated: 1 },
sandboxes: [],
}
return json(
route,
url.pathname === "/api/project" ? [project] : { id: project.id, directory: sessionA.directory },
)
return json(route, [project])
}
if (url.pathname === "/api/location")
return json(route, {
@@ -75,13 +75,16 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
const blocked: ServerResponse[] = []
const release = () => blocked.splice(0).forEach((response) => response.end(builds.new["/large.bin"]))
const server = createServer((request, response) => {
const path = new URL(request.url ?? "/", "http://localhost").pathname
const url = new URL(request.url ?? "/", "http://localhost")
const path = url.pathname
requests.push(path)
response.setHeader("cache-control", "no-store")
if (path === "/observer.html")
return void response.writeHead(200, { "content-type": "text/html" }).end("<title>Worker observer</title>")
if (path === "/api/health")
return void response.writeHead(200, { "content-type": "application/json" }).end('{"healthy":true}')
if (path === "/api/status")
return void response
.writeHead(200, { "content-type": "application/json" })
.end(`{"version":"test","pid":1,"urls":["${url.origin}"]}`)
if (path === "/sw.js" && state.legacy && state.version === "old") {
// Model the shipped worker's shared precache name and cache-first navigation behavior.
const urls = Object.keys(builds.old).filter(
@@ -331,8 +334,8 @@ fixture("upgrades the legacy shared precache only after old tabs close", async (
fixture("does not substitute cached HTML for API or missing asset navigations", async ({ page, site }) => {
await install(page, site.url)
const api = await page.goto(`${site.url}/api/health`)
expect(await api?.json()).toEqual({ healthy: true })
const api = await page.goto(`${site.url}/api/status`)
expect(await api?.json()).toEqual({ version: "test", pid: 1, urls: ["http://localhost"] })
expect(api?.fromServiceWorker()).toBe(false)
const asset = await page.goto(`${site.url}/_assets/missing.js`)
expect(asset?.status()).toBe(404)
+6 -8
View File
@@ -32,7 +32,7 @@ export class MockBadRequest extends Schema.TaggedError<MockBadRequest>()("MockBa
}) {}
const Group = HttpApiGroup.make("mock")
.add(HttpApiEndpoint.get("health", "/api/health", { success: Json }))
.add(HttpApiEndpoint.get("status", "/api/status", { success: Json }))
.add(
HttpApiEndpoint.get("event", "/api/event", {
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
@@ -77,15 +77,13 @@ const Group = HttpApiGroup.make("mock")
success: Json,
}),
)
.add(HttpApiEndpoint.get("projectCurrent", "/api/project/current", { success: Json }))
.add(HttpApiEndpoint.get("configPreferences", "/api/config/preferences", { success: Json }))
.add(HttpApiEndpoint.get("configShells", "/api/config/shell", { success: Json }))
.add(
HttpApiEndpoint.patch("configUpdatePreferences", "/api/config/preferences", {
payload: JsonPayload,
success: Json,
HttpApiEndpoint.patch("configUpdate", "/api/experimental/config", {
payload: Schema.Struct({ shell: Schema.NullOr(Schema.String) }),
success: HttpApiSchema.NoContent,
}),
)
.add(HttpApiEndpoint.get("configShells", "/api/config/shell", { success: Json }))
.add(HttpApiEndpoint.get("websearchProviders", "/api/websearch/provider", { success: Json }))
.add(
HttpApiEndpoint.get("worktreeList", "/api/worktree", {
@@ -235,7 +233,7 @@ const Group = HttpApiGroup.make("mock")
}),
)
.add(
HttpApiEndpoint.post("sessionRename", "/api/session/:sessionID/rename", {
HttpApiEndpoint.patch("sessionRename", "/api/session/:sessionID", {
params: SessionParams,
payload: JsonPayload,
success: NoContent,
+5 -21
View File
@@ -10,8 +10,8 @@ export interface MockServerConfig {
provider: unknown | (() => unknown)
integrationMethods?: Record<string, unknown[]>
onConnectKey?: (input: { integrationID: string; body: unknown }) => void
preferences?: Record<string, unknown>
shells?: unknown[]
configEntries?: unknown[]
websearchProviders?: unknown[]
directory: string
project: unknown
@@ -198,7 +198,7 @@ const corsHeaders = {
function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, string>; nextCursor: number }) {
const noContent = Effect.succeed(HttpApiSchema.NoContent.make())
const delay = config.messageDelay === undefined ? Effect.void : Effect.sleep(Duration.millis(config.messageDelay))
const preferences = { current: config.preferences ?? {} }
const configEntries = config.configEntries ?? []
return HttpApiBuilder.group(MockApi, "mock", (handlers) =>
handlers
.handleRaw("event", () => {
@@ -219,8 +219,8 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
}),
)
.handleAll({
health: () => Effect.succeed({ healthy: true, version: "2.0.0", pid: 1 }),
config: () => Effect.succeed([]),
status: () => Effect.succeed({ version: "2.0.0", pid: 1, urls: config.server ? [config.server] : [] }),
config: () => Effect.succeed(configEntries),
reference: () =>
Effect.succeed({
location: {
@@ -285,19 +285,8 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
canonical: project.canonical ?? config.directory,
})
},
projectCurrent: () =>
Effect.succeed({
id: (config.project as { id?: string }).id,
directory: config.directory,
canonical: config.directory,
}),
configPreferences: () => Effect.succeed(preferences.current),
configUpdatePreferences: (ctx) =>
Effect.sync(() => {
preferences.current = { ...preferences.current, ...ctx.payload }
return preferences.current
}),
configShells: () => Effect.succeed(config.shells ?? []),
configUpdate: () => noContent,
websearchProviders: () => Effect.succeed({ location: location(config), data: config.websearchProviders ?? [] }),
worktreeList: () =>
Effect.succeed([
@@ -658,11 +647,6 @@ export function currentSession(session: { id: string } & Record<string, unknown>
: typeof session.directory === "string"
? session.directory
: fallbackDirectory,
...(typeof session.workspaceID === "string"
? { workspaceID: session.workspaceID }
: "workspaceID" in location && typeof location.workspaceID === "string"
? { workspaceID: location.workspaceID }
: {}),
},
subpath: session.subpath ?? session.path,
revert: session.revert,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.2",
"version": "2.0.3",
"description": "",
"type": "module",
"exports": {
@@ -187,7 +187,7 @@ function ComposerStory(props: {
)
},
onStop: () =>
setStory("activity", props.continueOnStop ? "POST /interrupt · continue: true" : "Stop requested"),
setStory("activity", props.continueOnStop ? "POST /interrupt · resume: true" : "Stop requested"),
},
},
})
@@ -99,7 +99,7 @@ describe("Composer store", () => {
expect(prompt.state.prompt).toEqual([{ type: "text", content: "old", start: 0, end: 3 }])
})
test("prepends a slash skill to an attachment-only draft without flattening it", () => {
test("prepends a skill mention to an attachment-only draft without flattening it", () => {
const prompt = createPromptStore()
prompt.setPrompt([{ type: "file", path: "one", content: "@one", start: 0, end: 4 }], 4)
prompt.addMention(
@@ -107,14 +107,14 @@ describe("Composer store", () => {
type: "skill",
id: Skill.ID.make("show-me"),
name: Skill.Name.make("Show Me"),
content: "/show-me",
content: "@show-me",
start: 0,
end: 0,
},
{ start: 0, end: 0 },
)
expect(prompt.state.prompt).toMatchObject([
{ type: "skill", id: "show-me", content: "/show-me", start: 0, end: 8 },
{ type: "skill", id: "show-me", content: "@show-me", start: 0, end: 8 },
{ type: "text", content: " ", start: 8, end: 9 },
{ type: "file", path: "one", content: "@one", start: 9, end: 13 },
])
+1 -22
View File
@@ -21,7 +21,7 @@ import type { ImageAttachmentPart } from "./state"
import type { PromptHistoryComment } from "./history/entry"
import { createComposerHistory } from "./history/store"
import { composerPlaceholder } from "./placeholder"
import { createComposerSubmit, withSlashSkill } from "./submit"
import { createComposerSubmit } from "./submit"
export type ComposerModel = ComposerEditorModel & {
readonly model: ComposerControls["model"]
@@ -243,9 +243,6 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
type: "builtin" as const,
})),
])
const slashSkills = createMemo(() =>
skills().filter((skill) => skill.slash === true && !slashCommands().some((item) => item.trigger === skill.id)),
)
const commands = createMemo<ComposerSuggestion[]>(() => [
...slashCommands().map((item) => ({
id: item.id,
@@ -256,29 +253,12 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
description: item.description,
keybind: command.keybindParts(item.id),
})),
...slashSkills().map((skill) => ({
id: `skill:${skill.id}`,
kind: "skill" as const,
label: `/${skill.id}`,
trigger: skill.id,
title: skill.name,
description: skill.description,
mention: {
type: "skill" as const,
id: Skill.ID.make(skill.id),
name: Skill.Name.make(skill.name),
content: `/${skill.id}`,
start: 0,
end: 0,
},
})),
])
const variants = createMemo(() => ["default", ...adapter.controls().model.selection.variant.list()])
const submission = createComposerSubmit({
adapter,
mode,
commands: () => data.location.command.list({ directory: sdk().directory }),
skills: slashSkills,
editor: () => editor,
queueScroll: () => requestAnimationFrame(() => editor?.scrollIntoView({ block: "nearest" })),
addToHistory: (value, mode) => controller.addHistory(value, mode),
@@ -407,7 +387,6 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
// the composer value as a new prompt. Enter keeps it queued in
// place; the alternate action sends it as a steer.
if (queue?.editing()) {
prompt.set(withSlashSkill(prompt.current(), slashSkills()))
queue.confirmEdit(submitOptions?.alternate ? "steer" : "queue")
return
}
+1 -117
View File
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"
import type { ModelSelection } from "@/providers/models/selection"
import type { SessionMessageUser } from "@opencode/client/promise"
import { Skill } from "@opencode/schema/skill"
import { AbsolutePath } from "@opencode/schema/schema"
import type { ActiveComposerAdapter, ComposerControls, ComposerSession, NewSessionComposerAdapter } from "./adapter"
import { createMemoryComposerState } from "./state"
import { createComposerSubmit } from "./submit"
@@ -32,16 +31,6 @@ const selection = {
},
} satisfies ModelSelection
const slashSkill = Skill.Info.make({
id: Skill.ID.make("show-me"),
name: Skill.Name.make("Show Me"),
description: "Explain visually",
slash: true,
autoinvoke: false,
location: AbsolutePath.make("/skills/show-me/SKILL.md"),
content: "Explain visually",
})
function controls(): ComposerControls {
return {
agents: {
@@ -64,13 +53,11 @@ function submitInput(
notify = { missingSelection() {}, failed(_kind: "shell" | "command" | "prompt", _error: unknown) {} },
mode: "normal" | "shell" = "normal",
commands: () => readonly { name: string }[] | undefined = () => [],
skills: () => readonly Skill.Info[] | undefined = () => [],
) {
return createComposerSubmit({
adapter,
mode: () => mode,
commands,
skills,
editor: () => undefined,
queueScroll() {},
addToHistory() {},
@@ -268,107 +255,6 @@ describe("Composer submission", () => {
expect(state.current()[0]).toMatchObject({ content: "/review changes" })
})
test("submits a slash skill with its trailing text and attachments", async () => {
const state = createMemoryComposerState({ prompt: "/show-me explain " }).capture()
state.set([
{ type: "text", content: "/show-me explain ", start: 0, end: 17 },
{ type: "file", path: "src/cache.ts", content: "@src/cache.ts", start: 17, end: 30 },
])
const admitted = Promise.withResolvers<Parameters<ComposerSession["data"]["session"]["prompt"]>[0]>()
const target = session({ calls: [], prompt: async (value) => admitted.resolve(value) })
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => target,
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
await submitInput(
adapter,
undefined,
"normal",
() => [],
() => [slashSkill],
).submit(new Event("submit"))
const request = await admitted.promise
expect(request.text).toBe("/show-me explain @src/cache.ts")
expect(request.skills).toEqual([
expect.objectContaining({ id: "show-me", mention: { start: 0, end: 8, text: "/show-me" } }),
])
expect(request.files).toEqual([
{ uri: "file:///C:/repo/src/cache.ts", name: "cache.ts", mention: { start: 17, end: 30, text: "@src/cache.ts" } },
])
})
test.each([
{ text: "/show-me", slash: true, expected: ["show-me"] },
{ text: "/show-me\nexplain caching", slash: true, expected: ["show-me"] },
{ text: "/show-me\texplain caching", slash: true, expected: ["show-me"] },
{ text: "/show-me", slash: false, expected: [] },
{ text: "/show-me", slash: undefined, expected: [] },
{ text: "/show-me-extra", slash: true, expected: [] },
{ text: "Explain /show-me", slash: true, expected: [] },
])("resolves raw slash skill input $text with slash=$slash", async ({ text, slash, expected }) => {
const state = createMemoryComposerState({ prompt: text }).capture()
const admitted = Promise.withResolvers<Parameters<ComposerSession["data"]["session"]["prompt"]>[0]>()
const target = session({ calls: [], prompt: async (value) => admitted.resolve(value) })
const adapter: ActiveComposerAdapter = {
kind: "active-session",
state,
ready: () => true,
controls,
working: () => false,
session: () => target,
interrupt: async () => undefined,
submitted() {},
setEditor() {},
}
await submitInput(
adapter,
undefined,
"normal",
() => [],
() => [{ ...slashSkill, slash }],
).submit(new Event("submit"))
const request = await admitted.promise
expect(request.text).toBe(text)
expect(request.skills?.map((skill) => skill.id)).toEqual([...expected])
})
test("captures slash skills before creating a session in a new worktree", async () => {
const state = createMemoryComposerState({ prompt: "/show-me" }).capture()
let skills: readonly Skill.Info[] | undefined = [slashSkill]
const admitted = Promise.withResolvers<Parameters<ComposerSession["data"]["session"]["prompt"]>[0]>()
const target = session({ calls: [], prompt: async (value) => admitted.resolve(value) })
const adapter: NewSessionComposerAdapter = {
kind: "new-session",
state,
ready: () => true,
controls,
working: () => false,
submitted() {},
async start() {
skills = undefined
return { session: target, cleanupReady: Promise.resolve() }
},
}
await submitInput(
adapter,
undefined,
"normal",
() => [],
() => skills,
).submit(new Event("submit"))
expect((await admitted.promise).skills).toEqual([
expect.objectContaining({ id: "show-me", mention: { start: 0, end: 8, text: "/show-me" } }),
])
})
test("sends one captured value with explicit delivery after selection switches", async () => {
const state = createMemoryComposerState({ prompt: "ship it" }).capture()
const calls: string[] = []
@@ -652,7 +538,6 @@ describe("Composer submission", () => {
notify,
"normal",
() => [],
() => [slashSkill],
)
await submission.submit(new Event("submit"))
@@ -744,14 +629,13 @@ describe("Composer submission", () => {
undefined,
"normal",
() => catalog,
() => [{ ...slashSkill, id: Skill.ID.make("review") }],
).submit(new Event("submit"))
expect(await sent.promise).toBe("command")
expect(requests).toEqual([
{
sessionID: target.id,
command: "review",
name: "review",
text: "https://github.com/example/repo/pull/1",
files: [],
agents: [],
+3 -27
View File
@@ -1,7 +1,5 @@
import { SessionMessage } from "@opencode/schema/session-message"
import type { SessionMessageUser, SkillInfo } from "@opencode/client/promise"
import { Skill } from "@opencode/schema/skill"
import { Event } from "@opencode/schema/event"
import type { SessionMessageUser } from "@opencode/client/promise"
import type { Accessor } from "solid-js"
import type { PromptHistoryComment } from "./history/entry"
import type { ImageAttachmentPart, Prompt } from "./state"
@@ -30,7 +28,6 @@ type ComposerSubmitInput = {
adapter: ComposerAdapter
mode: Accessor<"normal" | "shell">
commands: Accessor<readonly { name: string }[] | undefined>
skills: Accessor<readonly Pick<SkillInfo, "id" | "name" | "slash">[] | undefined>
editor: () => HTMLDivElement | undefined
queueScroll: () => void
addToHistory: (prompt: Prompt, mode: "normal" | "shell") => void
@@ -71,7 +68,6 @@ export function createComposerSubmit(input: ComposerSubmitInput) {
const comments = input.comments.capture()
// Capture command intent before starting a session in a worktree whose catalog has not loaded.
const command = value.mode === "normal" ? findCommand(input.commands(), value.text) : undefined
if (value.mode === "normal" && !command) value.prompt = withSlashSkill(value.prompt, input.skills())
try {
const started =
@@ -281,7 +277,7 @@ function restoreSubmission(
}
async function sendShell(session: ComposerSession, value: ComposerSubmission) {
await session.api.shell({ sessionID: session.id, id: Event.ID.create(), command: value.text })
await session.api.shell({ sessionID: session.id, id: value.id, command: value.text })
}
function findCommand(commands: ReturnType<ComposerSubmitInput["commands"]>, text: string) {
@@ -292,26 +288,6 @@ function findCommand(commands: ReturnType<ComposerSubmitInput["commands"]>, text
return { command, arguments: arguments_.join(" ") }
}
export function withSlashSkill(prompt: Prompt, skills: ReturnType<ComposerSubmitInput["skills"]>): Prompt {
const first = prompt[0]
if (first?.type !== "text") return prompt
const name = /^\/(\S+)(?:\s|$)/.exec(first.content)?.[1]
const skill = skills?.find((item) => item.slash === true && item.id === name)
if (!skill || prompt.some((part) => part.type === "skill" && part.id === skill.id)) return prompt
const content = `/${skill.id}`
return [
{
type: "skill",
id: Skill.ID.make(skill.id),
name: Skill.Name.make(skill.name),
content,
start: 0,
end: content.length,
},
{ ...first, content: first.content.slice(content.length), start: content.length },
...prompt.slice(1),
]
}
async function sendCommand(
session: ComposerSession,
@@ -324,7 +300,7 @@ async function sendCommand(
if (value.delivery === "steer") await applySelection(session, value.selection, track)
await session.api.command({
sessionID: session.id,
command: command.command,
name: command.command,
text: command.arguments,
files: request.files.map((file) => ({ uri: file.uri, name: file.name, mention: file.mention })),
agents: request.agents,
+1 -1
View File
@@ -110,7 +110,7 @@ export function createHomeController() {
.list({ path: ".", location })
.then(async (files) => {
// TODO: Initialize empty directories when V2 exposes a native Git init API.
return ctx.sdk.api.project.current({ location })
return ctx.sdk.api.location.get({ location }).then((result) => result.project)
})
.then((project) => ctx.sync.child(item, { bootstrap: false })[1]("project", project.id))
.catch(() => undefined)
+1 -1
View File
@@ -576,7 +576,7 @@ export const dict = {
"context.stats.lastActivity": "Last Activity",
"context.usage.tokens": "Tokens",
"context.usage.usage": "Context Usage",
"context.usage.usage": "Context",
"context.usage.cost": "Cost",
"context.usage.clickToView": "Click to view context",
"context.usage.view": "View context usage",
@@ -90,7 +90,7 @@ test("rotates HTTP and PTY clients together", async () => {
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const request = input instanceof Request ? input : new Request(input, init)
requests.push({ url: request.url, authorization: request.headers.get("authorization") })
return Response.json({ healthy: true, version: "2.0.0-test", pid: 1 })
return Response.json({ version: "2.0.0-test", pid: 1, urls: [request.url] })
}) as typeof globalThis.fetch
const transport = createServerTransport({
http: { url: "http://127.0.0.1:4100", password: "first" },
@@ -98,23 +98,23 @@ test("rotates HTTP and PTY clients together", async () => {
})
const initialPty = transport.pty
await transport.api.health.get()
await transport.api.server.status()
const replacement = transport.update({
url: "http://127.0.0.1:4200",
password: "second",
})
await transport.api.health.get()
await transport.api.server.status()
expect(replacement).toBe(transport.api)
expect(transport.pty).not.toBe(initialPty)
expect(transport.url).toBe("http://127.0.0.1:4200")
expect(requests).toEqual([
{
url: "http://127.0.0.1:4100/api/health",
url: "http://127.0.0.1:4100/api/status",
authorization: `Basic ${btoa("opencode:first")}`,
},
{
url: "http://127.0.0.1:4200/api/health",
url: "http://127.0.0.1:4200/api/status",
authorization: `Basic ${btoa("opencode:second")}`,
},
])
@@ -2,8 +2,6 @@ import type { Config, Path, Project, ProviderAuthResponse } from "@/runtime/serv
import type {
LocationGetInput,
LocationGetOutput,
ProjectCurrentInput,
ProjectCurrentOutput,
ProjectListOutput,
} from "@opencode/client/promise"
import { showToast } from "@/shell/notifications/toast"
@@ -61,7 +59,6 @@ export const loadGlobalConfigQuery = (scope: ServerScope) =>
type ProjectApi = {
readonly list: () => Promise<ProjectListOutput>
readonly current: (input?: ProjectCurrentInput) => Promise<ProjectCurrentOutput>
}
type LocationApi = { readonly get: (input?: LocationGetInput) => Promise<LocationGetOutput> }
@@ -132,6 +129,7 @@ export async function bootstrapDirectory(input: {
mcp: boolean
api: {
readonly project: ProjectApi
readonly location: LocationApi
}
store: Store<State>
setStore: SetStoreFunction<State>
@@ -155,8 +153,8 @@ export async function bootstrapDirectory(input: {
seededProject
? undefined
: () =>
retry(() => input.api.project.current({ location: { directory: input.directory } })).then((project) =>
input.setStore("project", project.id),
retry(() => input.api.location.get({ location: { directory: input.directory } })).then((location) =>
input.setStore("project", location.project.id),
),
].filter((task): task is () => Promise<void> => !!task)
+9 -30
View File
@@ -17,11 +17,10 @@ describe("checkServerHealth", () => {
const headers: Array<string | null> = []
const fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
headers.push(new Headers(init?.headers).get("authorization"))
return Response.json({ healthy: true, version: "2.0.0" })
return Response.json({ version: "2.0.0", pid: 1, urls: [server.url] })
}) as typeof globalThis.fetch
const legacy = { ...server, username: "legacy", password }
expect(await checkServerHealth(legacy, fetch)).toEqual({ healthy: true, version: "2.0.0" })
expect(await checkServerHealth({ ...server, password }, fetch)).toEqual({ healthy: true, version: "2.0.0" })
expect(headers).toEqual([password ? `Basic ${btoa(`opencode:${password}`)}` : null])
})
@@ -29,7 +28,7 @@ describe("checkServerHealth", () => {
let request: URL | undefined
const fetch = (async (input: RequestInfo | URL) => {
request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
return new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
status: 200,
headers: { "content-type": "application/json" },
})
@@ -38,27 +37,7 @@ describe("checkServerHealth", () => {
const result = await checkServerHealth(server, fetch)
expect(result).toEqual({ healthy: true, version: "1.2.3" })
expect(request?.pathname).toBe("/api/health")
})
test("identifies a V1 server without a version as incompatible", async () => {
const requests: string[] = []
const fetch = (async (input: RequestInfo | URL) => {
const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
requests.push(url.pathname)
return new Response(
JSON.stringify(url.pathname === "/global/health" ? { version: "1.18.15" } : { healthy: true }),
{
status: 200,
headers: { "content-type": "application/json" },
},
)
}) as unknown as typeof globalThis.fetch
const result = await checkServerHealth(server, fetch)
expect(result).toEqual({ healthy: false, version: "1.18.15", incompatible: true })
expect(requests).toEqual(["/api/health", "/global/health"])
expect(request?.pathname).toBe("/api/status")
})
test("allows slow servers thirty seconds by default", async () => {
@@ -73,14 +52,14 @@ describe("checkServerHealth", () => {
})
const fetch = (async () =>
new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
status: 200,
headers: { "content-type": "application/json" },
})) as unknown as typeof globalThis.fetch
await checkServerHealth(server, fetch).finally(() => {
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
if (!timeout) delete (AbortSignal as Partial<typeof AbortSignal>).timeout
})
expect(timeoutMs).toBe(30_000)
@@ -121,7 +100,7 @@ describe("checkServerHealth", () => {
timeoutMs: 10,
}).finally(() => {
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
if (!timeout) delete (AbortSignal as Partial<typeof AbortSignal>).timeout
})
expect(aborted).toBe(true)
@@ -132,7 +111,7 @@ describe("checkServerHealth", () => {
let signal: AbortSignal | undefined
const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
signal = abortFromInput(input, init)
return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
return new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
status: 200,
headers: { "content-type": "application/json" },
})
@@ -151,7 +130,7 @@ describe("checkServerHealth", () => {
const fetch = (async () => {
count += 1
if (count < 3) throw new TypeError("network")
return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
return new Response(JSON.stringify({ version: "1.2.3", pid: 1, urls: [server.url] }), {
status: 200,
headers: { "content-type": "application/json" },
})
+3 -16
View File
@@ -95,23 +95,10 @@ export async function checkServerHealth(
fetch,
headers,
})
.health.get({ signal })
.then(async (x) => {
if (typeof x.healthy !== "boolean") return { error: new Error("Invalid health response") }
if (x.healthy && typeof x.version !== "string") {
const legacy = await fetch(new URL("/global/health", server.url), { headers, signal })
.then((response) => response.json())
.catch(() => undefined)
const version =
typeof legacy === "object" && legacy !== null && "version" in legacy && typeof legacy.version === "string"
? legacy.version
: "1"
return { data: { healthy: false, version, incompatible: true } }
}
return { data: { healthy: x.healthy, version: x.version } }
})
.server.status({ signal })
.then((status) => ({ data: { healthy: true as const, version: status.version } }))
.catch((error) => ({ error }))
if ("data" in current && current.data) return current.data
if ("data" in current) return current.data
if (signal?.aborted) return { healthy: false }
return next(count, current.error)
@@ -195,7 +195,7 @@ describe("createRequestQueue", () => {
input.tick(50)
input.queue.fetch("http://server/api/worktree?location[directory]=%2Fc").catch(() => undefined)
input.tick(100)
input.queue.fetch("http://server/api/health").catch(() => undefined)
input.queue.fetch("http://server/api/status").catch(() => undefined)
expect(input.logs).toEqual([])
input.tick(2_000)
await new Promise((resolve) => setTimeout(resolve, 20))
@@ -210,7 +210,7 @@ describe("createRequestQueue", () => {
],
queued: [
{ method: "GET", url: "http://server/api/worktree?location[directory]=%2Fc", ms: 2_100 },
{ method: "GET", url: "http://server/api/health", ms: 2_000 },
{ method: "GET", url: "http://server/api/status", ms: 2_000 },
],
},
},
@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
const dir = base64Encode(location().directory)
serverSDK.api.session
.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
.fork({ sessionID, before: item.id })
.then((forked) => {
data.session.remember(forked)
dialog.close()
+1 -1
View File
@@ -37,7 +37,7 @@ export function createActiveComposerAdapter(input: {
}),
interrupt: () =>
server.api.session
.interrupt({ sessionID: id, continue: true })
.interrupt({ sessionID: id, resume: true })
.then(() => undefined)
.catch(() => undefined),
}
@@ -60,7 +60,7 @@ export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data
const listed = await Promise.all(
inventory.locations.map((location) =>
input.sdk.api.permission.request
.list({ location: { directory: location.directory, workspace: location.workspaceID } })
.list({ location: { directory: location.directory } })
.then((pending) => {
if (!state.disposed) pending.data.forEach((request) => approve(request))
return true
@@ -103,7 +103,7 @@ export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data
]
return {
locations: [
...new Map(locations.map((item) => [`${item.directory}\u0000${item.workspaceID ?? ""}`, item])).values(),
...new Map(locations.map((item) => [item.directory, item])).values(),
],
complete: active !== undefined && synced.every(Boolean),
}
+1 -1
View File
@@ -48,7 +48,7 @@ export function createSessionRequestModel() {
providers: async (sessionID) => {
const session = data.session.get(sessionID) ?? (await serverSDK.api.session.get({ sessionID }))
const result = await serverSDK.api.websearch.providers({
location: { directory: session.location.directory, workspace: session.location.workspaceID },
location: { directory: session.location.directory },
})
return result.data.map((provider) => ({ value: provider.id, label: provider.name }))
},
+1 -1
View File
@@ -136,7 +136,7 @@ function ResolvedTargetSessionRoute() {
>
<Show when={directory()} fallback={<PendingSessionState sessionID={params.id} />}>
{(value) => (
<LocationProvider directory={value} workspaceID={() => current()?.location.workspaceID}>
<LocationProvider directory={value}>
<SessionUIProvider directory={value()} server={server.key}>
<TargetSessionPage />
</SessionUIProvider>
@@ -1,5 +1,4 @@
import { createMemo, createResource, onMount, type Accessor } from "solid-js"
import type { ConfigPreferences, ConfigUpdatePreferencesInput } from "@opencode/client/promise"
import type { ColorScheme } from "@opencode/ui/theme/context"
import { useTheme } from "@opencode/ui/theme/context"
import {
@@ -24,91 +23,47 @@ import { showToast } from "@/shell/notifications/toast"
export { createShellOptions, createSoundPreviewController } from "./behavior"
export type { ShellOption, ShellSelectOption } from "./behavior"
export function createServerPreferencesController(server: Accessor<ServerConnection.Any>) {
export function createServerShellController(server: Accessor<ServerConnection.Any>) {
const language = useLanguage()
const serverCtx = useServerCtx(server)
const source = () => ServerConnection.key(server())
const [preferences, preferencesActions] = createResource<ConfigPreferences, ServerConnection.Key>(
const [state, actions] = createResource(
source,
() =>
serverCtx()
.sdk.api.config.preferences()
.catch(() => ({})),
{ initialValue: {} },
async () => {
const context = serverCtx()
const [entries, shells] = await Promise.all([
context.sdk.api.config.get().catch(() => []),
context.sdk.api.config.shells().catch(() => []),
])
const boundary = entries.findIndex((entry) => entry.type === "directory")
const global = boundary === -1 ? entries : entries.slice(0, boundary)
return {
shells,
shell: global
.flatMap((entry) => (entry.type === "document" && entry.info.shell !== undefined ? [entry.info.shell] : []))
.at(-1),
}
},
{ initialValue: { shells: [], shell: undefined } },
)
const [shells] = createResource(
source,
() =>
serverCtx()
.sdk.api.config.shells()
.catch(() => []),
{ initialValue: [] },
)
const [providers] = createResource(
source,
() =>
serverCtx()
.sdk.api.websearch.providers()
.then((result) => result.data)
.catch(() => []),
{ initialValue: [] },
)
const update = async (patch: ConfigUpdatePreferencesInput) => {
const context = serverCtx()
const previous = preferences.latest
preferencesActions.mutate({
...previous,
...(patch.shell === undefined ? {} : { shell: patch.shell ?? undefined }),
...(patch.websearch === undefined ? {} : { websearch: patch.websearch ?? undefined }),
})
await context.sdk.api.config
.updatePreferences(patch)
.then(preferencesActions.mutate)
.catch((error: unknown) => {
preferencesActions.mutate(previous)
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
})
})
}
const websearchOptions = createMemo(() => {
const options = providers.latest.map((provider) => ({ value: provider.id, label: provider.name }))
const selected = preferences.latest.websearch
const configured = selected && selected.provider !== "random" ? selected.provider : undefined
return [
{ value: "random" as const, label: language.t("session.websearch.any") },
...options,
...(configured && !options.some((option) => option.value === configured)
? [{ value: configured, label: configured }]
: []),
{ value: false as const, label: language.t("session.websearch.disable") },
]
})
const websearchCurrent = createMemo(() => {
const selection = preferences.latest.websearch
const value = selection === false ? false : (selection?.provider ?? "random")
return websearchOptions().find((option) => option.value === value) ?? websearchOptions()[0]
})
return {
shell: {
shells: () => shells.latest,
current: () => preferences.latest.shell ?? "",
select: (value: string) => {
if (value === (preferences.latest.shell ?? "")) return
void update({ shell: value || null })
},
},
websearch: {
options: websearchOptions,
current: websearchCurrent,
select: (value: string | false) => {
void update({ websearch: value === false ? false : { provider: value } })
},
shells: () => state.latest.shells,
current: () => state.latest.shell ?? "",
select: (value: string) => {
if (value === (state.latest.shell ?? "")) return
const previous = state.latest
actions.mutate({ ...previous, shell: value || undefined })
void serverCtx()
.sdk.api.config.update({ shell: value || null })
.catch((error: unknown) => {
actions.mutate(previous)
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
})
})
},
}
}
@@ -208,6 +163,7 @@ export function createSoundSettingsController() {
}
}
export type ShellSettingsController = ReturnType<typeof createServerPreferencesController>["shell"]
export type ShellSettingsController = ReturnType<typeof createServerShellController>
export type AppearanceSettingsController = ReturnType<typeof createAppearanceSettingsController>
export type SoundSettingsController = ReturnType<typeof createSoundSettingsController>
@@ -105,7 +105,7 @@ export const SettingsProviders: Component<{
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all(
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id, location })),
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id })),
)
showToast({
variant: "success",
+5 -31
View File
@@ -1,5 +1,4 @@
import { Badge } from "@opencode/ui/badge"
import { Select } from "@opencode/ui/select"
import { useDialog } from "@opencode/ui/context/dialog"
import { createMemo, Show, type Component } from "solid-js"
import { ServerRowMenu } from "@/servers/registry/row-menu"
@@ -11,35 +10,11 @@ import { DialogServer } from "@/servers/connect/dialog"
import { AddServerMenu, WslServerSettings } from "@/servers/wsl/settings"
import { SshServerSettings } from "@/servers/ssh/settings"
import { SettingsList } from "@/settings/list"
import { SettingsRow } from "@/settings/row"
import { ShellSetting } from "@/settings/general/general"
import { createServerPreferencesController } from "@/settings/general/controllers"
import { createServerShellController } from "@/settings/general/controllers"
import type { SettingsServer } from "./inventory"
import "@/settings/settings.css"
const WebSearchSetting: Component<{
controller: ReturnType<typeof createServerPreferencesController>["websearch"]
}> = (props) => {
const language = useLanguage()
return (
<SettingsRow
title={language.t("settings.server.preferences.websearch.title")}
description={language.t("settings.server.preferences.websearch.description")}
>
<Select
data-action="settings-websearch"
options={props.controller.options()}
current={props.controller.current()}
value={(option) => String(option.value)}
label={(option) => option.label}
placement="bottom-end"
gutter={6}
onSelect={(option) => option && props.controller.select(option.value)}
/>
</SettingsRow>
)
}
export const SettingsServerGeneral: Component<{
entry: SettingsServer
nested?: boolean
@@ -118,22 +93,21 @@ export const SettingsServerGeneral: Component<{
</section>
<Show when={props.entry.connection} keyed>
{(server) => <ServerPreferences server={server} />}
{(server) => <ServerShell server={server} />}
</Show>
</div>
</>
)
}
function ServerPreferences(props: { server: ServerConnection.Any }) {
function ServerShell(props: { server: ServerConnection.Any }) {
const language = useLanguage()
const preferences = createServerPreferencesController(() => props.server)
const controller = createServerShellController(() => props.server)
return (
<section class="settings-section">
<h3 class="settings-section-title">{language.t("settings.tab.preferences")}</h3>
<SettingsList>
<ShellSetting controller={preferences.shell} />
<WebSearchSetting controller={preferences.websearch} />
<ShellSetting controller={controller} />
</SettingsList>
</section>
)
@@ -27,10 +27,10 @@ import "./project.css"
type SkillItem = {
name: string
location: string
path: string
}
const skillKey = (item: SkillItem) => `${item.name}\n${item.location}`
const skillKey = (item: SkillItem) => `${item.name}\n${item.path}`
const ExtensionCard: Component<{ children: JSX.Element }> = (props) => (
<SettingsList variant="catalog">{props.children}</SettingsList>
@@ -77,7 +77,7 @@ export function DirectoryPickerDialog(props: DirectoryPickerDialogProps) {
const home = createMemo(() => sync.data.path.home || "")
const location = createMemo(() => {
const current = props.location ?? fallbackPath()
return current ? { directory: current.directory, workspace: current.workspaceID } : undefined
return current ? { directory: current.directory } : undefined
})
const start = createMemo(
() =>
+1 -1
View File
@@ -27,7 +27,7 @@ test("detects iOS home-screen apps when the standalone media query does not matc
expect(isStandalone()).toBe(true)
} finally {
if (descriptor) Object.defineProperty(navigator, "standalone", descriptor)
if (!descriptor) Reflect.deleteProperty(navigator, "standalone")
if (!descriptor) delete (navigator as Navigator & { standalone?: boolean }).standalone
}
})
@@ -219,6 +219,7 @@ test("snapshots materialize only measured rows and restore their current geometr
new Proxy(measurements, {
get(target, key, receiver) {
if (typeof key === "string" && /^\d+$/.test(key)) reads.push(Number(key))
// oxlint-disable-next-line no-restricted-globals -- Proxy forwarding requires receiver-aware property access.
return Reflect.get(target, key, receiver)
},
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.2",
"version": "2.0.3",
"type": "module",
"license": "MIT",
"bin": {
+9 -9
View File
@@ -2,7 +2,7 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode/client/effect/service"
import { ServiceStatus } from "@opencode/protocol/groups/health"
import { ServerStatus } from "@opencode/protocol/groups/server"
import { Effect, Schema } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
@@ -42,12 +42,12 @@ try {
const credential = btoa(`opencode:${info.password}`)
const headers = { authorization: "Basic " + credential }
const token = encodeURIComponent(credential)
const health = await waitForReady(info.url, headers)
if (health.pid !== info.pid) throw new Error("Health process does not match registration")
const tokenHealth = await fetch(new URL(`/api/health?auth_token=${token}`, info.url), {
const status = await waitForReady(info.url, headers)
if (status.pid !== info.pid) throw new Error("Status process does not match registration")
const tokenStatus = await fetch(new URL(`/api/status?auth_token=${token}`, info.url), {
signal: AbortSignal.timeout(5_000),
})
if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication")
if (tokenStatus.status !== 200) throw new Error("Compiled service rejected query authentication")
const tokenOpenApi = await fetch(new URL(`/openapi.json?auth_token=${token}`, info.url), {
signal: AbortSignal.timeout(5_000),
})
@@ -58,10 +58,10 @@ try {
await fs.writeFile(plugin, pluginSource())
await waitForPlugin(info.url, headers)
const unauthorizedHealth = await fetch(new URL("/api/health", info.url), {
const unauthorizedStatus = await fetch(new URL("/api/status", info.url), {
signal: AbortSignal.timeout(5_000),
})
if (unauthorizedHealth.status !== 401) throw new Error("Compiled service exposed health without authentication")
if (unauthorizedStatus.status !== 401) throw new Error("Compiled service exposed status without authentication")
const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), {
signal: AbortSignal.timeout(5_000),
})
@@ -129,11 +129,11 @@ async function waitForRegistration() {
async function waitForReady(url: string, headers: HeadersInit) {
const deadline = Date.now() + 20_000
while (Date.now() < deadline) {
const response = await fetch(new URL("/api/health", url), {
const response = await fetch(new URL("/api/status", url), {
headers,
signal: AbortSignal.timeout(1_000),
}).catch(() => undefined)
if (response?.ok) return Schema.decodeUnknownPromise(ServiceStatus.Health)(await response.json())
if (response?.ok) return Schema.decodeUnknownPromise(ServerStatus)(await response.json())
await Bun.sleep(25)
}
throw new Error("Compiled service did not become ready")
+1 -1
View File
@@ -78,7 +78,7 @@ export async function syncEditedFiles(input: {
const files = Array.isArray(input.metadata.files)
? input.metadata.files.flatMap((file): string[] => {
if (!file || typeof file !== "object") return []
const path = Reflect.get(file, "file")
const path = "file" in file ? file.file : undefined
return typeof path === "string" ? [path] : []
})
: []
+8 -21
View File
@@ -6,7 +6,6 @@ import {
type OpenCodeClient,
type SessionInfo,
type SessionMessageInfo,
type SkillInfo,
} from "@opencode/client/promise"
import { withTimestampedFallback } from "@opencode/util/session-title-fallback"
import type {
@@ -71,7 +70,6 @@ type Catalog = {
readonly modes: Array<{ id: string; name: string; description?: string }>
readonly defaultModeID: string
readonly commands: CommandInfo[]
readonly skills: SkillInfo[]
}
type Attached = {
@@ -90,7 +88,6 @@ type PreparedPrompt = {
readonly synthetic: ReadonlyArray<string>
readonly slash?: { readonly name: string; readonly args: string }
readonly command?: CommandInfo
readonly skill?: SkillInfo
}
export interface Interface {
@@ -157,11 +154,8 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
update: {
sessionUpdate: "available_commands_update",
availableCommands: [
...state.catalog.commands,
...state.catalog.skills.filter(
(skill) => !state.catalog.commands.some((command) => command.name === skill.name),
),
].map((command) => ({ name: command.name, description: command.description ?? "" })),
...state.catalog.commands.map((command) => ({ name: command.name, description: command.description ?? "" })),
],
},
})
return state
@@ -269,7 +263,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
forkSession: async (params) => {
const forked = await input.client.session.fork({
sessionID: params.sessionId,
boundary: { type: "through" },
})
const state = await attach(forked, forked.location.directory, params.mcpServers ?? [])
await replay(state)
@@ -365,9 +358,8 @@ function preparePrompt(catalog: Catalog, prompt: PromptRequest["prompt"], messag
const files = visible.flatMap((part) => (part.type === "file" ? [{ uri: part.url, name: part.filename }] : []))
const slash = detectSlashCommand(text)
const command = slash ? catalog.commands.find((item) => item.name === slash.name) : undefined
const skill = slash ? catalog.skills.find((item) => item.name === slash.name) : undefined
const start = turnStart(messageID, slash, skill)
return { start, text, files, synthetic, slash, command, skill }
const start = turnStart(messageID, slash)
return { start, text, files, synthetic, slash, command }
}
async function submitPrompt(client: OpenCodeClient, session: Attached, prompt: PreparedPrompt, signal: AbortSignal) {
@@ -381,12 +373,11 @@ async function submitPrompt(client: OpenCodeClient, session: Attached, prompt: P
})
}
if (prompt.start.type === "compaction") return client.session.compact({ sessionID: session.id, id: prompt.start.id })
if (prompt.skill) return client.session.skill({ sessionID: session.id, id: prompt.start.id, skill: prompt.skill.id })
if (prompt.command) {
return client.session.command(
{
sessionID: session.id,
command: prompt.command.name,
name: prompt.command.name,
text: prompt.slash?.args ?? "",
files: prompt.files,
delivery: "steer",
@@ -400,25 +391,22 @@ async function submitPrompt(client: OpenCodeClient, session: Attached, prompt: P
)
}
function turnStart(messageID: string, slash: PreparedPrompt["slash"], skill: SkillInfo | undefined): TurnStart {
function turnStart(messageID: string, slash: PreparedPrompt["slash"]): TurnStart {
if (slash?.name === "compact") return { type: "compaction", id: messageID }
if (skill) return { type: "skill", id: messageID }
return { type: "input", id: messageID }
}
async function loadCatalog(client: OpenCodeClient, cwd: string): Promise<Catalog> {
const location = { directory: cwd }
await client.plugin.awaitActivation({ location })
// Some providers discover models in the background after activation has settled.
// Some providers discover models in the background after plugin startup begins.
const deadline = Date.now() + 5_000
let missing = "No models are available"
while (Date.now() < deadline) {
const [modelResult, defaultResult, agentResult, commandResult, skillResult] = await Promise.all([
const [modelResult, defaultResult, agentResult, commandResult] = await Promise.all([
client.model.list({ location }),
client.model.default({ location }),
client.agent.list({ location }),
client.command.list({ location }),
client.skill.list({ location }),
])
const models = modelResult.data.filter((model) => model.enabled)
const preferred = defaultResult.data
@@ -440,7 +428,6 @@ async function loadCatalog(client: OpenCodeClient, cwd: string): Promise<Catalog
modes: agents.map((agent) => ({ id: agent.id, name: agent.name, description: agent.description })),
defaultModeID: defaultAgent.id,
commands: commandResult.data,
skills: skillResult.data.filter((skill) => skill.slash !== false),
}
}
missing = defaultModel ? "No primary agents are available" : "No models are available"
@@ -8,11 +8,11 @@ describe("api request resolution", () => {
{
paths: {
"/api/session/{sessionID}": {
get: { operationId: "v2.session.get" },
get: { operationId: "session.get" },
},
},
},
"v2.session.get",
"session.get",
{ sessionID: "ses/a", workspace: "work" },
),
).toEqual({ method: "GET", path: "/api/session/ses%2Fa?workspace=work" })
@@ -21,8 +21,8 @@ describe("api request resolution", () => {
test("rejects a missing path parameter", () => {
expect(() =>
resolveOperation(
{ paths: { "/api/session/{sessionID}": { get: { operationId: "v2.session.get" } } } },
"v2.session.get",
{ paths: { "/api/session/{sessionID}": { get: { operationId: "session.get" } } } },
"session.get",
{},
),
).toThrow("Missing path parameter: sessionID")
@@ -30,6 +30,6 @@ describe("api request resolution", () => {
test("resolves curl-like method and path input", () => {
expect(rawRequest(["post", "/api/foo"])).toEqual({ method: "POST", path: "/api/foo" })
expect(rawRequest(["v2.session.list"])).toBeUndefined()
expect(rawRequest(["session.list"])).toBeUndefined()
})
})
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
import { createClient, loadIntegrations, location, request } from "./shared"
import { createClient, loadIntegrations, request } from "./shared"
import { chooseCredential, chooseIntegration } from "./account"
export default Runtime.handler(
@@ -35,7 +35,7 @@ const logout = Effect.fn("cli.auth.logout.run")(function* (input: {
const credentialID = yield* chooseCredential(integration, "log out", input.credential)
const progress = spinner()
progress.start("Removing credential...")
yield* request((signal) => client.credential.remove({ credentialID, location }, { signal })).pipe(
yield* request((signal) => client.credential.remove({ credentialID }, { signal })).pipe(
Effect.tap(() => Effect.sync(() => progress.stop(`Removed account from ${integration.name}`))),
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to remove credential", 1))),
)
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
import { Commands } from "../../commands"
import { Runtime } from "../../../framework/runtime"
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
import { createClient, loadIntegrations, location, request } from "./shared"
import { createClient, loadIntegrations, request } from "./shared"
import { chooseCredential, chooseIntegration } from "./account"
export default Runtime.handler(
@@ -35,7 +35,7 @@ const switchAccount = Effect.fn("cli.auth.switch.run")(function* (input: {
const credentialID = yield* chooseCredential(integration, "switch to", input.credential)
const progress = spinner()
progress.start("Switching account...")
yield* request((signal) => client.credential.activate({ credentialID, location }, { signal })).pipe(
yield* request((signal) => client.credential.activate({ credentialID }, { signal })).pipe(
Effect.tap(() => Effect.sync(() => progress.stop(`Switched account for ${integration.name}`))),
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to switch account", 1))),
)

Some files were not shown because too many files have changed in this diff Show More