mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 11:39:45 -04:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0605bd9c5 | |||
| 1719482a17 | |||
| 43901eae6d | |||
| 60cd82aec8 | |||
| c29968bf08 | |||
| b5b87deaa2 | |||
| 5053c1bfdd | |||
| d97aca139f | |||
| b2f0f45c9e | |||
| f64b024f46 | |||
| ec11d6ed03 | |||
| 0dcedc199c | |||
| e9c55a15d4 | |||
| 9bc4eea79a | |||
| 68b2b08853 | |||
| 7bd6d002bb | |||
| ab872ffb0e | |||
| 84fd347afa | |||
| e8f215bfbc | |||
| 445af9ce70 | |||
| bc51baa9a4 | |||
| ff0a0b0786 | |||
| 4eff0ee2db | |||
| cc0061f88b | |||
| f43f2b354c | |||
| c2ba0bef6b | |||
| 83b47693c3 | |||
| 1ca86ad973 | |||
| 7afe537c69 | |||
| e566c082f1 | |||
| bc93da4a46 | |||
| 4194d55522 | |||
| f0d1029719 | |||
| b274db88b0 | |||
| 9b7b402737 | |||
| 5e6370363b | |||
| dd6020656e | |||
| 8c758e443b | |||
| 0df6aed6ca | |||
| aa05fd23b3 | |||
| fd09760903 | |||
| 3a4772abb4 | |||
| 2cf65c025a | |||
| f3f1204802 | |||
| b3aea1c39d | |||
| 79a493e7f5 | |||
| 952d4d446f | |||
| 0b84e24e65 | |||
| 3776975d5c | |||
| d2c99ba97c | |||
| 6f3a3600b9 | |||
| 9ca650f97c | |||
| db3b54a30d | |||
| b4f769f695 | |||
| e5ef00b8b8 | |||
| 917d6449e3 | |||
| db31c42e39 | |||
| c79ced174e | |||
| 8ba8af1dd9 | |||
| 6e82f5d3b9 | |||
| 48d1a6e5b9 | |||
| bc47030d4d | |||
| e6d20440f9 | |||
| c9cbd2b1f4 | |||
| 292dfa3036 | |||
| 3bb0d7fda0 | |||
| 8977881e09 | |||
| e6c9b6bef7 | |||
| 2092350cfa | |||
| 5fb0d7c99c | |||
| a1b4843a33 | |||
| cc7827fe08 | |||
| 76e4d88d21 | |||
| 439ed66c7b | |||
| 047d434aa2 | |||
| d7651519f3 | |||
| 1eb3a43add | |||
| dcae95e2bb | |||
| 0cc507b8a9 | |||
| 727beae2d5 | |||
| cd64a17e37 | |||
| d35ca49c31 | |||
| 5cf97f1b96 | |||
| 65c6a71903 | |||
| a779027303 | |||
| 20fa444f31 | |||
| c66d84169a | |||
| 7dbe8c4c13 | |||
| 8864b01d0b | |||
| ec95b27308 | |||
| be2f74d44a | |||
| 912a801060 | |||
| 0215498f63 | |||
| 1539bd6794 | |||
| 939da6a5e7 | |||
| 0f27ee7b4c | |||
| 51cef27579 | |||
| a0c77b71ae | |||
| e62918224f | |||
| 30d140008f | |||
| a38d44265b | |||
| 0cce215de4 | |||
| a15ec425de | |||
| ae494ffce5 | |||
| 25c2150baf | |||
| 9314992f98 | |||
| 0fbe489e35 | |||
| 03e9789064 | |||
| 0c558a6856 | |||
| f297bd3b8b | |||
| 1bfc23f503 | |||
| 3839aafa25 | |||
| bd2f37bc90 | |||
| 120e4e7388 | |||
| 6f91bc7415 | |||
| c46f6ae112 | |||
| 285444aab4 | |||
| 56c33e84a3 | |||
| 73581b3c3b | |||
| 6e4d01f846 | |||
| 8646587c95 | |||
| fb47f06228 | |||
| 3acaa5a359 | |||
| 0726a25142 | |||
| 0df96ddeb0 | |||
| 887673310b | |||
| 693a1bff81 | |||
| b40ed3aa85 | |||
| 732bb9c3cb | |||
| 2fd1660b4d | |||
| 681526d348 | |||
| e78869c849 | |||
| 934935963d | |||
| f3912a2a8a | |||
| 45d58717a4 | |||
| 74e3155ef0 | |||
| 0af6c82563 | |||
| 686127f809 | |||
| 5256655c4d | |||
| d10b652637 | |||
| b03ca0d4e2 | |||
| 25aaea3d31 | |||
| cae7a139bc | |||
| 5ea62ab05f | |||
| faadc05c88 |
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
- v2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate:
|
generate:
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ jobs:
|
|||||||
|
|
||||||
build-electron:
|
build-electron:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
|
||||||
- version
|
- version
|
||||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
@@ -378,16 +377,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
|
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build
|
||||||
working-directory: packages/desktop
|
working-directory: packages/desktop
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||||
@@ -405,8 +402,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||||
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
GH_TOKEN: ${{ steps.committer.outputs.token }}
|
||||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
CSC_KEYCHAIN: build.keychain
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
|
||||||
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
|
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }}
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
|
|||||||
@@ -251,4 +251,3 @@ opencode-drive stop --name demo
|
|||||||
```bash
|
```bash
|
||||||
opencode-drive dir --name demo
|
opencode-drive dir --name demo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: rtl-aware-development
|
||||||
|
description: OpenCode Desktop should be RTL-aware. Use when implementing or reviewing RTL/LTR behavior in the web app, desktop app, CSS, menus, scrolling, resizing, icons, mixed-direction text, or Electron title bars.
|
||||||
|
---
|
||||||
|
|
||||||
|
# RTL-Aware Development
|
||||||
|
|
||||||
|
Treat direction as independent from language. Test English in both directions as well as real RTL and mixed-script content.
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Set `lang` and `dir` on the document, and propagate direction through component providers used by portaled menus and popovers. Do not change the selected locale merely to force RTL.
|
||||||
|
- Keep DOM and focus order semantic. Flexbox and Grid already follow `dir`; do not add `row-reverse`, CSS `order`, or reversed markup just to mirror a layout.
|
||||||
|
- Prefer logical CSS for semantic layout. Reserve physical coordinates for pointer positions, canvas geometry, native window controls, and other genuinely physical placement.
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Avoid */
|
||||||
|
padding-left: 12px;
|
||||||
|
right: 0;
|
||||||
|
border-right: 1px solid;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
/* Prefer */
|
||||||
|
padding-inline-start: 12px;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
border-inline-end: 1px solid;
|
||||||
|
text-align: start;
|
||||||
|
```
|
||||||
|
|
||||||
|
- Isolate mixed-direction text. Use `dir="auto"` or `<bdi>` for unknown text; keep code, URLs, IDs, and filesystem paths LTR without forcing the surrounding component LTR.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="file-row"><bdi dir="auto">README.md</bdi></span> <bdi dir="ltr"><code>C:\src\app.ts</code></bdi>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Mirror directional meaning, not every image. Back/forward, previous/next, disclosure, indentation, and directional progress may need mirroring. Do not mirror brands, clocks, media controls, charts, or text. Reverse physical gradients, `translateX`, SVG transforms, and animation deltas explicitly.
|
||||||
|
- Map interactions through direction. `clientX` remains physical; resizing a logical edge needs an RTL-aware delta. Logical previous/next keyboard controls may swap ArrowLeft/ArrowRight. Follow the relevant WAI-ARIA widget pattern.
|
||||||
|
- Do not assume LTR scrolling. RTL `scrollLeft` can start at `0` and become negative. Prefer `scrollIntoView({ inline: "nearest" })` or a tested direction-normalizing helper.
|
||||||
|
- For Electron title bars, prefer native caption controls and use `titleBarOverlay` plus `env(titlebar-area-*)` for the safe content rectangle. Keep Windows/macOS native-control avoidance and `trafficLightPosition` physical; keep app navigation inside that rectangle logical. Mark interactive titlebar children `app-region: no-drag`.
|
||||||
|
- Verify behavior, not screenshots alone. Check computed styles, pseudo-element geometry, hit zones, focus order, keyboard behavior, submenu direction, zoom/scaling, and both LTR and RTL scroll endpoints.
|
||||||
|
|
||||||
|
## Test Matrix
|
||||||
|
|
||||||
|
- English + LTR
|
||||||
|
- English + forced RTL
|
||||||
|
- A real RTL locale + RTL
|
||||||
|
- Mixed RTL/LTR content, long labels, numbers, code, and paths
|
||||||
|
- Keyboard, pointer resize, scrolling, menus/submenus, and Electron titlebar controls in both directions
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [RTL Styling 101, Ahmad Shadeed](https://rtlstyling.com/posts/rtl-styling/)
|
||||||
|
- [CSS-Tricks: RTL Styling 101](https://css-tricks.com/rtl-styling-101/)
|
||||||
|
- [CSS-Tricks: CSS Logical Properties and Values](https://css-tricks.com/css-logical-properties-and-values/)
|
||||||
|
- [W3C: Structural markup and right-to-left text](https://www.w3.org/International/questions/qa-html-dir)
|
||||||
|
- [W3C: Inline bidirectional markup](https://www.w3.org/International/articles/inline-bidi-markup/)
|
||||||
|
- [MDN: CSS logical properties and values](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Logical_properties_and_values)
|
||||||
|
- [MDN: `dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir)
|
||||||
|
- [MDN: `scrollLeft`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft)
|
||||||
|
- [web.dev: Logical properties](https://web.dev/learn/css/logical-properties/)
|
||||||
|
- [Electron: Custom title bar](https://www.electronjs.org/docs/latest/tutorial/custom-title-bar)
|
||||||
|
- [WAI-ARIA: Window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)
|
||||||
|
- [Kobalte: I18n Provider](https://kobalte.dev/docs/core/components/i18n-provider/)
|
||||||
@@ -252,33 +252,6 @@ These are not strictly enforced, they are just general guidelines:
|
|||||||
|
|
||||||
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
||||||
|
|
||||||
## Trust & Vouch System
|
|
||||||
|
|
||||||
This project uses [vouch](https://github.com/mitchellh/vouch) to manage contributor trust. The vouch list is maintained in [`.github/VOUCHED.td`](.github/VOUCHED.td).
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
|
|
||||||
- **Vouched users** are explicitly trusted contributors.
|
|
||||||
- **Denounced users** are explicitly blocked. Issues and pull requests from denounced users are automatically closed. If you have been denounced, you can request to be unvouched by reaching out to a maintainer on [Discord](https://opencode.ai/discord)
|
|
||||||
- **Everyone else** can participate normally — you don't need to be vouched to open issues or PRs.
|
|
||||||
|
|
||||||
### For maintainers
|
|
||||||
|
|
||||||
Collaborators with write access can manage the vouch list by commenting on any issue:
|
|
||||||
|
|
||||||
- `vouch` — vouch for the issue author
|
|
||||||
- `vouch @username` — vouch for a specific user
|
|
||||||
- `denounce` — denounce the issue author
|
|
||||||
- `denounce @username` — denounce a specific user
|
|
||||||
- `denounce @username <reason>` — denounce with a reason
|
|
||||||
- `unvouch` / `unvouch @username` — remove someone from the list
|
|
||||||
|
|
||||||
Changes are committed automatically to `.github/VOUCHED.td`.
|
|
||||||
|
|
||||||
### Denouncement policy
|
|
||||||
|
|
||||||
Denouncement is reserved for users who repeatedly submit low-quality AI-generated contributions, spam, or otherwise act in bad faith. It is not used for disagreements or honest mistakes.
|
|
||||||
|
|
||||||
## Issue Requirements
|
## Issue Requirements
|
||||||
|
|
||||||
All issues **must** use one of our issue templates:
|
All issues **must** use one of our issue templates:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -53,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@corvu/drawer": "catalog:",
|
"@corvu/drawer": "catalog:",
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
@@ -64,7 +64,6 @@
|
|||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/sdk": "file:vendor/opencode-ai-sdk-1.18.8-dev.tgz",
|
|
||||||
"@opencode-ai/session-ui": "workspace:*",
|
"@opencode-ai/session-ui": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -112,6 +111,7 @@
|
|||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"happy-dom": "20.11.1",
|
||||||
"tw-animate-css": "1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -388,13 +388,14 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.11.1",
|
"gitlab-ai-provider": "6.12.1",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"htmlparser2": "8.0.2",
|
"htmlparser2": "8.0.2",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"immer": "11.1.4",
|
"immer": "11.1.4",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
|
"mime-types": "3.0.2",
|
||||||
"tree-sitter-bash": "0.25.0",
|
"tree-sitter-bash": "0.25.0",
|
||||||
"tree-sitter-powershell": "0.25.10",
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
@@ -423,16 +424,16 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"electron-context-menu": "4.1.2",
|
"electron-context-menu": "4.1.2",
|
||||||
"electron-log": "^5",
|
"electron-log": "^5",
|
||||||
"electron-store": "11.0.2",
|
"electron-store": "11.0.2",
|
||||||
"electron-updater": "6.8.9",
|
"electron-updater": "6.8.9",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"marked": "^15",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "4.0.0",
|
"@actions/artifact": "4.0.0",
|
||||||
@@ -477,7 +478,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -489,21 +490,9 @@
|
|||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
|
||||||
"version": "1.18.8",
|
|
||||||
"dependencies": {
|
|
||||||
"effect": "catalog:",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tsconfig/bun": "catalog:",
|
|
||||||
"@types/node": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -536,7 +525,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -552,7 +541,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.101",
|
"@effect/platform-node-shared": "4.0.0-beta.101",
|
||||||
},
|
},
|
||||||
@@ -582,9 +571,23 @@
|
|||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/merman": {
|
||||||
|
"name": "@opencode-ai/merman",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
"@opentui/core": "catalog:",
|
||||||
|
"string-width": "catalog:",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/ai": "workspace:*",
|
"@opencode-ai/ai": "workspace:*",
|
||||||
@@ -609,9 +612,9 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opencode-ai/theme": "workspace:*",
|
"@opencode-ai/theme": "workspace:*",
|
||||||
"@opentui/core": ">=0.4.5",
|
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/keymap": ">=0.4.5",
|
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/solid": ">=0.4.5",
|
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
|
||||||
"solid-js": ">=1.9.0",
|
"solid-js": ">=1.9.0",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
"modal": "0.9.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
@@ -699,7 +703,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
@@ -714,15 +718,11 @@
|
|||||||
"@solid-primitives/media": "2.3.3",
|
"@solid-primitives/media": "2.3.3",
|
||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"katex": "0.16.27",
|
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
"marked": "catalog:",
|
"marked": "catalog:",
|
||||||
"marked-katex-extension": "5.1.6",
|
|
||||||
"marked-shiki": "catalog:",
|
|
||||||
"morphdom": "2.7.8",
|
"morphdom": "2.7.8",
|
||||||
"motion": "12.34.5",
|
"motion": "12.34.5",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
@@ -735,7 +735,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/katex": "0.16.7",
|
|
||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
@@ -766,7 +765,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "1.18.5",
|
"@opencode-ai/sdk": "1.18.5",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -779,7 +778,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -813,7 +812,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
@@ -832,7 +831,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -892,6 +891,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/merman": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/simulation": "workspace:*",
|
"@opencode-ai/simulation": "workspace:*",
|
||||||
@@ -902,7 +902,6 @@
|
|||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "catalog:",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
"clipboardy": "4.0.0",
|
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"get-east-asian-width": "catalog:",
|
"get-east-asian-width": "catalog:",
|
||||||
@@ -922,7 +921,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -938,7 +937,6 @@
|
|||||||
"katex": "0.16.27",
|
"katex": "0.16.27",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
"marked": "catalog:",
|
"marked": "catalog:",
|
||||||
"marked-katex-extension": "5.1.6",
|
|
||||||
"marked-shiki": "catalog:",
|
"marked-shiki": "catalog:",
|
||||||
"morphdom": "2.7.8",
|
"morphdom": "2.7.8",
|
||||||
"motion": "12.34.5",
|
"motion": "12.34.5",
|
||||||
@@ -948,6 +946,7 @@
|
|||||||
"remend": "catalog:",
|
"remend": "catalog:",
|
||||||
"shiki": "catalog:",
|
"shiki": "catalog:",
|
||||||
"solid-list": "catalog:",
|
"solid-list": "catalog:",
|
||||||
|
"solid-sonner": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1019,7 +1018,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1074,21 +1073,23 @@
|
|||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
|
||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.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",
|
"@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",
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
|
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||||
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
|
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
|
||||||
|
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -1106,9 +1107,9 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opentui/core": "0.4.5",
|
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/keymap": "0.4.5",
|
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/solid": "0.4.5",
|
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@pierre/diffs": "1.2.10",
|
"@pierre/diffs": "1.2.10",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
@@ -1141,7 +1142,7 @@
|
|||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "17.0.6",
|
"marked": "18.0.7",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
"opentui-spinner": "0.0.7",
|
"opentui-spinner": "0.0.7",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
@@ -1151,6 +1152,7 @@
|
|||||||
"shiki": "4.2.0",
|
"shiki": "4.2.0",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
|
"solid-sonner": "0.3.1",
|
||||||
"sst": "4.13.1",
|
"sst": "4.13.1",
|
||||||
"string-width": "7.2.0",
|
"string-width": "7.2.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
@@ -1523,6 +1525,18 @@
|
|||||||
|
|
||||||
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
|
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA=="],
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q=="],
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ=="],
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg=="],
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA=="],
|
||||||
|
|
||||||
|
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA=="],
|
||||||
|
|
||||||
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
|
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
|
||||||
|
|
||||||
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
|
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
|
||||||
@@ -1731,6 +1745,10 @@
|
|||||||
|
|
||||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||||
|
|
||||||
|
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="],
|
||||||
|
|
||||||
|
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="],
|
"@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="],
|
||||||
@@ -1825,6 +1843,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||||
|
|
||||||
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
|
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
|
||||||
|
|
||||||
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
|
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
|
||||||
@@ -2067,8 +2087,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"],
|
"@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"],
|
||||||
|
|
||||||
"@opencode-ai/effect-sqlite-node": ["@opencode-ai/effect-sqlite-node@workspace:packages/effect-sqlite-node"],
|
|
||||||
|
|
||||||
"@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"],
|
"@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"],
|
||||||
|
|
||||||
"@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"],
|
"@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"],
|
||||||
@@ -2077,6 +2095,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
|
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
|
||||||
|
|
||||||
|
"@opencode-ai/merman": ["@opencode-ai/merman@workspace:packages/merman"],
|
||||||
|
|
||||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||||
|
|
||||||
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
||||||
@@ -2147,27 +2167,27 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="],
|
"@opentui/core": ["@opentui/core@0.0.0-20260808-9ecf7c0a", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-darwin-x64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-arm64-musl": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-x64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-x64-musl": "0.0.0-20260808-9ecf7c0a", "@opentui/core-win32-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-win32-x64": "0.0.0-20260808-9ecf7c0a" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-y9MKL8LMup4ebW3uZ57RSsPyOv37sIZxuWiHq4XJK9wrzpzTMnVomzxpWOI0S/QtLH2QzZ7znnbF+IuvIQ1hLQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20260808-9ecf7c0a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Bn6fUZrwbUojkJG4YHowg/h/u3SPANmEivrl8YVbxBPCYOhZzQWxAVf7Fwg6PD0BpWk5gt9inQrir2DzWh5UpA=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20260808-9ecf7c0a", "", { "os": "darwin", "cpu": "x64" }, "sha512-lrVskzbjcT9yJCUgjOpvPpEq2ipmuDyHTMJ+OebsJFdATdryJLzRAr5BZiFGXEo4CJ2CS/+6vT+ki7dmhpM+0g=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "arm64" }, "sha512-czqIqdnBvyFRSJ0BEdNn7moRN0KQo5gvhSsOSfrzueHxsktgWcmk1cMeCvTkzC3JCBvCwlB1KnSdt4jlwNO1EQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="],
|
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "arm64" }, "sha512-6nixv6wlgjelUVJVHkJsCpuARb1rE/oxhIIthyLd062u5veoBt83cRGH9KmcZiwowKvso58hXn2KC21qAfylhQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "x64" }, "sha512-cxJMfrSsMsrZdU1Ke6awrxz0rdIJuI34l8Le3QlUbM5yLpp09ygJ7CElILMT50g3F+EnRf6BKvVmkbRrDzFQ6g=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="],
|
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "x64" }, "sha512-7zVbZ0XgCDi3yImmd9Cj+e0WhSuQzWo1ziSQiFgPCR8cs/dfrpYLgBlX9PP0gFOcJKeRccJJv7rnBetl3CB7tg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20260808-9ecf7c0a", "", { "os": "win32", "cpu": "arm64" }, "sha512-K3SNYJDvB7XD5nxi8jt4jWtgmqPG0Qbob0Px9afqMH3g/R5hIoi2P3GDbqn8JNv82c4dPIGJjXXuNhHa0tZQbw=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20260808-9ecf7c0a", "", { "os": "win32", "cpu": "x64" }, "sha512-3JCpPS8+Gz3PWlQHiFwqYuJ0AM0vKlfbe4+O+0m2EYej/QjN5KjL1nBfVLq5XDj4dVPEIktQKN6e//YzSzdaqA=="],
|
||||||
|
|
||||||
"@opentui/keymap": ["@opentui/keymap@0.4.5", "", { "dependencies": { "@opentui/core": "0.4.5" }, "peerDependencies": { "@opentui/react": "0.4.5", "@opentui/solid": "0.4.5", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg=="],
|
"@opentui/keymap": ["@opentui/keymap@0.0.0-20260808-9ecf7c0a", "", { "dependencies": { "@opentui/core": "0.0.0-20260808-9ecf7c0a" }, "peerDependencies": { "@opentui/react": "0.0.0-20260808-9ecf7c0a", "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-aGxw6P0RJSuUU3y3QtAnKa1B6tQShxR0mjmnN3Cadr4LoduSuWn/l1kE+27KwH0ap9WTAeGVsN7u2oawB9eWKw=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.4.5", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.5", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g=="],
|
"@opentui/solid": ["@opentui/solid@0.0.0-20260808-9ecf7c0a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20260808-9ecf7c0a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-Q7ouv2KxZKO0/Sh0RbhNd7kEw6E7tlrU+yw36xxlnyXOqlK3TkQ+u1RMSZulYAn8mP20pnYLM9OxY4pOC/wK8Q=="],
|
||||||
|
|
||||||
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
||||||
|
|
||||||
@@ -3311,6 +3331,8 @@
|
|||||||
|
|
||||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
|
"abort-controller-x": ["abort-controller-x@0.5.0", "", {}, "sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ=="],
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"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": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
@@ -3515,7 +3537,7 @@
|
|||||||
|
|
||||||
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
|
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
|
||||||
|
|
||||||
"bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="],
|
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
|
||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||||
|
|
||||||
@@ -3551,6 +3573,10 @@
|
|||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
|
||||||
|
|
||||||
|
"cbor-extract": ["cbor-extract@2.2.2", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", "@cbor-extract/cbor-extract-linux-arm": "2.2.2", "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", "@cbor-extract/cbor-extract-linux-x64": "2.2.2", "@cbor-extract/cbor-extract-win32-x64": "2.2.2" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng=="],
|
||||||
|
|
||||||
|
"cbor-x": ["cbor-x@1.6.5", "", { "optionalDependencies": { "cbor-extract": "^2.2.2" } }, "sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig=="],
|
||||||
|
|
||||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||||
|
|
||||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
||||||
@@ -3595,8 +3621,6 @@
|
|||||||
|
|
||||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||||
|
|
||||||
"clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="],
|
|
||||||
|
|
||||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||||
@@ -3625,7 +3649,7 @@
|
|||||||
|
|
||||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||||
|
|
||||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
|
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
|
||||||
|
|
||||||
@@ -4013,7 +4037,7 @@
|
|||||||
|
|
||||||
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
|
"eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
|
||||||
|
|
||||||
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||||
|
|
||||||
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||||
|
|
||||||
@@ -4163,7 +4187,7 @@
|
|||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.11.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-SJ6f5qa7P8md6lPrserryER3zerLkrezlnqqYQ2AbvDPpHLbwtbyk0FYJ5kNRcmbI80i/VMcsMBP0YIRdc3ucQ=="],
|
"gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="],
|
||||||
|
|
||||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||||
|
|
||||||
@@ -4295,7 +4319,7 @@
|
|||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||||
|
|
||||||
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||||
|
|
||||||
@@ -4439,8 +4463,6 @@
|
|||||||
|
|
||||||
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||||
|
|
||||||
"is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="],
|
|
||||||
|
|
||||||
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
|
||||||
@@ -4583,6 +4605,8 @@
|
|||||||
|
|
||||||
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
|
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
|
||||||
|
|
||||||
|
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||||
|
|
||||||
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
|
||||||
|
|
||||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||||
@@ -4635,9 +4659,7 @@
|
|||||||
|
|
||||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||||
|
|
||||||
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
"marked": ["marked@18.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA=="],
|
||||||
|
|
||||||
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
|
|
||||||
|
|
||||||
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
||||||
|
|
||||||
@@ -4779,7 +4801,7 @@
|
|||||||
|
|
||||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
|
|
||||||
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||||
|
|
||||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||||
|
|
||||||
@@ -4809,6 +4831,8 @@
|
|||||||
|
|
||||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||||
|
|
||||||
|
"modal": ["modal@0.9.0", "", { "dependencies": { "cbor-x": "^1.6.0", "long": "^5.3.1", "nice-grpc": "^2.1.12", "protobufjs": "^7.5.0", "smol-toml": "^1.3.3", "uuid": "^11.1.0" } }, "sha512-kCXcdJkhbJorf/q/6T9Wdlg6in9JmRnCNQnV6rVBMyeqNV/iXI6BYk4IzY4cvZ6dbauNeDMjk/Q08cbxvoIaXg=="],
|
||||||
|
|
||||||
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
|
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
|
||||||
|
|
||||||
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
|
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
|
||||||
@@ -4847,6 +4871,10 @@
|
|||||||
|
|
||||||
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
|
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
|
||||||
|
|
||||||
|
"nice-grpc": ["nice-grpc@2.1.16", "", { "dependencies": { "@grpc/grpc-js": "^1.14.0", "abort-controller-x": "^0.5.0", "nice-grpc-common": "^2.0.3" } }, "sha512-Cl3Pn00212Hl8/U6bpgMxmhZj5lyv3nWoJov4cd3FjWarktrMHP4DNvSjCnDwkMWYx4W1tyscEia4JX6Y4GVCQ=="],
|
||||||
|
|
||||||
|
"nice-grpc-common": ["nice-grpc-common@2.0.3", "", { "dependencies": { "ts-error": "^1.0.6" } }, "sha512-MEhnD3JMah0mgyivpb9hpRDbOBuXBxI/TVO+OK1h6rC97WM42HsPMR+zzRNQ0C5BqYJTw1nyWiQRD0DucO+pjQ=="],
|
||||||
|
|
||||||
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
|
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
|
||||||
|
|
||||||
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||||
@@ -4899,7 +4927,7 @@
|
|||||||
|
|
||||||
"npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="],
|
"npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="],
|
||||||
|
|
||||||
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
|
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||||
|
|
||||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||||
|
|
||||||
@@ -4925,7 +4953,7 @@
|
|||||||
|
|
||||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
|
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||||
|
|
||||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||||
|
|
||||||
@@ -5431,6 +5459,8 @@
|
|||||||
|
|
||||||
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
||||||
|
|
||||||
|
"solid-sonner": ["solid-sonner@0.3.1", "", { "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-F/+zi9yKJTHh5hX1UGJfkDvyC+F34Vi3jgy44NJwOKCgic1QtAon0b1iT9OsDO77RTgR+PCil+3Y5B8T2Owy1Q=="],
|
||||||
|
|
||||||
"solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
|
"solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
|
||||||
|
|
||||||
"solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="],
|
"solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="],
|
||||||
@@ -5525,7 +5555,7 @@
|
|||||||
|
|
||||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||||
|
|
||||||
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||||
|
|
||||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||||
|
|
||||||
@@ -5557,8 +5587,6 @@
|
|||||||
|
|
||||||
"svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="],
|
"svgo": ["svgo@4.0.2", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng=="],
|
||||||
|
|
||||||
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
|
|
||||||
|
|
||||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||||
@@ -5645,6 +5673,8 @@
|
|||||||
|
|
||||||
"ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
|
"ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
|
||||||
|
|
||||||
|
"ts-error": ["ts-error@1.0.6", "", {}, "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="],
|
||||||
|
|
||||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||||
|
|
||||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||||
@@ -6333,8 +6363,6 @@
|
|||||||
|
|
||||||
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||||
|
|
||||||
"@opencode-ai/app/@opencode-ai/sdk": ["@opencode-ai/sdk@vendor/opencode-ai-sdk-1.18.8-dev.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-C2nfk4x0sPINwE5V6DPkFSuH3PkUmKPWHPzxpXC1j+3Ui5hslLCWJbkk8WcOG1Lyt3C0+yp4ea64v/kmtYCO4w=="],
|
|
||||||
|
|
||||||
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
|
"@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
|
||||||
@@ -6345,8 +6373,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="],
|
"@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="],
|
||||||
|
|
||||||
"@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||||
|
|
||||||
"@opencode-ai/session-ui/@opencode-ai/sdk": ["@opencode-ai/sdk@../app/vendor/opencode-ai-sdk-1.18.8-dev.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-C2nfk4x0sPINwE5V6DPkFSuH3PkUmKPWHPzxpXC1j+3Ui5hslLCWJbkk8WcOG1Lyt3C0+yp4ea64v/kmtYCO4w=="],
|
"@opencode-ai/session-ui/@opencode-ai/sdk": ["@opencode-ai/sdk@../app/vendor/opencode-ai-sdk-1.18.8-dev.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-C2nfk4x0sPINwE5V6DPkFSuH3PkUmKPWHPzxpXC1j+3Ui5hslLCWJbkk8WcOG1Lyt3C0+yp4ea64v/kmtYCO4w=="],
|
||||||
@@ -6499,8 +6525,6 @@
|
|||||||
|
|
||||||
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||||
|
|
||||||
"@vercel/cli-exec/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
|
||||||
|
|
||||||
"@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.1", "", { "dependencies": { "@vercel/cli-config": "0.2.1", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-ufdalm2MWOYksyj8KVpWjoOFPJO6zoYpuyvIggIQ2bB0CFCjTCiTkGXHqAKwG77GVRjOaN3/8S5ITlZpXWmqOw=="],
|
"@vercel/functions/@vercel/oidc": ["@vercel/oidc@3.8.1", "", { "dependencies": { "@vercel/cli-config": "0.2.1", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-ufdalm2MWOYksyj8KVpWjoOFPJO6zoYpuyvIggIQ2bB0CFCjTCiTkGXHqAKwG77GVRjOaN3/8S5ITlZpXWmqOw=="],
|
||||||
|
|
||||||
"@vercel/nft/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
"@vercel/nft/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||||
@@ -6587,8 +6611,6 @@
|
|||||||
|
|
||||||
"blume/katex": ["katex@0.17.0", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw=="],
|
"blume/katex": ["katex@0.17.0", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw=="],
|
||||||
|
|
||||||
"blume/marked": ["marked@18.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA=="],
|
|
||||||
|
|
||||||
"blume/node-html-parser": ["node-html-parser@9.0.0", "", { "dependencies": { "css-select": "^5.1.0", "entities": "^8.0.0" } }, "sha512-MhdaHPyxnyYu/sf0TpiRvDnTrkum0UKHC7FdbDGIUQNlx3I7xzwXoyV0eMUMv/XU+lkJT1glOUzpDPq7b2p1Ew=="],
|
"blume/node-html-parser": ["node-html-parser@9.0.0", "", { "dependencies": { "css-select": "^5.1.0", "entities": "^8.0.0" } }, "sha512-MhdaHPyxnyYu/sf0TpiRvDnTrkum0UKHC7FdbDGIUQNlx3I7xzwXoyV0eMUMv/XU+lkJT1glOUzpDPq7b2p1Ew=="],
|
||||||
|
|
||||||
"blume/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
"blume/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
@@ -6605,6 +6627,8 @@
|
|||||||
|
|
||||||
"builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
|
"builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
|
||||||
|
|
||||||
|
"cbor-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
|
||||||
|
|
||||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
@@ -6685,9 +6709,11 @@
|
|||||||
|
|
||||||
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
"execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
"execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||||
|
|
||||||
"execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
"execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
|
"execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
@@ -6721,6 +6747,8 @@
|
|||||||
|
|
||||||
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
|
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
@@ -6749,6 +6777,8 @@
|
|||||||
|
|
||||||
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
|
|
||||||
|
"modal/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
|
||||||
|
|
||||||
"motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="],
|
"motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="],
|
||||||
|
|
||||||
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
||||||
@@ -6759,8 +6789,6 @@
|
|||||||
|
|
||||||
"node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
|
"node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
|
||||||
|
|
||||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
|
||||||
|
|
||||||
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
||||||
|
|
||||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||||
@@ -6849,12 +6877,8 @@
|
|||||||
|
|
||||||
"terser/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
"terser/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||||
|
|
||||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
|
||||||
|
|
||||||
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||||
|
|
||||||
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
|
||||||
|
|
||||||
"tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.0", "", {}, "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q=="],
|
"tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.0", "", {}, "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q=="],
|
||||||
|
|
||||||
"tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
@@ -7531,20 +7555,6 @@
|
|||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
|
||||||
|
|
||||||
"@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
"@vercel/functions/@vercel/oidc/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||||
|
|
||||||
"@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
@@ -7677,6 +7687,8 @@
|
|||||||
|
|
||||||
"blume/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"blume/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"blume/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"blume/node-html-parser/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
"blume/node-html-parser/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
||||||
|
|
||||||
"blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
@@ -7749,6 +7761,8 @@
|
|||||||
|
|
||||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||||
|
|
||||||
|
"mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"motion/framer-motion/motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="],
|
"motion/framer-motion/motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="],
|
||||||
|
|
||||||
"motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
"motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||||
@@ -8601,8 +8615,6 @@
|
|||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||||
|
|
||||||
"@vercel/cli-exec/execa/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|||||||
@@ -5,8 +5,27 @@
|
|||||||
- Use the `dev` branch database schema and migration registry as the V1 baseline.
|
- Use the `dev` branch database schema and migration registry as the V1 baseline.
|
||||||
- Remove migrations that exist only on the V2 branch.
|
- Remove migrations that exist only on the V2 branch.
|
||||||
- Generate one canonical migration from the `dev` schema to the final V2 schema.
|
- Generate one canonical migration from the `dev` schema to the final V2 schema.
|
||||||
- Add explicit data operations to that migration where generated DDL is insufficient.
|
- Keep the canonical migration focused on schema changes and dropping obsolete tables.
|
||||||
- Test the migration against a populated database at the exact `dev` schema.
|
- Run the V1 history backfill through an experimental server endpoint invoked by the CLI before it opens the TUI.
|
||||||
|
- Show committed session progress while the endpoint runs.
|
||||||
|
|
||||||
|
Expose `GET /api/experimental/migration/v1` for status and a blocking `POST /api/experimental/migration/v1` to run or
|
||||||
|
resume the backfill. The status is `required`, `running`, or `completed`. On startup, the CLI checks status first and
|
||||||
|
renders no migration UI when it is already complete. For required or running status, it shows a spinner and waits for the
|
||||||
|
blocking POST without a request timeout. While migration runs, poll GET once per second and render completed and total
|
||||||
|
session counts. GET derives total from all session rows and completed from rows through the stored cursor; the count
|
||||||
|
advances only after a session transaction commits. The POST returns `{ status: "completed" }`. Do not add a background
|
||||||
|
job or streaming progress protocol. Interrupted calls resume from the stored cursor.
|
||||||
|
Initially, only interactive TUI startup performs this check; noninteractive run, ACP, raw API, service, health, version,
|
||||||
|
and help flows do not trigger the backfill.
|
||||||
|
|
||||||
|
Keep migration behavior in Core: status, semaphore, checkpointing, V1 decoding, transformation, and database writes.
|
||||||
|
Protocol owns the experimental GET/POST contracts, Server handlers delegate to Core, and the interactive CLI owns only
|
||||||
|
the status check and spinner presentation.
|
||||||
|
|
||||||
|
Guard the endpoint with one process-local Effect `Semaphore`. Concurrent callers wait; after the active call completes,
|
||||||
|
waiting callers acquire the permit, observe the completion key, and return immediately. No distributed lock is required
|
||||||
|
for the current single elected server process.
|
||||||
|
|
||||||
## Preserve
|
## Preserve
|
||||||
|
|
||||||
@@ -15,20 +34,35 @@ The canonical V1 data remains in its existing tables. In particular, preserve `s
|
|||||||
Preserve `workspace` rows and existing `session.workspace_id` values unchanged. The migration must not clear or rebuild
|
Preserve `workspace` rows and existing `session.workspace_id` values unchanged. The migration must not clear or rebuild
|
||||||
workspace relationships.
|
workspace relationships.
|
||||||
|
|
||||||
Keep the `todo` table and its data unchanged. V2 does not currently migrate todos into another representation, and the
|
Preserve existing non-null `session.agent` and `session.model` selections. Fill missing values from the latest ordinary
|
||||||
generated migration must not drop the table.
|
V1 user message ordered by `time_created` and `id`, excluding compaction and subtask-only messages. Copy agent, provider
|
||||||
|
ID, model ID, and variant, normalizing an absent variant to `default`.
|
||||||
|
|
||||||
## Truncate
|
Recompute session usage aggregates from all canonical V1 assistant messages, including compaction or other internal
|
||||||
|
assistants omitted from the V2 projection. Overwrite session cost and input, output, reasoning, cache-read, and
|
||||||
|
cache-write token totals with those sums.
|
||||||
|
|
||||||
Truncate these pre-launch V2 tables before applying schema changes:
|
Clear persisted `session.revert` state. A staged revert is transient operational state and may refer to omitted projection
|
||||||
|
rows or unavailable snapshots; it must not resume automatically after upgrading. Preserve the underlying messages,
|
||||||
|
parts, and file history.
|
||||||
|
|
||||||
- `event`
|
Clear `session.time_compacting`, leave the new `time_suspended` column as `NULL`, and preserve session creation, update,
|
||||||
- `event_sequence`
|
and archive timestamps. Preserve project `time_initialized`; it is unrelated durable state.
|
||||||
- `session_message`
|
|
||||||
|
|
||||||
These rows are not canonical V1 data. Truncating `event` before adding the required `event.created` column means the
|
Keep the legacy `todo` table and its data physically unchanged, but do not include it in the final V2 Drizzle schema.
|
||||||
column needs neither a backfill nor a default. After truncation, rebuild `session_message` from canonical V1 `message`
|
After generation, remove the generated `DROP TABLE todo` statement from the canonical migration so the table remains as
|
||||||
and `part` rows rather than retaining its pre-launch V2 contents.
|
unmanaged legacy storage.
|
||||||
|
|
||||||
|
## Per-Session Replacement
|
||||||
|
|
||||||
|
Do not truncate `event`, `event_sequence`, or `session_message` globally before the backfill. A whole-table delete can
|
||||||
|
hold SQLite's writer lock long enough to block the running TUI.
|
||||||
|
|
||||||
|
Replace each legacy session's V2 state inside that session's checkpointed migration transaction. Delete `event` rows for
|
||||||
|
the session aggregate, delete its `session_message` rows, rebuild its projection from canonical V1 `message` and `part`
|
||||||
|
rows, and overwrite its `event_sequence` watermark. If migration of that session fails, all replacements roll back and
|
||||||
|
the durable cursor remains at the previously committed session. Rows owned by sessions outside the legacy migration set
|
||||||
|
remain untouched.
|
||||||
|
|
||||||
## Message Backfill
|
## Message Backfill
|
||||||
|
|
||||||
@@ -36,9 +70,23 @@ Backfill canonical V1 history from `message` and `part` into `session_message`.
|
|||||||
the migration. Preserving the V1 tables alone keeps the data safe but does not make existing history visible through the
|
the migration. Preserving the V1 tables alone keeps the data safe but does not make existing history visible through the
|
||||||
V2 session APIs, which read `session_message`.
|
V2 session APIs, which read `session_message`.
|
||||||
|
|
||||||
|
Do not fail the whole migration when a V1 message or part payload cannot be decoded. Skip an undecodable message's V2
|
||||||
|
projection and log its session and message IDs. Skip an undecodable part while continuing to map its message, and perform
|
||||||
|
special-message pairing only with decoded rows. Assign sequences after filtering. Leave every malformed source row
|
||||||
|
untouched in the V1 tables.
|
||||||
|
|
||||||
|
Skip and log orphan parts whose source message does not exist and parts with unknown or unsupported types. Continue
|
||||||
|
migrating the owning message and other valid parts. Include session, message, part ID, and observed type in warnings, and
|
||||||
|
leave skipped source rows unchanged.
|
||||||
|
|
||||||
Reuse each V1 `message.id` as the corresponding `session_message.id`. Stable IDs keep the migration deterministic and
|
Reuse each V1 `message.id` as the corresponding `session_message.id`. Stable IDs keep the migration deterministic and
|
||||||
avoid rewriting other persisted state that may refer to a message.
|
avoid rewriting other persisted state that may refer to a message.
|
||||||
|
|
||||||
|
For ordinary user and assistant rows, preserve source `message.time_created` and `message.time_updated`. Entirely
|
||||||
|
synthetic messages preserve their source timestamps, and synthetic rows split from mixed messages use the source user
|
||||||
|
timestamps. A collapsed compaction uses the compaction user creation time and the later update time of the compaction
|
||||||
|
user and summary assistant. Keep payload creation/completion times consistent with row timestamps.
|
||||||
|
|
||||||
Within each session, order V1 messages by `time_created` and then `id`, matching the existing V1 message index. Assign
|
Within each session, order V1 messages by `time_created` and then `id`, matching the existing V1 message index. Assign
|
||||||
contiguous `session_message.seq` values starting at `0`.
|
contiguous `session_message.seq` values starting at `0`.
|
||||||
|
|
||||||
@@ -46,15 +94,122 @@ Map ordinary V1 messages one-to-one by role. Each ordinary V1 user message becom
|
|||||||
V1 assistant message becomes one V2 `assistant` row. Fold the source message's ordered V1 parts into that row's V2
|
V1 assistant message becomes one V2 `assistant` row. Fold the source message's ordered V1 parts into that row's V2
|
||||||
payload.
|
payload.
|
||||||
|
|
||||||
|
Keep ordinary messages even when their transformed payload becomes empty after filtering. Preserve an empty V2 user row
|
||||||
|
with `text: ""` and an empty V2 assistant row with `content: []` so IDs, chronology, and conversation structure remain
|
||||||
|
stable. Omit only explicitly dropped internal concepts and undecodable messages.
|
||||||
|
|
||||||
Handle semantic marker parts before applying the ordinary mapping. In particular, a V1 user message containing a
|
Handle semantic marker parts before applying the ordinary mapping. In particular, a V1 user message containing a
|
||||||
`compaction` part and its paired assistant summary represent one compaction operation, not two ordinary messages. Special
|
`compaction` part and its paired assistant summary represent one compaction operation, not two ordinary messages. Special
|
||||||
part mappings must be decided explicitly before implementing the backfill.
|
part mappings must be decided explicitly before implementing the backfill.
|
||||||
|
|
||||||
|
Do not carry the V1 subtask concept into the V2 projection. Omit user messages containing only `subtask` parts and omit
|
||||||
|
the paired assistant task-tool messages generated from those markers. For mixed user messages, ignore the `subtask`
|
||||||
|
parts while preserving ordinary content, and still omit assistant task-tool messages generated by the skipped subtasks.
|
||||||
|
Keep all source rows unchanged in the V1 `message` and `part` tables.
|
||||||
|
|
||||||
|
Map ordinary V1 assistant `text` and `reasoning` parts into the V2 assistant `content` array in part order. Preserve text,
|
||||||
|
including empty assistant text parts used as structural separators. Map V1 part metadata to optional V2 provider state.
|
||||||
|
For reasoning, map `time.start` to `time.created` and optional `time.end` to `time.completed`.
|
||||||
|
|
||||||
|
Preserve V1 tool parts that are `pending` or `running`, but convert them to terminal V2 tool error states. Preserve the
|
||||||
|
call ID, tool name, parsed input, metadata, and available start time. Use the assistant message creation time when the V1
|
||||||
|
state has no start time. Set the error to type `tool.interrupted` with message
|
||||||
|
`Tool execution was interrupted before V2 migration`. Never resume migrated tool executions.
|
||||||
|
|
||||||
|
For a completed V1 tool part, use `callID` as the V2 tool content ID and preserve the tool name and parsed input. Set the
|
||||||
|
state to `completed`. Convert V1 output into the first text content item and convert stored output attachments into
|
||||||
|
following file content items with their URI, MIME type, and filename. Preserve state metadata. Map `time.start` to
|
||||||
|
`time.created` and `time.end` to `time.completed`. When `time.compacted` exists, use
|
||||||
|
`[Old tool result content cleared]` as the only output and omit attachments.
|
||||||
|
|
||||||
|
For a failed V1 tool part, preserve the call ID, tool name, parsed input, metadata, and timestamps, and set the V2 state
|
||||||
|
to `error`. Convert the V1 error string to a structured error with type `tool.execution`. If V1 metadata contains a string
|
||||||
|
`output`, preserve it as optional V2 text content. Map `time.start` to `time.created` and `time.end` to `time.completed`.
|
||||||
|
|
||||||
|
For an ordinary V1 assistant message, preserve agent, provider ID, model ID, optional variant, creation and completion
|
||||||
|
times, cost, and input/output/reasoning/cache token counts. Use `default` when the V1 variant is absent. Ignore V1
|
||||||
|
`tokens.total` because it is derivable and V2 does not persist it.
|
||||||
|
|
||||||
|
Use V1 assistant `parentID` only while pairing compactions and skipped subtasks with their originating user messages. Do
|
||||||
|
not persist it in ordinary V2 assistant rows; V2 uses ordered history rather than user/assistant parent links.
|
||||||
|
|
||||||
|
Ignore the optional V1 assistant `structured` output value. V2 has no equivalent top-level assistant field, and visible
|
||||||
|
text and tool content are migrated separately. Retain the original structured value only in the V1 `message` row.
|
||||||
|
|
||||||
|
Ignore V1 assistant `mode` and historical `path` (`cwd` and `root`). Mode is redundant with the preserved assistant
|
||||||
|
agent, and historical filesystem paths do not belong to the V2 assistant message contract. Retain them only in the V1
|
||||||
|
`message` row.
|
||||||
|
|
||||||
|
For assistant finish reasons, preserve `stop`, `length`, `tool-calls`, `content-filter`, `error`, and `unknown`. Map every
|
||||||
|
other nonempty V1 finish value to `unknown`, and leave the field absent when V1 omitted it. Do not retain unrecognized raw
|
||||||
|
finish values in metadata.
|
||||||
|
|
||||||
|
Map V1 assistant errors into the current V2 `{ type, message }` storage shape. Normalize Auth, content-filter, context
|
||||||
|
overflow, structured-output, output-length, aborted, API, and unknown errors to the established V2 string conventions,
|
||||||
|
preserve the message, and discard V1-only retryability and raw provider details.
|
||||||
|
|
||||||
|
Ignore V1 `retry` parts. Do not populate the V2 assistant `retry` field during migration; historical retry state is not
|
||||||
|
useful enough to preserve. The original retry rows remain in the V1 `part` table.
|
||||||
|
|
||||||
|
Do not emit V2 assistant content for V1 `step-start` and `step-finish` parts. Use the first available
|
||||||
|
`step-start.snapshot` as `assistant.snapshot.start` and the last available `step-finish.snapshot` as
|
||||||
|
`assistant.snapshot.end`. Continue to source finish, cost, and tokens from the assistant message itself. Ignore step
|
||||||
|
markers without snapshots.
|
||||||
|
|
||||||
|
Do not emit assistant content for standalone V1 `snapshot` or `patch` parts. If no start snapshot came from `step-start`,
|
||||||
|
use the first standalone snapshot value, then the first patch hash as a final fallback. Only `step-finish.snapshot` may
|
||||||
|
populate the end snapshot. Merge patch file lists into `assistant.snapshot.files` in first-seen order with duplicates
|
||||||
|
removed.
|
||||||
|
|
||||||
|
V2 follow-up: replace the open `SessionError.Error` string shape with a properly typed persisted error union. This is not
|
||||||
|
a blocker for the V1 migration, which should target the current storage contract.
|
||||||
|
|
||||||
V1 synthetic content is represented by user text parts with `synthetic: true`, not by a separate message role. A V1 user
|
V1 synthetic content is represented by user text parts with `synthetic: true`, not by a separate message role. A V1 user
|
||||||
message whose visible text parts are all synthetic should become a V2 `synthetic` message. If a V1 user message mixes
|
message whose visible text parts are all synthetic should become a V2 `synthetic` message. If a V1 user message mixes
|
||||||
ordinary and synthetic content, preserve the ordinary content in the V2 `user` row and emit the synthetic content as an
|
ordinary and synthetic content, preserve the ordinary content in the V2 `user` row and emit the synthetic content as an
|
||||||
adjacent V2 `synthetic` row. Ignore text parts marked `ignored`, matching V1 model-history behavior.
|
adjacent V2 `synthetic` row. Ignore text parts marked `ignored`, matching V1 model-history behavior.
|
||||||
|
|
||||||
|
For an ordinary V2 user message, take visible V1 text parts that are neither ignored nor synthetic, preserve part order,
|
||||||
|
and join their text with `"\n\n"`. Use an empty string when the message contains attachments but no ordinary text.
|
||||||
|
|
||||||
|
Ignore the optional V1 user-message `system` override. Do not create a V2 system message or preserve the override in
|
||||||
|
metadata. The original value remains in the V1 `message` row.
|
||||||
|
|
||||||
|
Ignore the optional V1 user-message `tools` map. It represented request-time tool enablement for a historical step and
|
||||||
|
must not affect future V2 execution. The original value remains in the V1 `message` row.
|
||||||
|
|
||||||
|
Ignore the optional V1 user-message `format` field and its schema. It controlled structured-output behavior for a
|
||||||
|
historical request and must not affect future V2 runs. Preserve visible assistant text normally; retain the original
|
||||||
|
format only in the V1 `message` row.
|
||||||
|
|
||||||
|
Ignore V1 user-message `summary` metadata, including title, body, and diffs. V2 user messages have no equivalent field,
|
||||||
|
and session-level summary data is already persisted separately. Retain the original summary only in the V1 `message`
|
||||||
|
row.
|
||||||
|
|
||||||
|
Map V1 `agent` parts into the V2 user message's `agents` array in part order. Preserve `name`. When the V1 part has
|
||||||
|
`source`, map its `value`, `start`, and `end` into the V2 attachment's `mention.text`, `mention.start`, and `mention.end`.
|
||||||
|
Omit `agents` when there are no agent parts.
|
||||||
|
|
||||||
|
Do not read the filesystem or network while migrating V1 file attachments. Attachment migration must be deterministic
|
||||||
|
from database contents alone. Convert persisted `data:` URLs; represent non-embedded `file:`, HTTP, and other external
|
||||||
|
URLs with deterministic text rather than fetching them. Keep the original V1 `part` rows unchanged.
|
||||||
|
|
||||||
|
For a V1 file backed by a `data:` URL, decode the URL and normalize its payload to base64 for the V2 attachment's `data`.
|
||||||
|
Preserve `mime` and optional `filename` as `name`. Use a V2 `uri` source with the original URI for a V1 resource source;
|
||||||
|
otherwise use an `inline` source. When V1 source text metadata exists, map its `value`, `start`, and `end` into the V2
|
||||||
|
attachment mention. Leave `description` unset and preserve file-part order in the V2 `files` array.
|
||||||
|
|
||||||
|
For a non-embedded V1 file, do not create a V2 file attachment. Append
|
||||||
|
`[Attachment unavailable after migration: <name-or-url> (<mime>)]` to the V2 user text in original part order, separated
|
||||||
|
by blank lines. Prefer the V1 filename, then resource URI, then part URL for the label. The original URL remains only in
|
||||||
|
the preserved V1 `part` row.
|
||||||
|
|
||||||
|
For a synthetic row split from a mixed user message, derive a generated-looking ID from the source message ID. Preserve
|
||||||
|
the source ID's 12-character timestamp component and replace its 14-character random component with a deterministic
|
||||||
|
base-62 encoding of a hash of `v1-synthetic:` plus the source message ID. If that candidate collides with an existing or
|
||||||
|
derived message ID, deterministically retry with an incrementing salt. Place the synthetic row immediately after its
|
||||||
|
source user row. Entirely synthetic messages continue to reuse their original message ID.
|
||||||
|
|
||||||
Use the V1 compaction user message ID as the ID of the collapsed V2 compaction message. This matches V2's use of the
|
Use the V1 compaction user message ID as the ID of the collapsed V2 compaction message. This matches V2's use of the
|
||||||
admitted compaction input ID and preserves references to the initiating message.
|
admitted compaction input ID and preserves references to the initiating message.
|
||||||
|
|
||||||
@@ -64,9 +219,13 @@ serialize the retained V1 tail beginning at `tail_start_id` for `recent`. Use an
|
|||||||
retained, and use the compaction user message creation time. Do not emit the paired summary assistant as a separate V2
|
retained, and use the compaction user message creation time. Do not emit the paired summary assistant as a separate V2
|
||||||
assistant row.
|
assistant row.
|
||||||
|
|
||||||
After rebuilding `session_message`, seed `event_sequence` with one row per migrated session. Set its watermark to that
|
Do not project incomplete or failed V1 compactions into `session_message`. Omit both the internal compaction user marker
|
||||||
session's maximum backfilled `session_message.seq`. This prevents new V2 events from reusing sequence numbers or sorting
|
and its paired summary assistant when no successful summary was completed. Assign final sequence numbers after filtering
|
||||||
before migrated history. The `event` table remains empty.
|
so omitted compactions leave no gaps. Their source rows remain preserved in the V1 `message` and `part` tables.
|
||||||
|
|
||||||
|
After rebuilding a session's `session_message`, replace its `event_sequence` watermark with that session's maximum
|
||||||
|
backfilled `session_message.seq`. This prevents new V2 events from reusing sequence numbers or sorting before migrated
|
||||||
|
history. The migrated session's prior `event` rows are removed in the same transaction.
|
||||||
|
|
||||||
## Drop
|
## Drop
|
||||||
|
|
||||||
@@ -74,6 +233,7 @@ Drop these pre-launch V2 tables without preserving or transforming their rows:
|
|||||||
|
|
||||||
- `session_input`
|
- `session_input`
|
||||||
- `session_context_epoch`
|
- `session_context_epoch`
|
||||||
|
- `data_migration`
|
||||||
|
|
||||||
Do not transfer `session_input` rows into `session_pending`.
|
Do not transfer `session_input` rows into `session_pending`.
|
||||||
|
|
||||||
@@ -103,16 +263,36 @@ schema.
|
|||||||
New nullable session columns, including `fork_session_id`, `fork_boundary`, and `time_suspended`, require no explicit
|
New nullable session columns, including `fork_session_id`, `fork_boundary`, and `time_suspended`, require no explicit
|
||||||
backfill. Existing rows naturally receive `NULL` when the generated migration adds the columns.
|
backfill. Existing rows naturally receive `NULL` when the generated migration adds the columns.
|
||||||
|
|
||||||
## Verification
|
## Execution
|
||||||
|
|
||||||
The canonical migration test should seed representative V1 sessions, messages, parts, todos, projects, accounts,
|
Before transforming V1 rows, look for `opencode-next.db` in the data directory. This file was used by pre-launch V2
|
||||||
credentials, permissions, shares, and workspaces. After migration, it should verify:
|
builds. Open it read-only with Bun SQLite and copy its `project`, `session`, and `session_message` rows directly into the
|
||||||
|
current `project`, `session_v2`, and `session_message` tables. Existing current projects and Sessions win ID collisions.
|
||||||
|
Do not copy its durable events or runtime caches; initialize each imported Session's `event_sequence` watermark from its
|
||||||
|
maximum message sequence. Commit each imported Session independently and leave the source database untouched.
|
||||||
|
|
||||||
- Preserved rows and encoded values remain unchanged.
|
The previous V2 import is part of this migration and uses the same completion marker. It needs no source-specific cursor:
|
||||||
- Todo rows remain available in the unchanged `todo` table.
|
the destination Session row is the per-Session idempotency boundary, so a retry skips transactions that already committed.
|
||||||
- `event` is empty, and stale pre-launch rows are absent from the rebuilt projections.
|
|
||||||
- Backfilled `session_message` rows represent the canonical V1 `message` and `part` history.
|
Store V1 backfill state in `kv`; do not retain a dedicated `data_migration` table. Store the last successfully migrated
|
||||||
- Each migrated session's `event_sequence` watermark matches its maximum backfilled message sequence.
|
session ID under `migration.v1-v2.session.cursor` and write `migration.v1-v2.completed` with value `true` after every
|
||||||
- Dropped tables no longer exist.
|
session finishes. Delete the cursor key on completion and return immediately on later calls when the completion key
|
||||||
- New tables exist and are empty.
|
exists.
|
||||||
- The final schema has no ungenerated changes.
|
|
||||||
|
Absence of the completion key means migration is required, including on a fresh database. Running the endpoint against a
|
||||||
|
database with no sessions completes immediately and writes the completion key; fresh database initialization does not
|
||||||
|
seed migration state specially.
|
||||||
|
|
||||||
|
Process sessions in stable ID order. Rebuild one session in one transaction, including its `session_message` rows,
|
||||||
|
session-level backfills, `event_sequence` watermark, and cursor update. If interrupted during a session, that transaction
|
||||||
|
rolls back and the next endpoint call retries the same session. If it committed, the next call continues after the stored
|
||||||
|
cursor. Mark the migration complete after the final session and return immediately on later calls.
|
||||||
|
|
||||||
|
Ensure the global project exists using the current platform's filesystem root as its worktree. Process every `session`
|
||||||
|
row, including archived, root, child, and empty sessions, as well as sessions whose messages are all skipped or internal.
|
||||||
|
Reassign beta and V1 Sessions whose referenced project row is missing to the global project and log a warning. Each
|
||||||
|
successfully committed session advances the cursor.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Detailed migration test design is deferred until after the canonical migration is implemented.
|
||||||
|
|||||||
+6
-2
@@ -288,8 +288,12 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
server: {
|
server: {
|
||||||
placement: { region: "aws:us-east-2" },
|
placement: { region: "aws:us-east-2" },
|
||||||
transform: {
|
transform: {
|
||||||
worker: {
|
worker: (args) => {
|
||||||
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [
|
||||||
|
...(flags ?? []),
|
||||||
|
"global_fetch_strictly_public",
|
||||||
|
])
|
||||||
|
args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-RFek0QoEEjsgbqmTE/SxQAmPtYyzs0IPR2ugFn5Okrs=",
|
"x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
|
||||||
"aarch64-linux": "sha256-BmAxapY1YrAFn7mVq3/6A9+6Au5UIvSqBboHMkyJH3I=",
|
"aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
|
||||||
"aarch64-darwin": "sha256-Sx3bGWQqLlgoa/RudJxanjSzhFRNklckT2ffnO2I5F4=",
|
"aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
|
||||||
"x86_64-darwin": "sha256-CMOhiisHNowg06qadvgg4K+60zrynglwiT0qKYQ4NiA="
|
"x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-7
@@ -46,9 +46,9 @@
|
|||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/standard-validator": "0.2.0",
|
"@hono/standard-validator": "0.2.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.4.5",
|
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/keymap": "0.4.5",
|
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/solid": "0.4.5",
|
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@tanstack/solid-virtual": "3.13.32",
|
"@tanstack/solid-virtual": "3.13.32",
|
||||||
"@shikijs/stream": "4.2.0",
|
"@shikijs/stream": "4.2.0",
|
||||||
"@standard-schema/spec": "1.1.0",
|
"@standard-schema/spec": "1.1.0",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"get-east-asian-width": "1.6.0",
|
"get-east-asian-width": "1.6.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "17.0.6",
|
"marked": "18.0.7",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
"remend": "1.3.0",
|
"remend": "1.3.0",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
@@ -100,6 +100,7 @@
|
|||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
"@sentry/vite-plugin": "4.6.0",
|
"@sentry/vite-plugin": "4.6.0",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
|
"solid-sonner": "0.3.1",
|
||||||
"vite-plugin-solid": "2.11.10",
|
"vite-plugin-solid": "2.11.10",
|
||||||
"@lydell/node-pty": "1.2.0-beta.12"
|
"@lydell/node-pty": "1.2.0-beta.12"
|
||||||
}
|
}
|
||||||
@@ -149,14 +150,16 @@
|
|||||||
"electron"
|
"electron"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"effect": "catalog:"
|
"effect": "catalog:"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
|
||||||
|
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
|
||||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||||
|
|||||||
@@ -368,11 +368,12 @@ Other provider exports listed above remain direct facades until they explicitly
|
|||||||
|
|
||||||
## Provider options & HTTP overlays
|
## Provider options & HTTP overlays
|
||||||
|
|
||||||
Three escape hatches in order of stability:
|
Request options in order of stability:
|
||||||
|
|
||||||
1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop).
|
1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop).
|
||||||
2. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `promptCacheKey`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
|
2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it.
|
||||||
3. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist.
|
3. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
|
||||||
|
4. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist.
|
||||||
|
|
||||||
Route/provider defaults are overridden by request-level values for each axis.
|
Route/provider defaults are overridden by request-level values for each axis.
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ const model = OpenAI.configure({
|
|||||||
//
|
//
|
||||||
// - `generation`: common controls such as max tokens, temperature, topP/topK,
|
// - `generation`: common controls such as max tokens, temperature, topP/topK,
|
||||||
// penalties, seed, and stop sequences.
|
// penalties, seed, and stop sequences.
|
||||||
|
// - `promptCacheKey`: stable cache affinity for protocols that support it.
|
||||||
// - `providerOptions`: namespaced provider-native behavior. For example,
|
// - `providerOptions`: namespaced provider-native behavior. For example,
|
||||||
// OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking
|
// OpenAI store behavior, Anthropic thinking, Gemini thinking config, or
|
||||||
// config, or OpenRouter routing/reasoning.
|
// OpenRouter routing/reasoning.
|
||||||
// - `http`: last-resort serializable overlays for final request body, headers,
|
// - `http`: last-resort serializable overlays for final request body, headers,
|
||||||
// and query params. Prefer typed `providerOptions` when a field is stable.
|
// and query params. Prefer typed `providerOptions` when a field is stable.
|
||||||
//
|
//
|
||||||
@@ -45,9 +46,7 @@ const request = LLM.request({
|
|||||||
system: "You are concise and practical.",
|
system: "You are concise and practical.",
|
||||||
prompt: "Tell me a joke",
|
prompt: "Tell me a joke",
|
||||||
generation: { maxTokens: 80, temperature: 0.7 },
|
generation: { maxTokens: 80, temperature: 0.7 },
|
||||||
providerOptions: {
|
promptCacheKey: "tutorial-joke",
|
||||||
openai: { promptCacheKey: "tutorial-joke" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 3. `generate` sends the request and collects the event stream into one
|
// 3. `generate` sends the request and collects the event stream into one
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises"
|
|||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
|
|
||||||
const RECORDINGS_DIR = path.resolve(import.meta.dir, "..", "test", "fixtures", "recordings")
|
const RECORDINGS_DIR = path.resolve(import.meta.dir, "..", "test", "fixtures", "recordings")
|
||||||
const MODELS_DEV_URL = "https://models.dev/api.json"
|
const MODELS_DEV_URL = "https://models.opencode.ai/api.json"
|
||||||
|
|
||||||
type JsonRecord = Record<string, unknown>
|
type JsonRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ const countHints = (request: LLMRequest) =>
|
|||||||
|
|
||||||
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
|
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
|
||||||
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
|
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
|
||||||
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")) return request
|
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
|
||||||
|
return request
|
||||||
const policy = resolve(request.cache)
|
const policy = resolve(request.cache)
|
||||||
if (!policy.tools && !policy.system && !policy.messages) return request
|
if (!policy.tools && !policy.system && !policy.messages) return request
|
||||||
|
|
||||||
|
|||||||
@@ -422,14 +422,12 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: Me
|
|||||||
|
|
||||||
// Tool results may carry structured text, images, and documents. Keep media as provider-native
|
// Tool results may carry structured text, images, and documents. Keep media as provider-native
|
||||||
// content instead of JSON-stringifying base64 into a prompt string.
|
// content instead of JSON-stringifying base64 into a prompt string.
|
||||||
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
|
const lowerToolResultContentItem = Effect.fnUntraced(function* (item: Tool.Content) {
|
||||||
item: Tool.Content,
|
|
||||||
) {
|
|
||||||
if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
|
if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
|
||||||
return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name })
|
return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name })
|
||||||
})
|
})
|
||||||
|
|
||||||
const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) {
|
const lowerToolResultContent = Effect.fnUntraced(function* (part: ToolResultPart) {
|
||||||
// Text / json / error results stay as a string for backward compatibility
|
// Text / json / error results stay as a string for backward compatibility
|
||||||
// with existing cassettes and provider expectations.
|
// with existing cassettes and provider expectations.
|
||||||
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
||||||
|
|||||||
@@ -25,8 +25,20 @@ import { ToolSchemaProjection } from "./utils/tool-schema"
|
|||||||
|
|
||||||
const ADAPTER = "gemini"
|
const ADAPTER = "gemini"
|
||||||
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
|
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
|
||||||
|
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
|
||||||
|
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator"
|
||||||
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
||||||
|
|
||||||
|
// Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by
|
||||||
|
// retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call
|
||||||
|
// from an older or external session. Model IDs are open-ended, so unknown Gemini aliases inherit current behavior.
|
||||||
|
const requiresThoughtSignatureFallback = (modelID: string) => {
|
||||||
|
if (!/(^|\/)gemini-/i.test(modelID)) return false
|
||||||
|
if (/(^|\/)gemini-(?:1|2)(?:[.-]|$)/i.test(modelID)) return false
|
||||||
|
if (/(^|\/)gemini-pro(?:-vision)?$/i.test(modelID)) return false
|
||||||
|
return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID)
|
||||||
|
}
|
||||||
|
|
||||||
export interface OptionsInput {
|
export interface OptionsInput {
|
||||||
readonly [key: string]: unknown
|
readonly [key: string]: unknown
|
||||||
readonly cachedContent?: string
|
readonly cachedContent?: string
|
||||||
@@ -145,6 +157,9 @@ const GeminiGenerationConfig = Schema.Struct({
|
|||||||
temperature: Schema.optional(Schema.Number),
|
temperature: Schema.optional(Schema.Number),
|
||||||
topP: Schema.optional(Schema.Number),
|
topP: Schema.optional(Schema.Number),
|
||||||
topK: Schema.optional(Schema.Number),
|
topK: Schema.optional(Schema.Number),
|
||||||
|
frequencyPenalty: Schema.optional(Schema.Number),
|
||||||
|
presencePenalty: Schema.optional(Schema.Number),
|
||||||
|
seed: Schema.optional(Schema.Number),
|
||||||
stopSequences: optionalArray(Schema.String),
|
stopSequences: optionalArray(Schema.String),
|
||||||
thinkingConfig: Schema.optional(GeminiThinkingConfig),
|
thinkingConfig: Schema.optional(GeminiThinkingConfig),
|
||||||
})
|
})
|
||||||
@@ -202,11 +217,13 @@ interface ParserState {
|
|||||||
// keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
|
// keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
|
||||||
//
|
//
|
||||||
// 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
|
// 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
|
||||||
// drop empty objects, derive `nullable: true` from `type: [..., "null"]`,
|
// drop empty root parameter schemas while preserving nested empty objects,
|
||||||
// coerce `const` to `[const]` enum, recurse properties/items, propagate
|
// expand type arrays into `anyOf`, derive `nullable: true` from null members,
|
||||||
|
// coerce `const` to `[const]` enum, recurse properties/items, and propagate
|
||||||
// only an allowlisted set of keys (description, required, format, type,
|
// only an allowlisted set of keys (description, required, format, type,
|
||||||
// properties, items, allOf, anyOf, oneOf, minLength). Anything outside the
|
// nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
|
||||||
// allowlist (e.g. `additionalProperties`, `$ref`) is silently dropped.
|
// Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
|
||||||
|
// silently dropped.
|
||||||
//
|
//
|
||||||
// Sanitize runs first, then project. The implementation lives in
|
// Sanitize runs first, then project. The implementation lives in
|
||||||
// `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
|
// `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
|
||||||
@@ -282,6 +299,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
|
|||||||
|
|
||||||
if (message.role === "assistant") {
|
if (message.role === "assistant") {
|
||||||
const parts: Array<Schema.Schema.Type<typeof GeminiContentPart>> = []
|
const parts: Array<Schema.Schema.Type<typeof GeminiContentPart>> = []
|
||||||
|
// Parallel Gemini 3 calls may carry one signature on the first call; unsigned sibling calls are valid.
|
||||||
|
let hasSignedToolCall = false
|
||||||
for (const part of message.content) {
|
for (const part of message.content) {
|
||||||
if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
|
if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
|
||||||
return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"])
|
return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"])
|
||||||
@@ -294,7 +313,17 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (part.type === "tool-call") {
|
if (part.type === "tool-call") {
|
||||||
parts.push(lowerToolCall(part))
|
const lowered = lowerToolCall(part)
|
||||||
|
const signature = lowered.thoughtSignature
|
||||||
|
parts.push({
|
||||||
|
...lowered,
|
||||||
|
thoughtSignature:
|
||||||
|
signature ??
|
||||||
|
(requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall
|
||||||
|
? SKIP_THOUGHT_SIGNATURE_VALIDATOR
|
||||||
|
: undefined),
|
||||||
|
})
|
||||||
|
if (signature !== undefined) hasSignedToolCall = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,6 +417,9 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
|
|||||||
temperature: generation?.temperature,
|
temperature: generation?.temperature,
|
||||||
topP: generation?.topP,
|
topP: generation?.topP,
|
||||||
topK: generation?.topK,
|
topK: generation?.topK,
|
||||||
|
frequencyPenalty: generation?.frequencyPenalty,
|
||||||
|
presencePenalty: generation?.presencePenalty,
|
||||||
|
seed: generation?.seed,
|
||||||
stopSequences: generation?.stop,
|
stopSequences: generation?.stop,
|
||||||
thinkingConfig: options.thinkingConfig,
|
thinkingConfig: options.thinkingConfig,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
|
|||||||
return { type: "input_image" as const, image_url: media.dataUrl }
|
return { type: "input_image" as const, image_url: media.dataUrl }
|
||||||
})
|
})
|
||||||
|
|
||||||
const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* (
|
const lowerUserContent = Effect.fnUntraced(function* (
|
||||||
part: LLMRequest["messages"][number]["content"][number],
|
part: LLMRequest["messages"][number]["content"][number],
|
||||||
request: LLMRequest,
|
request: LLMRequest,
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
@@ -370,7 +370,7 @@ const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* (
|
|||||||
|
|
||||||
// Tool results may carry structured text, images, and files. Keep media as provider-native
|
// Tool results may carry structured text, images, and files. Keep media as provider-native
|
||||||
// content instead of JSON-stringifying base64 into a prompt string.
|
// content instead of JSON-stringifying base64 into a prompt string.
|
||||||
const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* (
|
const lowerToolResultContentItem = Effect.fnUntraced(function* (
|
||||||
item: Content,
|
item: Content,
|
||||||
request: LLMRequest,
|
request: LLMRequest,
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
@@ -383,7 +383,7 @@ const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultConte
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(function* (
|
const lowerToolResultOutput = Effect.fnUntraced(function* (
|
||||||
part: ToolResultPart,
|
part: ToolResultPart,
|
||||||
request: LLMRequest,
|
request: LLMRequest,
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
@@ -539,7 +539,7 @@ const lowerOptions = (request: LLMRequest) => {
|
|||||||
return {
|
return {
|
||||||
...(options.instructions ? { instructions: options.instructions } : {}),
|
...(options.instructions ? { instructions: options.instructions } : {}),
|
||||||
...(options.store !== undefined ? { store: options.store } : {}),
|
...(options.store !== undefined ? { store: options.store } : {}),
|
||||||
...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
|
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
||||||
...(options.include ? { include: options.include } : {}),
|
...(options.include ? { include: options.include } : {}),
|
||||||
...(options.reasoningEffort || options.reasoningSummary
|
...(options.reasoningEffort || options.reasoningSummary
|
||||||
? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
|
? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ const OpenAIChatAssistantToolCall = Schema.Struct({
|
|||||||
})
|
})
|
||||||
type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall>
|
type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall>
|
||||||
|
|
||||||
|
// Intentionally omit Gemini's provider-specific `extra_content.google.thought_signature`
|
||||||
|
// extension until direct Google OpenAI-compatible routing is supported here:
|
||||||
|
// https://github.com/vercel/ai/issues/11590
|
||||||
|
// https://github.com/vercel/ai/pull/11745
|
||||||
|
// https://ai.google.dev/gemini-api/docs/thought-signatures#openai
|
||||||
|
|
||||||
const OpenAIChatUserContent = Schema.Union([
|
const OpenAIChatUserContent = Schema.Union([
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
type: Schema.Literal("text"),
|
type: Schema.Literal("text"),
|
||||||
@@ -126,6 +132,7 @@ export const bodyFields = {
|
|||||||
stream: Schema.Literal(true),
|
stream: Schema.Literal(true),
|
||||||
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
|
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
|
||||||
store: Schema.optional(Schema.Boolean),
|
store: Schema.optional(Schema.Boolean),
|
||||||
|
prompt_cache_key: Schema.optional(Schema.String),
|
||||||
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
||||||
max_completion_tokens: Schema.optional(Schema.Number),
|
max_completion_tokens: Schema.optional(Schema.Number),
|
||||||
max_tokens: Schema.optional(Schema.Number),
|
max_tokens: Schema.optional(Schema.Number),
|
||||||
@@ -145,22 +152,33 @@ export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody>
|
|||||||
// The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
|
// The event schema is one decoded SSE `data:` payload. `Framing.sse` splits the
|
||||||
// byte stream into strings, then `Protocol.jsonEvent` decodes each string into
|
// byte stream into strings, then `Protocol.jsonEvent` decodes each string into
|
||||||
// this provider-native event shape.
|
// this provider-native event shape.
|
||||||
const OpenAIChatUsage = Schema.Struct({
|
const OpenAIChatUsage = Schema.StructWithRest(
|
||||||
prompt_tokens: Schema.optional(Schema.Number),
|
Schema.Struct({
|
||||||
completion_tokens: Schema.optional(Schema.Number),
|
prompt_tokens: optionalNull(Schema.Number),
|
||||||
total_tokens: Schema.optional(Schema.Number),
|
completion_tokens: optionalNull(Schema.Number),
|
||||||
prompt_tokens_details: optionalNull(
|
total_tokens: optionalNull(Schema.Number),
|
||||||
Schema.Struct({
|
prompt_tokens_details: optionalNull(
|
||||||
cached_tokens: Schema.optional(Schema.Number),
|
Schema.StructWithRest(
|
||||||
cache_write_tokens: Schema.optional(Schema.Number),
|
Schema.Struct({
|
||||||
}),
|
cached_tokens: optionalNull(Schema.Number),
|
||||||
),
|
cache_write_tokens: optionalNull(Schema.Number),
|
||||||
completion_tokens_details: optionalNull(
|
}),
|
||||||
Schema.Struct({
|
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||||
reasoning_tokens: Schema.optional(Schema.Number),
|
),
|
||||||
}),
|
),
|
||||||
),
|
completion_tokens_details: optionalNull(
|
||||||
})
|
Schema.StructWithRest(
|
||||||
|
Schema.Struct({
|
||||||
|
reasoning_tokens: optionalNull(Schema.Number),
|
||||||
|
accepted_prediction_tokens: optionalNull(Schema.Number),
|
||||||
|
rejected_prediction_tokens: optionalNull(Schema.Number),
|
||||||
|
}),
|
||||||
|
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||||
|
)
|
||||||
|
|
||||||
const OpenAIChatToolCallDeltaFunction = Schema.Struct({
|
const OpenAIChatToolCallDeltaFunction = Schema.Struct({
|
||||||
name: optionalNull(Schema.String),
|
name: optionalNull(Schema.String),
|
||||||
@@ -168,7 +186,7 @@ const OpenAIChatToolCallDeltaFunction = Schema.Struct({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const OpenAIChatToolCallDelta = Schema.Struct({
|
const OpenAIChatToolCallDelta = Schema.Struct({
|
||||||
index: Schema.Number,
|
index: optionalNull(Schema.Number),
|
||||||
id: optionalNull(Schema.String),
|
id: optionalNull(Schema.String),
|
||||||
function: optionalNull(OpenAIChatToolCallDeltaFunction),
|
function: optionalNull(OpenAIChatToolCallDeltaFunction),
|
||||||
})
|
})
|
||||||
@@ -222,6 +240,8 @@ export interface ParserState {
|
|||||||
readonly reasoningDetails: Array<unknown>
|
readonly reasoningDetails: Array<unknown>
|
||||||
readonly reasoningDetailsObserved: boolean
|
readonly reasoningDetailsObserved: boolean
|
||||||
readonly reasoningEmitted: boolean
|
readonly reasoningEmitted: boolean
|
||||||
|
readonly latestToolIndex?: number
|
||||||
|
readonly nextToolIndex: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -351,12 +371,13 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
|||||||
return text
|
return text
|
||||||
})()
|
})()
|
||||||
const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined)
|
const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined)
|
||||||
|
const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined)
|
||||||
const result = {
|
const result = {
|
||||||
role: "assistant" as const,
|
role: "assistant" as const,
|
||||||
content: content.length === 0 ? null : ProviderShared.joinText(content),
|
content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
|
||||||
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
|
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
||||||
reasoning_details: details,
|
...(details !== undefined ? { reasoning_details: details } : {}),
|
||||||
cache_control: options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined),
|
...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
|
||||||
}
|
}
|
||||||
if (field === undefined || reasoningText === undefined) return result
|
if (field === undefined || reasoningText === undefined) return result
|
||||||
return { ...result, [field]: reasoningText }
|
return { ...result, [field]: reasoningText }
|
||||||
@@ -490,6 +511,7 @@ const lowerOptions = (request: LLMRequest) => {
|
|||||||
const options = OpenAIOptions.resolve(request)
|
const options = OpenAIOptions.resolve(request)
|
||||||
return {
|
return {
|
||||||
...(options.store !== undefined ? { store: options.store } : {}),
|
...(options.store !== undefined ? { store: options.store } : {}),
|
||||||
|
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
||||||
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
|
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,22 +581,34 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
|
|||||||
// satisfied on both sides.
|
// satisfied on both sides.
|
||||||
const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
|
const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
|
||||||
if (!usage) return undefined
|
if (!usage) return undefined
|
||||||
const cached = usage.prompt_tokens_details?.cached_tokens
|
const input = usage.prompt_tokens ?? undefined
|
||||||
const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens
|
const output = usage.completion_tokens ?? undefined
|
||||||
const reasoning = usage.completion_tokens_details?.reasoning_tokens
|
const cached = usage.prompt_tokens_details?.cached_tokens ?? undefined
|
||||||
const nonCached = ProviderShared.subtractTokens(usage.prompt_tokens, ProviderShared.sumTokens(cached, cacheWrite))
|
const cacheWrite = usage.prompt_tokens_details?.cache_write_tokens ?? undefined
|
||||||
|
const reasoning = usage.completion_tokens_details?.reasoning_tokens ?? undefined
|
||||||
|
const nonCached = ProviderShared.subtractTokens(input, ProviderShared.sumTokens(cached, cacheWrite))
|
||||||
return new Usage({
|
return new Usage({
|
||||||
inputTokens: usage.prompt_tokens,
|
inputTokens: input,
|
||||||
outputTokens: usage.completion_tokens,
|
outputTokens: output,
|
||||||
nonCachedInputTokens: nonCached,
|
nonCachedInputTokens: nonCached,
|
||||||
cacheReadInputTokens: cached,
|
cacheReadInputTokens: cached,
|
||||||
cacheWriteInputTokens: cacheWrite,
|
cacheWriteInputTokens: cacheWrite,
|
||||||
reasoningTokens: reasoning,
|
reasoningTokens: reasoning,
|
||||||
totalTokens: ProviderShared.totalTokens(usage.prompt_tokens, usage.completion_tokens, usage.total_tokens),
|
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
|
||||||
providerMetadata: { openai: usage },
|
providerMetadata: { openai: usage },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolIndexByID = (
|
||||||
|
tools: ParserState["tools"],
|
||||||
|
pendingTools: ParserState["pendingTools"],
|
||||||
|
id: string | undefined,
|
||||||
|
) => {
|
||||||
|
if (!id) return undefined
|
||||||
|
const entry = Object.entries({ ...pendingTools, ...tools }).find(([, tool]) => tool?.id === id)
|
||||||
|
return entry ? Number(entry[0]) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
const reasoningDelta = (
|
const reasoningDelta = (
|
||||||
delta: Schema.Schema.Type<typeof OpenAIChatDelta> | null | undefined,
|
delta: Schema.Schema.Type<typeof OpenAIChatDelta> | null | undefined,
|
||||||
configuredField?: string,
|
configuredField?: string,
|
||||||
@@ -657,25 +691,39 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
|||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
const usage = mapUsage(event.usage) ?? state.usage
|
const usage = mapUsage(event.usage) ?? state.usage
|
||||||
const choice = event.choices?.[0]
|
const choice = event.choices?.[0]
|
||||||
const finishReason = choice?.finish_reason
|
const rawFinishReason = choice?.finish_reason
|
||||||
? { normalized: mapFinishReason(choice.finish_reason), raw: choice.native_finish_reason ?? choice.finish_reason }
|
const finishReason =
|
||||||
: state.finishReason
|
rawFinishReason !== undefined && rawFinishReason !== null
|
||||||
|
? { normalized: mapFinishReason(rawFinishReason), raw: choice?.native_finish_reason ?? rawFinishReason }
|
||||||
|
: state.finishReason
|
||||||
const delta = choice?.delta
|
const delta = choice?.delta
|
||||||
const toolDeltas = delta?.tool_calls ?? []
|
const toolDeltas = delta?.tool_calls ?? []
|
||||||
let tools = state.tools
|
let tools = state.tools
|
||||||
let pendingTools = state.pendingTools
|
let pendingTools = state.pendingTools
|
||||||
|
let latestToolIndex = state.latestToolIndex
|
||||||
|
let nextToolIndex = state.nextToolIndex
|
||||||
|
|
||||||
let lifecycle = state.lifecycle
|
let lifecycle = state.lifecycle
|
||||||
|
|
||||||
const reasoning = reasoningDelta(delta, state.reasoningField)
|
const reasoning = reasoningDelta(delta, state.reasoningField)
|
||||||
const reasoningField = state.reasoningField ?? (!state.lifecycle.text.has("text-0") ? reasoning?.field : undefined)
|
const hasLateContent =
|
||||||
|
Boolean(delta?.content) ||
|
||||||
|
reasoning !== undefined ||
|
||||||
|
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
|
||||||
|
toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments))
|
||||||
|
if (state.finishReason !== undefined) {
|
||||||
|
if (hasLateContent)
|
||||||
|
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason")
|
||||||
|
return [{ ...state, usage }, events] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasoningField = state.reasoningField ?? reasoning?.field
|
||||||
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
|
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
|
||||||
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
|
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
|
||||||
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
|
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
|
||||||
const deltaMetadata = reasoningMetadata(reasoningField)
|
const deltaMetadata = reasoningMetadata(reasoningField)
|
||||||
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
|
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
|
||||||
if (!state.lifecycle.text.has("text-0") && text !== undefined)
|
if (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
|
||||||
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
|
|
||||||
else if (
|
else if (
|
||||||
reasoningDetailsObserved &&
|
reasoningDetailsObserved &&
|
||||||
!lifecycle.reasoning.has("reasoning-0") &&
|
!lifecycle.reasoning.has("reasoning-0") &&
|
||||||
@@ -694,24 +742,36 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
|||||||
lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content)
|
lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tool of toolDeltas) {
|
// Compatible providers may omit indexes. Prefer durable identity, then use
|
||||||
const current = tools[tool.index]
|
// batch position for parallel deltas or the latest call for sparse chunks.
|
||||||
const pending = pendingTools[tool.index]
|
for (const [position, tool] of toolDeltas.entries()) {
|
||||||
|
const matched = toolIndexByID(tools, pendingTools, tool.id || undefined)
|
||||||
|
const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position)
|
||||||
|
const fallbackTool = tools[fallback] ?? pendingTools[fallback]
|
||||||
|
const index =
|
||||||
|
tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
|
||||||
|
const current = tools[index]
|
||||||
|
const pending = pendingTools[index]
|
||||||
const id = current?.id ?? pending?.id ?? (tool.id || undefined)
|
const id = current?.id ?? pending?.id ?? (tool.id || undefined)
|
||||||
const name = current?.name ?? pending?.name ?? (tool.function?.name || undefined)
|
const name = current?.name ?? pending?.name ?? (tool.function?.name || undefined)
|
||||||
const text = `${pending?.input ?? ""}${tool.function?.arguments ?? ""}`
|
const text = `${pending?.input ?? ""}${tool.function?.arguments ?? ""}`
|
||||||
|
latestToolIndex = index
|
||||||
|
nextToolIndex = Math.max(nextToolIndex, index + 1)
|
||||||
if (!current && (!id || !name)) {
|
if (!current && (!id || !name)) {
|
||||||
pendingTools = { ...pendingTools, [tool.index]: { id: id || undefined, name: name || undefined, input: text } }
|
pendingTools = {
|
||||||
|
...pendingTools,
|
||||||
|
[index]: { id: id || undefined, name: name || undefined, input: text },
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (pending) {
|
if (pending) {
|
||||||
pendingTools = { ...pendingTools }
|
pendingTools = { ...pendingTools }
|
||||||
delete pendingTools[tool.index]
|
delete pendingTools[index]
|
||||||
}
|
}
|
||||||
const result = ToolStream.appendOrStart(
|
const result = ToolStream.appendOrStart(
|
||||||
ADAPTER,
|
ADAPTER,
|
||||||
tools,
|
tools,
|
||||||
tool.index,
|
index,
|
||||||
{ id: id || undefined, name: name || undefined, text },
|
{ id: id || undefined, name: name || undefined, text },
|
||||||
"OpenAI Chat tool call delta is missing id or name",
|
"OpenAI Chat tool call delta is missing id or name",
|
||||||
)
|
)
|
||||||
@@ -743,6 +803,8 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
|||||||
reasoningDetails: state.reasoningDetails,
|
reasoningDetails: state.reasoningDetails,
|
||||||
reasoningDetailsObserved,
|
reasoningDetailsObserved,
|
||||||
reasoningEmitted,
|
reasoningEmitted,
|
||||||
|
latestToolIndex,
|
||||||
|
nextToolIndex,
|
||||||
},
|
},
|
||||||
events,
|
events,
|
||||||
] as const
|
] as const
|
||||||
@@ -750,14 +812,18 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
|||||||
|
|
||||||
const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
const hasToolCalls = state.toolCallEvents.length > 0
|
const toolCallEvents =
|
||||||
|
state.finishReason === undefined && Object.keys(state.tools).length > 0
|
||||||
|
? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
|
||||||
|
: state.toolCallEvents
|
||||||
|
const hasToolCalls = toolCallEvents.length > 0
|
||||||
const reason = state.finishReason
|
const reason = state.finishReason
|
||||||
? {
|
? {
|
||||||
...state.finishReason,
|
...state.finishReason,
|
||||||
normalized:
|
normalized:
|
||||||
state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
|
state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
|
||||||
}
|
}
|
||||||
: undefined
|
: { normalized: hasToolCalls ? ("tool-calls" as const) : ("unknown" as const) }
|
||||||
const metadata = reasoningMetadata(
|
const metadata = reasoningMetadata(
|
||||||
state.reasoningField,
|
state.reasoningField,
|
||||||
state.reasoningDetailsObserved ? state.reasoningDetails : undefined,
|
state.reasoningDetailsObserved ? state.reasoningDetails : undefined,
|
||||||
@@ -767,9 +833,9 @@ const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
|||||||
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
|
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
|
||||||
: state.lifecycle
|
: state.lifecycle
|
||||||
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
|
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
|
||||||
const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
|
const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
|
||||||
events.push(...state.toolCallEvents)
|
events.push(...toolCallEvents)
|
||||||
if (reason) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
|
Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,6 +865,7 @@ export const protocol = Protocol.make({
|
|||||||
reasoningDetails: [],
|
reasoningDetails: [],
|
||||||
reasoningDetailsObserved: false,
|
reasoningDetailsObserved: false,
|
||||||
reasoningEmitted: false,
|
reasoningEmitted: false,
|
||||||
|
nextToolIndex: 0,
|
||||||
}),
|
}),
|
||||||
step,
|
step,
|
||||||
onHalt: finishEvents,
|
onHalt: finishEvents,
|
||||||
|
|||||||
@@ -61,37 +61,57 @@ const emptyObjectSchema = (schema: Record<string, unknown>) =>
|
|||||||
(!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) &&
|
(!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) &&
|
||||||
!schema.additionalProperties
|
!schema.additionalProperties
|
||||||
|
|
||||||
const projectNode = (schema: unknown): Record<string, unknown> | undefined => {
|
const projectNode = (schema: unknown, nested = false): Record<string, unknown> | undefined => {
|
||||||
if (!isRecord(schema)) return undefined
|
if (!isRecord(schema)) return undefined
|
||||||
if (emptyObjectSchema(schema)) return undefined
|
if (!nested && emptyObjectSchema(schema)) return undefined
|
||||||
return Object.fromEntries(
|
const types = Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null") : undefined
|
||||||
|
const anyOf = Array.isArray(schema.anyOf) ? schema.anyOf : undefined
|
||||||
|
const hasNullAnyOf = anyOf?.some((item) => isRecord(item) && item.type === "null") ?? false
|
||||||
|
const anyOfTypes = hasNullAnyOf ? anyOf?.filter((item) => !isRecord(item) || item.type !== "null") : anyOf
|
||||||
|
const flattenedAnyOf = hasNullAnyOf && anyOfTypes?.length === 1 ? projectNode(anyOfTypes[0], true) : undefined
|
||||||
|
const result = Object.fromEntries(
|
||||||
[
|
[
|
||||||
["description", schema.description],
|
["description", schema.description],
|
||||||
["required", schema.required],
|
["required", schema.required],
|
||||||
["format", schema.format],
|
["format", schema.format],
|
||||||
["type", Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null")[0] : schema.type],
|
["type", types ? (types.length === 0 ? "null" : undefined) : schema.type],
|
||||||
["nullable", Array.isArray(schema.type) && schema.type.includes("null") ? true : undefined],
|
[
|
||||||
|
"nullable",
|
||||||
|
(Array.isArray(schema.type) && schema.type.includes("null") && types && types.length > 0) || hasNullAnyOf
|
||||||
|
? true
|
||||||
|
: undefined,
|
||||||
|
],
|
||||||
["enum", schema.const !== undefined ? [schema.const] : schema.enum],
|
["enum", schema.const !== undefined ? [schema.const] : schema.enum],
|
||||||
[
|
[
|
||||||
"properties",
|
"properties",
|
||||||
isRecord(schema.properties)
|
isRecord(schema.properties)
|
||||||
? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value)]))
|
? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value, true)]))
|
||||||
: undefined,
|
: undefined,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"items",
|
"items",
|
||||||
Array.isArray(schema.items)
|
Array.isArray(schema.items)
|
||||||
? schema.items.map(projectNode)
|
? schema.items.map((item) => projectNode(item, true))
|
||||||
: schema.items === undefined
|
: schema.items === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: projectNode(schema.items),
|
: projectNode(schema.items, true),
|
||||||
],
|
],
|
||||||
["allOf", Array.isArray(schema.allOf) ? schema.allOf.map(projectNode) : undefined],
|
["allOf", Array.isArray(schema.allOf) ? schema.allOf.map((item) => projectNode(item, true)) : undefined],
|
||||||
["anyOf", Array.isArray(schema.anyOf) ? schema.anyOf.map(projectNode) : undefined],
|
[
|
||||||
["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map(projectNode) : undefined],
|
"anyOf",
|
||||||
|
anyOfTypes
|
||||||
|
? hasNullAnyOf && anyOfTypes.length === 1
|
||||||
|
? undefined
|
||||||
|
: anyOfTypes.map((item) => projectNode(item, true))
|
||||||
|
: types && types.length > 0
|
||||||
|
? types.map((type) => ({ type }))
|
||||||
|
: undefined,
|
||||||
|
],
|
||||||
|
["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map((item) => projectNode(item, true)) : undefined],
|
||||||
["minLength", schema.minLength],
|
["minLength", schema.minLength],
|
||||||
].filter((entry) => entry[1] !== undefined),
|
].filter((entry) => entry[1] !== undefined),
|
||||||
)
|
)
|
||||||
|
return flattenedAnyOf ? { ...result, ...flattenedAnyOf } : result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convert = (schema: unknown) => projectNode(sanitizeNode(schema))
|
export const convert = (schema: unknown) => projectNode(sanitizeNode(schema))
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export const ServiceTierSchema = Schema.Literals(ServiceTiers)
|
|||||||
export interface Resolved {
|
export interface Resolved {
|
||||||
readonly instructions?: string
|
readonly instructions?: string
|
||||||
readonly store?: boolean
|
readonly store?: boolean
|
||||||
readonly promptCacheKey?: string
|
|
||||||
readonly reasoningEffort?: string
|
readonly reasoningEffort?: string
|
||||||
readonly reasoningSummary?: "auto" | "concise" | "detailed"
|
readonly reasoningSummary?: "auto" | "concise" | "detailed"
|
||||||
readonly include?: ReadonlyArray<ResponseIncludable>
|
readonly include?: ReadonlyArray<ResponseIncludable>
|
||||||
@@ -50,7 +49,6 @@ export const resolve = (request: LLMRequest): Resolved => {
|
|||||||
return {
|
return {
|
||||||
instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
|
instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
|
||||||
store: typeof input?.store === "boolean" ? input.store : undefined,
|
store: typeof input?.store === "boolean" ? input.store : undefined,
|
||||||
promptCacheKey: typeof input?.promptCacheKey === "string" ? input.promptCacheKey : undefined,
|
|
||||||
reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
|
reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
|
||||||
reasoningSummary:
|
reasoningSummary:
|
||||||
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
|
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { ProviderPackage } from "../provider-package"
|
|||||||
import { ProviderID, type ModelID } from "../schema"
|
import { ProviderID, type ModelID } from "../schema"
|
||||||
import * as OpenAIChat from "../protocols/openai-chat"
|
import * as OpenAIChat from "../protocols/openai-chat"
|
||||||
import * as OpenAIResponses from "../protocols/openai-responses"
|
import * as OpenAIResponses from "../protocols/openai-responses"
|
||||||
|
import { ProviderShared } from "../protocols/shared"
|
||||||
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
||||||
|
|
||||||
export const id = ProviderID.make("azure")
|
export const id = ProviderID.make("azure")
|
||||||
@@ -19,7 +20,7 @@ export type LanguageModelOptions = AzureURL &
|
|||||||
ProviderAuthOption<"optional"> & {
|
ProviderAuthOption<"optional"> & {
|
||||||
readonly apiVersion?: string
|
readonly apiVersion?: string
|
||||||
readonly queryParams?: Record<string, string>
|
readonly queryParams?: Record<string, string>
|
||||||
readonly useCompletionUrls?: boolean
|
readonly useDeploymentBasedUrls?: boolean
|
||||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||||
}
|
}
|
||||||
export type Config = LanguageModelOptions
|
export type Config = LanguageModelOptions
|
||||||
@@ -29,27 +30,22 @@ export type Settings = ProviderPackage.Settings &
|
|||||||
readonly apiKey?: string
|
readonly apiKey?: string
|
||||||
readonly apiVersion?: string
|
readonly apiVersion?: string
|
||||||
readonly queryParams?: Readonly<Record<string, string>>
|
readonly queryParams?: Readonly<Record<string, string>>
|
||||||
|
readonly useDeploymentBasedUrls?: boolean
|
||||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
readonly providerOptions?: OpenAIProviderOptionsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai/v1`
|
const resourceBaseURL = (resourceName: string) => `https://${resourceName.trim()}.openai.azure.com/openai`
|
||||||
|
|
||||||
const responsesRoute = OpenAIResponses.route.with({
|
const responsesRoute = OpenAIResponses.route.with({
|
||||||
id: "azure-openai-responses",
|
id: "azure-openai-responses",
|
||||||
provider: id,
|
provider: id,
|
||||||
auth: routeAuth,
|
auth: routeAuth,
|
||||||
endpoint: {
|
|
||||||
query: { "api-version": "v1" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const chatRoute = OpenAIChat.route.with({
|
const chatRoute = OpenAIChat.route.with({
|
||||||
id: "azure-openai-chat",
|
id: "azure-openai-chat",
|
||||||
provider: id,
|
provider: id,
|
||||||
auth: routeAuth,
|
auth: routeAuth,
|
||||||
endpoint: {
|
|
||||||
query: { "api-version": "v1" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const routes = [responsesRoute, chatRoute]
|
export const routes = [responsesRoute, chatRoute]
|
||||||
@@ -59,7 +55,7 @@ const defaults = (input: Config) => {
|
|||||||
apiKey: _,
|
apiKey: _,
|
||||||
apiVersion: _apiVersion,
|
apiVersion: _apiVersion,
|
||||||
resourceName: _resourceName,
|
resourceName: _resourceName,
|
||||||
useCompletionUrls: _useCompletionUrls,
|
useDeploymentBasedUrls: _useDeploymentBasedUrls,
|
||||||
baseURL: _baseURL,
|
baseURL: _baseURL,
|
||||||
queryParams: _queryParams,
|
queryParams: _queryParams,
|
||||||
...rest
|
...rest
|
||||||
@@ -80,37 +76,39 @@ const auth = (input: Config) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const configuredRoute = <Body, Prepared>(route: RouteDef<Body, Prepared>, input: Config) =>
|
const configuredRoute = <Body, Prepared>(route: RouteDef<Body, Prepared>, input: Config, modelID: string | ModelID) =>
|
||||||
route.with({
|
route.with({
|
||||||
auth: auth(input),
|
auth: auth(input),
|
||||||
endpoint: {
|
endpoint: endpoint(input, modelID),
|
||||||
// AtLeastOne guarantees at least one is set; baseURL wins if both are.
|
|
||||||
baseURL: input.baseURL ?? resourceBaseURL(input.resourceName!),
|
|
||||||
query: {
|
|
||||||
...(input.apiVersion ? { "api-version": input.apiVersion } : {}),
|
|
||||||
...input.queryParams,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function endpoint(input: Config, modelID: string | ModelID) {
|
||||||
|
const baseURL = ProviderShared.trimBaseUrl(input.baseURL ?? resourceBaseURL(input.resourceName!))
|
||||||
|
const query = { "api-version": input.apiVersion ?? "v1", ...input.queryParams }
|
||||||
|
|
||||||
|
if (input.useDeploymentBasedUrls) return { baseURL: `${baseURL}/deployments/${modelID}`, query }
|
||||||
|
if (input.baseURL !== undefined && !new URL(input.baseURL).hostname.endsWith(".openai.azure.com")) {
|
||||||
|
return { baseURL, query: input.queryParams }
|
||||||
|
}
|
||||||
|
return { baseURL: `${baseURL}/v1`, query }
|
||||||
|
}
|
||||||
|
|
||||||
export const configure = (input: Config) => {
|
export const configure = (input: Config) => {
|
||||||
const configuredResponsesRoute = configuredRoute(responsesRoute, input)
|
|
||||||
const configuredChatRoute = configuredRoute(chatRoute, input)
|
|
||||||
const modelDefaults = defaults(input)
|
const modelDefaults = defaults(input)
|
||||||
|
|
||||||
const responses = (modelID: string | ModelID) =>
|
const responses = (modelID: string | ModelID) =>
|
||||||
configuredResponsesRoute
|
configuredRoute(responsesRoute, input, modelID)
|
||||||
.with(withOpenAIOptions(modelID, modelDefaults))
|
.with(withOpenAIOptions(modelID, modelDefaults))
|
||||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||||
|
|
||||||
const chat = (modelID: string | ModelID) =>
|
const chat = (modelID: string | ModelID) =>
|
||||||
configuredChatRoute
|
configuredRoute(chatRoute, input, modelID)
|
||||||
.with(withOpenAIOptions(modelID, modelDefaults))
|
.with(withOpenAIOptions(modelID, modelDefaults))
|
||||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
model: (modelID: string | ModelID) => (input.useCompletionUrls === true ? chat(modelID) : responses(modelID)),
|
model: responses,
|
||||||
responses,
|
responses,
|
||||||
chat,
|
chat,
|
||||||
configure,
|
configure,
|
||||||
@@ -131,6 +129,7 @@ const config = (settings: Settings): Config => {
|
|||||||
limits: settings.limits,
|
limits: settings.limits,
|
||||||
providerOptions: settings.providerOptions,
|
providerOptions: settings.providerOptions,
|
||||||
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
|
queryParams: settings.queryParams === undefined ? undefined : { ...settings.queryParams },
|
||||||
|
useDeploymentBasedUrls: settings.useDeploymentBasedUrls,
|
||||||
}
|
}
|
||||||
if (settings.baseURL !== undefined) return { ...common, baseURL: settings.baseURL }
|
if (settings.baseURL !== undefined) return { ...common, baseURL: settings.baseURL }
|
||||||
if (settings.resourceName !== undefined) return { ...common, resourceName: settings.resourceName }
|
if (settings.resourceName !== undefined) return { ...common, resourceName: settings.resourceName }
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
|
|
||||||
import type { RouteDefaultsInput } from "../route/client"
|
|
||||||
import { ProviderID, type ModelID } from "../schema"
|
|
||||||
import * as OpenAIChat from "../protocols/openai-chat"
|
|
||||||
import * as OpenAIResponses from "../protocols/openai-responses"
|
|
||||||
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
|
|
||||||
|
|
||||||
export const id = ProviderID.make("github-copilot")
|
|
||||||
|
|
||||||
// GitHub Copilot has no canonical public URL — callers (opencode, etc.) must
|
|
||||||
// supply `baseURL` explicitly.
|
|
||||||
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
|
|
||||||
ProviderAuthOption<"optional"> & {
|
|
||||||
readonly baseURL: string
|
|
||||||
readonly endpoint?: "chat" | "responses"
|
|
||||||
readonly providerOptions?: OpenAIProviderOptionsInput
|
|
||||||
}
|
|
||||||
|
|
||||||
export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: LanguageModelOptions["endpoint"]) => {
|
|
||||||
if (endpoint) return endpoint === "responses"
|
|
||||||
const model = String(modelID)
|
|
||||||
const match = /^gpt-(\d+)/.exec(model)
|
|
||||||
if (!match) return false
|
|
||||||
return Number(match[1]) >= 5 && !model.startsWith("gpt-5-mini")
|
|
||||||
}
|
|
||||||
|
|
||||||
export const routes = [OpenAIResponses.route, OpenAIChat.route]
|
|
||||||
|
|
||||||
const chatRoute = OpenAIChat.route.with({ provider: id })
|
|
||||||
const responsesRoute = OpenAIResponses.route.with({ provider: id })
|
|
||||||
|
|
||||||
const defaults = (options: LanguageModelOptions) => {
|
|
||||||
const { apiKey: _, auth: _auth, baseURL: _baseURL, endpoint: _endpoint, ...rest } = options
|
|
||||||
return rest
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuredResponsesRoute = (options: LanguageModelOptions) =>
|
|
||||||
responsesRoute.with({
|
|
||||||
endpoint: { baseURL: options.baseURL },
|
|
||||||
auth: AuthOptions.bearer(options, []),
|
|
||||||
})
|
|
||||||
|
|
||||||
const configuredChatRoute = (options: LanguageModelOptions) =>
|
|
||||||
chatRoute.with({
|
|
||||||
endpoint: { baseURL: options.baseURL },
|
|
||||||
auth: AuthOptions.bearer(options, []),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const configure = (options: LanguageModelOptions) => {
|
|
||||||
const responsesRoute = configuredResponsesRoute(options)
|
|
||||||
const chatRoute = configuredChatRoute(options)
|
|
||||||
const responses = (modelID: string | ModelID) =>
|
|
||||||
responsesRoute
|
|
||||||
.with(withOpenAIOptions(modelID, defaults(options)))
|
|
||||||
.model<OpenAIProviderOptionsInput>({ id: modelID })
|
|
||||||
const chat = (modelID: string | ModelID) =>
|
|
||||||
chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model<OpenAIProviderOptionsInput>({ id: modelID })
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
model: (modelID: string | ModelID) =>
|
|
||||||
shouldUseResponsesApi(modelID, options.endpoint) ? responses(modelID) : chat(modelID),
|
|
||||||
responses,
|
|
||||||
chat,
|
|
||||||
configure,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const provider = {
|
|
||||||
id,
|
|
||||||
configure,
|
|
||||||
}
|
|
||||||
@@ -15,8 +15,7 @@ export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput
|
|||||||
|
|
||||||
const VERSION = "vertex-2023-10-16" as const
|
const VERSION = "vertex-2023-10-16" as const
|
||||||
|
|
||||||
// models.dev uses this provider id even though the API contract is Anthropic Messages.
|
export const id = ProviderID.make("google-vertex")
|
||||||
export const id = ProviderID.make("google-vertex-anthropic")
|
|
||||||
|
|
||||||
export type Config = RouteDefaultsInput &
|
export type Config = RouteDefaultsInput &
|
||||||
GoogleVertexShared.OAuthOptions & {
|
GoogleVertexShared.OAuthOptions & {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export * as AmazonBedrockMantle from "./amazon-bedrock-mantle"
|
|||||||
export * as Azure from "./azure"
|
export * as Azure from "./azure"
|
||||||
export * as Cloudflare from "./cloudflare"
|
export * as Cloudflare from "./cloudflare"
|
||||||
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
|
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
|
||||||
export * as GitHubCopilot from "./github-copilot"
|
|
||||||
export * as Google from "./google"
|
export * as Google from "./google"
|
||||||
export * as GoogleVertex from "./google-vertex"
|
export * as GoogleVertex from "./google-vertex"
|
||||||
export * as GoogleVertexChat from "./google-vertex-chat"
|
export * as GoogleVertexChat from "./google-vertex-chat"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export interface OpenResponsesOptionsInput {
|
|||||||
readonly [key: string]: unknown
|
readonly [key: string]: unknown
|
||||||
readonly instructions?: string
|
readonly instructions?: string
|
||||||
readonly store?: boolean
|
readonly store?: boolean
|
||||||
readonly promptCacheKey?: string
|
|
||||||
readonly reasoningEffort?: ReasoningEffort
|
readonly reasoningEffort?: ReasoningEffort
|
||||||
readonly reasoningSummary?: "auto" | "concise" | "detailed"
|
readonly reasoningSummary?: "auto" | "concise" | "detailed"
|
||||||
readonly include?: ReadonlyArray<ResponseIncludable>
|
readonly include?: ReadonlyArray<ResponseIncludable>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
|
|||||||
const openai = Object.fromEntries(
|
const openai = Object.fromEntries(
|
||||||
definedEntries({
|
definedEntries({
|
||||||
store: options?.store,
|
store: options?.store,
|
||||||
promptCacheKey: options?.promptCacheKey,
|
|
||||||
reasoningEffort: options?.reasoningEffort,
|
reasoningEffort: options?.reasoningEffort,
|
||||||
reasoningSummary: options?.reasoningSummary,
|
reasoningSummary: options?.reasoningSummary,
|
||||||
include: options?.include,
|
include: options?.include,
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export interface OpenRouterOptions {
|
|||||||
readonly debug?: Readonly<{ echo_upstream_body?: boolean }>
|
readonly debug?: Readonly<{ echo_upstream_body?: boolean }>
|
||||||
readonly models?: ReadonlyArray<string>
|
readonly models?: ReadonlyArray<string>
|
||||||
readonly plugins?: ReadonlyArray<OpenRouterPlugin>
|
readonly plugins?: ReadonlyArray<OpenRouterPlugin>
|
||||||
readonly promptCacheKey?: string
|
|
||||||
readonly provider?: OpenRouterProviderRouting
|
readonly provider?: OpenRouterProviderRouting
|
||||||
readonly reasoning?: Readonly<{
|
readonly reasoning?: Readonly<{
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
@@ -122,6 +121,7 @@ export const protocol = Protocol.make({
|
|||||||
...body,
|
...body,
|
||||||
messages,
|
messages,
|
||||||
...bodyOptions(request.providerOptions?.openrouter),
|
...bodyOptions(request.providerOptions?.openrouter),
|
||||||
|
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
||||||
} as OpenRouterBody
|
} as OpenRouterBody
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -161,7 +161,6 @@ const bodyOptions = (input: unknown) => {
|
|||||||
...(isRecord(debug) ? { debug } : {}),
|
...(isRecord(debug) ? { debug } : {}),
|
||||||
...(typeof user === "string" ? { user } : {}),
|
...(typeof user === "string" ? { user } : {}),
|
||||||
...(isRecord(reasoning) ? { reasoning } : {}),
|
...(isRecord(reasoning) ? { reasoning } : {}),
|
||||||
...(typeof promptCacheKey === "string" ? { prompt_cache_key: promptCacheKey } : {}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const chatRoute = Route.make({
|
|||||||
protocol: OpenAIChat.protocol,
|
protocol: OpenAIChat.protocol,
|
||||||
endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }),
|
endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }),
|
||||||
transport: OpenAICompatibleChat.route.transport,
|
transport: OpenAICompatibleChat.route.transport,
|
||||||
|
headers: ({ request }): Record<string, string> =>
|
||||||
|
request.promptCacheKey ? { "x-grok-conv-id": request.promptCacheKey } : {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const routes = [responsesRoute, chatRoute]
|
export const routes = [responsesRoute, chatRoute]
|
||||||
|
|||||||
@@ -255,13 +255,7 @@ const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent,
|
|||||||
if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true
|
if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true
|
||||||
return Effect.succeed(event)
|
return Effect.succeed(event)
|
||||||
}),
|
}),
|
||||||
Stream.onEnd(
|
Stream.onEnd(Effect.suspend(() => (terminal ? Effect.void : Effect.fail(incompleteStreamError(route))))),
|
||||||
Effect.suspend(() =>
|
|
||||||
terminal
|
|
||||||
? Effect.void
|
|
||||||
: Effect.fail(incompleteStreamError(route)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cause, Context, Effect, Layer } from "effect"
|
import { Cause, Context, Effect, Layer, Option, Schema } from "effect"
|
||||||
import {
|
import {
|
||||||
FetchHttpClient,
|
FetchHttpClient,
|
||||||
Headers,
|
Headers,
|
||||||
@@ -198,8 +198,20 @@ const responseBody = (body: string | void, request: HttpClientRequest.HttpClient
|
|||||||
return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
|
return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decodeProviderBody = Schema.decodeUnknownOption(
|
||||||
|
Schema.fromJsonString(
|
||||||
|
Schema.Struct({
|
||||||
|
message: Schema.optionalKey(Schema.String),
|
||||||
|
error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const providerMessage = (status: number, body: { readonly body?: string }) => {
|
const providerMessage = (status: number, body: { readonly body?: string }) => {
|
||||||
if (body.body && body.body.length <= 500) return `Provider request failed with HTTP ${status}: ${body.body}`
|
if (body.body && body.body.length <= 500) {
|
||||||
|
const decoded = Option.getOrUndefined(decodeProviderBody(body.body))
|
||||||
|
return `Provider request failed with HTTP ${status}: ${decoded?.error?.message ?? decoded?.message ?? body.body}`
|
||||||
|
}
|
||||||
return `Provider request failed with HTTP ${status}`
|
return `Provider request failed with HTTP ${status}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,35 +45,34 @@ const isToolResultValue = (value: unknown): value is ToolResultValue =>
|
|||||||
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
|
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
|
||||||
"value" in value
|
"value" in value
|
||||||
|
|
||||||
export const ToolResultValue = Object.assign(
|
const toolResultValueSchema = Schema.Union([
|
||||||
Schema.Union([
|
Schema.Struct({
|
||||||
Schema.Struct({
|
type: Schema.Literal("json"),
|
||||||
type: Schema.Literal("json"),
|
value: Schema.Unknown,
|
||||||
value: Schema.Unknown,
|
}),
|
||||||
}),
|
Schema.Struct({
|
||||||
Schema.Struct({
|
type: Schema.Literal("text"),
|
||||||
type: Schema.Literal("text"),
|
value: Schema.Unknown,
|
||||||
value: Schema.Unknown,
|
}),
|
||||||
}),
|
Schema.Struct({
|
||||||
Schema.Struct({
|
type: Schema.Literal("error"),
|
||||||
type: Schema.Literal("error"),
|
value: Schema.Unknown,
|
||||||
value: Schema.Unknown,
|
}),
|
||||||
}),
|
Schema.Struct({
|
||||||
Schema.Struct({
|
type: Schema.Literal("content"),
|
||||||
type: Schema.Literal("content"),
|
value: Schema.Array(Tool.Content),
|
||||||
value: Schema.Array(Tool.Content),
|
}),
|
||||||
}),
|
]).annotate({ identifier: "LLM.ToolResult" })
|
||||||
]).annotate({ identifier: "LLM.ToolResult" }),
|
export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
|
||||||
{
|
|
||||||
is: isToolResultValue,
|
export const ToolResultValue = Object.assign(toolResultValueSchema, {
|
||||||
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
is: isToolResultValue,
|
||||||
if (isToolResultValue(value)) return value
|
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
||||||
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
if (isToolResultValue(value)) return value
|
||||||
return { type, value }
|
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
||||||
},
|
return { type, value }
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
|
|
||||||
|
|
||||||
export interface ToolOutput {
|
export interface ToolOutput {
|
||||||
readonly structured: unknown
|
readonly structured: unknown
|
||||||
@@ -272,6 +271,8 @@ export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
|||||||
providerOptions: Schema.optional(ProviderOptions),
|
providerOptions: Schema.optional(ProviderOptions),
|
||||||
http: Schema.optional(HttpOptions),
|
http: Schema.optional(HttpOptions),
|
||||||
cache: Schema.optional(CachePolicy),
|
cache: Schema.optional(CachePolicy),
|
||||||
|
// Stable cache affinity for protocols that support provider-managed prompt caching.
|
||||||
|
promptCacheKey: Schema.optional(Schema.String),
|
||||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
@@ -289,6 +290,7 @@ export namespace LLMRequest {
|
|||||||
providerOptions: request.providerOptions,
|
providerOptions: request.providerOptions,
|
||||||
http: request.http,
|
http: request.http,
|
||||||
cache: request.cache,
|
cache: request.cache,
|
||||||
|
promptCacheKey: request.promptCacheKey,
|
||||||
metadata: request.metadata,
|
metadata: request.metadata,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export class LanguageModelCompatibility extends Schema.Class<LanguageModelCompat
|
|||||||
toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
|
toolSchema: Schema.optional(LanguageModelToolSchemaCompatibility),
|
||||||
reasoningField: Schema.optional(Schema.String),
|
reasoningField: Schema.optional(Schema.String),
|
||||||
maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
|
maxTokensField: Schema.optional(LanguageModelMaxTokensFieldCompatibility),
|
||||||
|
requireFinishReason: Schema.optional(Schema.Boolean),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export namespace LanguageModelCompatibility {
|
export namespace LanguageModelCompatibility {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Effect, JsonSchema, Schema } from "effect"
|
import { Effect, JsonSchema, Schema } from "effect"
|
||||||
import { Tool } from "@opencode-ai/schema/tool"
|
import { Tool } from "@opencode-ai/schema/tool"
|
||||||
import type {
|
import type { ToolCallPart, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
|
||||||
ToolCallPart,
|
|
||||||
ToolDefinition as ToolDefinitionClass,
|
|
||||||
ToolOutput as ToolOutputType,
|
|
||||||
} from "./schema"
|
|
||||||
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
|
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,8 +240,7 @@ const project = (
|
|||||||
): ToolOutputType =>
|
): ToolOutputType =>
|
||||||
ToolOutput.make(
|
ToolOutput.make(
|
||||||
toStructuredOutput?.(output) ?? output,
|
toStructuredOutput?.(output) ?? output,
|
||||||
toModelOutput?.({ id, parameters, output }) ??
|
toModelOutput?.({ id, parameters, output }) ?? (typeof output === "string" ? [{ type: "text", text: output }] : []),
|
||||||
(typeof output === "string" ? [{ type: "text", text: output }] : []),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ToolFailure }
|
export { ToolFailure }
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import * as Anthropic from "../src/providers/anthropic"
|
|||||||
import * as AnthropicCompatible from "../src/providers/anthropic-compatible"
|
import * as AnthropicCompatible from "../src/providers/anthropic-compatible"
|
||||||
import * as Azure from "../src/providers/azure"
|
import * as Azure from "../src/providers/azure"
|
||||||
import * as Cloudflare from "../src/providers/cloudflare"
|
import * as Cloudflare from "../src/providers/cloudflare"
|
||||||
import * as GitHubCopilot from "../src/providers/github-copilot"
|
|
||||||
import * as Google from "../src/providers/google"
|
import * as Google from "../src/providers/google"
|
||||||
import * as GoogleVertex from "../src/providers/google-vertex"
|
import * as GoogleVertex from "../src/providers/google-vertex"
|
||||||
import * as GoogleVertexChat from "../src/providers/google-vertex-chat"
|
import * as GoogleVertexChat from "../src/providers/google-vertex-chat"
|
||||||
@@ -270,7 +269,3 @@ OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-
|
|||||||
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama")
|
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama")
|
||||||
// @ts-expect-error Cloudflare Workers AI model selectors only accept model ids.
|
// @ts-expect-error Cloudflare Workers AI model selectors only accept model ids.
|
||||||
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama", {})
|
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama", {})
|
||||||
|
|
||||||
GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1")
|
|
||||||
// @ts-expect-error GitHub Copilot model selectors only accept model ids.
|
|
||||||
GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1", {})
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
OpenRouter,
|
OpenRouter,
|
||||||
XAI,
|
XAI,
|
||||||
} from "@opencode-ai/ai/providers"
|
} from "@opencode-ai/ai/providers"
|
||||||
import * as GitHubCopilot from "@opencode-ai/ai/providers/github-copilot"
|
|
||||||
import {
|
import {
|
||||||
OpenAIChat,
|
OpenAIChat,
|
||||||
OpenAICompatibleChat,
|
OpenAICompatibleChat,
|
||||||
@@ -60,23 +59,6 @@ describe("public exports", () => {
|
|||||||
expect(XAI.provider.chat).toBe(XAI.chat)
|
expect(XAI.provider.chat).toBe(XAI.chat)
|
||||||
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
|
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
|
||||||
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
|
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
|
||||||
expect(
|
|
||||||
GitHubCopilot.configure({ baseURL: "https://api.githubcopilot.test", apiKey: "fixture" }).model,
|
|
||||||
).toBeFunction()
|
|
||||||
expect(
|
|
||||||
GitHubCopilot.configure({
|
|
||||||
baseURL: "https://api.githubcopilot.test",
|
|
||||||
apiKey: "fixture",
|
|
||||||
endpoint: "responses",
|
|
||||||
}).model("mai-code-1-flash-picker").route.id,
|
|
||||||
).toBe("openai-responses")
|
|
||||||
expect(
|
|
||||||
GitHubCopilot.configure({
|
|
||||||
baseURL: "https://api.githubcopilot.test",
|
|
||||||
apiKey: "fixture",
|
|
||||||
endpoint: "chat",
|
|
||||||
}).model("gpt-5").route.id,
|
|
||||||
).toBe("openai-chat")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("protocol barrels expose supported low-level routes", () => {
|
test("protocol barrels expose supported low-level routes", () => {
|
||||||
|
|||||||
+1
-7
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:anthropic-messages-cache", "provider:anthropic", "protocol:anthropic-messages", "cache", "tool"],
|
||||||
"prefix:anthropic-messages-cache",
|
|
||||||
"provider:anthropic",
|
|
||||||
"protocol:anthropic-messages",
|
|
||||||
"cache",
|
|
||||||
"tool"
|
|
||||||
],
|
|
||||||
"name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback",
|
"name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback",
|
||||||
"recordedAt": "2026-07-24T16:22:29.494Z"
|
"recordedAt": "2026-07-24T16:22:29.494Z"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-6
@@ -1,12 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:bedrock-converse-cache", "provider:amazon-bedrock", "protocol:bedrock-converse", "cache"],
|
||||||
"prefix:bedrock-converse-cache",
|
|
||||||
"provider:amazon-bedrock",
|
|
||||||
"protocol:bedrock-converse",
|
|
||||||
"cache"
|
|
||||||
],
|
|
||||||
"name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call",
|
"name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call",
|
||||||
"recordedAt": "2026-07-23T02:29:10.955Z"
|
"recordedAt": "2026-07-23T02:29:10.955Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:google-images", "provider:google", "protocol:google-images"],
|
||||||
"prefix:google-images",
|
|
||||||
"provider:google",
|
|
||||||
"protocol:google-images"
|
|
||||||
],
|
|
||||||
"name": "google-images/generates-an-image",
|
"name": "google-images/generates-an-image",
|
||||||
"recordedAt": "2026-07-19T16:05:51.868Z"
|
"recordedAt": "2026-07-19T16:05:51.868Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:openai-images", "provider:openai", "protocol:openai-images"],
|
||||||
"prefix:openai-images",
|
|
||||||
"provider:openai",
|
|
||||||
"protocol:openai-images"
|
|
||||||
],
|
|
||||||
"name": "openai-images/generates-an-image",
|
"name": "openai-images/generates-an-image",
|
||||||
"recordedAt": "2026-07-19T14:41:43.188Z"
|
"recordedAt": "2026-07-19T14:41:43.188Z"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-5
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:openai-responses-images", "provider:openai", "protocol:openai-responses"],
|
||||||
"prefix:openai-responses-images",
|
|
||||||
"provider:openai",
|
|
||||||
"protocol:openai-responses"
|
|
||||||
],
|
|
||||||
"name": "openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool",
|
"name": "openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool",
|
||||||
"recordedAt": "2026-07-19T14:57:16.284Z"
|
"recordedAt": "2026-07-19T14:57:16.284Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,12 +2,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"model": "anthropic/claude-sonnet-4.6",
|
"model": "anthropic/claude-sonnet-4.6",
|
||||||
"tags": [
|
"tags": ["prefix:openai-compatible-chat", "provider:openrouter", "protocol:openai-chat", "reasoning"],
|
||||||
"prefix:openai-compatible-chat",
|
|
||||||
"provider:openrouter",
|
|
||||||
"protocol:openai-chat",
|
|
||||||
"reasoning"
|
|
||||||
],
|
|
||||||
"name": "openrouter-reasoning",
|
"name": "openrouter-reasoning",
|
||||||
"recordedAt": "2026-07-18T11:28:39.267Z"
|
"recordedAt": "2026-07-18T11:28:39.267Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "tool", "tool-result"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:anthropic",
|
|
||||||
"protocol:anthropic-messages",
|
|
||||||
"tool",
|
|
||||||
"tool-result"
|
|
||||||
],
|
|
||||||
"name": "pdf/anthropic-tool-result",
|
"name": "pdf/anthropic-tool-result",
|
||||||
"recordedAt": "2026-07-22T18:15:39.002Z"
|
"recordedAt": "2026-07-22T18:15:39.002Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "user-input"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:anthropic",
|
|
||||||
"protocol:anthropic-messages",
|
|
||||||
"user-input"
|
|
||||||
],
|
|
||||||
"name": "pdf/anthropic-user-input",
|
"name": "pdf/anthropic-user-input",
|
||||||
"recordedAt": "2026-07-22T18:15:37.979Z"
|
"recordedAt": "2026-07-22T18:15:37.979Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "tool", "tool-result"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:amazon-bedrock",
|
|
||||||
"protocol:bedrock-converse",
|
|
||||||
"tool",
|
|
||||||
"tool-result"
|
|
||||||
],
|
|
||||||
"name": "pdf/bedrock-tool-result",
|
"name": "pdf/bedrock-tool-result",
|
||||||
"recordedAt": "2026-07-22T18:15:52.400Z"
|
"recordedAt": "2026-07-22T18:15:52.400Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "user-input"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:amazon-bedrock",
|
|
||||||
"protocol:bedrock-converse",
|
|
||||||
"user-input"
|
|
||||||
],
|
|
||||||
"name": "pdf/bedrock-user-input",
|
"name": "pdf/bedrock-user-input",
|
||||||
"recordedAt": "2026-07-22T18:15:48.408Z"
|
"recordedAt": "2026-07-22T18:15:48.408Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "tool", "tool-result"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:google",
|
|
||||||
"protocol:gemini",
|
|
||||||
"tool",
|
|
||||||
"tool-result"
|
|
||||||
],
|
|
||||||
"name": "pdf/gemini-tool-result",
|
"name": "pdf/gemini-tool-result",
|
||||||
"recordedAt": "2026-07-22T18:21:59.606Z"
|
"recordedAt": "2026-07-22T18:21:59.606Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "user-input"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:google",
|
|
||||||
"protocol:gemini",
|
|
||||||
"user-input"
|
|
||||||
],
|
|
||||||
"name": "pdf/gemini-user-input",
|
"name": "pdf/gemini-user-input",
|
||||||
"recordedAt": "2026-07-22T18:20:55.140Z"
|
"recordedAt": "2026-07-22T18:20:55.140Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "tool", "tool-result"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:openai",
|
|
||||||
"protocol:openai-responses",
|
|
||||||
"tool",
|
|
||||||
"tool-result"
|
|
||||||
],
|
|
||||||
"name": "pdf/openai-tool-result",
|
"name": "pdf/openai-tool-result",
|
||||||
"recordedAt": "2026-07-22T18:15:36.438Z"
|
"recordedAt": "2026-07-22T18:15:36.438Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "user-input"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:openai",
|
|
||||||
"protocol:openai-responses",
|
|
||||||
"user-input"
|
|
||||||
],
|
|
||||||
"name": "pdf/openai-user-input",
|
"name": "pdf/openai-user-input",
|
||||||
"recordedAt": "2026-07-22T18:15:34.867Z"
|
"recordedAt": "2026-07-22T18:15:34.867Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "tool", "tool-result"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:xai",
|
|
||||||
"protocol:openai-responses",
|
|
||||||
"tool",
|
|
||||||
"tool-result"
|
|
||||||
],
|
|
||||||
"name": "pdf/xai-tool-result",
|
"name": "pdf/xai-tool-result",
|
||||||
"recordedAt": "2026-07-22T18:15:43.608Z"
|
"recordedAt": "2026-07-22T18:15:43.608Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "user-input"],
|
||||||
"prefix:pdf",
|
|
||||||
"pdf",
|
|
||||||
"provider:xai",
|
|
||||||
"protocol:openai-responses",
|
|
||||||
"user-input"
|
|
||||||
],
|
|
||||||
"name": "pdf/xai-user-input",
|
"name": "pdf/xai-user-input",
|
||||||
"recordedAt": "2026-07-22T18:15:42.429Z"
|
"recordedAt": "2026-07-22T18:15:42.429Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,12 +2,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"model": "anthropic/claude-sonnet-4.6",
|
"model": "anthropic/claude-sonnet-4.6",
|
||||||
"tags": [
|
"tags": ["prefix:openai-compatible-chat", "provider:vercel-ai-gateway", "protocol:openai-chat", "reasoning"],
|
||||||
"prefix:openai-compatible-chat",
|
|
||||||
"provider:vercel-ai-gateway",
|
|
||||||
"protocol:openai-chat",
|
|
||||||
"reasoning"
|
|
||||||
],
|
|
||||||
"name": "vercel-ai-gateway-reasoning",
|
"name": "vercel-ai-gateway-reasoning",
|
||||||
"recordedAt": "2026-07-18T11:28:42.077Z"
|
"recordedAt": "2026-07-18T11:28:42.077Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": [
|
"tags": ["prefix:xai-images", "provider:xai", "protocol:xai-images"],
|
||||||
"prefix:xai-images",
|
|
||||||
"provider:xai",
|
|
||||||
"protocol:xai-images"
|
|
||||||
],
|
|
||||||
"name": "xai-images/generates-an-image",
|
"name": "xai-images/generates-an-image",
|
||||||
"recordedAt": "2026-07-19T15:56:20.098Z"
|
"recordedAt": "2026-07-19T15:56:20.098Z"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ describe("llm constructors", () => {
|
|||||||
const updated = LanguageModel.update(base, {
|
const updated = LanguageModel.update(base, {
|
||||||
route: responsesRoute,
|
route: responsesRoute,
|
||||||
defaults: { generation: { maxTokens: 20 } },
|
defaults: { generation: { maxTokens: 20 } },
|
||||||
compatibility: { toolSchema: "gemini" },
|
compatibility: { toolSchema: "gemini", requireFinishReason: false },
|
||||||
})
|
})
|
||||||
const updatedInput = LanguageModel.input(updated)
|
const updatedInput = LanguageModel.input(updated)
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ describe("llm constructors", () => {
|
|||||||
expect(String(updated.id)).toBe("fake-model")
|
expect(String(updated.id)).toBe("fake-model")
|
||||||
expect(updated.route).toBe(responsesRoute)
|
expect(updated.route).toBe(responsesRoute)
|
||||||
expect(updated.defaults?.generation).toEqual({ maxTokens: 20 })
|
expect(updated.defaults?.generation).toEqual({ maxTokens: 20 })
|
||||||
expect(updated.compatibility).toEqual({ toolSchema: "gemini" })
|
expect(updated.compatibility).toEqual({ toolSchema: "gemini", requireFinishReason: false })
|
||||||
expect(updatedInput.defaults).toBe(updated.defaults)
|
expect(updatedInput.defaults).toBe(updated.defaults)
|
||||||
expect(updatedInput.compatibility).toBe(updated.compatibility)
|
expect(updatedInput.compatibility).toBe(updated.compatibility)
|
||||||
expect(String(updatedInput.provider)).toBe("fake")
|
expect(String(updatedInput.provider)).toBe("fake")
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { CloudflareWorkersAI } from "../../src/providers"
|
|||||||
|
|
||||||
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
|
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
|
||||||
|
|
||||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } })
|
LLM.request({ model, prompt: "Hello", promptCacheKey: "cache" })
|
||||||
|
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model,
|
model,
|
||||||
prompt: "Hello",
|
prompt: "Hello",
|
||||||
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string.
|
// @ts-expect-error Prompt cache keys must be strings.
|
||||||
providerOptions: { openai: { promptCacheKey: 1 } },
|
promptCacheKey: 1,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { LLM } from "../../src"
|
|
||||||
import { GitHubCopilot } from "../../src/providers"
|
|
||||||
|
|
||||||
const model = GitHubCopilot.configure({ baseURL: "https://example.com" }).model("gpt-5")
|
|
||||||
|
|
||||||
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningSummary: "auto" } } })
|
|
||||||
|
|
||||||
LLM.request({
|
|
||||||
model,
|
|
||||||
prompt: "Hello",
|
|
||||||
// @ts-expect-error Copilot reasoning summaries use the OpenAI union.
|
|
||||||
providerOptions: { openai: { reasoningSummary: "full" } },
|
|
||||||
})
|
|
||||||
@@ -209,6 +209,27 @@ describe("provider package entrypoints", () => {
|
|||||||
expect(chat.route.id).toBe("azure-openai-chat")
|
expect(chat.route.id).toBe("azure-openai-chat")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("constructs Azure deployment URLs and preserves custom gateway URLs", async () => {
|
||||||
|
const Azure = await import("@opencode-ai/ai/providers/azure")
|
||||||
|
const deployment = Azure.model("custom-deployment", {
|
||||||
|
apiKey: "fixture",
|
||||||
|
resourceName: "opencode-test",
|
||||||
|
apiVersion: "2025-01-01-preview",
|
||||||
|
useDeploymentBasedUrls: true,
|
||||||
|
})
|
||||||
|
const gateway = Azure.model("gateway-model", {
|
||||||
|
apiKey: "fixture",
|
||||||
|
baseURL: "https://gateway.example/azure/",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deployment.route.endpoint).toMatchObject({
|
||||||
|
baseURL: "https://opencode-test.openai.azure.com/openai/deployments/custom-deployment",
|
||||||
|
query: { "api-version": "2025-01-01-preview" },
|
||||||
|
})
|
||||||
|
expect(gateway.route.endpoint.baseURL).toBe("https://gateway.example/azure")
|
||||||
|
expect(gateway.route.endpoint.query).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
test("maps Google package settings onto the Gemini model", async () => {
|
test("maps Google package settings onto the Gemini model", async () => {
|
||||||
const Google = await import("@opencode-ai/ai/providers/google")
|
const Google = await import("@opencode-ai/ai/providers/google")
|
||||||
const selected = Google.model("gemini-2.5-flash", {
|
const selected = Google.model("gemini-2.5-flash", {
|
||||||
|
|||||||
@@ -519,14 +519,8 @@ describe("Bedrock Converse route", () => {
|
|||||||
fixedBytes(
|
fixedBytes(
|
||||||
eventStreamBody(
|
eventStreamBody(
|
||||||
["messageStart", { role: "assistant" }],
|
["messageStart", { role: "assistant" }],
|
||||||
[
|
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } }],
|
||||||
"contentBlockDelta",
|
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
|
||||||
{ contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"contentBlockDelta",
|
|
||||||
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
|
|
||||||
],
|
|
||||||
["messageStop", { stopReason: "end_turn" }],
|
["messageStop", { stopReason: "end_turn" }],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -561,10 +555,7 @@ describe("Bedrock Converse route", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = eventStreamBody(
|
const body = eventStreamBody(
|
||||||
["messageStart", { role: "assistant" }],
|
["messageStart", { role: "assistant" }],
|
||||||
[
|
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
|
||||||
"contentBlockDelta",
|
|
||||||
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
|
|
||||||
],
|
|
||||||
["contentBlockStop", { contentBlockIndex: 0 }],
|
["contentBlockStop", { contentBlockIndex: 0 }],
|
||||||
["messageStop", { stopReason: "end_turn" }],
|
["messageStop", { stopReason: "end_turn" }],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ const model = Gemini.route
|
|||||||
})
|
})
|
||||||
.model({ id: "gemini-2.5-flash" })
|
.model({ id: "gemini-2.5-flash" })
|
||||||
|
|
||||||
|
const gemini3 = Gemini.route
|
||||||
|
.with({
|
||||||
|
endpoint: { baseURL: "https://generativelanguage.test/v1beta/" },
|
||||||
|
auth: Auth.header("x-goog-api-key", "test"),
|
||||||
|
})
|
||||||
|
.model({ id: "gemini-3-flash-preview" })
|
||||||
|
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
id: "req_1",
|
id: "req_1",
|
||||||
model,
|
model,
|
||||||
@@ -86,6 +93,39 @@ describe("Gemini route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("forwards standard Gemini generation options", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* compileRequest(
|
||||||
|
LLM.request({
|
||||||
|
model,
|
||||||
|
prompt: "Say hello.",
|
||||||
|
generation: {
|
||||||
|
maxTokens: 40,
|
||||||
|
temperature: 0.2,
|
||||||
|
topP: 0.8,
|
||||||
|
topK: 12,
|
||||||
|
frequencyPenalty: 0.3,
|
||||||
|
presencePenalty: 0.4,
|
||||||
|
seed: 42,
|
||||||
|
stop: ["done"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.generationConfig).toEqual({
|
||||||
|
maxOutputTokens: 40,
|
||||||
|
temperature: 0.2,
|
||||||
|
topP: 0.8,
|
||||||
|
topK: 12,
|
||||||
|
frequencyPenalty: 0.3,
|
||||||
|
presencePenalty: 0.4,
|
||||||
|
seed: 42,
|
||||||
|
stopSequences: ["done"],
|
||||||
|
thinkingConfig: undefined,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("lowers chronological system updates to wrapped user text in order", () =>
|
it.effect("lowers chronological system updates to wrapped user text in order", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prepared = yield* compileRequest(
|
const prepared = yield* compileRequest(
|
||||||
@@ -350,6 +390,100 @@ describe("Gemini route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("preserves nested empty object tool schemas", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* compileRequest(
|
||||||
|
LLM.request({
|
||||||
|
model,
|
||||||
|
prompt: "Use the tool.",
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "configure",
|
||||||
|
description: "Configure the operation",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["options"],
|
||||||
|
properties: {
|
||||||
|
options: { type: "object", description: "Optional provider settings", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.tools).toEqual([
|
||||||
|
{
|
||||||
|
functionDeclarations: [
|
||||||
|
{
|
||||||
|
name: "configure",
|
||||||
|
description: "Configure the operation",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
required: ["options"],
|
||||||
|
properties: {
|
||||||
|
options: { type: "object", description: "Optional provider settings", properties: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("projects Gemini type arrays without narrowing their allowed values", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* compileRequest(
|
||||||
|
LLM.request({
|
||||||
|
model,
|
||||||
|
prompt: "Use the tool.",
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "filter",
|
||||||
|
description: "Filter values",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: { type: ["number", "string"], description: "Status filter" },
|
||||||
|
maybe: { type: ["string", "null"] },
|
||||||
|
nothing: { type: ["null"] },
|
||||||
|
explicit: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||||
|
choice: { anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.tools?.[0]?.functionDeclarations[0]?.parameters).toEqual({
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
description: "Status filter",
|
||||||
|
anyOf: [{ type: "number" }, { type: "string" }],
|
||||||
|
},
|
||||||
|
maybe: {
|
||||||
|
nullable: true,
|
||||||
|
anyOf: [{ type: "string" }],
|
||||||
|
},
|
||||||
|
nothing: {
|
||||||
|
type: "null",
|
||||||
|
},
|
||||||
|
explicit: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
choice: {
|
||||||
|
anyOf: [{ type: "string" }, { type: "number" }],
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("parses text, reasoning, and usage stream fixtures", () =>
|
it.effect("parses text, reasoning, and usage stream fixtures", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = sseEvents(
|
const body = sseEvents(
|
||||||
@@ -536,6 +670,44 @@ describe("Gemini route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("replays unsigned Gemini 3 tool calls with the validator bypass sentinel", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* compileRequest(
|
||||||
|
LLM.request({
|
||||||
|
model: gemini3,
|
||||||
|
messages: [
|
||||||
|
Message.assistant([ToolCallPart.make({ id: "tool_0", name: "lookup", input: { query: "weather" } })]),
|
||||||
|
Message.tool({ id: "tool_0", name: "lookup", result: "done", resultType: "text" }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.contents).toEqual([
|
||||||
|
{
|
||||||
|
role: "model",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
functionCall: { id: undefined, name: "lookup", args: { query: "weather" } },
|
||||||
|
thoughtSignature: "skip_thought_signature_validator",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
functionResponse: {
|
||||||
|
id: undefined,
|
||||||
|
name: "lookup",
|
||||||
|
response: { name: "lookup", content: "done" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("emits streamed tool calls and maps finish reason", () =>
|
it.effect("emits streamed tool calls and maps finish reason", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = sseEvents({
|
const body = sseEvents({
|
||||||
|
|||||||
@@ -56,13 +56,14 @@ describe("Google Vertex providers", () => {
|
|||||||
|
|
||||||
it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () =>
|
it.effect("projects Anthropic Messages onto the Vertex raw-predict API", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const model = GoogleVertexMessages.configure({
|
||||||
|
accessToken: "vertex-token",
|
||||||
|
location: "eu",
|
||||||
|
project: "vertex-project",
|
||||||
|
}).model("claude-sonnet-4-6")
|
||||||
const response = yield* LLMClient.generate(
|
const response = yield* LLMClient.generate(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: GoogleVertexMessages.configure({
|
model,
|
||||||
accessToken: "vertex-token",
|
|
||||||
location: "eu",
|
|
||||||
project: "vertex-project",
|
|
||||||
}).model("claude-sonnet-4-6"),
|
|
||||||
prompt: "Say hello.",
|
prompt: "Say hello.",
|
||||||
}),
|
}),
|
||||||
).pipe(
|
).pipe(
|
||||||
@@ -97,6 +98,7 @@ describe("Google Vertex providers", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expect(model.provider).toBe("google-vertex")
|
||||||
expect(response.text).toBe("Hello.")
|
expect(response.text).toBe("Hello.")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
} from "../../src"
|
} from "../../src"
|
||||||
import * as Azure from "../../src/providers/azure"
|
import * as Azure from "../../src/providers/azure"
|
||||||
import * as OpenAI from "../../src/providers/openai"
|
import * as OpenAI from "../../src/providers/openai"
|
||||||
|
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||||
|
import * as XAI from "../../src/providers/xai"
|
||||||
import * as OpenAIChat from "../../src/protocols/openai-chat"
|
import * as OpenAIChat from "../../src/protocols/openai-chat"
|
||||||
import { ProviderShared } from "../../src/protocols/shared"
|
import { ProviderShared } from "../../src/protocols/shared"
|
||||||
import { Auth, LLMClient } from "../../src/route"
|
import { Auth, LLMClient } from "../../src/route"
|
||||||
@@ -100,6 +102,24 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("concatenates assistant text parts without adding separators", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* compileRequest(
|
||||||
|
LLM.request({
|
||||||
|
model,
|
||||||
|
messages: [
|
||||||
|
Message.assistant([
|
||||||
|
{ type: "text", text: "Hello" },
|
||||||
|
{ type: "text", text: " world" },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.messages).toEqual([{ role: "assistant", content: "Hello world" }])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("writes reasoning to a configured custom field on every assistant message", () =>
|
it.effect("writes reasoning to a configured custom field on every assistant message", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prepared = yield* compileRequest(
|
const prepared = yield* compileRequest(
|
||||||
@@ -154,6 +174,47 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("maps the request prompt cache key", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* compileRequest(
|
||||||
|
LLM.request({
|
||||||
|
model: OpenAICompatible.configure({
|
||||||
|
baseURL: "https://api.compatible.test/v1",
|
||||||
|
apiKey: "test",
|
||||||
|
}).model("compatible-model"),
|
||||||
|
prompt: "Hello",
|
||||||
|
promptCacheKey: "session_123",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.prompt_cache_key).toBe("session_123")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("maps the xAI Chat prompt cache key to conversation affinity", () =>
|
||||||
|
LLMClient.generate(
|
||||||
|
LLM.request({
|
||||||
|
model: XAI.configure({ apiKey: "test", baseURL: "https://api.x.ai/v1" }).chat("grok-4.5"),
|
||||||
|
prompt: "Hello",
|
||||||
|
promptCacheKey: "session_123",
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
dynamicResponse((input) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
|
||||||
|
expect(web.headers.get("x-grok-conv-id")).toBe("session_123")
|
||||||
|
const body = decodeJson(yield* Effect.promise(() => web.text()))
|
||||||
|
expect(ProviderShared.isRecord(body) ? body.prompt_cache_key : undefined).toBe("session_123")
|
||||||
|
return input.respond(sseEvents(deltaChunk({}, "stop")), {
|
||||||
|
headers: { "content-type": "text/event-stream" },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
|
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prepared = yield* compileRequest(
|
const prepared = yield* compileRequest(
|
||||||
@@ -535,7 +596,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prepared.body.messages).toEqual([{ role: "assistant", content: null, reasoning_content: "hidden" }])
|
expect(prepared.body.messages).toEqual([{ role: "assistant", content: "", reasoning_content: "hidden" }])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -784,7 +845,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("ignores scalar reasoning after content starts", () =>
|
it.effect("preserves scalar reasoning after content starts", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const details = [{ type: "reasoning.text", text: "detail", format: "unknown", index: 0 }]
|
const details = [{ type: "reasoning.text", text: "detail", format: "unknown", index: 0 }]
|
||||||
const response = yield* LLMClient.generate(request).pipe(
|
const response = yield* LLMClient.generate(request).pipe(
|
||||||
@@ -800,11 +861,11 @@ describe("OpenAI Chat route", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.reasoning).toBe("detail")
|
expect(response.reasoning).toBe("detailscalar")
|
||||||
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
|
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(2)
|
||||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(2)
|
||||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||||
openai: { reasoningDetails: details },
|
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -904,7 +965,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||||
|
|
||||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||||
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }])
|
expect(replay.body.messages).toEqual([{ role: "assistant", content: "", reasoning_details: details }])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -954,7 +1015,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(replay.body.messages).toEqual([
|
expect(replay.body.messages).toEqual([
|
||||||
{ role: "assistant", content: null, reasoning: "firstsecond", reasoning_details: [first, second] },
|
{ role: "assistant", content: "", reasoning: "firstsecond", reasoning_details: [first, second] },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -979,7 +1040,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(replay.body.messages).toEqual([
|
expect(replay.body.messages).toEqual([
|
||||||
{ role: "assistant", content: null, reasoning_content: "AB", reasoning_details: [detail] },
|
{ role: "assistant", content: "", reasoning_content: "AB", reasoning_details: [detail] },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -1001,7 +1062,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(replay.body.messages).toEqual([
|
expect(replay.body.messages).toEqual([
|
||||||
{ role: "assistant", content: null, reasoning_content: "thinking", reasoning_details: details },
|
{ role: "assistant", content: "", reasoning_content: "thinking", reasoning_details: details },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -1109,7 +1170,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("fails a streamed tool call when the provider ends without a finish reason", () =>
|
it.effect("finalizes a streamed tool call when the provider ends without a finish reason", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = sseEvents(
|
const body = sseEvents(
|
||||||
deltaChunk({
|
deltaChunk({
|
||||||
@@ -1121,27 +1182,31 @@ describe("OpenAI Chat route", () => {
|
|||||||
const input = LLMRequest.update(request, {
|
const input = LLMRequest.update(request, {
|
||||||
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
|
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
|
||||||
})
|
})
|
||||||
const events: LLMEvent[] = []
|
const response = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)))
|
||||||
const streamError = yield* LLMClient.stream(input).pipe(
|
|
||||||
Stream.runForEach((event) => Effect.sync(() => events.push(event))),
|
|
||||||
Effect.flip,
|
|
||||||
Effect.provide(fixedResponse(body)),
|
|
||||||
)
|
|
||||||
const error = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)), Effect.flip)
|
|
||||||
|
|
||||||
expect(events).toEqual([
|
expect(response.events).toEqual([
|
||||||
{ type: "step-start", index: 0 },
|
{ type: "step-start", index: 0 },
|
||||||
{ type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata: undefined },
|
{ type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata: undefined },
|
||||||
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' },
|
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' },
|
||||||
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' },
|
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' },
|
||||||
|
{ type: "tool-input-end", id: "call_1", name: "lookup", providerMetadata: undefined },
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
id: "call_1",
|
||||||
|
name: "lookup",
|
||||||
|
input: { query: "weather" },
|
||||||
|
providerExecuted: undefined,
|
||||||
|
providerMetadata: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "step-finish",
|
||||||
|
index: 0,
|
||||||
|
reason: { normalized: "tool-calls" },
|
||||||
|
usage: undefined,
|
||||||
|
providerMetadata: undefined,
|
||||||
|
},
|
||||||
|
{ type: "finish", reason: { normalized: "tool-calls" }, usage: undefined },
|
||||||
])
|
])
|
||||||
expect(events.filter(LLMEvent.is.toolCall)).toEqual([])
|
|
||||||
expect(streamError.reason).toMatchObject({
|
|
||||||
_tag: "InvalidProviderOutput",
|
|
||||||
classification: "incomplete-stream",
|
|
||||||
})
|
|
||||||
expect(streamError.message).toContain("The provider response ended unexpectedly.")
|
|
||||||
expect(error.message).toContain("The provider response ended unexpectedly.")
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { compileRequest } from "../../src/route/client"
|
|||||||
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
import * as OpenAICompatible from "../../src/providers/openai-compatible"
|
||||||
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
|
||||||
import { it } from "../lib/effect"
|
import { it } from "../lib/effect"
|
||||||
import { dynamicResponse } from "../lib/http"
|
import { dynamicResponse, fixedResponse } from "../lib/http"
|
||||||
import { sseEvents } from "../lib/sse"
|
import { sseEvents } from "../lib/sse"
|
||||||
|
|
||||||
const Json = Schema.fromJsonString(Schema.Unknown)
|
const Json = Schema.fromJsonString(Schema.Unknown)
|
||||||
@@ -253,4 +253,108 @@ describe("OpenAI-compatible Chat route", () => {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("accepts nullable usage and preserves provider fields", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const response = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
deltaChunk({ content: "Hello" }),
|
||||||
|
deltaChunk({}, "stop"),
|
||||||
|
usageChunk({
|
||||||
|
prompt_tokens: null,
|
||||||
|
completion_tokens: null,
|
||||||
|
total_tokens: null,
|
||||||
|
prompt_tokens_details: { cached_tokens: null, vendor_cache_tokens: 3 },
|
||||||
|
completion_tokens_details: {
|
||||||
|
reasoning_tokens: null,
|
||||||
|
accepted_prediction_tokens: null,
|
||||||
|
rejected_prediction_tokens: null,
|
||||||
|
},
|
||||||
|
cost: "0.001",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.usage).toMatchObject({
|
||||||
|
inputTokens: undefined,
|
||||||
|
outputTokens: undefined,
|
||||||
|
totalTokens: undefined,
|
||||||
|
providerMetadata: {
|
||||||
|
openai: {
|
||||||
|
prompt_tokens: null,
|
||||||
|
completion_tokens: null,
|
||||||
|
total_tokens: null,
|
||||||
|
prompt_tokens_details: { cached_tokens: null, vendor_cache_tokens: 3 },
|
||||||
|
cost: "0.001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("assembles indexless parallel tool calls across sparse chunks", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const response = yield* LLMClient.generate(
|
||||||
|
LLMRequest.update(request, {
|
||||||
|
tools: [
|
||||||
|
ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
deltaChunk({
|
||||||
|
tool_calls: [
|
||||||
|
{ id: "call_paris", function: { name: "weather", arguments: '{"city":"' } },
|
||||||
|
{ index: null, id: "call_london", function: { name: "weather", arguments: '{"city":"' } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
deltaChunk({ tool_calls: [{ function: { arguments: 'London"}' } }] }),
|
||||||
|
deltaChunk({ tool_calls: [{ id: "call_paris", function: { arguments: 'Paris"}' } }] }),
|
||||||
|
deltaChunk({}, "tool_calls"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.toolCalls).toMatchObject([
|
||||||
|
{ id: "call_paris", name: "weather", input: { city: "Paris" } },
|
||||||
|
{ id: "call_london", name: "weather", input: { city: "London" } },
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("treats an empty finish reason as terminal", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const response = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(fixedResponse(sseEvents(deltaChunk({ content: "Hello" }), deltaChunk({}, "")))),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.finishReason).toEqual({ normalized: "unknown", raw: "" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("rejects content after a terminal chunk", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const error = yield* LLMClient.generate(request).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
fixedResponse(
|
||||||
|
sseEvents(
|
||||||
|
deltaChunk({ content: "Hello" }),
|
||||||
|
deltaChunk({}, "stop"),
|
||||||
|
deltaChunk({ tool_calls: [{ index: 0, id: "call_1", function: { name: "lookup", arguments: "{}" } }] }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.flip,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(error.message).toContain("OpenAI Chat received content after the finish reason")
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const cacheRequest = LLM.request({
|
|||||||
system: LARGE_CACHEABLE_SYSTEM,
|
system: LARGE_CACHEABLE_SYSTEM,
|
||||||
prompt: "Say hi.",
|
prompt: "Say hi.",
|
||||||
generation: { maxTokens: 16, temperature: 0 },
|
generation: { maxTokens: 16, temperature: 0 },
|
||||||
providerOptions: { openai: { promptCacheKey: "recorded-cache-test" } },
|
promptCacheKey: "recorded-cache-test",
|
||||||
})
|
})
|
||||||
|
|
||||||
const recorded = recordedTests({
|
const recorded = recordedTests({
|
||||||
|
|||||||
@@ -682,9 +682,9 @@ describe("OpenAI Responses route", () => {
|
|||||||
LLM.request({
|
LLM.request({
|
||||||
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
|
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
|
||||||
prompt: "think",
|
prompt: "think",
|
||||||
|
promptCacheKey: "session_123",
|
||||||
providerOptions: {
|
providerOptions: {
|
||||||
openai: {
|
openai: {
|
||||||
promptCacheKey: "session_123",
|
|
||||||
reasoningEffort: "high",
|
reasoningEffort: "high",
|
||||||
reasoningSummary: "auto",
|
reasoningSummary: "auto",
|
||||||
include: ["reasoning.encrypted_content"],
|
include: ["reasoning.encrypted_content"],
|
||||||
@@ -803,17 +803,16 @@ describe("OpenAI Responses route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("request OpenAI provider options override route defaults", () =>
|
it.effect("maps the request prompt cache key", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prepared = yield* compileRequest(
|
const prepared = yield* compileRequest(
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model: OpenAI.configure({
|
model: OpenAI.configure({
|
||||||
baseURL: "https://api.openai.test/v1/",
|
baseURL: "https://api.openai.test/v1/",
|
||||||
apiKey: "test",
|
apiKey: "test",
|
||||||
providerOptions: { openai: { promptCacheKey: "model_cache" } },
|
|
||||||
}).model("gpt-4.1-mini"),
|
}).model("gpt-4.1-mini"),
|
||||||
prompt: "no cache",
|
prompt: "no cache",
|
||||||
providerOptions: { openai: { promptCacheKey: "request_cache" } },
|
promptCacheKey: "request_cache",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ describe("OpenRouter", () => {
|
|||||||
openrouter: {
|
openrouter: {
|
||||||
usage: true,
|
usage: true,
|
||||||
reasoning: { effort: "high" },
|
reasoning: { effort: "high" },
|
||||||
promptCacheKey: "session_123",
|
|
||||||
models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"],
|
models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"],
|
||||||
provider: { order: ["anthropic", "google"], require_parameters: true },
|
provider: { order: ["anthropic", "google"], require_parameters: true },
|
||||||
plugins: [{ id: "response-healing" }],
|
plugins: [{ id: "response-healing" }],
|
||||||
@@ -174,6 +173,7 @@ describe("OpenRouter", () => {
|
|||||||
},
|
},
|
||||||
}).model("anthropic/claude-3.7-sonnet:thinking"),
|
}).model("anthropic/claude-3.7-sonnet:thinking"),
|
||||||
prompt: "Think briefly.",
|
prompt: "Think briefly.",
|
||||||
|
promptCacheKey: "session_123",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,21 @@
|
|||||||
|
|
||||||
- Always prefer `createStore` over multiple `createSignal` calls
|
- Always prefer `createStore` over multiple `createSignal` calls
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors.
|
||||||
|
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
||||||
|
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
||||||
|
- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar.
|
||||||
|
- Prefer complete translated phrases. Do not concatenate grammatical fragments or make call sites assemble sentences. Keep placeholders to irreducible dynamic values such as names, paths, and counts.
|
||||||
|
- If a translation cannot be expressed by the current API, deepen the shared language/UI i18n module so one typed call owns locale selection, plural resolution, fallback, and interpolation. Do not leak that machinery into product code.
|
||||||
|
- Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`.
|
||||||
|
- For developer-facing terminology, prefer the words already used by the target language's developer community over literal dictionary translations. Cross-check maintained localized developer products such as Firefox, KDE, and VS Code; use at least two independent corpora when they are available. If established practice keeps an English loanword or acronym, keep it rather than inventing a translation.
|
||||||
|
- Translate complete UI phrases in context. A glossary hit is evidence, not permission to translate word-by-word. Check terse labels such as session, prompt, agent, model, fork, shell, terminal, workspace, and worktree in the same grammatical role before choosing a term.
|
||||||
|
- Before a locale is ready, audit recurring concepts for one consistent translation and review every value that still equals English. Classify retained English as a product name, provider/tool name, URL, code token, keyboard legend, acronym, asset name, or established borrowing; translate unexplained leftovers.
|
||||||
|
- In translation review notes, name the corpora used and call out uncertain or region-specific terminology so native speakers can focus review where it matters.
|
||||||
|
- Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels.
|
||||||
|
|
||||||
## Tool Calling
|
## Tool Calling
|
||||||
|
|
||||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
# V1 API Migration Checklist
|
|
||||||
|
|
||||||
The app is currently hybrid. In this document, V1 refers to the legacy unprefixed server APIs used by `@opencode-ai/sdk/v2`, despite the SDK package name.
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
- [x] Replace `GET /global/event` with `GET /api/event`.
|
|
||||||
- `src/context/server-sdk.tsx`
|
|
||||||
- [x] Reduce current granular session and message events into the existing app projections.
|
|
||||||
- `src/context/server-session-v2-reducer.ts`
|
|
||||||
- `src/context/server-session.ts`
|
|
||||||
- [ ] Remove transitional session event dependencies: `session.created`, `session.updated`, `session.diff`, `session.status`, `session.idle`, and `session.error`.
|
|
||||||
- `src/context/global-sync/event-reducer.ts`
|
|
||||||
- `src/context/server-session.ts`
|
|
||||||
- `src/context/notification.tsx`
|
|
||||||
- `src/pages/session/usage-exceeded-dialogs.tsx`
|
|
||||||
- [ ] Remove legacy message event compatibility: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`, and `message.part.delta`.
|
|
||||||
- `src/context/global-sync/event-reducer.ts`
|
|
||||||
- `src/context/server-session.ts`
|
|
||||||
- [x] Adapt current permission and question events to the existing request model.
|
|
||||||
- `src/context/global-sync/event-reducer.ts`
|
|
||||||
- `src/context/permission.tsx`
|
|
||||||
- [x] Consume current file watcher events.
|
|
||||||
- `src/context/file.tsx`
|
|
||||||
- [x] Consume current VCS events.
|
|
||||||
- `src/context/global-sync/event-reducer.ts`
|
|
||||||
- `src/pages/session.tsx`
|
|
||||||
- [x] Consume current `pty.exited` events.
|
|
||||||
- `src/context/terminal.tsx`
|
|
||||||
- [ ] Migrate LSP and reference events.
|
|
||||||
- `src/context/global-sync/event-reducer.ts`
|
|
||||||
|
|
||||||
## Sessions
|
|
||||||
|
|
||||||
- [x] Replace `GET /session/status` with one server-scoped `GET /api/session/active` snapshot plus V2 execution events.
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- [x] Migrate session listing from `GET /session`.
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- `src/context/directory-sync.ts`
|
|
||||||
- `src/pages/layout.tsx`
|
|
||||||
- [x] Migrate the remaining direct session read from `GET /session/:sessionID`.
|
|
||||||
- `src/components/titlebar.tsx`
|
|
||||||
- [x] Migrate session updates from `PATCH /session/:sessionID`.
|
|
||||||
- `src/context/directory-sync.ts`
|
|
||||||
- `src/context/layout.tsx`
|
|
||||||
- `src/pages/home.tsx`
|
|
||||||
- `src/pages/layout.tsx`
|
|
||||||
- `src/pages/session/timeline/message-timeline.tsx`
|
|
||||||
- `src/components/titlebar-tab-nav.tsx`
|
|
||||||
- Renames use `POST /api/session/:sessionID/rename`; archival uses `POST /api/session/:sessionID/archive`.
|
|
||||||
- [x] Migrate session deletion from `DELETE /session/:sessionID`.
|
|
||||||
- `src/pages/session/timeline/message-timeline.tsx`
|
|
||||||
- [x] Remove session diff loading from `GET /session/:sessionID/diff`.
|
|
||||||
- Historical Session diffs remain unavailable until the current API defines their snapshot semantics.
|
|
||||||
- [x] Migrate abort from `POST /session/:sessionID/abort`.
|
|
||||||
- `src/components/prompt-input/submit.ts`
|
|
||||||
- `src/pages/session/use-session-commands.tsx`
|
|
||||||
- `src/pages/session.tsx`
|
|
||||||
- [x] Migrate revert and unrevert from `POST /session/:sessionID/revert` and `POST /session/:sessionID/unrevert`.
|
|
||||||
- `src/pages/session/use-session-commands.tsx`
|
|
||||||
- `src/pages/session.tsx`
|
|
||||||
- [x] Replace `POST /session/:sessionID/summarize` with the current compact API.
|
|
||||||
- `src/pages/session/use-session-commands.tsx`
|
|
||||||
- [x] Migrate slash commands from `POST /session/:sessionID/command`.
|
|
||||||
- `src/components/prompt-input/submit.ts`
|
|
||||||
- [x] Migrate shell execution from `POST /session/:sessionID/shell`.
|
|
||||||
- `src/components/prompt-input/submit.ts`
|
|
||||||
- [x] Migrate session fork from `POST /session/:sessionID/fork`.
|
|
||||||
- `src/components/dialog-fork.tsx`
|
|
||||||
- [ ] Migrate sharing from `POST /session/:sessionID/share` and `DELETE /session/:sessionID/share`.
|
|
||||||
- `src/pages/session/use-session-commands.tsx`
|
|
||||||
- `src/pages/session/timeline/message-timeline.tsx`
|
|
||||||
- Blocked: the current API has no sharing contract or implementation.
|
|
||||||
|
|
||||||
## Session Compatibility Fallbacks
|
|
||||||
|
|
||||||
These calls are retained as fallback adapters. The current production path supplies the current session and message APIs.
|
|
||||||
|
|
||||||
- [ ] Remove fallback `GET /session/:sessionID` after compatibility support is unnecessary.
|
|
||||||
- `src/context/server-session.ts`
|
|
||||||
- [ ] Remove fallback `GET /session/:sessionID/message` after compatibility support is unnecessary.
|
|
||||||
- `src/context/server-session.ts`
|
|
||||||
- [ ] Remove fallback `GET /session/:sessionID/message/:messageID` after compatibility support is unnecessary.
|
|
||||||
- `src/context/server-session.ts`
|
|
||||||
|
|
||||||
## Filesystem
|
|
||||||
|
|
||||||
- [ ] Migrate file listing from `GET /file`.
|
|
||||||
- `src/context/file.tsx`
|
|
||||||
- [ ] Migrate file reads from `GET /file/content`.
|
|
||||||
- `src/context/file.tsx`
|
|
||||||
- `src/pages/session/review-tab.tsx`
|
|
||||||
- `src/pages/session/v2/review-panel-v2.tsx`
|
|
||||||
- [x] Migrate path discovery from `GET /path` to `GET /api/path`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- `src/components/dialog-select-directory.tsx`
|
|
||||||
- `src/components/dialog-select-directory-v2.tsx`
|
|
||||||
|
|
||||||
## Projects And Worktrees
|
|
||||||
|
|
||||||
- [x] Migrate project listing from `GET /project` to `GET /api/project`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- [x] Migrate the current project lookup from `GET /project/current` to `GET /api/project/current`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- [ ] Migrate Git initialization from `POST /project/git/init`.
|
|
||||||
- `src/pages/session.tsx`
|
|
||||||
- [x] Migrate project updates from `PATCH /project/:projectID` to `PATCH /api/project/:projectID`.
|
|
||||||
- `src/context/layout.tsx`
|
|
||||||
- `src/components/edit-project.ts`
|
|
||||||
- `src/pages/layout.tsx`
|
|
||||||
- [ ] Migrate experimental worktree listing, creation, removal, and reset from `/experimental/worktree`.
|
|
||||||
- `src/pages/layout.tsx`
|
|
||||||
- `src/components/prompt-input/submit.ts`
|
|
||||||
- Listing now uses `GET /api/project/:projectID/directories`; create, removal, and reset remain.
|
|
||||||
- [ ] Migrate instance disposal from `POST /instance/dispose`.
|
|
||||||
- `src/pages/layout.tsx`
|
|
||||||
|
|
||||||
## VCS
|
|
||||||
|
|
||||||
- [x] Migrate repository information from `GET /vcs` to `GET /api/vcs`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- [x] Migrate diffs from `GET /vcs/diff` to `GET /api/vcs/diff`.
|
|
||||||
- `src/pages/session.tsx`
|
|
||||||
- [x] Migrate status from `GET /vcs/status` to `GET /api/vcs/status`.
|
|
||||||
- `src/pages/layout.tsx`
|
|
||||||
|
|
||||||
## Configuration And Authentication
|
|
||||||
|
|
||||||
- [ ] Migrate global configuration reads from `GET /global/config`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- [ ] Migrate directory configuration reads from `GET /config`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- [ ] Migrate global configuration updates from `PATCH /global/config`.
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- [x] Migrate provider authentication method discovery from `GET /provider/auth` to `GET /api/integration/:integrationID`.
|
|
||||||
- `src/components/dialog-connect-provider.tsx`
|
|
||||||
- [x] Migrate built-in provider OAuth authorization and callbacks to `/api/integration/:integrationID/connect/oauth/*`.
|
|
||||||
- `src/components/dialog-connect-provider.tsx`
|
|
||||||
- [ ] Migrate remaining credentials from `PUT /auth/:providerID` and `DELETE /auth/:providerID`.
|
|
||||||
- Built-in provider key connections now use `POST /api/integration/:integrationID/connect/key`.
|
|
||||||
- `src/components/dialog-connect-provider.tsx`
|
|
||||||
- `src/components/dialog-custom-provider.tsx`
|
|
||||||
- `src/components/settings-providers.tsx`
|
|
||||||
- `src/components/settings-v2/providers.tsx`
|
|
||||||
- [ ] Migrate global disposal from `POST /global/dispose`.
|
|
||||||
- `src/components/dialog-connect-provider.tsx`
|
|
||||||
- `src/components/settings-providers.tsx`
|
|
||||||
- `src/components/settings-v2/providers.tsx`
|
|
||||||
|
|
||||||
## Permissions And Questions
|
|
||||||
|
|
||||||
- [x] Migrate permission listing from `GET /permission` to `GET /api/permission/request`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- `src/context/permission.tsx`
|
|
||||||
- [x] Migrate permission responses from `/session/:sessionID/permissions/:permissionID`.
|
|
||||||
- `src/context/permission.tsx`
|
|
||||||
- `src/pages/session/composer/session-composer-state.ts`
|
|
||||||
- [x] Migrate question listing from `GET /question` to `GET /api/question/request`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- [x] Migrate question replies and rejections from `/question/:requestID/*` to `/api/session/:sessionID/question/:requestID/*`.
|
|
||||||
- `src/pages/session/composer/session-question-dock.tsx`
|
|
||||||
|
|
||||||
## Commands, MCP, LSP, And References
|
|
||||||
|
|
||||||
- [x] Migrate command listing from `GET /command` to `GET /api/command`.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- [x] Migrate MCP listing, connection, and disconnection from `/mcp` to `/api/mcp`.
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- [ ] Replace legacy MCP authentication with the Integration OAuth workflow.
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- [x] Migrate experimental resource listing from `GET /experimental/resource` to `GET /api/mcp/resource`.
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- [ ] Migrate LSP status from `GET /lsp`.
|
|
||||||
- `src/context/server-sync.tsx`
|
|
||||||
- [x] Move `GET /api/reference` off the legacy generated SDK transport.
|
|
||||||
- `src/context/global-sync/bootstrap.ts`
|
|
||||||
|
|
||||||
## Search
|
|
||||||
|
|
||||||
- [x] Migrate global session search from `GET /experimental/session` to `GET /api/session`.
|
|
||||||
- `src/components/command-palette.ts`
|
|
||||||
- `src/components/dialog-command-palette-v2.tsx`
|
|
||||||
|
|
||||||
## PTY And Terminal
|
|
||||||
|
|
||||||
- [x] Migrate PTY creation, reads, updates, and deletion from `/pty` to `/api/pty`.
|
|
||||||
- `src/context/terminal.tsx`
|
|
||||||
- `src/components/terminal.tsx`
|
|
||||||
- [x] Migrate shell listing from `GET /pty/shells` to `GET /api/pty/shells`.
|
|
||||||
- `src/components/settings-general.tsx`
|
|
||||||
- `src/components/settings-v2/general.tsx`
|
|
||||||
- [x] Migrate connection tokens from `POST /pty/:ptyID/connect-token` to `POST /api/pty/:ptyID/connect-token`.
|
|
||||||
- `src/components/terminal.tsx`
|
|
||||||
- [x] Migrate the direct WebSocket connection from `/pty/:ptyID/connect` to `/api/pty/:ptyID/connect`.
|
|
||||||
- `src/components/terminal.tsx`
|
|
||||||
|
|
||||||
## Legacy Types And Adapters
|
|
||||||
|
|
||||||
These are not V1 network requests, but they keep the UI coupled to V1 data contracts.
|
|
||||||
|
|
||||||
- [ ] Replace the current-session-to-legacy-session adapter.
|
|
||||||
- `src/utils/session.ts`
|
|
||||||
- [ ] Replace the current-message-to-legacy-message-and-part adapter.
|
|
||||||
- `src/utils/session-message.ts`
|
|
||||||
- [ ] Replace current agent, provider, and model adapters to legacy SDK structures.
|
|
||||||
- `src/context/global-sync/utils.ts`
|
|
||||||
- [ ] Replace legacy `Session`, `Message`, `Part`, `PermissionRequest`, `QuestionRequest`, `Project`, `FileNode`, `FileDiffInfo`, and `Event` types throughout app state and rendering.
|
|
||||||
- [ ] Remove the `@opencode-ai/sdk` runtime dependency after all legacy calls and types are gone.
|
|
||||||
- `package.json`
|
|
||||||
|
|
||||||
## Test Infrastructure
|
|
||||||
|
|
||||||
- [ ] Replace V1 endpoint mocks with current API mocks.
|
|
||||||
- `e2e/utils/mock-server.ts`
|
|
||||||
- [x] Replace `/global/event` and `/event` interception with current event transport handling.
|
|
||||||
- `e2e/utils/sse-transport.ts`
|
|
||||||
- [ ] Replace `SessionV1` and legacy SDK fixtures in timeline performance tests.
|
|
||||||
- `e2e/performance/timeline-stability/fixture.ts`
|
|
||||||
- [ ] Remove remaining legacy SDK type fixtures from unit and browser tests.
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## Required Reading
|
||||||
|
|
||||||
|
- Before writing, changing, or reviewing E2E tests, ALWAYS read and follow Playwright's official [Best Practices](https://playwright.dev/docs/best-practices), [Auto-waiting](https://playwright.dev/docs/actionability), and [Assertions](https://playwright.dev/docs/test-assertions) guides.
|
||||||
|
- Use the official [Locators](https://playwright.dev/docs/locators), [Network](https://playwright.dev/docs/network), and [Test Isolation](https://playwright.dev/docs/browser-contexts) guides when those concerns apply.
|
||||||
|
|
||||||
|
## Test Hygiene
|
||||||
|
|
||||||
|
- Test user-visible behavior with isolated, deterministic data and scoped, unique locators.
|
||||||
|
- Prefer role, label, text, and explicit test-contract locators. Do not use `.first()` or `.last()` merely to silence strictness errors.
|
||||||
|
- Use locator actions, Playwright auto-waiting, and web-first assertions for observable readiness and outcomes.
|
||||||
|
- NEVER use `waitForTimeout`, `setTimeout`, sleeps, animation-frame counts, or other wall-clock delays to synchronize a test. Wait for the specific UI state, request, response, event, or application outcome instead.
|
||||||
|
- Do not treat navigation, a network response, DOM attachment, or visibility alone as proof that asynchronously rendered UI is ready. Assert the state the next action actually requires.
|
||||||
|
- Register event and network waits before the action that triggers them.
|
||||||
|
- Do not retry state-changing actions. Retry idempotent readiness checks, then perform the action once and assert its outcome.
|
||||||
|
- Keep action and assertion timeouts adaptive. Do not use short timeouts as readiness probes or rely on retries to hide flakes.
|
||||||
|
- Assert exact outcomes and identities so stale state, duplicate rendering, and interactions with the wrong element cannot pass.
|
||||||
@@ -45,10 +45,6 @@ describe("timeline fixture validation", () => {
|
|||||||
expect(first.payload.id).toMatch(/^evt_timeline_\d{4}$/)
|
expect(first.payload.id).toMatch(/^evt_timeline_\d{4}$/)
|
||||||
expect(Number(second.payload.id.slice(-4))).toBe(Number(first.payload.id.slice(-4)) + 1)
|
expect(Number(second.payload.id.slice(-4))).toBe(Number(first.payload.id.slice(-4)) + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses the projected tool ID as its call ID", () => {
|
|
||||||
expect(toolPart("call_1", "read", "running", {})).toMatchObject({ id: "call_1", callID: "call_1" })
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (false) {
|
if (false) {
|
||||||
|
|||||||
@@ -2,20 +2,13 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
|
|||||||
import { Event } from "@opencode-ai/schema/event"
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event"
|
import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event"
|
||||||
import { SessionV1 } from "@opencode-ai/schema/session-v1"
|
import { SessionV1 } from "@opencode-ai/schema/session-v1"
|
||||||
import type {
|
import type { SessionInfo, SessionStatus } from "@opencode-ai/client/promise"
|
||||||
AssistantMessage,
|
import type { AssistantMessage, Message, Part, ToolPart, ToolState, UserMessage } from "../../../src/types"
|
||||||
Message,
|
|
||||||
Part,
|
|
||||||
ToolPart,
|
|
||||||
ToolState,
|
|
||||||
UserMessage,
|
|
||||||
} from "../../../src/types"
|
|
||||||
import type { SessionV1Info, SessionStatus } from "@opencode-ai/client/promise"
|
|
||||||
import { expect, type Page } from "@playwright/test"
|
import { expect, type Page } from "@playwright/test"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { mockOpenCodeServer } from "../../utils/mock-server"
|
import { mockOpenCodeServer } from "../../utils/mock-server"
|
||||||
import { installSseTransport } from "../../utils/sse-transport"
|
import { installSseTransport } from "../../utils/sse-transport"
|
||||||
import { expectSessionTitle } from "../../utils/waits"
|
import { expectSessionReady } from "../../utils/waits"
|
||||||
|
|
||||||
export const directory = "C:/OpenCode/TimelineStability"
|
export const directory = "C:/OpenCode/TimelineStability"
|
||||||
export const projectID = "proj_timeline_stability"
|
export const projectID = "proj_timeline_stability"
|
||||||
@@ -25,7 +18,7 @@ export const assistantID = "msg_1001_timeline_assistant"
|
|||||||
export const title = "Timeline visual stability"
|
export const title = "Timeline visual stability"
|
||||||
export const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
export const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
||||||
|
|
||||||
type Session = SessionV1Info
|
type Session = SessionInfo
|
||||||
type GlobalEvent = {
|
type GlobalEvent = {
|
||||||
directory: string
|
directory: string
|
||||||
project?: string
|
project?: string
|
||||||
@@ -118,8 +111,9 @@ export async function setupTimeline(
|
|||||||
active?.info.role === "assistant" && active.info.time.completed === undefined ? { type: "busy" } : { type: "idle" },
|
active?.info.role === "assistant" && active.info.time.completed === undefined ? { type: "busy" } : { type: "idle" },
|
||||||
decodeOptions,
|
decodeOptions,
|
||||||
)
|
)
|
||||||
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
const transport = await installSseTransport<EventPayload>(page, {
|
const transport = await installSseTransport<EventPayload>(page, {
|
||||||
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
|
server,
|
||||||
retry: input.eventRetry ?? 20,
|
retry: input.eventRetry ?? 20,
|
||||||
})
|
})
|
||||||
await mockOpenCodeServer(page, {
|
await mockOpenCodeServer(page, {
|
||||||
@@ -168,8 +162,8 @@ export async function setupTimeline(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
|
await expectSessionReady(page, { server, sessionID, title })
|
||||||
await transport.waitForConnection()
|
await transport.waitForConnection()
|
||||||
await expectSessionTitle(page, title)
|
|
||||||
if (input.cpuRate && input.cpuRate > 1) {
|
if (input.cpuRate && input.cpuRate > 1) {
|
||||||
const devtools = await page.context().newCDPSession(page)
|
const devtools = await page.context().newCDPSession(page)
|
||||||
await devtools.send("Emulation.setCPUThrottlingRate", { rate: input.cpuRate })
|
await devtools.send("Emulation.setCPUThrottlingRate", { rate: input.cpuRate })
|
||||||
@@ -205,7 +199,9 @@ export async function setupTimeline(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
async waitForPart(partID: string) {
|
async waitForPart(partID: string) {
|
||||||
await expect(page.locator(`[data-timeline-part-id="${partID}"]`).first()).toBeVisible()
|
const part = page.locator(`[data-timeline-part-id="${partID}"]`)
|
||||||
|
await expect(part).toHaveCount(1)
|
||||||
|
await expect(part).toBeVisible()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,11 +533,11 @@ export function project() {
|
|||||||
export function session(input: Partial<Session> = {}): Session {
|
export function session(input: Partial<Session> = {}): Session {
|
||||||
return {
|
return {
|
||||||
id: sessionID,
|
id: sessionID,
|
||||||
slug: "timeline-stability",
|
|
||||||
projectID,
|
projectID,
|
||||||
directory,
|
location: { directory },
|
||||||
title,
|
title,
|
||||||
version: "dev",
|
cost: 0,
|
||||||
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
...input,
|
...input,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ test("shows all and expands historical diff summary without overlap", async ({ p
|
|||||||
summary: {
|
summary: {
|
||||||
diffs: Array.from({ length: 12 }, (_, index) => ({
|
diffs: Array.from({ length: 12 }, (_, index) => ({
|
||||||
file: `src/diff-${index}.ts`,
|
file: `src/diff-${index}.ts`,
|
||||||
|
status: "modified",
|
||||||
additions: 1,
|
additions: 1,
|
||||||
deletions: 1,
|
deletions: 1,
|
||||||
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
|
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
|
||||||
|
|||||||
@@ -85,8 +85,7 @@ async function mockServers(page: Page, requests: string[]) {
|
|||||||
const current = url.origin === serverA ? sessionA : sessionB
|
const current = url.origin === serverA ? sessionA : sessionB
|
||||||
const directory = url.searchParams.get("directory")
|
const directory = url.searchParams.get("directory")
|
||||||
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
|
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
|
||||||
if (url.pathname === "/api/event")
|
if (url.pathname === "/api/event") return sse(route)
|
||||||
return sse(route)
|
|
||||||
if (url.pathname === "/api/health") return json(route, { pid: 1 })
|
if (url.pathname === "/api/health") return json(route, { pid: 1 })
|
||||||
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
|
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/active") return json(route, { data: {} })
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { expect, test } from "@playwright/test"
|
||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
|
||||||
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
import { expectAppVisible } from "../utils/waits"
|
||||||
|
|
||||||
|
const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"]
|
||||||
|
const worktrees = NAMES.map((name) => `/opencode-demo/${name}`)
|
||||||
|
|
||||||
|
// The sixth project sits outside the five-item recent cap, so it is only reachable if the
|
||||||
|
// dialog hands every recent project to the list filter instead of a pre-truncated slice.
|
||||||
|
const OUTSIDE_CAP = "foxtrot-docs"
|
||||||
|
|
||||||
|
// Dialog rows carry data-directory-path; the sidebar project list does not, so this
|
||||||
|
// scopes assertions to the picker instead of matching the sidebar entry of the same name.
|
||||||
|
const rows = (page: Page) => page.locator("[data-directory-path]")
|
||||||
|
const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`)
|
||||||
|
|
||||||
|
async function openProjectDialog(page: Page) {
|
||||||
|
await mockOpenCodeServer(page, {
|
||||||
|
sessions: fixture.sessions,
|
||||||
|
provider: fixture.provider,
|
||||||
|
directory: fixture.directory,
|
||||||
|
project: fixture.project,
|
||||||
|
pageMessages,
|
||||||
|
fileList: () => [],
|
||||||
|
findFiles: () => [],
|
||||||
|
})
|
||||||
|
await page.addInitScript((dirs) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
"opencode.global.dat:server",
|
||||||
|
JSON.stringify({
|
||||||
|
projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) },
|
||||||
|
lastProject: {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, worktrees)
|
||||||
|
await page.goto("/")
|
||||||
|
const add = page.getByRole("button", { name: "Add project" }).first()
|
||||||
|
await expectAppVisible(add)
|
||||||
|
await add.click()
|
||||||
|
await expect(rows(page)).toHaveCount(5)
|
||||||
|
return page.getByRole("textbox").last()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("searches every recent project, not just the five most recent", async ({ page }) => {
|
||||||
|
const search = await openProjectDialog(page)
|
||||||
|
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
|
||||||
|
|
||||||
|
await search.fill("foxtrot")
|
||||||
|
|
||||||
|
await expect(row(page, OUTSIDE_CAP)).toHaveCount(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("still caps the idle recent list at five projects", async ({ page }) => {
|
||||||
|
await openProjectDialog(page)
|
||||||
|
|
||||||
|
await expect(row(page, NAMES[4])).toHaveCount(1)
|
||||||
|
await expect(row(page, OUTSIDE_CAP)).toHaveCount(0)
|
||||||
|
})
|
||||||
@@ -181,12 +181,17 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
|
|||||||
return json(route, true)
|
return json(route, true)
|
||||||
}
|
}
|
||||||
if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
|
if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
|
||||||
if (url.pathname === "/api/event")
|
if (url.pathname === "/api/event") return sse(route)
|
||||||
return sse(route)
|
|
||||||
if (url.pathname === "/api/provider")
|
if (url.pathname === "/api/provider")
|
||||||
return json(route, {
|
return json(route, {
|
||||||
location: { directory },
|
location: { directory },
|
||||||
data: [{ id: remote ? "server-b" : "server-a", name: remote ? "Server B Provider" : "Server A Provider", package: "test" }],
|
data: [
|
||||||
|
{
|
||||||
|
id: remote ? "server-b" : "server-a",
|
||||||
|
name: remote ? "Server B Provider" : "Server A Provider",
|
||||||
|
package: "test",
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
if (url.pathname === "/api/model") return json(route, { location: { directory }, data: [model(remote)] })
|
if (url.pathname === "/api/model") return json(route, { location: { directory }, data: [model(remote)] })
|
||||||
if (url.pathname === "/api/model/default") return json(route, { location: { directory }, data: model(remote) })
|
if (url.pathname === "/api/model/default") return json(route, { location: { directory }, data: model(remote) })
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ async function mockServers(page: Page) {
|
|||||||
const current = url.origin === serverA ? sessionA : sessionB
|
const current = url.origin === serverA ? sessionA : sessionB
|
||||||
const directory = url.searchParams.get("directory")
|
const directory = url.searchParams.get("directory")
|
||||||
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
|
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
|
||||||
if (url.pathname === "/api/event")
|
if (url.pathname === "/api/event") return sse(route, url.pathname === "/api/event")
|
||||||
return sse(route, url.pathname === "/api/event")
|
|
||||||
if (url.pathname === "/api/health") return json(route, { pid: 1 })
|
if (url.pathname === "/api/health") return json(route, { pid: 1 })
|
||||||
if (url.pathname === "/api/session/active")
|
if (url.pathname === "/api/session/active")
|
||||||
return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} })
|
return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} })
|
||||||
|
|||||||
+8
-10
@@ -1,19 +1,18 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, test } from "@playwright/test"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
||||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
|
|
||||||
const draftID = "draft_legacy_new_session"
|
const draftID = "draft_removed_layout_preference"
|
||||||
const directory = "C:/OpenCode/LegacyNewSession"
|
const directory = "C:/OpenCode/RemovedLayoutPreference"
|
||||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
|
||||||
test("redirects a draft to the legacy new-session route", async ({ page }) => {
|
test("ignores persisted old layout preferences when opening drafts", async ({ page }) => {
|
||||||
await mockOpenCodeServer(page, {
|
await mockOpenCodeServer(page, {
|
||||||
directory,
|
directory,
|
||||||
project: {
|
project: {
|
||||||
id: "proj_legacy_new_session",
|
id: "proj_removed_layout_preference",
|
||||||
worktree: directory,
|
worktree: directory,
|
||||||
vcs: "git",
|
vcs: "git",
|
||||||
name: "legacy-new-session",
|
name: "removed-layout-preference",
|
||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
sandboxes: [],
|
sandboxes: [],
|
||||||
},
|
},
|
||||||
@@ -24,7 +23,6 @@ test("redirects a draft to the legacy new-session route", async ({ page }) => {
|
|||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
({ directory, draftID, server }) => {
|
({ directory, draftID, server }) => {
|
||||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
|
||||||
localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" }))
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.window.browser.dat:tabs",
|
"opencode.window.browser.dat:tabs",
|
||||||
JSON.stringify([{ type: "draft", draftID, server, directory }]),
|
JSON.stringify([{ type: "draft", draftID, server, directory }]),
|
||||||
@@ -35,7 +33,7 @@ test("redirects a draft to the legacy new-session route", async ({ page }) => {
|
|||||||
|
|
||||||
await page.goto(`/new-session?draftId=${draftID}`)
|
await page.goto(`/new-session?draftId=${draftID}`)
|
||||||
|
|
||||||
await expect(page).toHaveURL(`/${base64Encode(directory)}/session`)
|
await expect(page).toHaveURL(`/new-session?draftId=${draftID}`)
|
||||||
await expect(page.locator("header[data-tauri-drag-region]")).toBeVisible()
|
await expect(page.locator("body")).toHaveAttribute("data-new-layout", "")
|
||||||
await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
|
await expect(page.getByRole("textbox", { name: "Prompt" })).toBeVisible()
|
||||||
})
|
})
|
||||||
@@ -18,6 +18,7 @@ test("opens the comment editor when code is clicked", async ({ page }) => {
|
|||||||
await line.click()
|
await line.click()
|
||||||
|
|
||||||
await expect(review.getByRole("textbox")).toBeVisible()
|
await expect(review.getByRole("textbox")).toBeVisible()
|
||||||
|
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 2")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("opens the comment editor when a line number is clicked", async ({ page }) => {
|
test("opens the comment editor when a line number is clicked", async ({ page }) => {
|
||||||
@@ -27,6 +28,7 @@ test("opens the comment editor when a line number is clicked", async ({ page })
|
|||||||
await lineNumber.click()
|
await lineNumber.click()
|
||||||
|
|
||||||
await expect(review.getByRole("textbox")).toBeVisible()
|
await expect(review.getByRole("textbox")).toBeVisible()
|
||||||
|
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 1")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("opens the comment editor for a line number range", async ({ page }) => {
|
test("opens the comment editor for a line number range", async ({ page }) => {
|
||||||
@@ -36,15 +38,10 @@ test("opens the comment editor for a line number range", async ({ page }) => {
|
|||||||
await expectAppVisible(start)
|
await expectAppVisible(start)
|
||||||
await expectAppVisible(end)
|
await expectAppVisible(end)
|
||||||
|
|
||||||
const from = await start.boundingBox()
|
await start.dragTo(end)
|
||||||
const to = await end.boundingBox()
|
|
||||||
if (!from || !to) throw new Error("Missing line number bounds")
|
|
||||||
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2)
|
|
||||||
await page.mouse.down()
|
|
||||||
await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2)
|
|
||||||
await page.mouse.up()
|
|
||||||
|
|
||||||
await expect(review.getByRole("textbox")).toBeVisible()
|
await expect(review.getByRole("textbox")).toBeVisible()
|
||||||
|
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on lines 1-3")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("shows a comment button when a line number is hovered", async ({ page }) => {
|
test("shows a comment button when a line number is hovered", async ({ page }) => {
|
||||||
@@ -54,31 +51,38 @@ test("shows a comment button when a line number is hovered", async ({ page }) =>
|
|||||||
|
|
||||||
const comment = review.getByRole("button", { name: "Comment", exact: true })
|
const comment = review.getByRole("button", { name: "Comment", exact: true })
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await page.mouse.move(0, 0)
|
|
||||||
await lineNumber.hover()
|
await lineNumber.hover()
|
||||||
await expect(comment).toBeVisible({ timeout: 500 })
|
await expect(lineNumber).toHaveAttribute("data-hovered", "")
|
||||||
await comment.click({ timeout: 500 })
|
await expect(comment).toHaveCount(1)
|
||||||
}).toPass()
|
await expect(comment).toHaveCSS("pointer-events", "auto")
|
||||||
|
await comment.focus()
|
||||||
|
await expect(comment).toBeFocused()
|
||||||
|
}).toPass({ timeout: 10_000 })
|
||||||
|
await comment.press("Enter")
|
||||||
await expect(review.getByRole("textbox")).toBeVisible()
|
await expect(review.getByRole("textbox")).toBeVisible()
|
||||||
|
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 1")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("stages a submitted line comment in the prompt context", async ({ page }) => {
|
test("stages a submitted line comment in the prompt context", async ({ page }) => {
|
||||||
const requests: string[] = []
|
|
||||||
page.on("request", (request) => {
|
page.on("request", (request) => {
|
||||||
if (request.method() !== "GET") requests.push(`${request.method()} ${new URL(request.url()).pathname}`)
|
expect.soft(request.method(), `unexpected ${request.method()} ${new URL(request.url()).pathname}`).toBe("GET")
|
||||||
})
|
})
|
||||||
|
|
||||||
const review = page.locator('[data-component="session-review"]')
|
const review = page.locator('[data-component="session-review"]')
|
||||||
await review.getByText("export const value = 'after'", { exact: true }).click()
|
await review.getByText("export const value = 'after'", { exact: true }).click()
|
||||||
await review.getByRole("textbox").fill("Use the existing value instead")
|
const textbox = review.getByRole("textbox")
|
||||||
await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click()
|
await expect(textbox).toBeVisible()
|
||||||
|
await expect(review.locator('[data-slot="line-comment-editor-label"]')).toHaveText("Commenting on line 2")
|
||||||
|
await textbox.fill("Use the existing value instead")
|
||||||
|
const submit = review.locator('[data-slot="line-comment-action"][data-variant="primary"]')
|
||||||
|
await expect(submit).toBeEnabled()
|
||||||
|
await submit.click()
|
||||||
|
|
||||||
await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible()
|
await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible()
|
||||||
await page.getByRole("tab", { name: "Session" }).click()
|
await page.getByRole("tab", { name: "Session" }).click()
|
||||||
const context = page.getByText("Use the existing value instead", { exact: true }).last()
|
const context = page.getByText("Use the existing value instead", { exact: true }).last()
|
||||||
await expect(context).toBeVisible()
|
await expect(context).toBeVisible()
|
||||||
await expect(context.locator("..")).toContainText("review.ts:2")
|
await expect(context.locator("..")).toContainText("review.ts:2")
|
||||||
expect(requests).toEqual([])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function openReview(page: Page) {
|
async function openReview(page: Page) {
|
||||||
@@ -144,15 +148,22 @@ async function openReview(page: Page) {
|
|||||||
|
|
||||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
await expectSessionTitle(page, title)
|
await expectSessionTitle(page, title)
|
||||||
const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff")
|
const changes = page.getByRole("tab", { name: "Changes" })
|
||||||
await page.getByRole("tab", { name: "Changes" }).click()
|
const diffResponse = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.request().method() === "GET" && response.ok() && new URL(response.url()).pathname === "/api/vcs/diff",
|
||||||
|
)
|
||||||
|
await changes.click()
|
||||||
expect((await (await diffResponse).json()).data).toHaveLength(1)
|
expect((await (await diffResponse).json()).data).toHaveLength(1)
|
||||||
|
await expect(page.getByRole("tab", { selected: true })).toHaveAccessibleName(/Files Changed/)
|
||||||
|
|
||||||
const review = page.locator('[data-component="session-review"]')
|
const review = page.locator('[data-component="session-review"]')
|
||||||
await expectAppVisible(review)
|
await expectAppVisible(review)
|
||||||
await review
|
const file = review.locator('[data-file="src/review.ts"]')
|
||||||
.getByRole("heading", { name: /review\.ts/ })
|
await expectAppVisible(file)
|
||||||
.getByRole("button")
|
const trigger = file.getByRole("button", { expanded: false })
|
||||||
.first()
|
await expect(trigger).toHaveCount(1)
|
||||||
.click()
|
await trigger.click()
|
||||||
|
await expect(file.getByRole("button", { expanded: true })).toBeVisible()
|
||||||
|
await expect(file.getByText("export const value = 'after'", { exact: true })).toBeVisible()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test, type Page } from "@playwright/test"
|
import { expect, test, type Page } from "@playwright/test"
|
||||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||||
import { expectSessionTitle } from "../utils/waits"
|
import { expectSessionReady } from "../utils/waits"
|
||||||
|
|
||||||
const directory = "C:/OpenCode/ReviewTerminalStacked"
|
const directory = "C:/OpenCode/ReviewTerminalStacked"
|
||||||
const projectID = "proj_review_terminal_stacked"
|
const projectID = "proj_review_terminal_stacked"
|
||||||
@@ -19,10 +19,6 @@ const branchDiffs = [
|
|||||||
|
|
||||||
test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => {
|
test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => {
|
||||||
test.setTimeout(120_000)
|
test.setTimeout(120_000)
|
||||||
const events: Array<{ directory: string; payload: Record<string, unknown> }> = []
|
|
||||||
const sessionStatus = { [sessionID]: { type: "idle" as "busy" | "idle" } }
|
|
||||||
let detailVersion = 1
|
|
||||||
let detailFailures = 1
|
|
||||||
await page.setViewportSize({ width: 1400, height: 900 })
|
await page.setViewportSize({ width: 1400, height: 900 })
|
||||||
await mockOpenCodeServer(page, {
|
await mockOpenCodeServer(page, {
|
||||||
protocol: "v2",
|
protocol: "v2",
|
||||||
@@ -57,10 +53,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sessionStatus: () => sessionStatus,
|
sessionStatus: { [sessionID]: { type: "idle" } },
|
||||||
pageMessages: () => ({ items: [] }),
|
pageMessages: () => ({ items: [] }),
|
||||||
events: () => events.splice(0, 1),
|
|
||||||
eventRetry: 16,
|
|
||||||
})
|
})
|
||||||
await page.route(/\/api\/vcs(?:\?.*)?$/, (route) =>
|
await page.route(/\/api\/vcs(?:\?.*)?$/, (route) =>
|
||||||
route.fulfill({
|
route.fulfill({
|
||||||
@@ -73,20 +67,12 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await page.route("**/api/vcs/diff**", (route) => {
|
await page.route("**/api/vcs/diff**", (route) => {
|
||||||
const url = new URL(route.request().url())
|
|
||||||
const scope = url.searchParams.get("location[directory]")?.replaceAll("\\", "/")
|
|
||||||
const detail = scope?.endsWith("/src/branch/d00027")
|
|
||||||
if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" })
|
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
location: { directory, project: { id: projectID, directory, canonical: directory } },
|
location: { directory, project: { id: projectID, directory, canonical: directory } },
|
||||||
data: detail
|
data: branchDiffs,
|
||||||
? branchDiffs
|
|
||||||
.filter((diff) => diff.file.startsWith("src/branch/d00027/"))
|
|
||||||
.map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
|
|
||||||
: branchDiffs,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -146,7 +132,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||||||
})
|
})
|
||||||
|
|
||||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
await expectSessionTitle(page, title)
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
|
await expectSessionReady(page, { server, sessionID, title })
|
||||||
await expect(page.locator("#review-panel")).toBeVisible()
|
await expect(page.locator("#review-panel")).toBeVisible()
|
||||||
await expectTree(page, 2_773, "action.yml")
|
await expectTree(page, 2_773, "action.yml")
|
||||||
await expect(page.locator("#session-side-panel-review-tab")).toHaveText("Files Changed 2740")
|
await expect(page.locator("#session-side-panel-review-tab")).toHaveText("Files Changed 2740")
|
||||||
@@ -154,77 +141,6 @@ test("keeps the review tree and terminal sized when both panels are open", async
|
|||||||
await expect(page.locator("#terminal-panel")).toBeVisible()
|
await expect(page.locator("#terminal-panel")).toBeVisible()
|
||||||
await expectTree(page, 2_773, "action.yml")
|
await expectTree(page, 2_773, "action.yml")
|
||||||
await expectStackGeometry(page)
|
await expectStackGeometry(page)
|
||||||
|
|
||||||
const treeViewport = page.locator('#review-panel [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
|
|
||||||
await treeViewport.hover()
|
|
||||||
await page.mouse.wheel(0, 100_000)
|
|
||||||
await expect
|
|
||||||
.poll(() => treeViewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
|
|
||||||
.toBeLessThanOrEqual(1)
|
|
||||||
const lastFile = page.getByRole("button", { name: "generated-2738.ts" })
|
|
||||||
await expect(lastFile).toBeVisible()
|
|
||||||
const bottomGap = await lastFile.evaluate((element) => {
|
|
||||||
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!.getBoundingClientRect()
|
|
||||||
return viewport.bottom - element.getBoundingClientRect().bottom
|
|
||||||
})
|
|
||||||
expect(bottomGap).toBeGreaterThanOrEqual(0)
|
|
||||||
expect(bottomGap).toBeLessThanOrEqual(16)
|
|
||||||
const lazyDiff = page.waitForRequest((request) => {
|
|
||||||
const url = new URL(request.url())
|
|
||||||
return (
|
|
||||||
url.pathname === "/api/vcs/diff" &&
|
|
||||||
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
|
|
||||||
)
|
|
||||||
})
|
|
||||||
await lastFile.click()
|
|
||||||
await lazyDiff
|
|
||||||
const preview = page.locator('[data-slot="session-review-v2-diff-scroll"]')
|
|
||||||
await expect(preview).toContainText("after-1")
|
|
||||||
detailVersion = 2
|
|
||||||
sessionStatus[sessionID] = { type: "busy" }
|
|
||||||
events.push(statusEvent("busy"))
|
|
||||||
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
|
|
||||||
const refreshedDiff = page.waitForRequest((request) => {
|
|
||||||
const url = new URL(request.url())
|
|
||||||
return (
|
|
||||||
url.pathname === "/api/vcs/diff" &&
|
|
||||||
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
|
|
||||||
)
|
|
||||||
})
|
|
||||||
sessionStatus[sessionID] = { type: "idle" }
|
|
||||||
events.push(statusEvent("idle"))
|
|
||||||
await refreshedDiff
|
|
||||||
await expect(preview).toContainText("after-2")
|
|
||||||
const filter = page.getByRole("searchbox", { name: "Filter files" })
|
|
||||||
await filter.fill("generated-2738")
|
|
||||||
await expectTree(page, 1, "generated-2738.ts")
|
|
||||||
await filter.fill("")
|
|
||||||
await expectTree(page, 2_773, "generated-2738.ts")
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Toggle file tree" }).click()
|
|
||||||
await expect(page.locator('[data-slot="session-review-v2-sidebar"]')).toHaveCount(0)
|
|
||||||
await expect(page.locator('#review-panel [data-component="file-tree-v2"]')).toHaveCount(0)
|
|
||||||
await page.getByRole("button", { name: "Toggle file tree" }).click()
|
|
||||||
await expectTree(page, 2_773, "generated-2738.ts")
|
|
||||||
|
|
||||||
await page.keyboard.press("Control+Backquote")
|
|
||||||
await expect(page.locator("#terminal-panel")).toHaveCount(0)
|
|
||||||
await expectTree(page, 2_773, "generated-2738.ts")
|
|
||||||
await page.keyboard.press("Control+Backquote")
|
|
||||||
await expect(page.locator("#terminal-panel")).toBeVisible()
|
|
||||||
await expectTree(page, 2_773, "generated-2738.ts")
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Toggle review" }).click()
|
|
||||||
await expect(page.locator("#review-panel")).toHaveCount(0)
|
|
||||||
await page.getByRole("button", { name: "Toggle review" }).click()
|
|
||||||
await expectTree(page, 2_773, "generated-2738.ts")
|
|
||||||
await page.setViewportSize({ width: 1_000, height: 700 })
|
|
||||||
await expectTree(page, 2_773, "generated-2738.ts")
|
|
||||||
await expectStackGeometry(page)
|
|
||||||
await page.setViewportSize({ width: 1_000, height: 120 })
|
|
||||||
await page.setViewportSize({ width: 1_400, height: 900 })
|
|
||||||
await expectTree(page, 2_773, "generated-2738.ts")
|
|
||||||
await expectStackGeometry(page)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function expectTree(page: Page, total: number, file: string) {
|
async function expectTree(page: Page, total: number, file: string) {
|
||||||
@@ -255,36 +171,32 @@ async function expectStackGeometry(page: Page) {
|
|||||||
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!
|
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!
|
||||||
const reviewParent = review.parentElement!.getBoundingClientRect()
|
const reviewParent = review.parentElement!.getBoundingClientRect()
|
||||||
const terminalParent = terminal.parentElement!.getBoundingClientRect()
|
const terminalParent = terminal.parentElement!.getBoundingClientRect()
|
||||||
|
const sidebar = review.querySelector<HTMLElement>('[data-slot="session-review-v2-sidebar"]')!
|
||||||
return {
|
return {
|
||||||
review: review.getBoundingClientRect().height,
|
review: review.getBoundingClientRect().height,
|
||||||
reviewParent: reviewParent.height,
|
reviewParent: reviewParent.height,
|
||||||
terminal: terminal.getBoundingClientRect().height,
|
terminal: terminal.getBoundingClientRect().height,
|
||||||
terminalParent: terminalParent.height,
|
terminalParent: terminalParent.height,
|
||||||
|
sidebar: sidebar.getBoundingClientRect().width,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(Math.abs(geometry.review - geometry.reviewParent)).toBeLessThanOrEqual(1)
|
expect(Math.abs(geometry.review - geometry.reviewParent)).toBeLessThanOrEqual(1)
|
||||||
expect(Math.abs(geometry.terminal - geometry.terminalParent)).toBeLessThanOrEqual(1)
|
expect(Math.abs(geometry.terminal - geometry.terminalParent)).toBeLessThanOrEqual(1)
|
||||||
|
expect(geometry.sidebar).toBeGreaterThanOrEqual(240)
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64Encode(value: string) {
|
function base64Encode(value: string) {
|
||||||
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusEvent(type: "busy" | "idle") {
|
function fileDiff(file: string, additions: number, loaded = true) {
|
||||||
return {
|
|
||||||
directory,
|
|
||||||
payload: { type: "session.status", properties: { sessionID, status: { type } } },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileDiff(file: string, additions: number, loaded = true, version = 1) {
|
|
||||||
return {
|
return {
|
||||||
file,
|
file,
|
||||||
additions,
|
additions,
|
||||||
deletions: 0,
|
deletions: 0,
|
||||||
status: "modified",
|
status: "modified",
|
||||||
patch: loaded
|
patch: loaded
|
||||||
? `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after-${version}'\n`
|
? `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n`
|
||||||
: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}`,
|
: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const projectID = "proj_context_resize_regression"
|
|||||||
const sessionID = "ses_context_resize_regression"
|
const sessionID = "ses_context_resize_regression"
|
||||||
const title = "Context resize regression"
|
const title = "Context resize regression"
|
||||||
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
|
||||||
const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"]
|
const contextIDs = ["ctx_0100_read", "ctx_0101_glob", "ctx_0102_grep", "ctx_0103_list"]
|
||||||
const followingTextID = "prt_0104_text"
|
const followingTextID = `${id("msg_assistant", 10)}:text:0`
|
||||||
|
|
||||||
type Message = {
|
type Message = {
|
||||||
info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
|
info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
|
||||||
@@ -295,7 +295,7 @@ function contextTool(
|
|||||||
sessionID,
|
sessionID,
|
||||||
messageID,
|
messageID,
|
||||||
type: "tool",
|
type: "tool",
|
||||||
callID: `call_${partID}`,
|
callID: partID,
|
||||||
tool,
|
tool,
|
||||||
state: {
|
state: {
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test"
|
|||||||
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
|
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
|
||||||
|
|
||||||
for (const profile of [
|
for (const profile of [
|
||||||
{ locale: "de", label: "Erkundet" },
|
{ locale: "de", label: "Erkundung abgeschlossen" },
|
||||||
{ locale: "ar", label: "تم الاستكشاف" },
|
{ locale: "ar", label: "تم الاستكشاف" },
|
||||||
] as const) {
|
] as const) {
|
||||||
test(`projects translated context status in ${profile.locale}`, async ({ page }) => {
|
test(`projects translated context status in ${profile.locale}`, async ({ page }) => {
|
||||||
|
|||||||
@@ -86,15 +86,10 @@ test.describe("session timeline projection", () => {
|
|||||||
],
|
],
|
||||||
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
|
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
|
||||||
)
|
)
|
||||||
const aborted = assistantMessage(
|
const aborted = assistantMessage([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], {
|
||||||
[
|
id: "msg_1001_assistant_aborted",
|
||||||
{ id: "prt_before_abort", type: "text", text: "Before interruption" },
|
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
|
||||||
],
|
})
|
||||||
{
|
|
||||||
id: "msg_1001_assistant_aborted",
|
|
||||||
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
|
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
|
||||||
id: "msg_1002_assistant_failed",
|
id: "msg_1002_assistant_failed",
|
||||||
error: {
|
error: {
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, test, type Page } from "@playwright/test"
|
||||||
import {
|
import { partUpdated, setupTimeline, textPart } from "../performance/timeline-stability/fixture"
|
||||||
assistantMessage,
|
|
||||||
partUpdated,
|
|
||||||
setupTimeline,
|
|
||||||
status,
|
|
||||||
textPart,
|
|
||||||
userMessage,
|
|
||||||
} from "../performance/timeline-stability/fixture"
|
|
||||||
|
|
||||||
test("keeps one connection open while delivering multiple events", async ({ page }) => {
|
test("keeps one connection open while delivering multiple events", async ({ page }) => {
|
||||||
const timeline = await setupTimeline(page)
|
const timeline = await setupTimeline(page)
|
||||||
@@ -17,7 +10,7 @@ test("keeps one connection open while delivering multiple events", async ({ page
|
|||||||
await timeline.waitForPart("prt_transport_first")
|
await timeline.waitForPart("prt_transport_first")
|
||||||
await timeline.waitForPart("prt_transport_second")
|
await timeline.waitForPart("prt_transport_second")
|
||||||
expect(first.connectionID).toBe(second.connectionID)
|
expect(first.connectionID).toBe(second.connectionID)
|
||||||
expect(await timeline.transport.connections()).toHaveLength(1)
|
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
|
||||||
expect(await timeline.transport.acknowledgements()).toHaveLength(2)
|
expect(await timeline.transport.acknowledgements()).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -51,20 +44,24 @@ test("parses split JSON and a split multibyte code point", async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
|
test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
|
||||||
const timeline = await setupTimeline(page, {
|
const timeline = await setupTimeline(page)
|
||||||
messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
|
const partID = "prt_transport_heartbeat_sentinel"
|
||||||
})
|
const sentinel = await timeline.transport.send(partUpdated(textPart(partID, "heartbeat sentinel")))
|
||||||
const before = await page.locator("[data-timeline-row]").allTextContents()
|
await timeline.waitForPart(partID)
|
||||||
|
await expect(page.locator(`[data-timeline-part-id="${partID}"] [data-component="markdown"]`)).toHaveAttribute(
|
||||||
|
"data-markdown-ready",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
const before = await timelineRows(page)
|
||||||
|
const heartbeat = await timeline.transport.heartbeat()
|
||||||
|
|
||||||
await timeline.transport.heartbeat()
|
await expect.poll(() => timelineRows(page)).toEqual(before)
|
||||||
await timeline.settle()
|
expect(heartbeat.connectionID).toBe(sentinel.connectionID)
|
||||||
|
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
|
||||||
expect(await page.locator("[data-timeline-row]").allTextContents()).toEqual(before)
|
|
||||||
expect(await timeline.transport.connections()).toHaveLength(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("reconnects after a clean close", async ({ page }) => {
|
test("reconnects after a clean close", async ({ page }) => {
|
||||||
const timeline = await setupTimeline(page, { eventRetry: 10 })
|
const timeline = await setupTimeline(page)
|
||||||
const first = await timeline.transport.waitForConnection()
|
const first = await timeline.transport.waitForConnection()
|
||||||
|
|
||||||
await timeline.transport.close()
|
await timeline.transport.close()
|
||||||
@@ -77,13 +74,14 @@ test("reconnects after a clean close", async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("reconnects after a stream error", async ({ page }) => {
|
test("reconnects after a stream error", async ({ page }) => {
|
||||||
const timeline = await setupTimeline(page, { eventRetry: 10 })
|
const timeline = await setupTimeline(page)
|
||||||
const first = await timeline.transport.waitForConnection()
|
const first = await timeline.transport.waitForConnection()
|
||||||
|
|
||||||
await timeline.transport.error("contract failure")
|
await timeline.transport.error("contract failure")
|
||||||
const second = await timeline.transport.waitForConnection({ after: first.id })
|
const second = await timeline.transport.waitForConnection({ after: first.id })
|
||||||
await timeline.transport.send(status("busy"))
|
await timeline.transport.send(partUpdated(textPart("prt_transport_error", "after error")))
|
||||||
|
|
||||||
|
await timeline.waitForPart("prt_transport_error")
|
||||||
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
|
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
|
||||||
expect(second.id).toBeGreaterThan(first.id)
|
expect(second.id).toBeGreaterThan(first.id)
|
||||||
expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
|
expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
|
||||||
@@ -112,5 +110,18 @@ test("passes through non-event fetches", async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(health).toEqual({ healthy: true, version: "2.0.0", pid: 1 })
|
expect(health).toEqual({ healthy: true, version: "2.0.0", pid: 1 })
|
||||||
expect(await timeline.transport.connections()).toHaveLength(1)
|
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function timelineRows(page: Page) {
|
||||||
|
return page.locator("[data-timeline-row]").evaluateAll((rows) =>
|
||||||
|
rows.map((row) => ({
|
||||||
|
kind: row.getAttribute("data-timeline-row"),
|
||||||
|
message: row.getAttribute("data-message-id"),
|
||||||
|
parts: Array.from(row.querySelectorAll("[data-timeline-part-id]"), (part) =>
|
||||||
|
part.getAttribute("data-timeline-part-id"),
|
||||||
|
),
|
||||||
|
text: row.textContent,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type EventPayload = {
|
|||||||
|
|
||||||
test.use({ viewport: { width: 1440, height: 900 }, reducedMotion: "no-preference" })
|
test.use({ viewport: { width: 1440, height: 900 }, reducedMotion: "no-preference" })
|
||||||
|
|
||||||
test("animates todo lifecycle without replaying it across session tabs", async ({ page }) => {
|
test("animates todo opening without replaying it across session tabs", async ({ page }) => {
|
||||||
test.setTimeout(90_000)
|
test.setTimeout(90_000)
|
||||||
const events: EventPayload[] = []
|
const events: EventPayload[] = []
|
||||||
const todos: Record<string, typeof activeTodos> = { [sourceID]: [], [otherID]: [] }
|
const todos: Record<string, typeof activeTodos> = { [sourceID]: [], [otherID]: [] }
|
||||||
@@ -86,28 +86,8 @@ test("animates todo lifecycle without replaying it across session tabs", async (
|
|||||||
await switchSession(page, otherID, otherTitle)
|
await switchSession(page, otherID, otherTitle)
|
||||||
await expect(dock).toHaveCount(0)
|
await expect(dock).toHaveCount(0)
|
||||||
|
|
||||||
const returningOpen = sampleDock(page, 700)
|
|
||||||
await switchSession(page, sourceID, sourceTitle)
|
|
||||||
const openSamples = (await returningOpen).filter((sample) => sample.present)
|
|
||||||
expect(openSamples.length).toBeGreaterThan(0)
|
|
||||||
expect(openSamples[0]!.opacity).toBeGreaterThan(0.98)
|
|
||||||
expect(openSamples[0]!.height).toBeGreaterThan(70)
|
|
||||||
await expect(dock.locator('[data-state="in_progress"]')).toHaveCount(1)
|
|
||||||
|
|
||||||
const completedTodos = activeTodos.map((todo) => ({ ...todo, status: "completed" }))
|
|
||||||
const closing = sampleDock(page, 1_000)
|
|
||||||
todos[sourceID] = completedTodos
|
|
||||||
events.push(todoEvent(sourceID, completedTodos))
|
|
||||||
await expect(dock).toHaveCount(0)
|
|
||||||
expect((await closing).some((sample) => sample.opacity > 0.05 && sample.opacity < 0.95)).toBe(true)
|
|
||||||
todos[sourceID] = []
|
|
||||||
events.push(todoEvent(sourceID, []))
|
|
||||||
|
|
||||||
await switchSession(page, otherID, otherTitle)
|
|
||||||
const returningEmpty = sampleDock(page, 700)
|
|
||||||
await switchSession(page, sourceID, sourceTitle)
|
await switchSession(page, sourceID, sourceTitle)
|
||||||
await expect(dock).toHaveCount(0)
|
await expect(dock).toHaveCount(0)
|
||||||
expect((await returningEmpty).every((sample) => !sample.present)).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function session(id: string, title: string, created: number) {
|
function session(id: string, title: string, created: number) {
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ async function mockServer(page: Page) {
|
|||||||
if (url.origin !== server) return route.fallback()
|
if (url.origin !== server) return route.fallback()
|
||||||
if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname))
|
if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname))
|
||||||
return new Promise(() => {})
|
return new Promise(() => {})
|
||||||
if (url.pathname === "/api/event")
|
if (url.pathname === "/api/event") return sse(route)
|
||||||
return sse(route)
|
|
||||||
if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
|
if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
|
||||||
if (url.pathname === "/api/session/active") return json(route, { data: {} })
|
if (url.pathname === "/api/session/active") return json(route, { data: {} })
|
||||||
const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`)
|
const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`)
|
||||||
|
|||||||
@@ -300,9 +300,8 @@ export const fixture = {
|
|||||||
.filter((message) => message.info.role === "user")
|
.filter((message) => message.info.role === "user")
|
||||||
.map((message) => message.info.id),
|
.map((message) => message.info.id),
|
||||||
targetPartIDs: targetMessages.flatMap(currentPartIDs),
|
targetPartIDs: targetMessages.flatMap(currentPartIDs),
|
||||||
expandedShellPartID: targetMessages
|
expandedShellPartID: targetMessages.flatMap((message) => message.parts).find((part) => part.tool === "bash")!
|
||||||
.flatMap((message) => message.parts)
|
.callID,
|
||||||
.find((part) => part.tool === "bash")!.callID,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (path === "/global/health")
|
if (path === "/global/health")
|
||||||
return config.protocol === "v2" ? json(route, {}) : json(route, { healthy: true })
|
return config.protocol === "v2" ? json(route, {}, undefined, 404) : json(route, { healthy: true })
|
||||||
if (path === "/api/health" && config.protocol === "v2")
|
if (path === "/api/health") return json(route, { healthy: true, version: "2.0.0", pid: 1 })
|
||||||
return json(route, { healthy: true, version: "2.0.0", pid: 1 })
|
|
||||||
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
|
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
|
||||||
if (path === "/provider") return json(route, providerConfig(config))
|
if (path === "/provider") return json(route, providerConfig(config))
|
||||||
if (path === "/provider/auth") return json(route, config.integrationMethods ?? {})
|
if (path === "/provider/auth") return json(route, config.integrationMethods ?? {})
|
||||||
@@ -145,7 +144,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
location: location(config),
|
location: location(config),
|
||||||
data: currentProviders(providerConfig(config)),
|
data: currentProviders(providerConfig(config)),
|
||||||
})
|
})
|
||||||
if (path === "/api/model") return json(route, { location: location(config), data: currentModels(providerConfig(config)) })
|
if (path === "/api/model")
|
||||||
|
return json(route, { location: location(config), data: currentModels(providerConfig(config)) })
|
||||||
if (path === "/api/model/default")
|
if (path === "/api/model/default")
|
||||||
return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) })
|
return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) })
|
||||||
if (path === "/api/integration") return json(route, { location: location(config), data: [] })
|
if (path === "/api/integration") return json(route, { location: location(config), data: [] })
|
||||||
@@ -207,7 +207,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
const fileRead = path.match(/^\/api\/fs\/read\/(.+)$/)?.[1]
|
const fileRead = path.match(/^\/api\/fs\/read\/(.+)$/)?.[1]
|
||||||
if (fileRead && config.fileContent) {
|
if (fileRead && config.fileContent) {
|
||||||
const value = await config.fileContent(decodeURIComponent(fileRead))
|
const value = await config.fileContent(decodeURIComponent(fileRead))
|
||||||
const content = value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "")
|
const content =
|
||||||
|
value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "")
|
||||||
return route.fulfill({ status: 200, body: content, headers: { "content-type": "application/octet-stream" } })
|
return route.fulfill({ status: 200, body: content, headers: { "content-type": "application/octet-stream" } })
|
||||||
}
|
}
|
||||||
if (path === "/api/fs/find" && config.findFiles) {
|
if (path === "/api/fs/find" && config.findFiles) {
|
||||||
@@ -282,7 +283,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
|
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
||||||
}
|
}
|
||||||
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") return json(route, true)
|
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST")
|
||||||
|
return json(route, true)
|
||||||
if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST")
|
if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST")
|
||||||
return json(route, true)
|
return json(route, true)
|
||||||
if (
|
if (
|
||||||
@@ -387,11 +389,13 @@ function providerConfig(config: MockServerConfig) {
|
|||||||
|
|
||||||
function currentProviders(value: unknown) {
|
function currentProviders(value: unknown) {
|
||||||
if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : []
|
if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : []
|
||||||
return value.all.filter(record).flatMap((provider) =>
|
return value.all
|
||||||
typeof provider.id === "string" && typeof provider.name === "string"
|
.filter(record)
|
||||||
? [{ id: provider.id, name: provider.name, package: provider.id }]
|
.flatMap((provider) =>
|
||||||
: [],
|
typeof provider.id === "string" && typeof provider.name === "string"
|
||||||
)
|
? [{ id: provider.id, name: provider.name, package: provider.id }]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentModels(value: unknown) {
|
function currentModels(value: unknown) {
|
||||||
@@ -441,9 +445,7 @@ function currentDefaultModel(value: unknown) {
|
|||||||
if (!record(value) || !record(value.default)) return null
|
if (!record(value) || !record(value.default)) return null
|
||||||
const selected = value.default
|
const selected = value.default
|
||||||
const models = currentModels(value)
|
const models = currentModels(value)
|
||||||
return models.find(
|
return models.find((model) => model.providerID === selected.providerID && model.id === selected.modelID) ?? null
|
||||||
(model) => model.providerID === selected.providerID && model.id === selected.modelID,
|
|
||||||
) ?? null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentPermission(value: unknown) {
|
function currentPermission(value: unknown) {
|
||||||
@@ -563,22 +565,16 @@ function legacyAgent(part: Record<string, unknown>): PromptAgentAttachment[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mentionFrom(value: Record<string, unknown> | undefined) {
|
function mentionFrom(value: Record<string, unknown> | undefined) {
|
||||||
if (
|
if (!value || typeof value.value !== "string" || typeof value.start !== "number" || typeof value.end !== "number")
|
||||||
!value ||
|
|
||||||
typeof value.value !== "string" ||
|
|
||||||
typeof value.start !== "number" ||
|
|
||||||
typeof value.end !== "number"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
return { text: value.value, start: value.start, end: value.end }
|
return { text: value.value, start: value.start, end: value.end }
|
||||||
}
|
}
|
||||||
|
|
||||||
function legacyAssistantContent(
|
function legacyAssistantContent(part: Record<string, unknown>, created: number): SessionMessageAssistant["content"] {
|
||||||
part: Record<string, unknown>,
|
|
||||||
created: number,
|
|
||||||
): SessionMessageAssistant["content"] {
|
|
||||||
if (part.type === "text" && typeof part.text === "string")
|
if (part.type === "text" && typeof part.text === "string")
|
||||||
return [{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) }]
|
return [
|
||||||
|
{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) },
|
||||||
|
]
|
||||||
if (part.type === "reasoning" && typeof part.text === "string") {
|
if (part.type === "reasoning" && typeof part.text === "string") {
|
||||||
const time = record(part.time) ? part.time : undefined
|
const time = record(part.time) ? part.time : undefined
|
||||||
return [
|
return [
|
||||||
@@ -619,7 +615,12 @@ function legacyAssistantContent(
|
|||||||
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
|
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
|
||||||
}
|
}
|
||||||
if (state.status === "pending")
|
if (state.status === "pending")
|
||||||
return [{ ...base, state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) } }]
|
return [
|
||||||
|
{
|
||||||
|
...base,
|
||||||
|
state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) },
|
||||||
|
},
|
||||||
|
]
|
||||||
if (state.status === "completed")
|
if (state.status === "completed")
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Page } from "@playwright/test"
|
|||||||
export type SseConnectionRecord = {
|
export type SseConnectionRecord = {
|
||||||
id: number
|
id: number
|
||||||
url: string
|
url: string
|
||||||
path: "/global/event" | "/event" | "/api/event"
|
path: "/api/event"
|
||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
openedAt: number
|
openedAt: number
|
||||||
endedAt?: number
|
endedAt?: number
|
||||||
@@ -174,11 +174,7 @@ export async function installSseTransport<T>(
|
|||||||
const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const request = new Request(input, init)
|
const request = new Request(input, init)
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
if (
|
if (url.origin !== server || url.pathname !== "/api/event") return originalFetch(request)
|
||||||
url.origin !== server ||
|
|
||||||
(url.pathname !== "/global/event" && url.pathname !== "/event" && url.pathname !== "/api/event")
|
|
||||||
)
|
|
||||||
return originalFetch(request)
|
|
||||||
|
|
||||||
const id = ++nextConnectionID
|
const id = ++nextConnectionID
|
||||||
const record = {
|
const record = {
|
||||||
@@ -193,18 +189,9 @@ export async function installSseTransport<T>(
|
|||||||
record.controller = controller
|
record.controller = controller
|
||||||
connections.push(record)
|
connections.push(record)
|
||||||
if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`))
|
if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`))
|
||||||
if (url.pathname === "/api/event")
|
controller.enqueue(
|
||||||
controller.enqueue(
|
encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })),
|
||||||
encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })),
|
)
|
||||||
)
|
|
||||||
if (url.pathname === "/global/event")
|
|
||||||
controller.enqueue(
|
|
||||||
encoder.encode(
|
|
||||||
frame({
|
|
||||||
payload: { id: `evt_mock_connected_${id}`, type: "server.connected", properties: {} },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
request.signal.addEventListener(
|
request.signal.addEventListener(
|
||||||
"abort",
|
"abort",
|
||||||
() => {
|
() => {
|
||||||
@@ -247,18 +234,23 @@ export async function installSseTransport<T>(
|
|||||||
return {
|
return {
|
||||||
server,
|
server,
|
||||||
async waitForConnection(input = {}) {
|
async waitForConnection(input = {}) {
|
||||||
await page.waitForFunction(
|
const connection = await page.waitForFunction(
|
||||||
(after) => {
|
(after) => {
|
||||||
const transport = (window as BrowserTransport).__testSseTransport
|
const transport = (window as BrowserTransport).__testSseTransport
|
||||||
const connections = transport?.command({ type: "connections" }) as SseConnectionRecord[] | undefined
|
const connections = transport?.command({ type: "connections" }) as SseConnectionRecord[] | undefined
|
||||||
return connections?.some((connection) => connection.id > after)
|
return connections?.findLast((connection) => connection.id > after && connection.endedAt === undefined)
|
||||||
},
|
},
|
||||||
input.after ?? 0,
|
input.after ?? 0,
|
||||||
{ timeout: input.timeout },
|
{ timeout: input.timeout },
|
||||||
)
|
)
|
||||||
return (await command<SseConnectionRecord[]>({ type: "connections" })).findLast(
|
let result: SseConnectionRecord | undefined
|
||||||
(connection) => connection.id > (input.after ?? 0),
|
try {
|
||||||
)!
|
result = await connection.jsonValue()
|
||||||
|
} finally {
|
||||||
|
await connection.dispose()
|
||||||
|
}
|
||||||
|
if (!result) throw new Error("SSE transport connection disappeared while waiting")
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
send(payload, eventOptions) {
|
send(payload, eventOptions) {
|
||||||
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false })
|
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false })
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, type Locator, type Page } from "@playwright/test"
|
import { expect, type Locator, type Page } from "@playwright/test"
|
||||||
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
|
|
||||||
export const APP_READY_TIMEOUT = 30_000
|
export const APP_READY_TIMEOUT = 30_000
|
||||||
|
|
||||||
@@ -9,3 +10,8 @@ export async function expectAppVisible(locator: Locator) {
|
|||||||
export async function expectSessionTitle(page: Page, title: string) {
|
export async function expectSessionTitle(page: Page, title: string) {
|
||||||
await expectAppVisible(page.getByRole("heading", { name: title }))
|
await expectAppVisible(page.getByRole("heading", { name: title }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function expectSessionReady(page: Page, input: { server: string; sessionID: string; title: string }) {
|
||||||
|
await expect(page).toHaveURL(`/server/${base64Encode(input.server)}/session/${input.sessionID}`)
|
||||||
|
await expectSessionTitle(page, input.title)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@
|
|||||||
<meta property="twitter:image" content="/social-share.png" />
|
<meta property="twitter:image" content="/social-share.png" />
|
||||||
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden bg-v2-background-bg-deep">
|
<body
|
||||||
|
data-new-layout
|
||||||
|
class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"
|
||||||
|
>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div>
|
<div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div>
|
||||||
<script src="/src/entry.tsx" type="module"></script>
|
<script src="/src/entry.tsx" type="module"></script>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.8",
|
"version": "1.18.15",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./desktop-menu": "./src/desktop-menu.ts",
|
"./desktop-menu": "./src/desktop-menu.ts",
|
||||||
|
"./i18n/desktop-native": "./src/i18n/desktop-native.ts",
|
||||||
"./updater": "./src/updater.ts",
|
"./updater": "./src/updater.ts",
|
||||||
"./wsl/types": "./src/wsl/types.ts",
|
"./wsl/types": "./src/wsl/types.ts",
|
||||||
"./vite": "./vite.js",
|
"./vite": "./vite.js",
|
||||||
@@ -19,9 +20,9 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"test": "bun run test:unit && bun run test:browser",
|
"test": "bun run test:unit && bun run test:browser",
|
||||||
"test:unit": "bun test --only-failures --preload ./happydom.ts ./src",
|
"test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src",
|
||||||
"test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser",
|
"test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser",
|
||||||
"test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
|
"test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:local": "playwright test",
|
"test:e2e:local": "playwright test",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"happy-dom": "20.11.1",
|
||||||
"tw-animate-css": "1.4.0",
|
"tw-animate-css": "1.4.0",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
@@ -56,7 +58,6 @@
|
|||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/sdk": "file:vendor/opencode-ai-sdk-1.18.8-dev.tgz",
|
|
||||||
"@opencode-ai/session-ui": "workspace:*",
|
"@opencode-ai/session-ui": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
|
|||||||
+63
-197
@@ -3,25 +3,14 @@ import * as Sentry from "@sentry/solid"
|
|||||||
import { I18nProvider } from "@opencode-ai/ui/context"
|
import { I18nProvider } from "@opencode-ai/ui/context"
|
||||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||||
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
||||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
|
||||||
import { File } from "@opencode-ai/session-ui/file"
|
import { File } from "@opencode-ai/session-ui/file"
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import {
|
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
||||||
type BaseRouterProps,
|
|
||||||
Navigate,
|
|
||||||
Route,
|
|
||||||
Router,
|
|
||||||
useLocation,
|
|
||||||
useNavigate,
|
|
||||||
useParams,
|
|
||||||
useSearchParams,
|
|
||||||
} from "@solidjs/router"
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
createEffect,
|
createEffect,
|
||||||
@@ -43,7 +32,7 @@ import { CommandProvider, useCommand, type CommandOption } from "@/context/comma
|
|||||||
import { CommentsProvider } from "@/context/comments"
|
import { CommentsProvider } from "@/context/comments"
|
||||||
import { FileProvider } from "@/context/file"
|
import { FileProvider } from "@/context/file"
|
||||||
import { ServerSDKProvider } from "@/context/server-sdk"
|
import { ServerSDKProvider } from "@/context/server-sdk"
|
||||||
import { ServerSyncProvider, useServerSync } from "@/context/server-sync"
|
import { ServerSyncProvider } from "@/context/server-sync"
|
||||||
import { GlobalProvider, useGlobal } from "@/context/global"
|
import { GlobalProvider, useGlobal } from "@/context/global"
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||||
@@ -54,58 +43,36 @@ import { PermissionProvider } from "@/context/permission"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { PromptProvider } from "@/context/prompt"
|
import { PromptProvider } from "@/context/prompt"
|
||||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
import { SettingsProvider } from "@/context/settings"
|
||||||
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
||||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
import { WslServersProvider } from "@/wsl/context"
|
import { WslServersProvider } from "@/wsl/context"
|
||||||
import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
|
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||||
import LegacyLayout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import NewLayout from "@/pages/layout-new"
|
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
import { useCheckServerHealth } from "./utils/server-health"
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||||
import { createSessionLineage } from "@/pages/session/session-lineage"
|
import { decode64 } from "@/utils/base64"
|
||||||
|
|
||||||
import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
|
import { TargetSessionRouteContent } from "@/pages/session"
|
||||||
import { NewHome } from "@/pages/home"
|
import { Home } from "@/pages/home"
|
||||||
import { LegacyHome } from "@/pages/home/legacy-home"
|
|
||||||
|
|
||||||
const NewSession = lazy(() => import("@/pages/new-session"))
|
const NewSession = lazy(() => import("@/pages/new-session"))
|
||||||
|
|
||||||
const SessionRoute = () => {
|
const DirectoryDraftRedirect = () => {
|
||||||
const settings = useSettings()
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
||||||
const sdk = useSDK()
|
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
|
|
||||||
if (params.id && settings.general.newLayoutDesigns()) {
|
|
||||||
const sessionID = params.id
|
|
||||||
return (
|
|
||||||
<Show when={tabs.ready()}>
|
|
||||||
{(_) => {
|
|
||||||
const persisted = tabs.store.filter((item) => item.type === "session")
|
|
||||||
return <Navigate href={sessionHref(legacySessionServer(persisted, sessionID, server.key), sessionID)} />
|
|
||||||
}}
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the new layout is enabled, the legacy new-session route (/:dir/session with no id)
|
|
||||||
// is replaced by a draft at /new-session?draftId=…
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!settings.general.newLayoutDesigns()) return
|
if (search.draftId || !tabs.ready()) return
|
||||||
if (params.id || search.draftId) return
|
const directory = decode64(params.dir)
|
||||||
if (!tabs.ready() || !sdk().directory) return
|
if (!directory) return
|
||||||
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
tabs.newDraft({ server: server.key, directory }, search.prompt)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return null
|
||||||
<SessionRouteErrorBoundary sessionID={params.id}>
|
|
||||||
<SessionPage />
|
|
||||||
</SessionRouteErrorBoundary>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TargetServerRoute(props: ParentProps) {
|
function TargetServerRoute(props: ParentProps) {
|
||||||
@@ -117,9 +84,7 @@ function TargetServerRoute(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Owns the server-identity remount. Session changes must NOT remount this
|
// Owns the server-identity remount. Session changes must not remount this subtree.
|
||||||
// subtree (SessionRouteErrorBoundary resets and createSessionLineage
|
|
||||||
// re-resolves reactively instead); both rely on this key for server changes.
|
|
||||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||||
<ServerSDKProvider server={conn}>
|
<ServerSDKProvider server={conn}>
|
||||||
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
||||||
@@ -134,35 +99,6 @@ const TargetSessionRoute = () => (
|
|||||||
</TargetServerRoute>
|
</TargetServerRoute>
|
||||||
)
|
)
|
||||||
|
|
||||||
function LegacyTargetSessionRoute() {
|
|
||||||
const params = useParams<{ serverKey: string; id: string }>()
|
|
||||||
return (
|
|
||||||
<TargetServerRoute>
|
|
||||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)}>
|
|
||||||
<LegacyTargetSessionRedirect />
|
|
||||||
</SessionRouteErrorBoundary>
|
|
||||||
</TargetServerRoute>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LegacyTargetSessionRedirect() {
|
|
||||||
const params = useParams<{ id: string }>()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const sync = useServerSync()
|
|
||||||
const current = createSessionLineage(
|
|
||||||
() => params.id,
|
|
||||||
() => sync().session.lineage,
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const directory = current()?.session.directory
|
|
||||||
if (!directory) return
|
|
||||||
navigate(legacySessionHref(directory, params.id), { replace: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
||||||
// server via ServerKey, then provide the server-scoped shell for that server.
|
// server via ServerKey, then provide the server-scoped shell for that server.
|
||||||
function SelectedServerProviders(props: ParentProps) {
|
function SelectedServerProviders(props: ParentProps) {
|
||||||
@@ -175,17 +111,8 @@ function SelectedServerProviders(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LegacyServerLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
|
||||||
return (
|
|
||||||
<SelectedServerProviders>
|
|
||||||
<LegacyServerScopedShell serverScoped={props.serverScoped}>{props.children}</LegacyServerScopedShell>
|
|
||||||
</SelectedServerProviders>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DraftRoute() {
|
function DraftRoute() {
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const settings = useSettings()
|
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
return (
|
return (
|
||||||
<Show when={tabs.ready()}>
|
<Show when={tabs.ready()}>
|
||||||
@@ -194,14 +121,7 @@ function DraftRoute() {
|
|||||||
keyed
|
keyed
|
||||||
fallback={<Navigate href="/" />}
|
fallback={<Navigate href="/" />}
|
||||||
>
|
>
|
||||||
{(draft) => (
|
{(draft) => <ResolvedDraftRoute draft={draft} />}
|
||||||
<Show
|
|
||||||
when={settings.general.newLayoutDesigns()}
|
|
||||||
fallback={<Navigate href={`/${base64Encode(draft.directory)}/session`} />}
|
|
||||||
>
|
|
||||||
<ResolvedDraftRoute draft={draft} />
|
|
||||||
</Show>
|
|
||||||
)}
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -234,31 +154,13 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
|||||||
|
|
||||||
function UiI18nBridge(props: ParentProps) {
|
function UiI18nBridge(props: ParentProps) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
|
return (
|
||||||
}
|
<I18nProvider
|
||||||
|
value={{ locale: language.intl, layoutLocale: language.layoutLocale, t: language.t, plural: language.plural }}
|
||||||
function LayoutCompatibility(props: ParentProps) {
|
>
|
||||||
const global = useGlobal()
|
{props.children}
|
||||||
const navigate = useNavigate()
|
</I18nProvider>
|
||||||
const server = useServer()
|
)
|
||||||
const settings = useSettings()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (settings.general.newLayoutDesigns()) return
|
|
||||||
const current = server.current
|
|
||||||
if (!current) return
|
|
||||||
const protocol = global.ensureServerCtx(current).sdk.protocolKind()
|
|
||||||
if (protocol !== "v2") return
|
|
||||||
const next = global.servers.list().find((s) => {
|
|
||||||
if (ServerConnection.key(s) === ServerConnection.key(current)) return false
|
|
||||||
return global.ensureServerCtx(s).sdk.protocolKind() !== "v2"
|
|
||||||
})
|
|
||||||
if (!next) return
|
|
||||||
navigate("/")
|
|
||||||
queueMicrotask(() => server.setActive(ServerConnection.key(next)))
|
|
||||||
})
|
|
||||||
|
|
||||||
return <>{props.children}</>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -287,17 +189,11 @@ function QueryProvider(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BodyDesignClass() {
|
function BodyDesignClass() {
|
||||||
const settings = useSettings()
|
|
||||||
|
|
||||||
createRenderEffect(() => {
|
createRenderEffect(() => {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
|
document.body.toggleAttribute("data-new-layout", true)
|
||||||
const enabled = settings.general.newLayoutDesigns()
|
document.body.classList.remove("text-12-regular")
|
||||||
document.body.toggleAttribute("data-new-layout", enabled)
|
document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]")
|
||||||
document.body.classList.toggle("text-12-regular", !enabled)
|
|
||||||
document.body.classList.toggle("font-(family-name:--font-family-text)", enabled)
|
|
||||||
document.body.classList.toggle("text-[13px]", enabled)
|
|
||||||
document.body.classList.toggle("font-[440]", enabled)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -327,7 +223,7 @@ function DesktopCommands() {
|
|||||||
if (platform.platform === "desktop" && platform.exportDebugLogs) {
|
if (platform.platform === "desktop" && platform.exportDebugLogs) {
|
||||||
commands.push({
|
commands.push({
|
||||||
id: "logs.export",
|
id: "logs.export",
|
||||||
title: "Export logs",
|
title: language.t("command.logs.export"),
|
||||||
category: language.t("command.category.settings"),
|
category: language.t("command.category.settings"),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
void platform.exportDebugLogs?.()
|
void platform.exportDebugLogs?.()
|
||||||
@@ -340,7 +236,6 @@ function DesktopCommands() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-scoped providers shared by the legacy shell and the top-level new shell.
|
|
||||||
type ServerScopedShellProps = ParentProps<{
|
type ServerScopedShellProps = ParentProps<{
|
||||||
directory?: () => string | undefined
|
directory?: () => string | undefined
|
||||||
serverScoped?: JSX.Element
|
serverScoped?: JSX.Element
|
||||||
@@ -355,19 +250,11 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LegacyServerScopedShell(props: ServerScopedShellProps) {
|
function AppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
||||||
return (
|
|
||||||
<ServerScopedProviders directory={props.directory} serverScoped={props.serverScoped}>
|
|
||||||
<LegacyLayout>{props.children}</LegacyLayout>
|
|
||||||
</ServerScopedProviders>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
|
||||||
return (
|
return (
|
||||||
<SelectedServerProviders>
|
<SelectedServerProviders>
|
||||||
<ServerScopedProviders serverScoped={props.serverScoped}>
|
<ServerScopedProviders serverScoped={props.serverScoped}>
|
||||||
<NewLayout>{props.children}</NewLayout>
|
<Layout>{props.children}</Layout>
|
||||||
</ServerScopedProviders>
|
</ServerScopedProviders>
|
||||||
</SelectedServerProviders>
|
</SelectedServerProviders>
|
||||||
)
|
)
|
||||||
@@ -385,7 +272,12 @@ function DraftProviders(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
export function AppBaseProviders(
|
||||||
|
props: ParentProps<{
|
||||||
|
locale?: Locale
|
||||||
|
onNativeTranslations?: Parameters<typeof LanguageProvider>[0]["onNativeTranslations"]
|
||||||
|
}>,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
@@ -394,7 +286,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
void window.api?.setTitlebar?.({ mode, scheme })
|
void window.api?.setTitlebar?.({ mode, scheme })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LanguageProvider locale={props.locale}>
|
<LanguageProvider locale={props.locale} onNativeTranslations={props.onNativeTranslations}>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallback={(error) => {
|
fallback={(error) => {
|
||||||
@@ -405,9 +297,7 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
|||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<WslServersProvider>
|
<WslServersProvider>
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<MarkedProvider>
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
|
||||||
</MarkedProvider>
|
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
</WslServersProvider>
|
</WslServersProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
@@ -556,7 +446,7 @@ export function AppInterface(props: {
|
|||||||
startup?: Promise<void>
|
startup?: Promise<void>
|
||||||
serverScoped?: JSX.Element
|
serverScoped?: JSX.Element
|
||||||
}) {
|
}) {
|
||||||
// The visual new layout lives in the router root so it remains mounted across
|
// The visual layout lives in the router root so it remains mounted across
|
||||||
// route changes. Draft and session routes override only their server-bound data
|
// route changes. Draft and session routes override only their server-bound data
|
||||||
// providers beneath it.
|
// providers beneath it.
|
||||||
const ServerShell = (shellProps: ParentProps) => (
|
const ServerShell = (shellProps: ParentProps) => (
|
||||||
@@ -577,26 +467,22 @@ export function AppInterface(props: {
|
|||||||
<GlobalProvider>
|
<GlobalProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
||||||
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
<Dynamic
|
||||||
<Dynamic
|
component={props.router ?? Router}
|
||||||
component={props.router ?? Router}
|
root={(routerProps) => (
|
||||||
root={(routerProps) => (
|
<TabsProvider>
|
||||||
<TabsProvider>
|
<PermissionProvider>
|
||||||
<PermissionProvider>
|
<NotificationProvider>
|
||||||
<NotificationProvider>
|
<ServerShell>
|
||||||
<ServerShell>
|
<AppLayout serverScoped={props.serverScoped}>{routerProps.children}</AppLayout>
|
||||||
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
</ServerShell>
|
||||||
<NewAppLayout serverScoped={props.serverScoped}>{routerProps.children}</NewAppLayout>
|
</NotificationProvider>
|
||||||
</Show>
|
</PermissionProvider>
|
||||||
</ServerShell>
|
</TabsProvider>
|
||||||
</NotificationProvider>
|
)}
|
||||||
</PermissionProvider>
|
>
|
||||||
</TabsProvider>
|
<Routes />
|
||||||
)}
|
</Dynamic>
|
||||||
>
|
|
||||||
<Routes serverScoped={props.serverScoped} />
|
|
||||||
</Dynamic>
|
|
||||||
</Show>
|
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
</GlobalProvider>
|
</GlobalProvider>
|
||||||
@@ -604,40 +490,20 @@ export function AppInterface(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Routes(props: { serverScoped?: JSX.Element }) {
|
function Routes() {
|
||||||
const settings = useSettings()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Route
|
<Route path="/" component={Home} />
|
||||||
component={(routeProps) => (
|
<Route path="/:dir" component={DirectoryDraftRedirect} />
|
||||||
<LegacyServerLayout serverScoped={props.serverScoped}>{routeProps.children}</LegacyServerLayout>
|
<Route path="/:dir/session" component={DirectoryDraftRedirect} />
|
||||||
)}
|
<Route path="/:dir/session/:id" component={LegacySessionRedirect} />
|
||||||
>
|
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
||||||
<Show when={!settings.general.newLayoutDesigns()}>
|
|
||||||
{
|
|
||||||
<>
|
|
||||||
<Route path="/" component={LegacyHome} />
|
|
||||||
<Route path="/server/:serverKey/session/:id" component={LegacyTargetSessionRoute} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</Show>
|
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
|
||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
<Show when={settings.general.newLayoutDesigns()}>
|
|
||||||
<Route path="/" component={NewHome} />
|
|
||||||
<Route path="/:dir/session/:id" component={NewLayoutLegacySessionRedirect} />
|
|
||||||
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
|
||||||
</Show>
|
|
||||||
<Route path="/new-session" component={DraftRoute} />
|
<Route path="/new-session" component={DraftRoute} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewLayoutLegacySessionRedirect() {
|
function LegacySessionRedirect() {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const params = useParams<{ id: string }>()
|
const params = useParams<{ id: string }>()
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 187 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB |
@@ -14,7 +14,6 @@ import { useTabs } from "@/context/tabs"
|
|||||||
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { normalizeSessionInfo } from "@/utils/session"
|
|
||||||
|
|
||||||
export type CommandPaletteEntry = {
|
export type CommandPaletteEntry = {
|
||||||
id: string
|
id: string
|
||||||
@@ -146,7 +145,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
|||||||
server: ServerConnection.key(serverSDK.server),
|
server: ServerConnection.key(serverSDK.server),
|
||||||
opened: serverCtx.projects.list,
|
opened: serverCtx.projects.list,
|
||||||
stored: () => serverCtx.sync.data.project,
|
stored: () => serverCtx.sync.data.project,
|
||||||
load: (search, signal) => serverSDK.currentApi.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||||
untitled: () => language.t("command.session.new"),
|
untitled: () => language.t("command.session.new"),
|
||||||
category: () => language.t("command.category.session"),
|
category: () => language.t("command.category.session"),
|
||||||
})
|
})
|
||||||
@@ -257,7 +256,6 @@ export function createServerSessionEntries(props: {
|
|||||||
.load(search, current.signal)
|
.load(search, current.signal)
|
||||||
.then((result) =>
|
.then((result) =>
|
||||||
result.data
|
result.data
|
||||||
.map(normalizeSessionInfo)
|
|
||||||
.filter((session) => !session.time.archived)
|
.filter((session) => !session.time.archived)
|
||||||
.map((session) => {
|
.map((session) => {
|
||||||
const project =
|
const project =
|
||||||
@@ -266,9 +264,9 @@ export function createServerSessionEntries(props: {
|
|||||||
id: `session:${props.server}:${session.id}`,
|
id: `session:${props.server}:${session.id}`,
|
||||||
type: "session" as const,
|
type: "session" as const,
|
||||||
title: session.title || props.untitled(),
|
title: session.title || props.untitled(),
|
||||||
description: project ? displayName(project) : getFilename(session.directory),
|
description: project ? displayName(project) : getFilename(session.location.directory),
|
||||||
category: props.category(),
|
category: props.category(),
|
||||||
directory: session.directory,
|
directory: session.location.directory,
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
server: props.server,
|
server: props.server,
|
||||||
project,
|
project,
|
||||||
|
|||||||
@@ -60,35 +60,44 @@ function Cell(props: {
|
|||||||
label: string
|
label: string
|
||||||
tip: string
|
tip: string
|
||||||
value: string
|
value: string
|
||||||
wide?: boolean
|
span?: 2 | 3
|
||||||
}) {
|
}) {
|
||||||
const content = () => (
|
const content = () => (
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex min-w-0 items-center": true,
|
"flex min-w-0 items-center": true,
|
||||||
"min-h-[20px] w-fit flex-row justify-start gap-1.5 px-1.5 py-0.5 text-left": !!props.inline,
|
"min-h-[20px] w-fit justify-start px-1.5 py-0.5 text-left": !!props.inline,
|
||||||
"justify-center text-center": !props.inline,
|
"justify-center text-center": !props.inline,
|
||||||
"min-h-[42px] w-full flex-col rounded-[8px] px-0.5 py-1": !props.inline,
|
"min-h-[42px] w-full flex-col rounded-[8px] px-0.5 py-1": !props.inline,
|
||||||
"col-span-2": !!props.wide && !props.inline,
|
"col-span-2": props.span === 2 && !props.inline,
|
||||||
|
"col-span-3": props.span === 3 && !props.inline,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
|
"flex min-w-0": true,
|
||||||
|
"-translate-y-px items-baseline gap-1.5": !!props.inline,
|
||||||
|
"flex-col items-center": !props.inline,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.label}
|
<div
|
||||||
</div>
|
classList={{
|
||||||
<div
|
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
|
||||||
classList={{
|
}}
|
||||||
"uppercase leading-none font-bold tabular-nums": true,
|
>
|
||||||
"text-[11px]": !!props.inline,
|
{props.label}
|
||||||
"text-[13px] sm:text-[14px]": !props.inline,
|
</div>
|
||||||
"text-text-on-critical-base": !!props.bad,
|
<div
|
||||||
"opacity-70": !!props.dim,
|
classList={{
|
||||||
}}
|
"uppercase leading-none font-bold tabular-nums": true,
|
||||||
>
|
"text-[11px]": !!props.inline,
|
||||||
{props.value}
|
"text-[13px] sm:text-[14px]": !props.inline,
|
||||||
|
"text-text-on-critical-base": !!props.bad,
|
||||||
|
"opacity-70": !!props.dim,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.value}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -108,37 +117,50 @@ function Cell(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => void }) {
|
function ToggleCell(props: {
|
||||||
|
active: boolean
|
||||||
|
inline?: boolean
|
||||||
|
label: string
|
||||||
|
onClick: () => void
|
||||||
|
tip: string
|
||||||
|
value: string
|
||||||
|
}) {
|
||||||
const content = () => (
|
const content = () => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Force focus styles on all interactive elements"
|
aria-label={`${props.label}: ${props.value}`}
|
||||||
aria-pressed={props.active}
|
aria-pressed={props.active}
|
||||||
classList={{
|
classList={{
|
||||||
"flex min-w-0 items-center font-mono uppercase hover:bg-surface-raised-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-border-focus": true,
|
"flex min-w-0 items-center font-mono uppercase hover:bg-surface-raised-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-border-focus": true,
|
||||||
"min-h-[20px] w-fit flex-row justify-start gap-1.5 rounded px-1.5 py-0.5 text-left": !!props.inline,
|
"min-h-[20px] w-fit justify-start rounded px-1.5 py-0.5 text-left": !!props.inline,
|
||||||
"min-h-[42px] w-full flex-col justify-center rounded-[8px] px-0.5 py-1 text-center": !props.inline,
|
"min-h-[42px] w-full flex-col justify-center rounded-[8px] px-0.5 py-1 text-center": !props.inline,
|
||||||
"bg-surface-raised-base text-text-strong": props.active,
|
"bg-surface-raised-base text-text-strong": props.active,
|
||||||
}}
|
}}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
<span class="text-[10px] leading-none font-black tracking-[0.04em] opacity-70">FOCUS</span>
|
<span
|
||||||
<span classList={{ "leading-none font-bold": true, "text-[11px]": !!props.inline, "text-[13px]": !props.inline }}>
|
classList={{
|
||||||
{props.active ? "ON" : "OFF"}
|
flex: true,
|
||||||
|
"-translate-y-px items-baseline gap-1.5": !!props.inline,
|
||||||
|
"flex-col items-center": !props.inline,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="text-[10px] leading-none font-black tracking-[0.04em] opacity-70">{props.label}</span>
|
||||||
|
<span class="text-[11px] leading-none font-bold">{props.value}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (props.inline) {
|
if (props.inline) {
|
||||||
return (
|
return (
|
||||||
<TooltipV2 value="Force focus styles on all interactive elements" placement="top">
|
<TooltipV2 value={props.tip} placement="top">
|
||||||
{content()}
|
{content()}
|
||||||
</TooltipV2>
|
</TooltipV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip value="Force focus styles on all interactive elements" placement="top">
|
<Tooltip value={props.tip} placement="top">
|
||||||
{content()}
|
{content()}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
@@ -457,7 +479,7 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
"gap-[9px]": !!props.inline,
|
"gap-[9px]": !!props.inline,
|
||||||
"gap-px": !props.inline,
|
"gap-px": !props.inline,
|
||||||
"flex w-full flex-nowrap items-center justify-start": !!props.inline,
|
"flex w-full flex-nowrap items-center justify-start": !!props.inline,
|
||||||
"grid-cols-5": !props.inline,
|
"grid-cols-4": !props.inline,
|
||||||
grid: !props.inline,
|
grid: !props.inline,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -539,10 +561,25 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
bad={bad(heap(), 0.8)}
|
bad={bad(heap(), 0.8)}
|
||||||
dim={state.heap.used === undefined}
|
dim={state.heap.used === undefined}
|
||||||
inline={props.inline}
|
inline={props.inline}
|
||||||
wide={!platform.setForceFocus}
|
span={platform.setForceFocus ? 2 : 3}
|
||||||
|
/>
|
||||||
|
<ToggleCell
|
||||||
|
active={language.direction() === "rtl"}
|
||||||
|
inline={props.inline}
|
||||||
|
label={language.t("debugBar.direction.label")}
|
||||||
|
tip={language.t("debugBar.direction.tip")}
|
||||||
|
value={language.t(`debugBar.direction.${language.direction()}`)}
|
||||||
|
onClick={() => language.setDirection(language.direction() === "rtl" ? "ltr" : "rtl")}
|
||||||
/>
|
/>
|
||||||
{platform.setForceFocus && (
|
{platform.setForceFocus && (
|
||||||
<FocusCell active={state.focus} inline={props.inline} onClick={() => void toggleFocus()} />
|
<ToggleCell
|
||||||
|
active={state.focus}
|
||||||
|
inline={props.inline}
|
||||||
|
label={language.t("debugBar.focus.label")}
|
||||||
|
tip={language.t("debugBar.focus.tip")}
|
||||||
|
value={language.t(state.focus ? "debugBar.focus.on" : "debugBar.focus.off")}
|
||||||
|
onClick={() => void toggleFocus()}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export function DialogHomeCommandPaletteV2(props: {
|
|||||||
server: ServerConnection.key(props.server),
|
server: ServerConnection.key(props.server),
|
||||||
opened: serverCtx.projects.list,
|
opened: serverCtx.projects.list,
|
||||||
stored: () => serverCtx.sync.data.project,
|
stored: () => serverCtx.sync.data.project,
|
||||||
load: (search, signal) => serverCtx.sdk.currentApi.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
load: (search, signal) => serverCtx.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||||
untitled: () => language.t("command.session.new"),
|
untitled: () => language.t("command.session.new"),
|
||||||
category: () => language.t("command.category.session"),
|
category: () => language.t("command.category.session"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode-ai/client/promise"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
@@ -13,33 +12,21 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
|||||||
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import {
|
import { type Accessor, type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
|
||||||
type Accessor,
|
import { createStore } from "solid-js/store"
|
||||||
type Component,
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createResource,
|
|
||||||
createUniqueId,
|
|
||||||
For,
|
|
||||||
Match,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
Show,
|
|
||||||
Switch,
|
|
||||||
} from "solid-js"
|
|
||||||
import { createStore, produce } from "solid-js/store"
|
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { Link } from "@/components/link"
|
import { ExternalLink } from "@/components/external-link"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
import { CustomProviderForm } from "./dialog-custom-provider"
|
import { CustomProviderForm } from "./dialog-custom-provider"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
|
import { createProviderConnectionController, type ProviderConnectMethod } from "./provider-connection-controller"
|
||||||
|
|
||||||
const CUSTOM_ID = "_custom"
|
const CUSTOM_ID = "_custom"
|
||||||
type ConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
|
type IntegrationForm = NonNullable<ProviderConnectMethod["form"]>[number]
|
||||||
|
type StringForm = Extract<IntegrationForm, { type: "string" }>
|
||||||
|
|
||||||
export function useProviderConnectController(options: { onBack?: () => void } = {}) {
|
export function useProviderConnectController(options: { onBack?: () => void } = {}) {
|
||||||
const [store, setStore] = createStore({ selected: undefined as string | undefined })
|
const [store, setStore] = createStore({ selected: undefined as string | undefined })
|
||||||
@@ -383,120 +370,29 @@ function ProviderConnection(props: {
|
|||||||
}) {
|
}) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const newLayout = settings.general.newLayoutDesigns
|
const newLayout = settings.general.newLayoutDesigns
|
||||||
const providers = useProviders(() => props.directory?.())
|
const providers = useProviders(() => props.directory?.())
|
||||||
const directory = () => props.directory?.() ?? decode64(params.dir)
|
const directory = () => props.directory?.() ?? decode64(params.dir)
|
||||||
const location = () => {
|
|
||||||
const value = directory()
|
|
||||||
return value ? { directory: value } : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const alive = { value: true }
|
|
||||||
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
alive.value = false
|
|
||||||
if (timer.current === undefined) return
|
|
||||||
clearTimeout(timer.current)
|
|
||||||
timer.current = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const provider = createMemo(
|
const provider = createMemo(
|
||||||
() => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!,
|
() => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!,
|
||||||
)
|
)
|
||||||
const fallback = createMemo<ConnectMethod[]>(() => [
|
const controller = createProviderConnectionController({
|
||||||
{
|
provider: () => props.provider,
|
||||||
type: "key" as const,
|
directory,
|
||||||
label: language.t("provider.connect.method.apiKey"),
|
onComplete: () => {
|
||||||
|
dialog.close()
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("provider.connect.toast.connected.title", { provider: provider().name }),
|
||||||
|
description: language.t("provider.connect.toast.connected.description", { provider: provider().name }),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
])
|
|
||||||
const [integration] = createResource(
|
|
||||||
() => ({ provider: props.provider, directory: directory() }),
|
|
||||||
(input) =>
|
|
||||||
serverSDK()
|
|
||||||
.currentApi.integration.get({
|
|
||||||
integrationID: input.provider,
|
|
||||||
location: input.directory ? { directory: input.directory } : undefined,
|
|
||||||
})
|
|
||||||
.then((result) => result.data),
|
|
||||||
)
|
|
||||||
const loading = createMemo(() => integration.loading)
|
|
||||||
const methods = createMemo<ConnectMethod[]>(() => {
|
|
||||||
const values = integration.latest?.methods.filter(
|
|
||||||
(method): method is ConnectMethod => method.type === "key" || method.type === "oauth",
|
|
||||||
)
|
|
||||||
return values?.length ? values : fallback()
|
|
||||||
})
|
})
|
||||||
const [store, setStore] = createStore({
|
|
||||||
methodIndex: undefined as undefined | number,
|
|
||||||
authorization: undefined as undefined | IntegrationOauthConnectOutput["data"],
|
|
||||||
promptInputs: undefined as undefined | Record<string, string>,
|
|
||||||
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
|
|
||||||
error: undefined as string | undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
type Action =
|
|
||||||
| { type: "method.select"; index: number }
|
|
||||||
| { type: "method.reset" }
|
|
||||||
| { type: "auth.prompt" }
|
|
||||||
| { type: "auth.inputs"; inputs: Record<string, string> }
|
|
||||||
| { type: "auth.pending" }
|
|
||||||
| { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] }
|
|
||||||
| { type: "auth.error"; error: string }
|
|
||||||
|
|
||||||
function dispatch(action: Action) {
|
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
|
||||||
if (action.type === "method.select") {
|
|
||||||
draft.methodIndex = action.index
|
|
||||||
draft.authorization = undefined
|
|
||||||
draft.promptInputs = undefined
|
|
||||||
draft.state = undefined
|
|
||||||
draft.error = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (action.type === "method.reset") {
|
|
||||||
draft.methodIndex = undefined
|
|
||||||
draft.authorization = undefined
|
|
||||||
draft.promptInputs = undefined
|
|
||||||
draft.state = undefined
|
|
||||||
draft.error = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (action.type === "auth.prompt") {
|
|
||||||
draft.state = "prompt"
|
|
||||||
draft.error = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (action.type === "auth.inputs") {
|
|
||||||
draft.promptInputs = action.inputs
|
|
||||||
draft.state = undefined
|
|
||||||
draft.error = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (action.type === "auth.pending") {
|
|
||||||
draft.state = "pending"
|
|
||||||
draft.error = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (action.type === "auth.complete") {
|
|
||||||
draft.state = "complete"
|
|
||||||
draft.authorization = action.authorization
|
|
||||||
draft.error = undefined
|
|
||||||
return
|
|
||||||
}
|
|
||||||
draft.state = "error"
|
|
||||||
draft.error = action.error
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
|
|
||||||
|
|
||||||
const methodLabel = (value?: { type?: string; label?: string }) => {
|
const methodLabel = (value?: { type?: string; label?: string }) => {
|
||||||
if (!value) return ""
|
if (!value) return ""
|
||||||
if (value.type === "key") return language.t("provider.connect.method.apiKey")
|
if (value.type === "key") return language.t("provider.connect.method.apiKey")
|
||||||
@@ -509,160 +405,116 @@ function ProviderConnection(props: {
|
|||||||
const hint = suffix?.[1]
|
const hint = suffix?.[1]
|
||||||
return {
|
return {
|
||||||
label: suffix ? label.slice(0, -suffix[0].length) : label,
|
label: suffix ? label.slice(0, -suffix[0].length) : label,
|
||||||
hint: hint ? hint[0].toUpperCase() + hint.slice(1) : value?.type === "key" ? "Browser" : undefined,
|
hint:
|
||||||
|
hint?.toLowerCase() === "headless"
|
||||||
|
? language.t("provider.connect.method.headless")
|
||||||
|
: hint?.toLowerCase() === "browser" || (!hint && value?.type === "key")
|
||||||
|
? language.t("provider.connect.method.browser")
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatError(value: unknown, fallback: string): string {
|
function AuthFormView() {
|
||||||
if (value && typeof value === "object" && "data" in value) {
|
|
||||||
const data = (value as { data?: { message?: unknown } }).data
|
|
||||||
if (typeof data?.message === "string" && data.message) return data.message
|
|
||||||
}
|
|
||||||
if (value && typeof value === "object" && "error" in value) {
|
|
||||||
const nested = formatError((value as { error?: unknown }).error, "")
|
|
||||||
if (nested) return nested
|
|
||||||
}
|
|
||||||
if (value && typeof value === "object" && "message" in value) {
|
|
||||||
const message = (value as { message?: unknown }).message
|
|
||||||
if (typeof message === "string" && message) return message
|
|
||||||
}
|
|
||||||
if (value instanceof Error && value.message) return value.message
|
|
||||||
if (typeof value === "string" && value) return value
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectMethod(index: number, inputs?: Record<string, string>) {
|
|
||||||
if (timer.current !== undefined) {
|
|
||||||
clearTimeout(timer.current)
|
|
||||||
timer.current = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const method = methods()[index]
|
|
||||||
dispatch({ type: "method.select", index })
|
|
||||||
|
|
||||||
if (method.type === "oauth") {
|
|
||||||
if (method.prompts?.length && !inputs) {
|
|
||||||
dispatch({ type: "auth.prompt" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dispatch({ type: "auth.pending" })
|
|
||||||
await serverSDK()
|
|
||||||
.currentApi.integration.oauth.connect({
|
|
||||||
integrationID: props.provider,
|
|
||||||
methodID: method.id,
|
|
||||||
inputs: inputs ?? {},
|
|
||||||
location: location(),
|
|
||||||
})
|
|
||||||
.then((x) => {
|
|
||||||
if (!alive.value) return
|
|
||||||
dispatch({ type: "auth.complete", authorization: x.data })
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (!alive.value) return
|
|
||||||
dispatch({ type: "auth.error", error: formatError(e, language.t("common.requestFailed")) })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuthPromptsView() {
|
|
||||||
const [formStore, setFormStore] = createStore({
|
const [formStore, setFormStore] = createStore({
|
||||||
value: {} as Record<string, string>,
|
value: {} as Record<string, string>,
|
||||||
index: 0,
|
index: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const prompts = createMemo(() => {
|
const fields = createMemo<StringForm[]>(() => {
|
||||||
const value = method()
|
const value = controller.currentMethod()
|
||||||
return value?.type === "oauth" ? (value.prompts ?? []) : []
|
return (value?.form ?? []).flatMap((field) => (field.type === "string" ? [field] : []))
|
||||||
})
|
})
|
||||||
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
|
const matches = (field: StringForm, value: Record<string, string>) => {
|
||||||
if (!prompt.when) return true
|
return (field.when ?? []).every((condition) => {
|
||||||
const actual = value[prompt.when.key]
|
const actual = value[condition.key]
|
||||||
if (actual === undefined) return false
|
if (actual === undefined) return false
|
||||||
return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
|
return condition.op === "eq" ? actual === condition.value : actual !== condition.value
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const current = createMemo(() => {
|
const current = createMemo(() => {
|
||||||
const all = prompts()
|
const all = fields()
|
||||||
const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
|
const index = all.findIndex((field, index) => index >= formStore.index && matches(field, formStore.value))
|
||||||
if (index === -1) return
|
if (index === -1) return undefined
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
prompt: all[index],
|
field: all[index],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const valid = createMemo(() => {
|
const valid = createMemo(() => {
|
||||||
const item = current()
|
const item = current()
|
||||||
if (!item || item.prompt.type !== "text") return false
|
if (!item || item.field.options) return false
|
||||||
const value = formStore.value[item.prompt.key] ?? ""
|
if (!item.field.required) return true
|
||||||
return value.trim().length > 0
|
return (formStore.value[item.field.key] ?? "").trim().length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
async function next(index: number, value: Record<string, string>) {
|
async function next(index: number, value: Record<string, string>) {
|
||||||
if (store.methodIndex === undefined) return
|
const selected = controller.methodIndex()
|
||||||
const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
|
if (selected === undefined) return
|
||||||
|
const next = fields().findIndex((field, i) => i > index && matches(field, value))
|
||||||
if (next !== -1) {
|
if (next !== -1) {
|
||||||
setFormStore("index", next)
|
setFormStore("index", next)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await selectMethod(store.methodIndex, value)
|
await controller.auth.select(selected, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const item = current()
|
const item = current()
|
||||||
if (!item || item.prompt.type !== "text") return
|
if (!item || item.field.options) return
|
||||||
if (!valid()) return
|
if (!valid()) return
|
||||||
await next(item.index, formStore.value)
|
await next(item.index, formStore.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = () => current()
|
const item = () => current()
|
||||||
const text = createMemo(() => {
|
const text = createMemo(() => {
|
||||||
const prompt = item()?.prompt
|
const field = item()?.field
|
||||||
if (!prompt || prompt.type !== "text") return
|
if (!field || field.options) return undefined
|
||||||
return prompt
|
return field
|
||||||
})
|
})
|
||||||
const select = createMemo(() => {
|
const select = createMemo(() => {
|
||||||
const prompt = item()?.prompt
|
const field = item()?.field
|
||||||
if (!prompt || prompt.type !== "select") return
|
if (!field?.options) return undefined
|
||||||
return prompt
|
return field
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={item()?.prompt.type === "text"}>
|
<Match when={item()?.field.options === undefined}>
|
||||||
<TextField
|
<TextField
|
||||||
type="text"
|
type="text"
|
||||||
label={text()?.message ?? ""}
|
label={text()?.title ?? ""}
|
||||||
placeholder={text()?.placeholder}
|
placeholder={text()?.placeholder}
|
||||||
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
const prompt = text()
|
const field = text()
|
||||||
if (!prompt) return
|
if (!field) return
|
||||||
setFormStore("value", prompt.key, value)
|
setFormStore("value", field.key, value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
|
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
|
||||||
{language.t("common.continue")}
|
{language.t("common.continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={item()?.prompt.type === "select"}>
|
<Match when={item()?.field.options !== undefined}>
|
||||||
<div class="w-full flex flex-col gap-1.5">
|
<div class="w-full flex flex-col gap-1.5">
|
||||||
<div class="text-14-regular text-text-base">{select()?.message}</div>
|
<div class="text-14-regular text-text-base">{select()?.title}</div>
|
||||||
<div>
|
<div>
|
||||||
<List
|
<List
|
||||||
class="px-3"
|
class="px-3"
|
||||||
items={select()?.options ?? []}
|
items={select()?.options ?? []}
|
||||||
key={(x) => x.value}
|
key={(x) => x.value}
|
||||||
current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
|
current={select()?.options?.find((x) => x.value === formStore.value[select()!.key])}
|
||||||
onSelect={(value) => {
|
onSelect={(value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
const prompt = select()
|
const field = select()
|
||||||
if (!prompt) return
|
if (!field) return
|
||||||
const nextValue = {
|
const nextValue = {
|
||||||
...formStore.value,
|
...formStore.value,
|
||||||
[prompt.key]: value.value,
|
[field.key]: value.value,
|
||||||
}
|
}
|
||||||
setFormStore("value", prompt.key, value.value)
|
setFormStore("value", field.key, value.value)
|
||||||
void next(item()!.index, nextValue)
|
void next(item()!.index, nextValue)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -672,7 +524,7 @@ function ProviderConnection(props: {
|
|||||||
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||||
</div>
|
</div>
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
<span class="text-14-regular text-text-weak">{option.hint}</span>
|
<span class="text-14-regular text-text-weak">{option.description}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
@@ -693,32 +545,9 @@ function ProviderConnection(props: {
|
|||||||
listRef?.onKeyDown(e)
|
listRef?.onKeyDown(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
let auto = false
|
|
||||||
createEffect(() => {
|
|
||||||
if (auto) return
|
|
||||||
if (loading()) return
|
|
||||||
if (methods().length === 1) {
|
|
||||||
auto = true
|
|
||||||
void selectMethod(0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function complete() {
|
|
||||||
await serverSync()
|
|
||||||
.refreshProviders()
|
|
||||||
.catch(() => undefined)
|
|
||||||
dialog.close()
|
|
||||||
showToast({
|
|
||||||
variant: "success",
|
|
||||||
icon: "circle-check",
|
|
||||||
title: language.t("provider.connect.toast.connected.title", { provider: provider().name }),
|
|
||||||
description: language.t("provider.connect.toast.connected.description", { provider: provider().name }),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (methods().length > 1 && store.methodIndex !== undefined) {
|
if (controller.methods().length > 1 && controller.methodIndex() !== undefined) {
|
||||||
dispatch({ type: "method.reset" })
|
controller.auth.reset()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
props.onBack()
|
props.onBack()
|
||||||
@@ -734,14 +563,14 @@ function ProviderConnection(props: {
|
|||||||
{language.t("provider.connect.selectMethod", { provider: provider().name })}
|
{language.t("provider.connect.selectMethod", { provider: provider().name })}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<For each={methods()}>
|
<For each={controller.methods()}>
|
||||||
{(item, index) => {
|
{(item, index) => {
|
||||||
const details = () => methodDetails(item)
|
const details = () => methodDetails(item)
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="group flex h-9 w-full items-center gap-2 rounded-md px-3 text-left text-[13px] leading-5 tracking-[-0.04px] hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
class="group flex h-9 w-full items-center gap-2 rounded-md px-3 text-left text-[13px] leading-5 tracking-[-0.04px] hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
||||||
onClick={() => void selectMethod(index())}
|
onClick={() => void controller.auth.select(index())}
|
||||||
>
|
>
|
||||||
<span class="flex h-2 w-4 shrink-0 items-center justify-center rounded-[1px] bg-v2-background-bg-base shadow-[var(--v2-elevation-button-neutral)]">
|
<span class="flex h-2 w-4 shrink-0 items-center justify-center rounded-[1px] bg-v2-background-bg-base shadow-[var(--v2-elevation-button-neutral)]">
|
||||||
<span class="hidden h-0.5 w-2.5 bg-v2-icon-icon-base group-hover:block group-focus-visible:block" />
|
<span class="hidden h-0.5 w-2.5 bg-v2-icon-icon-base group-hover:block group-focus-visible:block" />
|
||||||
@@ -769,11 +598,11 @@ function ProviderConnection(props: {
|
|||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
listRef = ref
|
listRef = ref
|
||||||
}}
|
}}
|
||||||
items={methods}
|
items={controller.methods}
|
||||||
key={(m) => m?.label ?? m?.type}
|
key={(m) => m?.label ?? m?.type}
|
||||||
onSelect={async (selected, index) => {
|
onSelect={async (selected, index) => {
|
||||||
if (!selected) return
|
if (!selected) return
|
||||||
void selectMethod(index)
|
void controller.auth.select(index)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
@@ -806,9 +635,9 @@ function ProviderConnection(props: {
|
|||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const form = e.currentTarget as HTMLFormElement
|
if (!(e.currentTarget instanceof HTMLFormElement)) return
|
||||||
const formData = new FormData(form)
|
const value = new FormData(e.currentTarget).get("apiKey")
|
||||||
const apiKey = formData.get("apiKey") as string
|
const apiKey = typeof value === "string" ? value : ""
|
||||||
|
|
||||||
if (!apiKey?.trim()) {
|
if (!apiKey?.trim()) {
|
||||||
setFormStore("error", language.t("provider.connect.apiKey.required"))
|
setFormStore("error", language.t("provider.connect.apiKey.required"))
|
||||||
@@ -816,12 +645,7 @@ function ProviderConnection(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFormStore("error", undefined)
|
setFormStore("error", undefined)
|
||||||
await serverSDK().currentApi.integration.connect.key({
|
await controller.auth.connectKey(apiKey)
|
||||||
integrationID: props.provider,
|
|
||||||
location: location(),
|
|
||||||
key: apiKey,
|
|
||||||
})
|
|
||||||
await complete()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLayout())
|
if (newLayout())
|
||||||
@@ -836,12 +660,12 @@ function ProviderConnection(props: {
|
|||||||
<div>{language.t("provider.connect.opencodeZen.line2")}</div>
|
<div>{language.t("provider.connect.opencodeZen.line2")}</div>
|
||||||
<div>
|
<div>
|
||||||
{language.t("provider.connect.opencodeZen.visit.prefix")}
|
{language.t("provider.connect.opencodeZen.visit.prefix")}
|
||||||
<Link
|
<ExternalLink
|
||||||
href="https://opencode.ai/zen"
|
href="https://opencode.ai/zen"
|
||||||
class="text-v2-text-text-base focus-visible:rounded-xs focus-visible:outline-2 focus-visible:outline-v2-border-border-focus"
|
class="text-v2-text-text-base focus-visible:rounded-xs focus-visible:outline-2 focus-visible:outline-v2-border-border-focus"
|
||||||
>
|
>
|
||||||
{language.t("provider.connect.opencodeZen.visit.link")}
|
{language.t("provider.connect.opencodeZen.visit.link")}
|
||||||
</Link>
|
</ExternalLink>
|
||||||
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -886,9 +710,9 @@ function ProviderConnection(props: {
|
|||||||
<div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line2")}</div>
|
<div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line2")}</div>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
{language.t("provider.connect.opencodeZen.visit.prefix")}
|
{language.t("provider.connect.opencodeZen.visit.prefix")}
|
||||||
<Link href="https://opencode.ai/zen" tabIndex={-1}>
|
<ExternalLink href="https://opencode.ai/zen" tabIndex={-1}>
|
||||||
{language.t("provider.connect.opencodeZen.visit.link")}
|
{language.t("provider.connect.opencodeZen.visit.link")}
|
||||||
</Link>
|
</ExternalLink>
|
||||||
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -936,9 +760,9 @@ function ProviderConnection(props: {
|
|||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const form = e.currentTarget as HTMLFormElement
|
if (!(e.currentTarget instanceof HTMLFormElement)) return
|
||||||
const formData = new FormData(form)
|
const value = new FormData(e.currentTarget).get("code")
|
||||||
const code = formData.get("code") as string
|
const code = typeof value === "string" ? value : ""
|
||||||
|
|
||||||
if (!code?.trim()) {
|
if (!code?.trim()) {
|
||||||
setFormStore("error", language.t("provider.connect.oauth.code.required"))
|
setFormStore("error", language.t("provider.connect.oauth.code.required"))
|
||||||
@@ -946,20 +770,7 @@ function ProviderConnection(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFormStore("error", undefined)
|
setFormStore("error", undefined)
|
||||||
const result = await serverSDK()
|
setFormStore("error", await controller.auth.completeCode(code))
|
||||||
.currentApi.integration.oauth.complete({
|
|
||||||
integrationID: props.provider,
|
|
||||||
attemptID: store.authorization!.attemptID,
|
|
||||||
location: location(),
|
|
||||||
code,
|
|
||||||
})
|
|
||||||
.then(() => ({ ok: true as const }))
|
|
||||||
.catch((error) => ({ ok: false as const, error }))
|
|
||||||
if (result.ok) {
|
|
||||||
await complete()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid")))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLayout())
|
if (newLayout())
|
||||||
@@ -967,14 +778,14 @@ function ProviderConnection(props: {
|
|||||||
<div class="flex flex-col gap-5 px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
|
<div class="flex flex-col gap-5 px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted">
|
||||||
<div>
|
<div>
|
||||||
{language.t("provider.connect.oauth.code.visit.prefix")}
|
{language.t("provider.connect.oauth.code.visit.prefix")}
|
||||||
<Link href={store.authorization!.url} class="text-v2-text-text-base">
|
<ExternalLink href={controller.authorization()!.url} class="text-v2-text-text-base">
|
||||||
{language.t("provider.connect.oauth.code.visit.link")}
|
{language.t("provider.connect.oauth.code.visit.link")}
|
||||||
</Link>
|
</ExternalLink>
|
||||||
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
|
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
|
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-5 self-stretch">
|
||||||
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
|
<label class="flex w-full flex-col gap-1 font-[530] leading-4 text-v2-text-text-base">
|
||||||
{language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
|
{language.t("provider.connect.oauth.code.label", { method: controller.currentMethod()?.label ?? "" })}
|
||||||
<TextInputV2
|
<TextInputV2
|
||||||
ref={codeInput}
|
ref={codeInput}
|
||||||
class="!w-full"
|
class="!w-full"
|
||||||
@@ -1006,7 +817,9 @@ function ProviderConnection(props: {
|
|||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
{language.t("provider.connect.oauth.code.visit.prefix")}
|
{language.t("provider.connect.oauth.code.visit.prefix")}
|
||||||
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.code.visit.link")}</Link>
|
<ExternalLink href={controller.authorization()!.url}>
|
||||||
|
{language.t("provider.connect.oauth.code.visit.link")}
|
||||||
|
</ExternalLink>
|
||||||
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
|
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||||
@@ -1014,7 +827,9 @@ function ProviderConnection(props: {
|
|||||||
autofocus={!newLayout()}
|
autofocus={!newLayout()}
|
||||||
ref={codeInput}
|
ref={codeInput}
|
||||||
type="text"
|
type="text"
|
||||||
label={language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
|
label={language.t("provider.connect.oauth.code.label", {
|
||||||
|
method: controller.currentMethod()?.label ?? "",
|
||||||
|
})}
|
||||||
placeholder={language.t("provider.connect.oauth.code.placeholder")}
|
placeholder={language.t("provider.connect.oauth.code.placeholder")}
|
||||||
name="code"
|
name="code"
|
||||||
value={formStore.value}
|
value={formStore.value}
|
||||||
@@ -1032,52 +847,20 @@ function ProviderConnection(props: {
|
|||||||
|
|
||||||
function OAuthAutoView() {
|
function OAuthAutoView() {
|
||||||
const code = createMemo(() => {
|
const code = createMemo(() => {
|
||||||
const instructions = store.authorization?.instructions
|
const instructions = controller.authorization()?.instructions
|
||||||
if (instructions?.includes(":")) {
|
if (instructions?.includes(":")) {
|
||||||
return instructions.split(":").pop()?.trim()
|
return instructions.split(":").pop()?.trim()
|
||||||
}
|
}
|
||||||
return instructions
|
return instructions
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const poll = async () => {
|
|
||||||
const authorization = store.authorization
|
|
||||||
if (!authorization || !alive.value) return
|
|
||||||
const result = await serverSDK()
|
|
||||||
.currentApi.integration.oauth.status({
|
|
||||||
integrationID: props.provider,
|
|
||||||
attemptID: authorization.attemptID,
|
|
||||||
location: location(),
|
|
||||||
})
|
|
||||||
.then((value) => ({ ok: true as const, status: value.data }))
|
|
||||||
.catch((error) => ({ ok: false as const, error }))
|
|
||||||
if (!alive.value) return
|
|
||||||
if (!result.ok) {
|
|
||||||
dispatch({ type: "auth.error", error: formatError(result.error, language.t("common.requestFailed")) })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (result.status.status === "complete") {
|
|
||||||
await complete()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (result.status.status === "failed") {
|
|
||||||
dispatch({ type: "auth.error", error: result.status.message })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (result.status.status === "expired") {
|
|
||||||
dispatch({ type: "auth.error", error: language.t("common.requestFailed") })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
timer.current = setTimeout(poll, 1_000)
|
|
||||||
}
|
|
||||||
void poll()
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
{language.t("provider.connect.oauth.auto.visit.prefix")}
|
{language.t("provider.connect.oauth.auto.visit.prefix")}
|
||||||
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.auto.visit.link")}</Link>
|
<ExternalLink href={controller.authorization()!.url}>
|
||||||
|
{language.t("provider.connect.oauth.auto.visit.link")}
|
||||||
|
</ExternalLink>
|
||||||
{language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
|
{language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -1110,7 +893,9 @@ function ProviderConnection(props: {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
<Match
|
||||||
|
when={props.provider === "anthropic" && controller.currentMethod()?.label?.toLowerCase().includes("max")}
|
||||||
|
>
|
||||||
{language.t("provider.connect.title.anthropicProMax")}
|
{language.t("provider.connect.title.anthropicProMax")}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
|
<Match when={true}>{language.t("provider.connect.title", { provider: provider().name })}</Match>
|
||||||
@@ -1121,10 +906,10 @@ function ProviderConnection(props: {
|
|||||||
<div
|
<div
|
||||||
onKeyDown={handleKey}
|
onKeyDown={handleKey}
|
||||||
tabIndex={newLayout() ? undefined : 0}
|
tabIndex={newLayout() ? undefined : 0}
|
||||||
autofocus={!newLayout() && store.methodIndex === undefined ? true : undefined}
|
autofocus={!newLayout() && controller.methodIndex() === undefined ? true : undefined}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={loading()}>
|
<Match when={controller.loading()}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -1132,10 +917,10 @@ function ProviderConnection(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.methodIndex === undefined}>
|
<Match when={controller.methodIndex() === undefined}>
|
||||||
<MethodSelection />
|
<MethodSelection />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "pending"}>
|
<Match when={controller.auth.state() === "pending"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -1143,26 +928,26 @@ function ProviderConnection(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "prompt"}>
|
<Match when={controller.auth.state() === "form"}>
|
||||||
<AuthPromptsView />
|
<AuthFormView />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "error"}>
|
<Match when={controller.auth.state() === "error"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2">
|
||||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||||
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
|
<span>{language.t("provider.connect.status.failed", { error: controller.auth.error() ?? "" })}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={method()?.type === "key"}>
|
<Match when={controller.currentMethod()?.type === "key"}>
|
||||||
<ApiAuthView />
|
<ApiAuthView />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={method()?.type === "oauth"}>
|
<Match when={controller.currentMethod()?.type === "oauth"}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={store.authorization?.mode === "code"}>
|
<Match when={controller.authorization()?.mode === "code"}>
|
||||||
<OAuthCodeView />
|
<OAuthCodeView />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.authorization?.mode === "auto"}>
|
<Match when={controller.authorization()?.mode === "auto"}>
|
||||||
<OAuthAutoView />
|
<OAuthAutoView />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user