mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 03:19:59 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5539d8ef46 |
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- v2
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate:
|
generate:
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ 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
|
||||||
@@ -377,14 +378,16 @@ 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 }}
|
||||||
@@ -402,7 +405,8 @@ 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_KEYCHAIN: build.keychain
|
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
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,3 +251,4 @@ opencode-drive stop --name demo
|
|||||||
```bash
|
```bash
|
||||||
opencode-drive dir --name demo
|
opencode-drive dir --name demo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
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,6 +252,33 @@ 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.
|
Before Width: | Height: | Size: 62 KiB |
@@ -53,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@corvu/drawer": "catalog:",
|
"@corvu/drawer": "catalog:",
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"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.15",
|
"version": "1.18.8",
|
||||||
"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.15",
|
"version": "1.18.8",
|
||||||
"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.15",
|
"version": "1.18.8",
|
||||||
"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.15",
|
"version": "1.18.8",
|
||||||
"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,14 +388,13 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.12.1",
|
"gitlab-ai-provider": "6.11.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",
|
||||||
@@ -424,16 +423,16 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"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",
|
||||||
@@ -478,7 +477,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -492,7 +491,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -525,7 +524,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -541,7 +540,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.101",
|
"@effect/platform-node-shared": "4.0.0-beta.101",
|
||||||
},
|
},
|
||||||
@@ -571,23 +570,9 @@
|
|||||||
"@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.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/ai": "workspace:*",
|
"@opencode-ai/ai": "workspace:*",
|
||||||
@@ -693,7 +678,6 @@
|
|||||||
"@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:",
|
||||||
@@ -703,7 +687,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
@@ -718,11 +702,15 @@
|
|||||||
"@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,6 +723,7 @@
|
|||||||
"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:",
|
||||||
@@ -765,7 +754,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "1.18.5",
|
"@opencode-ai/sdk": "1.18.5",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -778,7 +767,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -812,7 +801,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"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",
|
||||||
@@ -831,7 +820,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -891,7 +880,6 @@
|
|||||||
"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:*",
|
||||||
@@ -922,7 +910,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -938,6 +926,7 @@
|
|||||||
"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",
|
||||||
@@ -947,7 +936,6 @@
|
|||||||
"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 +1007,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"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,18 +1062,16 @@
|
|||||||
"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": "catalog:",
|
||||||
@@ -1143,7 +1129,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": "18.0.7",
|
"marked": "17.0.6",
|
||||||
"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",
|
||||||
@@ -1153,7 +1139,6 @@
|
|||||||
"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",
|
||||||
@@ -1526,18 +1511,6 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -1746,10 +1719,6 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -1844,8 +1813,6 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -2096,8 +2063,6 @@
|
|||||||
|
|
||||||
"@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"],
|
||||||
@@ -3332,8 +3297,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -3574,10 +3537,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -3652,7 +3611,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@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -4190,7 +4149,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.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=="],
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -4610,8 +4569,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -4664,7 +4621,9 @@
|
|||||||
|
|
||||||
"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@18.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA=="],
|
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
@@ -4836,8 +4795,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -4876,10 +4833,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -5464,8 +5417,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -5680,8 +5631,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -6380,6 +6329,8 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -6620,6 +6571,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -6636,8 +6589,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -6754,8 +6705,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -6784,8 +6733,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -6886,8 +6833,12 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -7710,8 +7661,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -7784,8 +7733,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|||||||
+2
-6
@@ -288,12 +288,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
server: {
|
server: {
|
||||||
placement: { region: "aws:us-east-2" },
|
placement: { region: "aws:us-east-2" },
|
||||||
transform: {
|
transform: {
|
||||||
worker: (args) => {
|
worker: {
|
||||||
args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [
|
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
|
||||||
...(flags ?? []),
|
|
||||||
"global_fetch_strictly_public",
|
|
||||||
])
|
|
||||||
args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }]
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
|
"x86_64-linux": "sha256-RFek0QoEEjsgbqmTE/SxQAmPtYyzs0IPR2ugFn5Okrs=",
|
||||||
"aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
|
"aarch64-linux": "sha256-BmAxapY1YrAFn7mVq3/6A9+6Au5UIvSqBboHMkyJH3I=",
|
||||||
"aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
|
"aarch64-darwin": "sha256-Sx3bGWQqLlgoa/RudJxanjSzhFRNklckT2ffnO2I5F4=",
|
||||||
"x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
|
"x86_64-darwin": "sha256-CMOhiisHNowg06qadvgg4K+60zrynglwiT0qKYQ4NiA="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -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": "18.0.7",
|
"marked": "17.0.6",
|
||||||
"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,7 +100,6 @@
|
|||||||
"@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"
|
||||||
}
|
}
|
||||||
@@ -158,8 +157,6 @@
|
|||||||
"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,12 +368,11 @@ Other provider exports listed above remain direct facades until they explicitly
|
|||||||
|
|
||||||
## Provider options & HTTP overlays
|
## Provider options & HTTP overlays
|
||||||
|
|
||||||
Request options in order of stability:
|
Three escape hatches 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. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it.
|
2. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `promptCacheKey`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
|
||||||
3. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
|
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.
|
||||||
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,10 +33,9 @@ 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 store behavior, Anthropic thinking, Gemini thinking config, or
|
// OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking
|
||||||
// OpenRouter routing/reasoning.
|
// config, or 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.
|
||||||
//
|
//
|
||||||
@@ -46,7 +45,9 @@ 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 },
|
||||||
promptCacheKey: "tutorial-joke",
|
providerOptions: {
|
||||||
|
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.opencode.ai/api.json"
|
const MODELS_DEV_URL = "https://models.dev/api.json"
|
||||||
|
|
||||||
type JsonRecord = Record<string, unknown>
|
type JsonRecord = Record<string, unknown>
|
||||||
|
|
||||||
|
|||||||
@@ -133,8 +133,7 @@ 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"))
|
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")) return request
|
||||||
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,12 +422,14 @@ 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.fnUntraced(function* (item: Tool.Content) {
|
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
|
||||||
|
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.fnUntraced(function* (part: ToolResultPart) {
|
const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(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,20 +25,8 @@ 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
|
||||||
@@ -157,9 +145,6 @@ 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),
|
||||||
})
|
})
|
||||||
@@ -217,13 +202,11 @@ 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 root parameter schemas while preserving nested empty objects,
|
// drop empty objects, derive `nullable: true` from `type: [..., "null"]`,
|
||||||
// expand type arrays into `anyOf`, derive `nullable: true` from null members,
|
// coerce `const` to `[const]` enum, recurse properties/items, propagate
|
||||||
// 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,
|
||||||
// nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
|
// properties, items, allOf, anyOf, oneOf, minLength). Anything outside the
|
||||||
// Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
|
// allowlist (e.g. `additionalProperties`, `$ref`) is silently dropped.
|
||||||
// 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
|
||||||
@@ -299,8 +282,6 @@ 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"])
|
||||||
@@ -313,17 +294,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (part.type === "tool-call") {
|
if (part.type === "tool-call") {
|
||||||
const lowered = lowerToolCall(part)
|
parts.push(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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,9 +388,6 @@ 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.fnUntraced(function* (
|
const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(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.fnUntraced(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.fnUntraced(function* (
|
const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* (
|
||||||
item: Content,
|
item: Content,
|
||||||
request: LLMRequest,
|
request: LLMRequest,
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
@@ -383,7 +383,7 @@ const lowerToolResultContentItem = Effect.fnUntraced(function* (
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const lowerToolResultOutput = Effect.fnUntraced(function* (
|
const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(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 } : {}),
|
||||||
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
|
...(options.promptCacheKey ? { prompt_cache_key: options.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 } }
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ 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),
|
||||||
@@ -371,13 +370,12 @@ 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 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
|
content: content.length === 0 ? null : ProviderShared.joinText(content),
|
||||||
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
|
||||||
...(details !== undefined ? { reasoning_details: details } : {}),
|
reasoning_details: details,
|
||||||
...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
|
cache_control: options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined),
|
||||||
}
|
}
|
||||||
if (field === undefined || reasoningText === undefined) return result
|
if (field === undefined || reasoningText === undefined) return result
|
||||||
return { ...result, [field]: reasoningText }
|
return { ...result, [field]: reasoningText }
|
||||||
@@ -511,7 +509,6 @@ 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 } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -710,20 +707,23 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
|||||||
Boolean(delta?.content) ||
|
Boolean(delta?.content) ||
|
||||||
reasoning !== undefined ||
|
reasoning !== undefined ||
|
||||||
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
|
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
|
||||||
toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments))
|
toolDeltas.some(
|
||||||
|
(tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments),
|
||||||
|
)
|
||||||
if (state.finishReason !== undefined) {
|
if (state.finishReason !== undefined) {
|
||||||
if (hasLateContent)
|
if (hasLateContent)
|
||||||
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason")
|
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason")
|
||||||
return [{ ...state, usage }, events] as const
|
return [{ ...state, usage }, events] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
const reasoningField = state.reasoningField ?? reasoning?.field
|
const reasoningField = state.reasoningField ?? (!state.lifecycle.text.has("text-0") ? reasoning?.field : undefined)
|
||||||
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 (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
|
if (!state.lifecycle.text.has("text-0") && text !== undefined)
|
||||||
|
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") &&
|
||||||
@@ -749,7 +749,8 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
|||||||
const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position)
|
const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position)
|
||||||
const fallbackTool = tools[fallback] ?? pendingTools[fallback]
|
const fallbackTool = tools[fallback] ?? pendingTools[fallback]
|
||||||
const index =
|
const index =
|
||||||
tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
|
tool.index ?? matched ??
|
||||||
|
(tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
|
||||||
const current = tools[index]
|
const current = tools[index]
|
||||||
const pending = pendingTools[index]
|
const pending = pendingTools[index]
|
||||||
const id = current?.id ?? pending?.id ?? (tool.id || undefined)
|
const id = current?.id ?? pending?.id ?? (tool.id || undefined)
|
||||||
@@ -812,18 +813,14 @@ 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 toolCallEvents =
|
const hasToolCalls = state.toolCallEvents.length > 0
|
||||||
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,
|
||||||
}
|
}
|
||||||
: { normalized: hasToolCalls ? ("tool-calls" as const) : ("unknown" as const) }
|
: undefined
|
||||||
const metadata = reasoningMetadata(
|
const metadata = reasoningMetadata(
|
||||||
state.reasoningField,
|
state.reasoningField,
|
||||||
state.reasoningDetailsObserved ? state.reasoningDetails : undefined,
|
state.reasoningDetailsObserved ? state.reasoningDetails : undefined,
|
||||||
@@ -833,9 +830,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 = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
|
const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
|
||||||
events.push(...toolCallEvents)
|
events.push(...state.toolCallEvents)
|
||||||
Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
|
if (reason) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,57 +61,37 @@ 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, nested = false): Record<string, unknown> | undefined => {
|
const projectNode = (schema: unknown): Record<string, unknown> | undefined => {
|
||||||
if (!isRecord(schema)) return undefined
|
if (!isRecord(schema)) return undefined
|
||||||
if (!nested && emptyObjectSchema(schema)) return undefined
|
if (emptyObjectSchema(schema)) return undefined
|
||||||
const types = Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null") : undefined
|
return Object.fromEntries(
|
||||||
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", types ? (types.length === 0 ? "null" : undefined) : schema.type],
|
["type", Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null")[0] : 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, true)]))
|
? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value)]))
|
||||||
: undefined,
|
: undefined,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"items",
|
"items",
|
||||||
Array.isArray(schema.items)
|
Array.isArray(schema.items)
|
||||||
? schema.items.map((item) => projectNode(item, true))
|
? schema.items.map(projectNode)
|
||||||
: schema.items === undefined
|
: schema.items === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: projectNode(schema.items, true),
|
: projectNode(schema.items),
|
||||||
],
|
],
|
||||||
["allOf", Array.isArray(schema.allOf) ? schema.allOf.map((item) => projectNode(item, true)) : undefined],
|
["allOf", Array.isArray(schema.allOf) ? schema.allOf.map(projectNode) : undefined],
|
||||||
[
|
["anyOf", Array.isArray(schema.anyOf) ? schema.anyOf.map(projectNode) : undefined],
|
||||||
"anyOf",
|
["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map(projectNode) : undefined],
|
||||||
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,6 +33,7 @@ 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>
|
||||||
@@ -49,6 +50,7 @@ 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"
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ 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,6 +5,7 @@ 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,6 +17,7 @@ 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,6 +55,7 @@ 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
|
||||||
@@ -121,7 +122,6 @@ 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,6 +161,7 @@ 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,8 +47,6 @@ 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,7 +255,13 @@ 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(Effect.suspend(() => (terminal ? Effect.void : Effect.fail(incompleteStreamError(route))))),
|
Stream.onEnd(
|
||||||
|
Effect.suspend(() =>
|
||||||
|
terminal
|
||||||
|
? Effect.void
|
||||||
|
: Effect.fail(incompleteStreamError(route)),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cause, Context, Effect, Layer, Option, Schema } from "effect"
|
import { Cause, Context, Effect, Layer } from "effect"
|
||||||
import {
|
import {
|
||||||
FetchHttpClient,
|
FetchHttpClient,
|
||||||
Headers,
|
Headers,
|
||||||
@@ -198,20 +198,8 @@ 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) {
|
if (body.body && body.body.length <= 500) return `Provider request failed with HTTP ${status}: ${body.body}`
|
||||||
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,34 +45,35 @@ 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
|
||||||
|
|
||||||
const toolResultValueSchema = Schema.Union([
|
export const ToolResultValue = Object.assign(
|
||||||
Schema.Struct({
|
Schema.Union([
|
||||||
type: Schema.Literal("json"),
|
Schema.Struct({
|
||||||
value: Schema.Unknown,
|
type: Schema.Literal("json"),
|
||||||
}),
|
value: Schema.Unknown,
|
||||||
Schema.Struct({
|
}),
|
||||||
type: Schema.Literal("text"),
|
Schema.Struct({
|
||||||
value: Schema.Unknown,
|
type: Schema.Literal("text"),
|
||||||
}),
|
value: Schema.Unknown,
|
||||||
Schema.Struct({
|
}),
|
||||||
type: Schema.Literal("error"),
|
Schema.Struct({
|
||||||
value: Schema.Unknown,
|
type: Schema.Literal("error"),
|
||||||
}),
|
value: Schema.Unknown,
|
||||||
Schema.Struct({
|
}),
|
||||||
type: Schema.Literal("content"),
|
Schema.Struct({
|
||||||
value: Schema.Array(Tool.Content),
|
type: Schema.Literal("content"),
|
||||||
}),
|
value: Schema.Array(Tool.Content),
|
||||||
]).annotate({ identifier: "LLM.ToolResult" })
|
}),
|
||||||
export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
|
]).annotate({ identifier: "LLM.ToolResult" }),
|
||||||
|
{
|
||||||
export const ToolResultValue = Object.assign(toolResultValueSchema, {
|
is: isToolResultValue,
|
||||||
is: isToolResultValue,
|
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
||||||
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
if (isToolResultValue(value)) return value
|
||||||
if (isToolResultValue(value)) return value
|
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
||||||
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
|
return { type, value }
|
||||||
return { type, value }
|
},
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
|
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
|
||||||
|
|
||||||
export interface ToolOutput {
|
export interface ToolOutput {
|
||||||
readonly structured: unknown
|
readonly structured: unknown
|
||||||
@@ -271,8 +272,6 @@ 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)),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
@@ -290,7 +289,6 @@ 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
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 { ToolCallPart, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
|
import type {
|
||||||
|
ToolCallPart,
|
||||||
|
ToolDefinition as ToolDefinitionClass,
|
||||||
|
ToolOutput as ToolOutputType,
|
||||||
|
} from "./schema"
|
||||||
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
|
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,7 +244,8 @@ const project = (
|
|||||||
): ToolOutputType =>
|
): ToolOutputType =>
|
||||||
ToolOutput.make(
|
ToolOutput.make(
|
||||||
toStructuredOutput?.(output) ?? output,
|
toStructuredOutput?.(output) ?? output,
|
||||||
toModelOutput?.({ id, parameters, output }) ?? (typeof output === "string" ? [{ type: "text", text: output }] : []),
|
toModelOutput?.({ id, parameters, output }) ??
|
||||||
|
(typeof output === "string" ? [{ type: "text", text: output }] : []),
|
||||||
)
|
)
|
||||||
|
|
||||||
export { ToolFailure }
|
export { ToolFailure }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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"
|
||||||
@@ -269,3 +270,7 @@ 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,6 +10,7 @@ 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,
|
||||||
@@ -59,6 +60,23 @@ 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", () => {
|
||||||
|
|||||||
+7
-1
@@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:anthropic-messages-cache", "provider:anthropic", "protocol:anthropic-messages", "cache", "tool"],
|
"tags": [
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-1
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:bedrock-converse-cache", "provider:amazon-bedrock", "protocol:bedrock-converse", "cache"],
|
"tags": [
|
||||||
|
"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,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:google-images", "provider:google", "protocol:google-images"],
|
"tags": [
|
||||||
|
"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,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:openai-images", "provider:openai", "protocol:openai-images"],
|
"tags": [
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
+5
-1
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:openai-responses-images", "provider:openai", "protocol:openai-responses"],
|
"tags": [
|
||||||
|
"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,7 +2,12 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"model": "anthropic/claude-sonnet-4.6",
|
"model": "anthropic/claude-sonnet-4.6",
|
||||||
"tags": ["prefix:openai-compatible-chat", "provider:openrouter", "protocol:openai-chat", "reasoning"],
|
"tags": [
|
||||||
|
"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,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "tool", "tool-result"],
|
"tags": [
|
||||||
|
"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,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "user-input"],
|
"tags": [
|
||||||
|
"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,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "tool", "tool-result"],
|
"tags": [
|
||||||
|
"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,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "user-input"],
|
"tags": [
|
||||||
|
"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,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "tool", "tool-result"],
|
"tags": [
|
||||||
|
"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,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "user-input"],
|
"tags": [
|
||||||
|
"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,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "tool", "tool-result"],
|
"tags": [
|
||||||
|
"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,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "user-input"],
|
"tags": [
|
||||||
|
"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,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "tool", "tool-result"],
|
"tags": [
|
||||||
|
"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,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "user-input"],
|
"tags": [
|
||||||
|
"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,7 +2,12 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"model": "anthropic/claude-sonnet-4.6",
|
"model": "anthropic/claude-sonnet-4.6",
|
||||||
"tags": ["prefix:openai-compatible-chat", "provider:vercel-ai-gateway", "protocol:openai-chat", "reasoning"],
|
"tags": [
|
||||||
|
"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,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"tags": ["prefix:xai-images", "provider:xai", "protocol:xai-images"],
|
"tags": [
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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", promptCacheKey: "cache" })
|
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } })
|
||||||
|
|
||||||
LLM.request({
|
LLM.request({
|
||||||
model,
|
model,
|
||||||
prompt: "Hello",
|
prompt: "Hello",
|
||||||
// @ts-expect-error Prompt cache keys must be strings.
|
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string.
|
||||||
promptCacheKey: 1,
|
providerOptions: { openai: { promptCacheKey: 1 } },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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" } },
|
||||||
|
})
|
||||||
@@ -519,8 +519,14 @@ describe("Bedrock Converse route", () => {
|
|||||||
fixedBytes(
|
fixedBytes(
|
||||||
eventStreamBody(
|
eventStreamBody(
|
||||||
["messageStart", { role: "assistant" }],
|
["messageStart", { role: "assistant" }],
|
||||||
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } }],
|
[
|
||||||
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
|
"contentBlockDelta",
|
||||||
|
{ contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"contentBlockDelta",
|
||||||
|
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
|
||||||
|
],
|
||||||
["messageStop", { stopReason: "end_turn" }],
|
["messageStop", { stopReason: "end_turn" }],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -555,7 +561,10 @@ 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,13 +16,6 @@ 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,
|
||||||
@@ -93,39 +86,6 @@ 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(
|
||||||
@@ -390,100 +350,6 @@ 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(
|
||||||
@@ -670,44 +536,6 @@ 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({
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ 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"
|
||||||
@@ -102,24 +100,6 @@ 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(
|
||||||
@@ -174,47 +154,6 @@ 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(
|
||||||
@@ -596,7 +535,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(prepared.body.messages).toEqual([{ role: "assistant", content: "", reasoning_content: "hidden" }])
|
expect(prepared.body.messages).toEqual([{ role: "assistant", content: null, reasoning_content: "hidden" }])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -845,7 +784,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("preserves scalar reasoning after content starts", () =>
|
it.effect("ignores 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(
|
||||||
@@ -861,11 +800,11 @@ describe("OpenAI Chat route", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(response.reasoning).toBe("detailscalar")
|
expect(response.reasoning).toBe("detail")
|
||||||
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(2)
|
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
|
||||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(2)
|
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
openai: { reasoningDetails: details },
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -965,7 +904,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: "", reasoning_details: details }])
|
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1015,7 +954,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(replay.body.messages).toEqual([
|
expect(replay.body.messages).toEqual([
|
||||||
{ role: "assistant", content: "", reasoning: "firstsecond", reasoning_details: [first, second] },
|
{ role: "assistant", content: null, reasoning: "firstsecond", reasoning_details: [first, second] },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -1040,7 +979,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(replay.body.messages).toEqual([
|
expect(replay.body.messages).toEqual([
|
||||||
{ role: "assistant", content: "", reasoning_content: "AB", reasoning_details: [detail] },
|
{ role: "assistant", content: null, reasoning_content: "AB", reasoning_details: [detail] },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -1062,7 +1001,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(replay.body.messages).toEqual([
|
expect(replay.body.messages).toEqual([
|
||||||
{ role: "assistant", content: "", reasoning_content: "thinking", reasoning_details: details },
|
{ role: "assistant", content: null, reasoning_content: "thinking", reasoning_details: details },
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -1170,7 +1109,7 @@ describe("OpenAI Chat route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("finalizes a streamed tool call when the provider ends without a finish reason", () =>
|
it.effect("fails 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({
|
||||||
@@ -1182,31 +1121,27 @@ 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 response = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)))
|
const events: LLMEvent[] = []
|
||||||
|
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(response.events).toEqual([
|
expect(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.")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -300,9 +300,7 @@ describe("OpenAI-compatible Chat route", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const response = yield* LLMClient.generate(
|
const response = yield* LLMClient.generate(
|
||||||
LLMRequest.update(request, {
|
LLMRequest.update(request, {
|
||||||
tools: [
|
tools: [ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } })],
|
||||||
ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } }),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
).pipe(
|
).pipe(
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
|
|||||||
@@ -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 },
|
||||||
promptCacheKey: "recorded-cache-test",
|
providerOptions: { openai: { 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,16 +803,17 @@ describe("OpenAI Responses route", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("maps the request prompt cache key", () =>
|
it.effect("request OpenAI provider options override route defaults", () =>
|
||||||
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",
|
||||||
promptCacheKey: "request_cache",
|
providerOptions: { openai: { promptCacheKey: "request_cache" } },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ 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" }],
|
||||||
@@ -173,7 +174,6 @@ 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,21 +19,6 @@
|
|||||||
|
|
||||||
- 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,16 +0,0 @@
|
|||||||
## 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.
|
|
||||||
@@ -8,7 +8,7 @@ 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 { expectSessionReady } from "../../utils/waits"
|
import { expectSessionTitle } 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"
|
||||||
@@ -111,9 +111,8 @@ 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,
|
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
|
||||||
retry: input.eventRetry ?? 20,
|
retry: input.eventRetry ?? 20,
|
||||||
})
|
})
|
||||||
await mockOpenCodeServer(page, {
|
await mockOpenCodeServer(page, {
|
||||||
@@ -162,8 +161,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 })
|
||||||
@@ -199,9 +198,7 @@ export async function setupTimeline(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
async waitForPart(partID: string) {
|
async waitForPart(partID: string) {
|
||||||
const part = page.locator(`[data-timeline-part-id="${partID}"]`)
|
await expect(page.locator(`[data-timeline-part-id="${partID}"]`).first()).toBeVisible()
|
||||||
await expect(part).toHaveCount(1)
|
|
||||||
await expect(part).toBeVisible()
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,8 +177,8 @@ test("shows all and expands historical diff summary without overlap", async ({ p
|
|||||||
const firstUser = userMessage(undefined, {
|
const firstUser = userMessage(undefined, {
|
||||||
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",
|
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,7 +85,8 @@ 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") return sse(route)
|
if (url.pathname === "/api/event")
|
||||||
|
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: {} })
|
||||||
|
|||||||
+10
-8
@@ -1,18 +1,19 @@
|
|||||||
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_removed_layout_preference"
|
const draftID = "draft_legacy_new_session"
|
||||||
const directory = "C:/OpenCode/RemovedLayoutPreference"
|
const directory = "C:/OpenCode/LegacyNewSession"
|
||||||
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("ignores persisted old layout preferences when opening drafts", async ({ page }) => {
|
test("redirects a draft to the legacy new-session route", async ({ page }) => {
|
||||||
await mockOpenCodeServer(page, {
|
await mockOpenCodeServer(page, {
|
||||||
directory,
|
directory,
|
||||||
project: {
|
project: {
|
||||||
id: "proj_removed_layout_preference",
|
id: "proj_legacy_new_session",
|
||||||
worktree: directory,
|
worktree: directory,
|
||||||
vcs: "git",
|
vcs: "git",
|
||||||
name: "removed-layout-preference",
|
name: "legacy-new-session",
|
||||||
time: { created: 1700000000000, updated: 1700000000000 },
|
time: { created: 1700000000000, updated: 1700000000000 },
|
||||||
sandboxes: [],
|
sandboxes: [],
|
||||||
},
|
},
|
||||||
@@ -23,6 +24,7 @@ test("ignores persisted old layout preferences when opening drafts", async ({ pa
|
|||||||
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 }]),
|
||||||
@@ -33,7 +35,7 @@ test("ignores persisted old layout preferences when opening drafts", async ({ pa
|
|||||||
|
|
||||||
await page.goto(`/new-session?draftId=${draftID}`)
|
await page.goto(`/new-session?draftId=${draftID}`)
|
||||||
|
|
||||||
await expect(page).toHaveURL(`/new-session?draftId=${draftID}`)
|
await expect(page).toHaveURL(`/${base64Encode(directory)}/session`)
|
||||||
await expect(page.locator("body")).toHaveAttribute("data-new-layout", "")
|
await expect(page.locator("header[data-tauri-drag-region]")).toBeVisible()
|
||||||
await expect(page.getByRole("textbox", { name: "Prompt" })).toBeVisible()
|
await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
|
||||||
})
|
})
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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,17 +181,12 @@ 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") return sse(route)
|
if (url.pathname === "/api/event")
|
||||||
|
return sse(route)
|
||||||
if (url.pathname === "/api/provider")
|
if (url.pathname === "/api/provider")
|
||||||
return json(route, {
|
return json(route, {
|
||||||
location: { directory },
|
location: { directory },
|
||||||
data: [
|
data: [{ id: remote ? "server-b" : "server-a", name: remote ? "Server B Provider" : "Server A Provider", package: "test" }],
|
||||||
{
|
|
||||||
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,7 +58,8 @@ 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") return sse(route, url.pathname === "/api/event")
|
if (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" } } : {} })
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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 }) => {
|
||||||
@@ -28,7 +27,6 @@ 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 }) => {
|
||||||
@@ -38,10 +36,15 @@ test("opens the comment editor for a line number range", async ({ page }) => {
|
|||||||
await expectAppVisible(start)
|
await expectAppVisible(start)
|
||||||
await expectAppVisible(end)
|
await expectAppVisible(end)
|
||||||
|
|
||||||
await start.dragTo(end)
|
const from = await start.boundingBox()
|
||||||
|
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 }) => {
|
||||||
@@ -51,38 +54,31 @@ 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(lineNumber).toHaveAttribute("data-hovered", "")
|
await expect(comment).toBeVisible({ timeout: 500 })
|
||||||
await expect(comment).toHaveCount(1)
|
await comment.click({ timeout: 500 })
|
||||||
await expect(comment).toHaveCSS("pointer-events", "auto")
|
}).toPass()
|
||||||
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) => {
|
||||||
expect.soft(request.method(), `unexpected ${request.method()} ${new URL(request.url()).pathname}`).toBe("GET")
|
if (request.method() !== "GET") requests.push(`${request.method()} ${new URL(request.url()).pathname}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
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()
|
||||||
const textbox = review.getByRole("textbox")
|
await review.getByRole("textbox").fill("Use the existing value instead")
|
||||||
await expect(textbox).toBeVisible()
|
await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click()
|
||||||
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) {
|
||||||
@@ -148,22 +144,15 @@ 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 changes = page.getByRole("tab", { name: "Changes" })
|
const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff")
|
||||||
const diffResponse = page.waitForResponse(
|
await page.getByRole("tab", { name: "Changes" }).click()
|
||||||
(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)
|
||||||
const file = review.locator('[data-file="src/review.ts"]')
|
await review
|
||||||
await expectAppVisible(file)
|
.getByRole("heading", { name: /review\.ts/ })
|
||||||
const trigger = file.getByRole("button", { expanded: false })
|
.getByRole("button")
|
||||||
await expect(trigger).toHaveCount(1)
|
.first()
|
||||||
await trigger.click()
|
.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 { expectSessionReady } from "../utils/waits"
|
import { expectSessionTitle } 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,6 +19,10 @@ 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",
|
||||||
@@ -53,8 +57,10 @@ 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: { [sessionID]: { type: "idle" } },
|
sessionStatus: () => sessionStatus,
|
||||||
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({
|
||||||
@@ -67,12 +73,20 @@ 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: branchDiffs,
|
data: detail
|
||||||
|
? branchDiffs
|
||||||
|
.filter((diff) => diff.file.startsWith("src/branch/d00027/"))
|
||||||
|
.map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
|
||||||
|
: branchDiffs,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -132,8 +146,7 @@ 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}`)
|
||||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
await expectSessionTitle(page, title)
|
||||||
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")
|
||||||
@@ -141,6 +154,77 @@ 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) {
|
||||||
@@ -171,32 +255,36 @@ 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 fileDiff(file: string, additions: number, loaded = true) {
|
function statusEvent(type: "busy" | "idle") {
|
||||||
|
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'\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-${version}'\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}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: "Erkundung abgeschlossen" },
|
{ locale: "de", label: "Erkundet" },
|
||||||
{ 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,10 +86,15 @@ 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([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], {
|
const aborted = assistantMessage(
|
||||||
id: "msg_1001_assistant_aborted",
|
[
|
||||||
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
|
{ id: "prt_before_abort", type: "text", text: "Before interruption" },
|
||||||
})
|
],
|
||||||
|
{
|
||||||
|
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,5 +1,12 @@
|
|||||||
import { expect, test, type Page } from "@playwright/test"
|
import { expect, test } from "@playwright/test"
|
||||||
import { partUpdated, setupTimeline, textPart } from "../performance/timeline-stability/fixture"
|
import {
|
||||||
|
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)
|
||||||
@@ -10,7 +17,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)
|
||||||
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
|
expect(await timeline.transport.connections()).toHaveLength(1)
|
||||||
expect(await timeline.transport.acknowledgements()).toHaveLength(2)
|
expect(await timeline.transport.acknowledgements()).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -44,24 +51,20 @@ 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, {
|
||||||
const partID = "prt_transport_heartbeat_sentinel"
|
messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
|
||||||
const sentinel = await timeline.transport.send(partUpdated(textPart(partID, "heartbeat sentinel")))
|
})
|
||||||
await timeline.waitForPart(partID)
|
const before = await page.locator("[data-timeline-row]").allTextContents()
|
||||||
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 expect.poll(() => timelineRows(page)).toEqual(before)
|
await timeline.transport.heartbeat()
|
||||||
expect(heartbeat.connectionID).toBe(sentinel.connectionID)
|
await timeline.settle()
|
||||||
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)
|
const timeline = await setupTimeline(page, { eventRetry: 10 })
|
||||||
const first = await timeline.transport.waitForConnection()
|
const first = await timeline.transport.waitForConnection()
|
||||||
|
|
||||||
await timeline.transport.close()
|
await timeline.transport.close()
|
||||||
@@ -74,14 +77,13 @@ 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)
|
const timeline = await setupTimeline(page, { eventRetry: 10 })
|
||||||
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(partUpdated(textPart("prt_transport_error", "after error")))
|
await timeline.transport.send(status("busy"))
|
||||||
|
|
||||||
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")
|
||||||
@@ -110,18 +112,5 @@ 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 })
|
||||||
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(1)
|
expect(await timeline.transport.connections()).toHaveLength(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,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ 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") return sse(route)
|
if (url.pathname === "/api/event")
|
||||||
|
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}`)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test("keeps the terminal session alive when switching session tabs in a workspac
|
|||||||
const connection = new URL(connections[0]!)
|
const connection = new URL(connections[0]!)
|
||||||
expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`)
|
expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`)
|
||||||
expect(connection.searchParams.get("location[directory]")).toBe(directory)
|
expect(connection.searchParams.get("location[directory]")).toBe(directory)
|
||||||
expect(connection.searchParams.get("ticket")).toBe("e2e-ticket")
|
expect(connection.searchParams.get("ticket")).toBeNull()
|
||||||
await writeProbe(page)
|
await writeProbe(page)
|
||||||
|
|
||||||
await switchTab(page, titleB)
|
await switchTab(page, titleB)
|
||||||
|
|||||||
@@ -300,8 +300,9 @@ 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.flatMap((message) => message.parts).find((part) => part.tool === "bash")!
|
expandedShellPartID: targetMessages
|
||||||
.callID,
|
.flatMap((message) => message.parts)
|
||||||
|
.find((part) => part.tool === "bash")!.callID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,8 +144,7 @@ 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")
|
if (path === "/api/model") return json(route, { location: location(config), data: currentModels(providerConfig(config)) })
|
||||||
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,8 +206,7 @@ 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 =
|
const content = value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "")
|
||||||
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) {
|
||||||
@@ -283,8 +281,7 @@ 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")
|
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") return json(route, true)
|
||||||
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 (
|
||||||
@@ -389,13 +386,11 @@ 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
|
return value.all.filter(record).flatMap((provider) =>
|
||||||
.filter(record)
|
typeof provider.id === "string" && typeof provider.name === "string"
|
||||||
.flatMap((provider) =>
|
? [{ id: provider.id, name: provider.name, package: provider.id }]
|
||||||
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) {
|
||||||
@@ -445,7 +440,9 @@ 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((model) => model.providerID === selected.providerID && model.id === selected.modelID) ?? null
|
return models.find(
|
||||||
|
(model) => model.providerID === selected.providerID && model.id === selected.modelID,
|
||||||
|
) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentPermission(value: unknown) {
|
function currentPermission(value: unknown) {
|
||||||
@@ -565,16 +562,22 @@ function legacyAgent(part: Record<string, unknown>): PromptAgentAttachment[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mentionFrom(value: Record<string, unknown> | undefined) {
|
function mentionFrom(value: Record<string, unknown> | undefined) {
|
||||||
if (!value || typeof value.value !== "string" || typeof value.start !== "number" || typeof value.end !== "number")
|
if (
|
||||||
|
!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(part: Record<string, unknown>, created: number): SessionMessageAssistant["content"] {
|
function legacyAssistantContent(
|
||||||
|
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 [
|
return [{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) }]
|
||||||
{ 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 [
|
||||||
@@ -615,12 +618,7 @@ function legacyAssistantContent(part: Record<string, unknown>, created: number):
|
|||||||
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
|
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
|
||||||
}
|
}
|
||||||
if (state.status === "pending")
|
if (state.status === "pending")
|
||||||
return [
|
return [{ ...base, state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) } }]
|
||||||
{
|
|
||||||
...base,
|
|
||||||
state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if (state.status === "completed")
|
if (state.status === "completed")
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -174,7 +174,8 @@ 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 (url.origin !== server || url.pathname !== "/api/event") return originalFetch(request)
|
if (url.origin !== server || url.pathname !== "/api/event")
|
||||||
|
return originalFetch(request)
|
||||||
|
|
||||||
const id = ++nextConnectionID
|
const id = ++nextConnectionID
|
||||||
const record = {
|
const record = {
|
||||||
@@ -234,23 +235,18 @@ export async function installSseTransport<T>(
|
|||||||
return {
|
return {
|
||||||
server,
|
server,
|
||||||
async waitForConnection(input = {}) {
|
async waitForConnection(input = {}) {
|
||||||
const connection = await page.waitForFunction(
|
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?.findLast((connection) => connection.id > after && connection.endedAt === undefined)
|
return connections?.some((connection) => connection.id > after)
|
||||||
},
|
},
|
||||||
input.after ?? 0,
|
input.after ?? 0,
|
||||||
{ timeout: input.timeout },
|
{ timeout: input.timeout },
|
||||||
)
|
)
|
||||||
let result: SseConnectionRecord | undefined
|
return (await command<SseConnectionRecord[]>({ type: "connections" })).findLast(
|
||||||
try {
|
(connection) => connection.id > (input.after ?? 0),
|
||||||
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,5 +1,4 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -10,8 +9,3 @@ 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,10 +20,7 @@
|
|||||||
<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
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden bg-v2-background-bg-deep">
|
||||||
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,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.15",
|
"version": "1.18.8",
|
||||||
"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",
|
||||||
@@ -20,9 +19,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 --conditions=solid --only-failures --preload ./happydom.ts ./src",
|
"test:unit": "bun test --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 --conditions=solid --watch --preload ./happydom.ts ./src",
|
"test:unit:watch": "bun test --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",
|
||||||
|
|||||||
+177
-63
@@ -3,14 +3,25 @@ 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 { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
import {
|
||||||
|
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,
|
||||||
@@ -32,7 +43,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 } from "@/context/server-sync"
|
import { ServerSyncProvider, useServerSync } 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"
|
||||||
@@ -43,36 +54,58 @@ 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 } from "@/context/settings"
|
import { SettingsProvider, useSettings } from "@/context/settings"
|
||||||
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||||
import { WslServersProvider } from "@/wsl/context"
|
import { WslServersProvider } from "@/wsl/context"
|
||||||
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||||
import Layout from "@/pages/layout"
|
import LegacyLayout 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 { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { createSessionLineage } from "@/pages/session/session-lineage"
|
||||||
|
|
||||||
import { TargetSessionRouteContent } from "@/pages/session"
|
import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
|
||||||
import { Home } from "@/pages/home"
|
import { NewHome } 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 DirectoryDraftRedirect = () => {
|
const SessionRoute = () => {
|
||||||
|
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 (search.draftId || !tabs.ready()) return
|
if (!settings.general.newLayoutDesigns()) return
|
||||||
const directory = decode64(params.dir)
|
if (params.id || search.draftId) return
|
||||||
if (!directory) return
|
if (!tabs.ready() || !sdk().directory) return
|
||||||
tabs.newDraft({ server: server.key, directory }, search.prompt)
|
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
||||||
})
|
})
|
||||||
|
|
||||||
return null
|
return (
|
||||||
|
<SessionRouteErrorBoundary sessionID={params.id}>
|
||||||
|
<SessionPage />
|
||||||
|
</SessionRouteErrorBoundary>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TargetServerRoute(props: ParentProps) {
|
function TargetServerRoute(props: ParentProps) {
|
||||||
@@ -84,7 +117,9 @@ function TargetServerRoute(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Owns the server-identity remount. Session changes must not remount this subtree.
|
// Owns the server-identity remount. Session changes must NOT remount this
|
||||||
|
// 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>
|
||||||
@@ -99,6 +134,35 @@ 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.location.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) {
|
||||||
@@ -111,8 +175,17 @@ 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()}>
|
||||||
@@ -121,7 +194,14 @@ function DraftRoute() {
|
|||||||
keyed
|
keyed
|
||||||
fallback={<Navigate href="/" />}
|
fallback={<Navigate href="/" />}
|
||||||
>
|
>
|
||||||
{(draft) => <ResolvedDraftRoute draft={draft} />}
|
{(draft) => (
|
||||||
|
<Show
|
||||||
|
when={settings.general.newLayoutDesigns()}
|
||||||
|
fallback={<Navigate href={`/${base64Encode(draft.directory)}/session`} />}
|
||||||
|
>
|
||||||
|
<ResolvedDraftRoute draft={draft} />
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -154,13 +234,11 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
|||||||
|
|
||||||
function UiI18nBridge(props: ParentProps) {
|
function UiI18nBridge(props: ParentProps) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
return (
|
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
|
||||||
<I18nProvider
|
}
|
||||||
value={{ locale: language.intl, layoutLocale: language.layoutLocale, t: language.t, plural: language.plural }}
|
|
||||||
>
|
function LayoutCompatibility(props: ParentProps) {
|
||||||
{props.children}
|
return <>{props.children}</>
|
||||||
</I18nProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -189,11 +267,17 @@ 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)
|
|
||||||
document.body.classList.remove("text-12-regular")
|
const enabled = settings.general.newLayoutDesigns()
|
||||||
document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]")
|
document.body.toggleAttribute("data-new-layout", enabled)
|
||||||
|
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
|
||||||
@@ -223,7 +307,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: language.t("command.logs.export"),
|
title: "Export logs",
|
||||||
category: language.t("command.category.settings"),
|
category: language.t("command.category.settings"),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
void platform.exportDebugLogs?.()
|
void platform.exportDebugLogs?.()
|
||||||
@@ -236,6 +320,7 @@ 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
|
||||||
@@ -250,11 +335,19 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
function LegacyServerScopedShell(props: ServerScopedShellProps) {
|
||||||
|
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}>
|
||||||
<Layout>{props.children}</Layout>
|
<NewLayout>{props.children}</NewLayout>
|
||||||
</ServerScopedProviders>
|
</ServerScopedProviders>
|
||||||
</SelectedServerProviders>
|
</SelectedServerProviders>
|
||||||
)
|
)
|
||||||
@@ -272,12 +365,7 @@ function DraftProviders(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppBaseProviders(
|
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
|
||||||
props: ParentProps<{
|
|
||||||
locale?: Locale
|
|
||||||
onNativeTranslations?: Parameters<typeof LanguageProvider>[0]["onNativeTranslations"]
|
|
||||||
}>,
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
@@ -286,7 +374,7 @@ export function AppBaseProviders(
|
|||||||
void window.api?.setTitlebar?.({ mode, scheme })
|
void window.api?.setTitlebar?.({ mode, scheme })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LanguageProvider locale={props.locale} onNativeTranslations={props.onNativeTranslations}>
|
<LanguageProvider locale={props.locale}>
|
||||||
<UiI18nBridge>
|
<UiI18nBridge>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallback={(error) => {
|
fallback={(error) => {
|
||||||
@@ -297,7 +385,9 @@ export function AppBaseProviders(
|
|||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<WslServersProvider>
|
<WslServersProvider>
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
<MarkedProvider>
|
||||||
|
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
|
||||||
|
</MarkedProvider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
</WslServersProvider>
|
</WslServersProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
@@ -446,7 +536,7 @@ export function AppInterface(props: {
|
|||||||
startup?: Promise<void>
|
startup?: Promise<void>
|
||||||
serverScoped?: JSX.Element
|
serverScoped?: JSX.Element
|
||||||
}) {
|
}) {
|
||||||
// The visual layout lives in the router root so it remains mounted across
|
// The visual new 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) => (
|
||||||
@@ -467,22 +557,26 @@ export function AppInterface(props: {
|
|||||||
<GlobalProvider>
|
<GlobalProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
||||||
<Dynamic
|
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
||||||
component={props.router ?? Router}
|
<Dynamic
|
||||||
root={(routerProps) => (
|
component={props.router ?? Router}
|
||||||
<TabsProvider>
|
root={(routerProps) => (
|
||||||
<PermissionProvider>
|
<TabsProvider>
|
||||||
<NotificationProvider>
|
<PermissionProvider>
|
||||||
<ServerShell>
|
<NotificationProvider>
|
||||||
<AppLayout serverScoped={props.serverScoped}>{routerProps.children}</AppLayout>
|
<ServerShell>
|
||||||
</ServerShell>
|
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
||||||
</NotificationProvider>
|
<NewAppLayout serverScoped={props.serverScoped}>{routerProps.children}</NewAppLayout>
|
||||||
</PermissionProvider>
|
</Show>
|
||||||
</TabsProvider>
|
</ServerShell>
|
||||||
)}
|
</NotificationProvider>
|
||||||
>
|
</PermissionProvider>
|
||||||
<Routes />
|
</TabsProvider>
|
||||||
</Dynamic>
|
)}
|
||||||
|
>
|
||||||
|
<Routes serverScoped={props.serverScoped} />
|
||||||
|
</Dynamic>
|
||||||
|
</Show>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
</GlobalProvider>
|
</GlobalProvider>
|
||||||
@@ -490,20 +584,40 @@ export function AppInterface(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Routes() {
|
function Routes(props: { serverScoped?: JSX.Element }) {
|
||||||
|
const settings = useSettings()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Route path="/" component={Home} />
|
<Route
|
||||||
<Route path="/:dir" component={DirectoryDraftRedirect} />
|
component={(routeProps) => (
|
||||||
<Route path="/:dir/session" component={DirectoryDraftRedirect} />
|
<LegacyServerLayout serverScoped={props.serverScoped}>{routeProps.children}</LegacyServerLayout>
|
||||||
<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 LegacySessionRedirect() {
|
function NewLayoutLegacySessionRedirect() {
|
||||||
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.
|
After Width: | Height: | Size: 187 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
@@ -60,44 +60,35 @@ function Cell(props: {
|
|||||||
label: string
|
label: string
|
||||||
tip: string
|
tip: string
|
||||||
value: string
|
value: string
|
||||||
span?: 2 | 3
|
wide?: boolean
|
||||||
}) {
|
}) {
|
||||||
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 justify-start px-1.5 py-0.5 text-left": !!props.inline,
|
"min-h-[20px] w-fit flex-row justify-start gap-1.5 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.span === 2 && !props.inline,
|
"col-span-2": !!props.wide && !props.inline,
|
||||||
"col-span-3": props.span === 3 && !props.inline,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex min-w-0": true,
|
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
|
||||||
"-translate-y-px items-baseline gap-1.5": !!props.inline,
|
|
||||||
"flex-col items-center": !props.inline,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
{props.label}
|
||||||
classList={{
|
</div>
|
||||||
"text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70": true,
|
<div
|
||||||
}}
|
classList={{
|
||||||
>
|
"uppercase leading-none font-bold tabular-nums": true,
|
||||||
{props.label}
|
"text-[11px]": !!props.inline,
|
||||||
</div>
|
"text-[13px] sm:text-[14px]": !props.inline,
|
||||||
<div
|
"text-text-on-critical-base": !!props.bad,
|
||||||
classList={{
|
"opacity-70": !!props.dim,
|
||||||
"uppercase leading-none font-bold tabular-nums": true,
|
}}
|
||||||
"text-[11px]": !!props.inline,
|
>
|
||||||
"text-[13px] sm:text-[14px]": !props.inline,
|
{props.value}
|
||||||
"text-text-on-critical-base": !!props.bad,
|
|
||||||
"opacity-70": !!props.dim,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.value}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -117,50 +108,37 @@ function Cell(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToggleCell(props: {
|
function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => void }) {
|
||||||
active: boolean
|
|
||||||
inline?: boolean
|
|
||||||
label: string
|
|
||||||
onClick: () => void
|
|
||||||
tip: string
|
|
||||||
value: string
|
|
||||||
}) {
|
|
||||||
const content = () => (
|
const content = () => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`${props.label}: ${props.value}`}
|
aria-label="Force focus styles on all interactive elements"
|
||||||
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 justify-start rounded px-1.5 py-0.5 text-left": !!props.inline,
|
"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-[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
|
<span class="text-[10px] leading-none font-black tracking-[0.04em] opacity-70">FOCUS</span>
|
||||||
classList={{
|
<span classList={{ "leading-none font-bold": true, "text-[11px]": !!props.inline, "text-[13px]": !props.inline }}>
|
||||||
flex: true,
|
{props.active ? "ON" : "OFF"}
|
||||||
"-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={props.tip} placement="top">
|
<TooltipV2 value="Force focus styles on all interactive elements" placement="top">
|
||||||
{content()}
|
{content()}
|
||||||
</TooltipV2>
|
</TooltipV2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip value={props.tip} placement="top">
|
<Tooltip value="Force focus styles on all interactive elements" placement="top">
|
||||||
{content()}
|
{content()}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
@@ -479,7 +457,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-4": !props.inline,
|
"grid-cols-5": !props.inline,
|
||||||
grid: !props.inline,
|
grid: !props.inline,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -561,25 +539,10 @@ 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}
|
||||||
span={platform.setForceFocus ? 2 : 3}
|
wide={!platform.setForceFocus}
|
||||||
/>
|
|
||||||
<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 && (
|
||||||
<ToggleCell
|
<FocusCell active={state.focus} inline={props.inline} onClick={() => void toggleFocus()} />
|
||||||
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>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FormAnswer, IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode-ai/client/promise"
|
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"
|
||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { ExternalLink } from "@/components/external-link"
|
import { Link } from "@/components/link"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
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"
|
||||||
@@ -40,8 +40,6 @@ import { decode64 } from "@/utils/base64"
|
|||||||
|
|
||||||
const CUSTOM_ID = "_custom"
|
const CUSTOM_ID = "_custom"
|
||||||
type ConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
|
type ConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
|
||||||
type IntegrationForm = NonNullable<ConnectMethod["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 })
|
||||||
@@ -436,16 +434,16 @@ function ProviderConnection(props: {
|
|||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
methodIndex: undefined as undefined | number,
|
methodIndex: undefined as undefined | number,
|
||||||
authorization: undefined as undefined | IntegrationOauthConnectOutput["data"],
|
authorization: undefined as undefined | IntegrationOauthConnectOutput["data"],
|
||||||
formAnswer: undefined as FormAnswer | undefined,
|
promptInputs: undefined as undefined | Record<string, string>,
|
||||||
state: "pending" as undefined | "pending" | "complete" | "error" | "form",
|
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
|
||||||
error: undefined as string | undefined,
|
error: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: "method.select"; index: number }
|
| { type: "method.select"; index: number }
|
||||||
| { type: "method.reset" }
|
| { type: "method.reset" }
|
||||||
| { type: "auth.form" }
|
| { type: "auth.prompt" }
|
||||||
| { type: "auth.answer"; answer: FormAnswer | undefined }
|
| { type: "auth.inputs"; inputs: Record<string, string> }
|
||||||
| { type: "auth.pending" }
|
| { type: "auth.pending" }
|
||||||
| { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] }
|
| { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] }
|
||||||
| { type: "auth.error"; error: string }
|
| { type: "auth.error"; error: string }
|
||||||
@@ -456,7 +454,7 @@ function ProviderConnection(props: {
|
|||||||
if (action.type === "method.select") {
|
if (action.type === "method.select") {
|
||||||
draft.methodIndex = action.index
|
draft.methodIndex = action.index
|
||||||
draft.authorization = undefined
|
draft.authorization = undefined
|
||||||
draft.formAnswer = undefined
|
draft.promptInputs = undefined
|
||||||
draft.state = undefined
|
draft.state = undefined
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
@@ -464,18 +462,18 @@ function ProviderConnection(props: {
|
|||||||
if (action.type === "method.reset") {
|
if (action.type === "method.reset") {
|
||||||
draft.methodIndex = undefined
|
draft.methodIndex = undefined
|
||||||
draft.authorization = undefined
|
draft.authorization = undefined
|
||||||
draft.formAnswer = undefined
|
draft.promptInputs = undefined
|
||||||
draft.state = undefined
|
draft.state = undefined
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action.type === "auth.form") {
|
if (action.type === "auth.prompt") {
|
||||||
draft.state = "form"
|
draft.state = "prompt"
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action.type === "auth.answer") {
|
if (action.type === "auth.inputs") {
|
||||||
draft.formAnswer = action.answer
|
draft.promptInputs = action.inputs
|
||||||
draft.state = undefined
|
draft.state = undefined
|
||||||
draft.error = undefined
|
draft.error = undefined
|
||||||
return
|
return
|
||||||
@@ -511,12 +509,7 @@ 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 ? hint[0].toUpperCase() + hint.slice(1) : value?.type === "key" ? "Browser" : undefined,
|
||||||
hint?.toLowerCase() === "headless"
|
|
||||||
? language.t("provider.connect.method.headless")
|
|
||||||
: hint?.toLowerCase() === "browser" || (!hint && value?.type === "key")
|
|
||||||
? language.t("provider.connect.method.browser")
|
|
||||||
: undefined,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +531,7 @@ function ProviderConnection(props: {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectMethod(index: number, answer?: FormAnswer) {
|
async function selectMethod(index: number, inputs?: Record<string, string>) {
|
||||||
if (timer.current !== undefined) {
|
if (timer.current !== undefined) {
|
||||||
clearTimeout(timer.current)
|
clearTimeout(timer.current)
|
||||||
timer.current = undefined
|
timer.current = undefined
|
||||||
@@ -547,17 +540,9 @@ function ProviderConnection(props: {
|
|||||||
const method = methods()[index]
|
const method = methods()[index]
|
||||||
dispatch({ type: "method.select", index })
|
dispatch({ type: "method.select", index })
|
||||||
|
|
||||||
if (method.form?.length && !answer) {
|
|
||||||
dispatch({ type: "auth.form" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (method.type === "key") {
|
|
||||||
dispatch({ type: "auth.answer", answer })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (method.type === "oauth") {
|
if (method.type === "oauth") {
|
||||||
if (method.form?.some((field) => field.type !== "string")) {
|
if (method.prompts?.length && !inputs) {
|
||||||
dispatch({ type: "auth.error", error: "This authentication form contains unsupported fields" })
|
dispatch({ type: "auth.prompt" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dispatch({ type: "auth.pending" })
|
dispatch({ type: "auth.pending" })
|
||||||
@@ -565,7 +550,7 @@ function ProviderConnection(props: {
|
|||||||
.api.integration.oauth.connect({
|
.api.integration.oauth.connect({
|
||||||
integrationID: props.provider,
|
integrationID: props.provider,
|
||||||
methodID: method.id,
|
methodID: method.id,
|
||||||
...(answer ? { answer } : {}),
|
inputs: inputs ?? {},
|
||||||
location: location(),
|
location: location(),
|
||||||
})
|
})
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
@@ -579,42 +564,41 @@ function ProviderConnection(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthFormView() {
|
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 fields = createMemo<StringForm[]>(() => {
|
const prompts = createMemo(() => {
|
||||||
const value = method()
|
const value = method()
|
||||||
return (value?.form ?? []).flatMap((field) => (field.type === "string" ? [field] : []))
|
return value?.type === "oauth" ? (value.prompts ?? []) : []
|
||||||
})
|
})
|
||||||
const matches = (field: StringForm, value: Record<string, string>) => {
|
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
|
||||||
return (field.when ?? []).every((condition) => {
|
if (!prompt.when) return true
|
||||||
const actual = value[condition.key]
|
const actual = value[prompt.when.key]
|
||||||
if (actual === undefined) return false
|
if (actual === undefined) return false
|
||||||
return condition.op === "eq" ? actual === condition.value : actual !== condition.value
|
return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const current = createMemo(() => {
|
const current = createMemo(() => {
|
||||||
const all = fields()
|
const all = prompts()
|
||||||
const index = all.findIndex((field, index) => index >= formStore.index && matches(field, formStore.value))
|
const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
field: all[index],
|
prompt: all[index],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const valid = createMemo(() => {
|
const valid = createMemo(() => {
|
||||||
const item = current()
|
const item = current()
|
||||||
if (!item || item.field.options) return false
|
if (!item || item.prompt.type !== "text") return false
|
||||||
if (!item.field.required) return true
|
const value = formStore.value[item.prompt.key] ?? ""
|
||||||
return (formStore.value[item.field.key] ?? "").trim().length > 0
|
return value.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
|
if (store.methodIndex === undefined) return
|
||||||
const next = fields().findIndex((field, i) => i > index && matches(field, value))
|
const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
|
||||||
if (next !== -1) {
|
if (next !== -1) {
|
||||||
setFormStore("index", next)
|
setFormStore("index", next)
|
||||||
return
|
return
|
||||||
@@ -625,60 +609,60 @@ function ProviderConnection(props: {
|
|||||||
async function handleSubmit(e: SubmitEvent) {
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const item = current()
|
const item = current()
|
||||||
if (!item || item.field.options) return
|
if (!item || item.prompt.type !== "text") 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 field = item()?.field
|
const prompt = item()?.prompt
|
||||||
if (!field || field.options) return
|
if (!prompt || prompt.type !== "text") return
|
||||||
return field
|
return prompt
|
||||||
})
|
})
|
||||||
const select = createMemo(() => {
|
const select = createMemo(() => {
|
||||||
const field = item()?.field
|
const prompt = item()?.prompt
|
||||||
if (!field?.options) return
|
if (!prompt || prompt.type !== "select") return
|
||||||
return field
|
return prompt
|
||||||
})
|
})
|
||||||
|
|
||||||
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()?.field.options === undefined}>
|
<Match when={item()?.prompt.type === "text"}>
|
||||||
<TextField
|
<TextField
|
||||||
type="text"
|
type="text"
|
||||||
label={text()?.title ?? ""}
|
label={text()?.message ?? ""}
|
||||||
placeholder={text()?.placeholder}
|
placeholder={text()?.placeholder}
|
||||||
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
const field = text()
|
const prompt = text()
|
||||||
if (!field) return
|
if (!prompt) return
|
||||||
setFormStore("value", field.key, value)
|
setFormStore("value", prompt.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()?.field.options !== undefined}>
|
<Match when={item()?.prompt.type === "select"}>
|
||||||
<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()?.title}</div>
|
<div class="text-14-regular text-text-base">{select()?.message}</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 field = select()
|
const prompt = select()
|
||||||
if (!field) return
|
if (!prompt) return
|
||||||
const nextValue = {
|
const nextValue = {
|
||||||
...formStore.value,
|
...formStore.value,
|
||||||
[field.key]: value.value,
|
[prompt.key]: value.value,
|
||||||
}
|
}
|
||||||
setFormStore("value", field.key, value.value)
|
setFormStore("value", prompt.key, value.value)
|
||||||
void next(item()!.index, nextValue)
|
void next(item()!.index, nextValue)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -688,7 +672,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.description}</span>
|
<span class="text-14-regular text-text-weak">{option.hint}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
@@ -836,7 +820,6 @@ function ProviderConnection(props: {
|
|||||||
integrationID: props.provider,
|
integrationID: props.provider,
|
||||||
location: location(),
|
location: location(),
|
||||||
key: apiKey,
|
key: apiKey,
|
||||||
...(store.formAnswer ? { answer: store.formAnswer } : {}),
|
|
||||||
})
|
})
|
||||||
await complete()
|
await complete()
|
||||||
}
|
}
|
||||||
@@ -853,12 +836,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")}
|
||||||
<ExternalLink
|
<Link
|
||||||
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")}
|
||||||
</ExternalLink>
|
</Link>
|
||||||
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -903,9 +886,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")}
|
||||||
<ExternalLink href="https://opencode.ai/zen" tabIndex={-1}>
|
<Link href="https://opencode.ai/zen" tabIndex={-1}>
|
||||||
{language.t("provider.connect.opencodeZen.visit.link")}
|
{language.t("provider.connect.opencodeZen.visit.link")}
|
||||||
</ExternalLink>
|
</Link>
|
||||||
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -984,9 +967,9 @@ 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")}
|
||||||
<ExternalLink href={store.authorization!.url} class="text-v2-text-text-base">
|
<Link href={store.authorization!.url} class="text-v2-text-text-base">
|
||||||
{language.t("provider.connect.oauth.code.visit.link")}
|
{language.t("provider.connect.oauth.code.visit.link")}
|
||||||
</ExternalLink>
|
</Link>
|
||||||
{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">
|
||||||
@@ -1023,9 +1006,7 @@ 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")}
|
||||||
<ExternalLink href={store.authorization!.url}>
|
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.code.visit.link")}</Link>
|
||||||
{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">
|
||||||
@@ -1096,9 +1077,7 @@ 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.auto.visit.prefix")}
|
{language.t("provider.connect.oauth.auto.visit.prefix")}
|
||||||
<ExternalLink href={store.authorization!.url}>
|
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.auto.visit.link")}</Link>
|
||||||
{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
|
||||||
@@ -1164,8 +1143,8 @@ function ProviderConnection(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "form"}>
|
<Match when={store.state === "prompt"}>
|
||||||
<AuthFormView />
|
<AuthPromptsView />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "error"}>
|
<Match when={store.state === "error"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { TextField } from "@opencode-ai/ui/text-field"
|
|||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { batch, For } from "solid-js"
|
import { batch, For } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { ExternalLink } from "@/components/external-link"
|
import { Link } from "@/components/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 { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
|
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
|
||||||
@@ -42,6 +43,7 @@ export function DialogCustomProvider(props: Props) {
|
|||||||
export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
|
const serverSDK = useServerSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const [form, setForm] = createStore<FormState>({
|
const [form, setForm] = createStore<FormState>({
|
||||||
@@ -130,7 +132,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
|||||||
const saveMutation = useMutation(() => ({
|
const saveMutation = useMutation(() => ({
|
||||||
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>): Promise<typeof result> => {
|
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>): Promise<typeof result> => {
|
||||||
// TODO: Restore custom providers when V2 exposes config and arbitrary credential APIs.
|
// TODO: Restore custom providers when V2 exposes config and arbitrary credential APIs.
|
||||||
throw new Error(language.t("provider.custom.unavailable"))
|
throw new Error(`Custom provider ${result.providerID} is unavailable`)
|
||||||
},
|
},
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
dialog.close()
|
dialog.close()
|
||||||
@@ -166,9 +168,9 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
|||||||
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
|
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
|
||||||
<p class="text-14-regular text-text-base">
|
<p class="text-14-regular text-text-base">
|
||||||
{language.t("provider.custom.description.prefix")}
|
{language.t("provider.custom.description.prefix")}
|
||||||
<ExternalLink href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
|
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
|
||||||
{language.t("provider.custom.description.link")}
|
{language.t("provider.custom.description.link")}
|
||||||
</ExternalLink>
|
</Link>
|
||||||
{language.t("provider.custom.description.suffix")}
|
{language.t("provider.custom.description.suffix")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { For, Show } from "solid-js"
|
||||||
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
|
import { ServerConnection } from "@/context/server"
|
||||||
|
import { createEditProjectModel } from "./edit-project"
|
||||||
|
|
||||||
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
|
|
||||||
|
export function DialogEditProject(props: { project: LocalProject; server: ServerConnection.Any }) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const model = createEditProjectModel(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
|
||||||
|
<form onSubmit={model.submit} class="flex flex-col gap-6 p-6 pt-0">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<TextField
|
||||||
|
autofocus
|
||||||
|
type="text"
|
||||||
|
label={language.t("dialog.project.edit.name")}
|
||||||
|
placeholder={model.folderName()}
|
||||||
|
value={model.store.name}
|
||||||
|
onChange={(v) => model.setStore("name", v)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
|
||||||
|
<div class="flex gap-3 items-start">
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
onMouseEnter={() => model.setStore("iconHover", true)}
|
||||||
|
onMouseLeave={() => model.setStore("iconHover", false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative size-16 rounded-md transition-colors cursor-pointer"
|
||||||
|
classList={{
|
||||||
|
"border-text-interactive-base bg-surface-info-base/20": model.store.dragOver,
|
||||||
|
"border-border-base hover:border-border-strong": !model.store.dragOver,
|
||||||
|
"overflow-hidden": !!model.store.iconOverride,
|
||||||
|
}}
|
||||||
|
onDrop={model.drop}
|
||||||
|
onDragOver={model.dragOver}
|
||||||
|
onDragLeave={model.dragLeave}
|
||||||
|
onClick={model.iconClick}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={getProjectAvatarSource(props.project.id, {
|
||||||
|
color: model.store.color,
|
||||||
|
url: props.project.icon?.url,
|
||||||
|
override: model.store.iconOverride,
|
||||||
|
})}
|
||||||
|
fallback={
|
||||||
|
<div class="size-full flex items-center justify-center">
|
||||||
|
<Avatar
|
||||||
|
fallback={model.store.name || model.defaultName()}
|
||||||
|
{...getAvatarColors(model.store.color)}
|
||||||
|
class="size-full text-[32px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(src) => (
|
||||||
|
<img
|
||||||
|
src={src()}
|
||||||
|
alt={language.t("dialog.project.edit.icon.alt")}
|
||||||
|
class="size-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
|
classList={{
|
||||||
|
"opacity-100": model.store.iconHover && !model.store.iconOverride,
|
||||||
|
"opacity-0": !(model.store.iconHover && !model.store.iconOverride),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
|
||||||
|
classList={{
|
||||||
|
"opacity-100": model.store.iconHover && !!model.store.iconOverride,
|
||||||
|
"opacity-0": !(model.store.iconHover && !!model.store.iconOverride),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="icon-upload"
|
||||||
|
ref={(el) => {
|
||||||
|
model.setIconInput(el)
|
||||||
|
}}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
onChange={model.inputChange}
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
|
||||||
|
<span>{language.t("dialog.project.edit.icon.hint")}</span>
|
||||||
|
<span>{language.t("dialog.project.edit.icon.recommended")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={!model.store.iconOverride}>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
<For each={AVATAR_COLOR_KEYS}>
|
||||||
|
{(color) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={language.t("dialog.project.edit.color.select", { color })}
|
||||||
|
aria-pressed={model.store.color === color}
|
||||||
|
classList={{
|
||||||
|
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
|
||||||
|
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
|
||||||
|
model.store.color === color,
|
||||||
|
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
|
||||||
|
model.store.color !== color,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (model.store.color === color && !props.project.icon?.url) return
|
||||||
|
model.setStore("color", model.store.color === color ? undefined : color)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
fallback={model.store.name || model.defaultName()}
|
||||||
|
{...getAvatarColors(color)}
|
||||||
|
class="size-full rounded"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
label={language.t("dialog.project.edit.worktree.startup")}
|
||||||
|
description={language.t("dialog.project.edit.worktree.startup.description")}
|
||||||
|
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
||||||
|
value={model.store.startup}
|
||||||
|
onChange={(v) => model.setStore("startup", v)}
|
||||||
|
spellcheck={false}
|
||||||
|
class="max-h-14 w-full overflow-y-auto font-mono text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button type="button" variant="ghost" size="large" onClick={model.close}>
|
||||||
|
{language.t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="primary" size="large" disabled={!model.supported || model.save.isPending}>
|
||||||
|
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,8 +18,6 @@ interface DialogSelectDirectoryProps {
|
|||||||
server: ServerConnection.Any
|
server: ServerConnection.Any
|
||||||
}
|
}
|
||||||
|
|
||||||
const RECENT_PROJECT_LIMIT = 5
|
|
||||||
|
|
||||||
type Row = {
|
type Row = {
|
||||||
absolute: string
|
absolute: string
|
||||||
search: string
|
search: string
|
||||||
@@ -109,6 +107,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
return projects
|
return projects
|
||||||
.map((project, index) => ({ project, at: byProject.get(project.worktree) ?? 0, index }))
|
.map((project, index) => ({ project, at: byProject.get(project.worktree) ?? 0, index }))
|
||||||
.sort((a, b) => b.at - a.at || a.index - b.index)
|
.sort((a, b) => b.at - a.at || a.index - b.index)
|
||||||
|
.slice(0, 5)
|
||||||
.map(({ project }) => {
|
.map(({ project }) => {
|
||||||
const row = toRow(project.worktree, home(), "recent")
|
const row = toRow(project.worktree, home(), "recent")
|
||||||
const name = project.name || getFilename(project.worktree)
|
const name = project.name || getFilename(project.worktree)
|
||||||
@@ -122,10 +121,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
|||||||
const items = async (value: string) => {
|
const items = async (value: string) => {
|
||||||
const results = await directories(value)
|
const results = await directories(value)
|
||||||
const directoryRows = results.map((absolute) => toRow(absolute, home(), "folders"))
|
const directoryRows = results.map((absolute) => toRow(absolute, home(), "folders"))
|
||||||
// Cap the idle list only. Once a query narrows the results, every project stays searchable.
|
return uniqueRows([...recentProjects(), ...directoryRows])
|
||||||
const recent = recentProjects()
|
|
||||||
const visible = value ? recent : recent.slice(0, RECENT_PROJECT_LIMIT)
|
|
||||||
return uniqueRows([...visible, ...directoryRows])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolve(absolute: string) {
|
function resolve(absolute: string) {
|
||||||
|
|||||||
@@ -6,33 +6,21 @@ import { Icon } from "@opencode-ai/ui/icon"
|
|||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Show } from "solid-js"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
|
import { useNavigate } from "@solidjs/router"
|
||||||
|
import { createEffect, createMemo, createResource, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||||
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/server"
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
|
import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { type ServerDomainController } from "@/components/server/server-management-controller"
|
import { useTabs } from "@/context/tabs"
|
||||||
|
|
||||||
type ServerConnectionFormController = {
|
const DEFAULT_USERNAME = "opencode"
|
||||||
state: {
|
|
||||||
adding: () => boolean
|
|
||||||
busy: () => boolean
|
|
||||||
value: () => string
|
|
||||||
name: () => string
|
|
||||||
username: () => string
|
|
||||||
password: () => string
|
|
||||||
error: () => string
|
|
||||||
status: () => boolean | undefined
|
|
||||||
}
|
|
||||||
change: {
|
|
||||||
value: (value: string) => void
|
|
||||||
name: (value: string) => void
|
|
||||||
username: (value: string) => void
|
|
||||||
password: (value: string) => void
|
|
||||||
}
|
|
||||||
reset: () => void
|
|
||||||
submit: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServerFormProps {
|
interface ServerFormProps {
|
||||||
value: string
|
value: string
|
||||||
@@ -51,6 +39,76 @@ interface ServerFormProps {
|
|||||||
onBack: () => void
|
onBack: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function useDefaultServer() {
|
||||||
|
const language = useLanguage()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const [defaultKey, defaultUrlActions] = createResource(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const key = await platform.getDefaultServer?.()
|
||||||
|
if (!key) return null
|
||||||
|
return key
|
||||||
|
} catch (err) {
|
||||||
|
showRequestError(language, err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ initialValue: null },
|
||||||
|
)
|
||||||
|
|
||||||
|
const canDefault = createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer)
|
||||||
|
const setDefault = async (key: ServerConnection.Key | null) => {
|
||||||
|
try {
|
||||||
|
await platform.setDefaultServer?.(key)
|
||||||
|
defaultUrlActions.mutate(key)
|
||||||
|
} catch (err) {
|
||||||
|
showRequestError(language, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { defaultKey: () => defaultKey.latest, canDefault, setDefault }
|
||||||
|
}
|
||||||
|
|
||||||
|
function useServerPreview() {
|
||||||
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
|
const looksComplete = (value: string) => {
|
||||||
|
const normalized = normalizeServerUrl(value)
|
||||||
|
if (!normalized) return false
|
||||||
|
const host = normalized.replace(/^https?:\/\//, "").split("/")[0]
|
||||||
|
if (!host) return false
|
||||||
|
if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true
|
||||||
|
return host.includes(".") || host.includes(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewStatus = async (
|
||||||
|
value: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
setStatus: (value: boolean | undefined) => void,
|
||||||
|
) => {
|
||||||
|
setStatus(undefined)
|
||||||
|
if (!looksComplete(value)) return
|
||||||
|
const normalized = normalizeServerUrl(value)
|
||||||
|
if (!normalized) return
|
||||||
|
const http: ServerConnection.HttpBase = { url: normalized }
|
||||||
|
if (username) http.username = username
|
||||||
|
if (password) http.password = password
|
||||||
|
const result = await checkServerHealth(http)
|
||||||
|
setStatus(result.healthy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { previewStatus }
|
||||||
|
}
|
||||||
|
|
||||||
function ServerForm(props: ServerFormProps) {
|
function ServerForm(props: ServerFormProps) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const keyDown = (event: KeyboardEvent) => {
|
const keyDown = (event: KeyboardEvent) => {
|
||||||
@@ -116,11 +174,387 @@ function ServerForm(props: ServerFormProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServerConnectionList(props: {
|
export function DialogSelectServer() {
|
||||||
domain: ServerDomainController
|
const dialog = useDialog()
|
||||||
onAdd: () => void
|
const controller = useServerManagementController({ onSelect: dialog.close })
|
||||||
onEdit: (server: ServerConnection.Http) => void
|
|
||||||
}) {
|
return (
|
||||||
|
<Dialog title={controller.formTitle()}>
|
||||||
|
<div class="flex flex-1 min-h-0 flex-col px-5">
|
||||||
|
<Show when={controller.isFormMode()} fallback={<ServerConnectionList controller={controller} />}>
|
||||||
|
<ServerConnectionForm controller={controller} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useServerManagementController(options: { onSelect?: () => void; navigateOnAdd?: boolean } = {}) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const server = useServer()
|
||||||
|
const tabs = useTabs()
|
||||||
|
const global = useGlobal()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const language = useLanguage()
|
||||||
|
const { defaultKey, canDefault, setDefault } = useDefaultServer()
|
||||||
|
const { previewStatus } = useServerPreview()
|
||||||
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
addServer: {
|
||||||
|
url: "",
|
||||||
|
name: "",
|
||||||
|
username: DEFAULT_USERNAME,
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
showForm: false,
|
||||||
|
status: undefined as boolean | undefined,
|
||||||
|
},
|
||||||
|
editServer: {
|
||||||
|
id: undefined as string | undefined,
|
||||||
|
value: "",
|
||||||
|
name: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
status: undefined as boolean | undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetAdd = () => {
|
||||||
|
setStore("addServer", {
|
||||||
|
url: "",
|
||||||
|
name: "",
|
||||||
|
username: DEFAULT_USERNAME,
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
showForm: false,
|
||||||
|
status: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const resetEdit = () => {
|
||||||
|
setStore("editServer", {
|
||||||
|
id: undefined,
|
||||||
|
value: "",
|
||||||
|
name: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
status: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (value: string) => {
|
||||||
|
const normalized = normalizeServerUrl(value)
|
||||||
|
if (!normalized) {
|
||||||
|
resetAdd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
http: { url: normalized },
|
||||||
|
}
|
||||||
|
if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim()
|
||||||
|
if (store.addServer.password) conn.http.password = store.addServer.password
|
||||||
|
if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username
|
||||||
|
const result = await checkServerHealth(conn.http)
|
||||||
|
if (!result.healthy) {
|
||||||
|
setStore("addServer", { error: language.t("dialog.server.add.error") })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAdd()
|
||||||
|
if (options.navigateOnAdd === false) {
|
||||||
|
server.add(conn)
|
||||||
|
options.onSelect?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await select(conn, true)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const editMutation = useMutation(() => ({
|
||||||
|
mutationFn: async (input: { original: ServerConnection.Any; value: string }) => {
|
||||||
|
if (input.original.type !== "http") return
|
||||||
|
const normalized = normalizeServerUrl(input.value)
|
||||||
|
if (!normalized) {
|
||||||
|
resetEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = store.editServer.name.trim() || undefined
|
||||||
|
const username = store.editServer.username || undefined
|
||||||
|
const password = store.editServer.password || undefined
|
||||||
|
const existingName = input.original.displayName
|
||||||
|
if (
|
||||||
|
normalized === input.original.http.url &&
|
||||||
|
name === existingName &&
|
||||||
|
username === input.original.http.username &&
|
||||||
|
password === input.original.http.password
|
||||||
|
) {
|
||||||
|
resetEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn: ServerConnection.Http = {
|
||||||
|
type: "http",
|
||||||
|
displayName: name,
|
||||||
|
http: { url: normalized, username, password },
|
||||||
|
}
|
||||||
|
const result = await checkServerHealth(conn.http)
|
||||||
|
if (!result.healthy) {
|
||||||
|
setStore("editServer", { error: language.t("dialog.server.add.error") })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (normalized === input.original.http.url) {
|
||||||
|
server.add(conn)
|
||||||
|
} else {
|
||||||
|
replaceServer(input.original, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEdit()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => {
|
||||||
|
const originalKey = ServerConnection.key(original)
|
||||||
|
const active = server.key
|
||||||
|
tabs.removeServer(originalKey)
|
||||||
|
const newConn = server.add(next)
|
||||||
|
if (!newConn) return
|
||||||
|
const nextActive = active === originalKey ? ServerConnection.key(newConn) : active
|
||||||
|
if (nextActive) server.setActive(nextActive)
|
||||||
|
server.remove(originalKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = createMemo(() => {
|
||||||
|
const current = server.current
|
||||||
|
const list = server.list
|
||||||
|
if (!current) return list
|
||||||
|
if (!list.includes(current)) return [current, ...list]
|
||||||
|
return [current, ...list.filter((x) => x !== current)]
|
||||||
|
})
|
||||||
|
|
||||||
|
const settings = useSettings()
|
||||||
|
const current = createMemo<ServerConnection.Any | undefined>(() =>
|
||||||
|
settings.general.newLayoutDesigns()
|
||||||
|
? undefined
|
||||||
|
: (items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const sortedItems = createMemo(() => {
|
||||||
|
const raw = items()
|
||||||
|
const list = raw
|
||||||
|
if (!list.length) return list
|
||||||
|
const active = current()
|
||||||
|
const order = new Map(list.map((url, index) => [url, index] as const))
|
||||||
|
const rank = (value?: ServerHealth) => {
|
||||||
|
if (value?.healthy === true) return 0
|
||||||
|
if (value?.healthy === false) return 2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return list.slice().sort((a, b) => {
|
||||||
|
if (a === active) return -1
|
||||||
|
if (b === active) return 1
|
||||||
|
const diff =
|
||||||
|
rank(global.servers.health[ServerConnection.key(a)]) - rank(global.servers.health[ServerConnection.key(b)])
|
||||||
|
if (diff !== 0) return diff
|
||||||
|
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function select(conn: ServerConnection.Any, persist?: boolean) {
|
||||||
|
if (!persist && global.servers.health[ServerConnection.key(conn)]?.healthy === false) return
|
||||||
|
options.onSelect?.()
|
||||||
|
if (persist && conn.type === "http") {
|
||||||
|
server.add(conn)
|
||||||
|
navigate("/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddChange = (value: string) => {
|
||||||
|
if (addMutation.isPending) return
|
||||||
|
setStore("addServer", { url: value, error: "" })
|
||||||
|
void previewStatus(value, store.addServer.username, store.addServer.password, (next) =>
|
||||||
|
setStore("addServer", { status: next }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddNameChange = (value: string) => {
|
||||||
|
if (addMutation.isPending) return
|
||||||
|
setStore("addServer", { name: value, error: "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddUsernameChange = (value: string) => {
|
||||||
|
if (addMutation.isPending) return
|
||||||
|
setStore("addServer", { username: value, error: "" })
|
||||||
|
void previewStatus(store.addServer.url, value, store.addServer.password, (next) =>
|
||||||
|
setStore("addServer", { status: next }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddPasswordChange = (value: string) => {
|
||||||
|
if (addMutation.isPending) return
|
||||||
|
setStore("addServer", { password: value, error: "" })
|
||||||
|
void previewStatus(store.addServer.url, store.addServer.username, value, (next) =>
|
||||||
|
setStore("addServer", { status: next }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditChange = (value: string) => {
|
||||||
|
if (editMutation.isPending) return
|
||||||
|
setStore("editServer", { value, error: "" })
|
||||||
|
void previewStatus(value, store.editServer.username, store.editServer.password, (next) =>
|
||||||
|
setStore("editServer", { status: next }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditNameChange = (value: string) => {
|
||||||
|
if (editMutation.isPending) return
|
||||||
|
setStore("editServer", { name: value, error: "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditUsernameChange = (value: string) => {
|
||||||
|
if (editMutation.isPending) return
|
||||||
|
setStore("editServer", { username: value, error: "" })
|
||||||
|
void previewStatus(store.editServer.value, value, store.editServer.password, (next) =>
|
||||||
|
setStore("editServer", { status: next }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditPasswordChange = (value: string) => {
|
||||||
|
if (editMutation.isPending) return
|
||||||
|
setStore("editServer", { password: value, error: "" })
|
||||||
|
void previewStatus(store.editServer.value, store.editServer.username, value, (next) =>
|
||||||
|
setStore("editServer", { status: next }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = createMemo<"list" | "add" | "edit">(() => {
|
||||||
|
if (store.editServer.id) return "edit"
|
||||||
|
if (store.addServer.showForm) return "add"
|
||||||
|
return "list"
|
||||||
|
})
|
||||||
|
|
||||||
|
const editing = createMemo(() => {
|
||||||
|
if (!store.editServer.id) return
|
||||||
|
return items().find((x) => x.type === "http" && x.http.url === store.editServer.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
resetAdd()
|
||||||
|
resetEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const startAdd = () => {
|
||||||
|
resetEdit()
|
||||||
|
setStore("addServer", {
|
||||||
|
showForm: true,
|
||||||
|
url: "",
|
||||||
|
name: "",
|
||||||
|
username: DEFAULT_USERNAME,
|
||||||
|
password: "",
|
||||||
|
error: "",
|
||||||
|
status: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEdit = (conn: ServerConnection.Http) => {
|
||||||
|
resetAdd()
|
||||||
|
setStore("editServer", {
|
||||||
|
id: conn.http.url,
|
||||||
|
value: conn.http.url,
|
||||||
|
name: conn.displayName ?? "",
|
||||||
|
username: conn.http.username ?? "",
|
||||||
|
password: conn.http.password ?? "",
|
||||||
|
error: "",
|
||||||
|
status: global.servers.health[ServerConnection.key(conn)]?.healthy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
if (mode() === "add") {
|
||||||
|
if (addMutation.isPending) return
|
||||||
|
setStore("addServer", { error: "" })
|
||||||
|
addMutation.mutate(store.addServer.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const original = editing()
|
||||||
|
if (!original) return
|
||||||
|
if (editMutation.isPending) return
|
||||||
|
setStore("editServer", { error: "" })
|
||||||
|
editMutation.mutate({ original, value: store.editServer.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormMode = createMemo(() => mode() !== "list")
|
||||||
|
const isAddMode = createMemo(() => mode() === "add")
|
||||||
|
const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending))
|
||||||
|
|
||||||
|
const formTitle = createMemo(() => {
|
||||||
|
if (!isFormMode()) return language.t("dialog.server.title")
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-2 -ml-2">
|
||||||
|
<IconButton icon="arrow-left" variant="ghost" onClick={resetForm} aria-label={language.t("common.goBack")} />
|
||||||
|
<span>{isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!store.editServer.id) return
|
||||||
|
if (editing()) return
|
||||||
|
resetEdit()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleRemove(key: ServerConnection.Key) {
|
||||||
|
try {
|
||||||
|
if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key)
|
||||||
|
tabs.removeServer(key)
|
||||||
|
server.remove(key)
|
||||||
|
if ((await platform.getDefaultServer?.()) === key) {
|
||||||
|
await setDefault(null)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRequestError(language, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultKey,
|
||||||
|
canDefault,
|
||||||
|
current,
|
||||||
|
sortedItems,
|
||||||
|
status: () => global.servers.health,
|
||||||
|
isFormMode,
|
||||||
|
isAddMode,
|
||||||
|
formTitle,
|
||||||
|
formBusy,
|
||||||
|
formValue: () => (isAddMode() ? store.addServer.url : store.editServer.value),
|
||||||
|
formName: () => (isAddMode() ? store.addServer.name : store.editServer.name),
|
||||||
|
formUsername: () => (isAddMode() ? store.addServer.username : store.editServer.username),
|
||||||
|
formPassword: () => (isAddMode() ? store.addServer.password : store.editServer.password),
|
||||||
|
formError: () => (isAddMode() ? store.addServer.error : store.editServer.error),
|
||||||
|
formStatus: () => (isAddMode() ? store.addServer.status : store.editServer.status),
|
||||||
|
select,
|
||||||
|
setDefault,
|
||||||
|
startAdd,
|
||||||
|
startEdit,
|
||||||
|
resetForm,
|
||||||
|
submitForm,
|
||||||
|
canRemove: server.canRemove,
|
||||||
|
handleRemove,
|
||||||
|
handleFormChange: () => (isAddMode() ? handleAddChange : handleEditChange),
|
||||||
|
handleFormNameChange: () => (isAddMode() ? handleAddNameChange : handleEditNameChange),
|
||||||
|
handleFormUsernameChange: () => (isAddMode() ? handleAddUsernameChange : handleEditUsernameChange),
|
||||||
|
handleFormPasswordChange: () => (isAddMode() ? handleAddPasswordChange : handleEditPasswordChange),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServerConnectionList(props: { controller: ReturnType<typeof useServerManagementController> }) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
|
||||||
@@ -134,10 +568,10 @@ export function ServerConnectionList(props: {
|
|||||||
}}
|
}}
|
||||||
noInitialSelection
|
noInitialSelection
|
||||||
emptyMessage={language.t("dialog.server.empty")}
|
emptyMessage={language.t("dialog.server.empty")}
|
||||||
items={props.domain.collection.items}
|
items={props.controller.sortedItems}
|
||||||
key={(x) => x.http.url}
|
key={(x) => x.http.url}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (x && !settings.general.newLayoutDesigns()) void props.domain.selection.select(x)
|
if (x && !settings.general.newLayoutDesigns()) void props.controller.select(x)
|
||||||
}}
|
}}
|
||||||
divider={true}
|
divider={true}
|
||||||
>
|
>
|
||||||
@@ -146,15 +580,15 @@ export function ServerConnectionList(props: {
|
|||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||||
<div class="flex flex-col h-full items-center w-5">
|
<div class="flex flex-col h-full items-center w-5">
|
||||||
<ServerHealthIndicator health={props.domain.collection.health()[key]} />
|
<ServerHealthIndicator health={props.controller.status()[key]} />
|
||||||
</div>
|
</div>
|
||||||
<ServerRow
|
<ServerRow
|
||||||
conn={i}
|
conn={i}
|
||||||
dimmed={props.domain.collection.health()[key]?.healthy === false}
|
dimmed={props.controller.status()[key]?.healthy === false}
|
||||||
status={props.domain.collection.health()[key]}
|
status={props.controller.status()[key]}
|
||||||
class="flex items-center gap-3 min-w-0 flex-1"
|
class="flex items-center gap-3 min-w-0 flex-1"
|
||||||
badge={
|
badge={
|
||||||
<Show when={props.domain.defaults.key() === ServerConnection.key(i)}>
|
<Show when={props.controller.defaultKey() === ServerConnection.key(i)}>
|
||||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||||
{language.t("dialog.server.status.default")}
|
{language.t("dialog.server.status.default")}
|
||||||
</span>
|
</span>
|
||||||
@@ -163,12 +597,7 @@ export function ServerConnectionList(props: {
|
|||||||
showCredentials
|
showCredentials
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-center gap-4 pl-4">
|
<div class="flex items-center justify-center gap-4 pl-4">
|
||||||
<Show
|
<Show when={props.controller.current() && ServerConnection.key(props.controller.current()!) === key}>
|
||||||
when={
|
|
||||||
props.domain.collection.current() &&
|
|
||||||
ServerConnection.key(props.domain.collection.current()!) === key
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon name="check" class="h-6" />
|
<Icon name="check" class="h-6" />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -187,27 +616,27 @@ export function ServerConnectionList(props: {
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (i.type !== "http") return
|
if (i.type !== "http") return
|
||||||
props.onEdit(i)
|
props.controller.startEdit(i)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<Show when={props.domain.defaults.available() && props.domain.defaults.key() !== key}>
|
<Show when={props.controller.canDefault() && props.controller.defaultKey() !== key}>
|
||||||
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(key)}>
|
<DropdownMenu.Item onSelect={() => props.controller.setDefault(key)}>
|
||||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.domain.defaults.available() && props.domain.defaults.key() === key}>
|
<Show when={props.controller.canDefault() && props.controller.defaultKey() === key}>
|
||||||
<DropdownMenu.Item onSelect={() => props.domain.defaults.set(null)}>
|
<DropdownMenu.Item onSelect={() => props.controller.setDefault(null)}>
|
||||||
<DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>
|
||||||
{language.t("dialog.server.menu.defaultRemove")}
|
{language.t("dialog.server.menu.defaultRemove")}
|
||||||
</DropdownMenu.ItemLabel>
|
</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.domain.connection.canRemove(key)}>
|
<Show when={props.controller.canRemove(key)}>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => props.domain.connection.remove(key)}
|
onSelect={() => props.controller.handleRemove(ServerConnection.key(i))}
|
||||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||||
@@ -228,7 +657,7 @@ export function ServerConnectionList(props: {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={props.onAdd}
|
onClick={props.controller.startAdd}
|
||||||
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{language.t("dialog.server.add.button")}
|
{language.t("dialog.server.add.button")}
|
||||||
@@ -238,38 +667,38 @@ export function ServerConnectionList(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServerConnectionForm(props: { form: ServerConnectionFormController }) {
|
export function ServerConnectionForm(props: { controller: ReturnType<typeof useServerManagementController> }) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-1 min-h-0 flex-col gap-4">
|
<div class="flex flex-1 min-h-0 flex-col gap-4">
|
||||||
<ServerForm
|
<ServerForm
|
||||||
value={props.form.state.value()}
|
value={props.controller.formValue()}
|
||||||
name={props.form.state.name()}
|
name={props.controller.formName()}
|
||||||
username={props.form.state.username()}
|
username={props.controller.formUsername()}
|
||||||
password={props.form.state.password()}
|
password={props.controller.formPassword()}
|
||||||
placeholder={language.t("dialog.server.add.placeholder")}
|
placeholder={language.t("dialog.server.add.placeholder")}
|
||||||
busy={props.form.state.busy()}
|
busy={props.controller.formBusy()}
|
||||||
error={props.form.state.error()}
|
error={props.controller.formError()}
|
||||||
status={props.form.state.status()}
|
status={props.controller.formStatus()}
|
||||||
onChange={props.form.change.value}
|
onChange={props.controller.handleFormChange()}
|
||||||
onNameChange={props.form.change.name}
|
onNameChange={props.controller.handleFormNameChange()}
|
||||||
onUsernameChange={props.form.change.username}
|
onUsernameChange={props.controller.handleFormUsernameChange()}
|
||||||
onPasswordChange={props.form.change.password}
|
onPasswordChange={props.controller.handleFormPasswordChange()}
|
||||||
onSubmit={props.form.submit}
|
onSubmit={props.controller.submitForm}
|
||||||
onBack={props.form.reset}
|
onBack={props.controller.resetForm}
|
||||||
/>
|
/>
|
||||||
<div class="shrink-0 pb-5">
|
<div class="shrink-0 pb-5">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={props.form.submit}
|
onClick={props.controller.submitForm}
|
||||||
disabled={props.form.state.busy()}
|
disabled={props.controller.formBusy()}
|
||||||
class="px-3 py-1.5"
|
class="px-3 py-1.5"
|
||||||
>
|
>
|
||||||
{props.form.state.busy()
|
{props.controller.formBusy()
|
||||||
? language.t("dialog.server.add.checking")
|
? language.t("dialog.server.add.checking")
|
||||||
: props.form.state.adding()
|
: props.controller.isAddMode()
|
||||||
? language.t("dialog.server.add.button")
|
? language.t("dialog.server.add.button")
|
||||||
: language.t("common.save")}
|
: language.t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Component, createSignal, startTransition } from "solid-js"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { SettingsGeneral } from "./settings-general"
|
||||||
|
import { SettingsKeybinds } from "./settings-keybinds"
|
||||||
|
import { SettingsProviders } from "./settings-providers"
|
||||||
|
import { SettingsModels } from "./settings-models"
|
||||||
|
import { SettingsServers } from "./settings-servers"
|
||||||
|
|
||||||
|
export const DialogSettings: Component<{ defaultValue?: string }> = (props) => {
|
||||||
|
const language = useLanguage()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
|
||||||
|
|
||||||
|
const showProviders = () => {
|
||||||
|
void dialog.show(() => <DialogSettings defaultValue="providers" />)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog size="x-large" transition>
|
||||||
|
<Tabs
|
||||||
|
orientation="vertical"
|
||||||
|
variant="settings"
|
||||||
|
value={tab()}
|
||||||
|
onChange={(value) => void startTransition(() => setTab(value))}
|
||||||
|
class="h-full settings-dialog"
|
||||||
|
>
|
||||||
|
<Tabs.List>
|
||||||
|
<div class="flex flex-col justify-between h-full w-full gap-4">
|
||||||
|
<div class="flex flex-col gap-3 w-full pt-3">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
|
||||||
|
<div class="flex flex-col gap-1.5 w-full">
|
||||||
|
<Tabs.Trigger value="general">
|
||||||
|
<Icon name="sliders" />
|
||||||
|
{language.t("settings.tab.general")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="shortcuts">
|
||||||
|
<Icon name="keyboard" />
|
||||||
|
{language.t("settings.tab.shortcuts")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="servers">
|
||||||
|
<Icon name="server" />
|
||||||
|
{language.t("status.popover.tab.servers")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
|
||||||
|
<div class="flex flex-col gap-1.5 w-full">
|
||||||
|
<Tabs.Trigger value="providers">
|
||||||
|
<Icon name="providers" />
|
||||||
|
{language.t("settings.providers.title")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="models">
|
||||||
|
<Icon name="models" />
|
||||||
|
{language.t("settings.models.title")}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
||||||
|
<span>{language.t("app.name.desktop")}</span>
|
||||||
|
<span class="text-11-regular">v{platform.version}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="general" class="no-scrollbar">
|
||||||
|
<SettingsGeneral />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="shortcuts" class="no-scrollbar">
|
||||||
|
<SettingsKeybinds />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="servers" class="no-scrollbar">
|
||||||
|
<SettingsServers />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="providers" class="no-scrollbar">
|
||||||
|
<SettingsProviders onBack={showProviders} />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="models" class="no-scrollbar">
|
||||||
|
<SettingsModels />
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
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"
|
||||||
@@ -15,11 +14,10 @@ export type DialogGoUpsellProps = {
|
|||||||
|
|
||||||
export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
|
||||||
const runAction = () => {
|
const runAction = () => {
|
||||||
if (props.link) platform.openExternal(props.link)
|
if (props.link) platform.openLink(props.link)
|
||||||
props.onClose?.()
|
props.onClose?.()
|
||||||
dialog.close()
|
dialog.close()
|
||||||
}
|
}
|
||||||
@@ -34,7 +32,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
|||||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button variant="ghost" size="large" onClick={dismiss}>
|
<Button variant="ghost" size="large" onClick={dismiss}>
|
||||||
{language.t("dialog.usageExceeded.dontShowAgain")}
|
Don't show again
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="large" onClick={runAction}>
|
<Button variant="primary" size="large" onClick={runAction}>
|
||||||
{props.actionLabel}
|
{props.actionLabel}
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ test("resolves directory autocomplete from the current browser root", async () =
|
|||||||
directories.push(input.location?.directory ?? "")
|
directories.push(input.location?.directory ?? "")
|
||||||
return Promise.resolve({ data: [] })
|
return Promise.resolve({ data: [] })
|
||||||
},
|
},
|
||||||
list: () => Promise.resolve({ data: [] }),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||||
@@ -153,67 +152,6 @@ test("resolves directory autocomplete from the current browser root", async () =
|
|||||||
expect(directories).toEqual(["/repo", "/repo/src"])
|
expect(directories).toEqual(["/repo", "/repo/src"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps indexed directory results for servers that support empty search", async () => {
|
|
||||||
const sdk = {
|
|
||||||
api: {
|
|
||||||
file: {
|
|
||||||
find: () => Promise.resolve({ data: [{ path: "projects/", type: "directory" }] }),
|
|
||||||
list: () => Promise.reject(new Error("listing should not run when search returns results")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
|
||||||
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
|
||||||
|
|
||||||
expect(await search("")).toEqual(["/home/luke/projects"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("lists the default directory when empty search is unsupported", async () => {
|
|
||||||
const calls: string[] = []
|
|
||||||
const directories = Array.from({ length: 60 }, (_, index) => ({
|
|
||||||
path: `project-${index}/`,
|
|
||||||
type: "directory" as const,
|
|
||||||
}))
|
|
||||||
const sdk = {
|
|
||||||
api: {
|
|
||||||
file: {
|
|
||||||
find: () => Promise.resolve({ data: [] }),
|
|
||||||
list: (input: { location?: { directory?: string } }) => {
|
|
||||||
calls.push(input.location?.directory ?? "")
|
|
||||||
return Promise.resolve({
|
|
||||||
data: [...directories, { path: "README.md", type: "file" }],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
|
||||||
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
|
||||||
|
|
||||||
const results = await search("")
|
|
||||||
expect(results).toHaveLength(60)
|
|
||||||
expect(results.at(-1)).toBe("/home/luke/project-59")
|
|
||||||
expect(calls).toEqual(["/home/luke"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("matches the default directory listing when typed search is unsupported", async () => {
|
|
||||||
const sdk = {
|
|
||||||
api: {
|
|
||||||
file: {
|
|
||||||
find: () => Promise.resolve({ data: [] }),
|
|
||||||
list: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
data: [
|
|
||||||
{ path: "Documents/", type: "directory" },
|
|
||||||
{ path: "Downloads/", type: "directory" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
|
||||||
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
|
||||||
|
|
||||||
expect(await search("documents")).toEqual(["/home/luke/Documents"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("searches from an absolute root without a default base", async () => {
|
test("searches from an absolute root without a default base", async () => {
|
||||||
const directories: string[] = []
|
const directories: string[] = []
|
||||||
const sdk = {
|
const sdk = {
|
||||||
|
|||||||
@@ -379,14 +379,7 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
|
|||||||
.then((result) => result.data.map((entry) => entry.path))
|
.then((result) => result.data.map((entry) => entry.path))
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
if (!active()) return []
|
if (!active()) return []
|
||||||
if (results.length) {
|
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
|
||||||
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
|
|
||||||
}
|
|
||||||
const fallback = query
|
|
||||||
? await match(input.directory, query, 50)
|
|
||||||
: (await directories(input.directory)).map((item) => item.absolute)
|
|
||||||
if (!active()) return []
|
|
||||||
return fallback
|
|
||||||
}
|
}
|
||||||
const segments = query.replace(/^\/+/, "").split("/")
|
const segments = query.replace(/^\/+/, "").split("/")
|
||||||
const head = segments.slice(0, -1).filter((part) => part && part !== ".")
|
const head = segments.slice(0, -1).filter((part) => part && part !== ".")
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ComponentProps, splitProps } from "solid-js"
|
|
||||||
|
|
||||||
export interface ExternalLinkProps extends Omit<ComponentProps<"a">, "href"> {
|
|
||||||
href: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ExternalLink(props: ExternalLinkProps) {
|
|
||||||
const [local, rest] = splitProps(props, ["href", "children", "class", "target", "rel"])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={local.href}
|
|
||||||
class={`text-text-strong underline ${local.class ?? ""}`}
|
|
||||||
target={local.target ?? "_blank"}
|
|
||||||
rel={local.rel ?? "noopener noreferrer"}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{local.children}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -29,14 +29,14 @@ export type { Kind } from "@/components/file-tree"
|
|||||||
|
|
||||||
const INDENT_STEP = 16
|
const INDENT_STEP = 16
|
||||||
|
|
||||||
function rowPaddingStart(level: number, type: FileNode["type"]) {
|
function rowPaddingLeft(level: number, type: FileNode["type"]) {
|
||||||
if (type === "directory") return 8 + level * INDENT_STEP
|
if (type === "directory") return 8 + level * INDENT_STEP
|
||||||
if (level === 0) return 8
|
if (level === 0) return 8
|
||||||
return 8 + level * INDENT_STEP - INDENT_STEP
|
return 8 + level * INDENT_STEP - INDENT_STEP
|
||||||
}
|
}
|
||||||
|
|
||||||
function guideLineStart(level: number) {
|
function guideLineLeft(level: number) {
|
||||||
return rowPaddingStart(level, "directory") + 8
|
return rowPaddingLeft(level, "directory") + 8
|
||||||
}
|
}
|
||||||
|
|
||||||
export const kindLabel = (kind: Kind) => {
|
export const kindLabel = (kind: Kind) => {
|
||||||
@@ -87,7 +87,7 @@ const FileTreeNodeV2 = (
|
|||||||
...local.classList,
|
...local.classList,
|
||||||
[local.class ?? ""]: !!local.class,
|
[local.class ?? ""]: !!local.class,
|
||||||
}}
|
}}
|
||||||
style={`padding-inline-start: ${rowPaddingStart(local.level, local.node.type)}px`}
|
style={`padding-left: ${rowPaddingLeft(local.level, local.node.type)}px`}
|
||||||
draggable={local.draggable}
|
draggable={local.draggable}
|
||||||
onDragStart={(event: DragEvent) => {
|
onDragStart={(event: DragEvent) => {
|
||||||
if (!local.draggable) return
|
if (!local.draggable) return
|
||||||
@@ -99,9 +99,7 @@ const FileTreeNodeV2 = (
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{local.children}
|
{local.children}
|
||||||
<span class="flex-1 min-w-0 text-start text-12-medium whitespace-nowrap truncate">
|
<span class="flex-1 min-w-0 text-12-medium whitespace-nowrap truncate">{local.node.name}</span>
|
||||||
<bdi dir="auto">{local.node.name}</bdi>
|
|
||||||
</span>
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const value = kind()
|
const value = kind()
|
||||||
if (!value || local.node.type !== "file") return null
|
if (!value || local.node.type !== "file") return null
|
||||||
@@ -118,7 +116,7 @@ const FileTreeNodeV2 = (
|
|||||||
function GuideLines(props: { level: number }) {
|
function GuideLines(props: { level: number }) {
|
||||||
return (
|
return (
|
||||||
<For each={Array.from({ length: props.level })}>
|
<For each={Array.from({ length: props.level })}>
|
||||||
{(_, index) => <div data-slot="file-tree-v2-guide" style={`inset-inline-start: ${guideLineStart(index())}px`} />}
|
{(_, index) => <div data-slot="file-tree-v2-guide" style={`left: ${guideLineLeft(index())}px`} />}
|
||||||
</For>
|
</For>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -229,7 +227,7 @@ export default function FileTreeV2(props: {
|
|||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: "0",
|
top: "0",
|
||||||
"inset-inline-start": "0",
|
left: "0",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: `${item().size}px`,
|
height: `${item().size}px`,
|
||||||
transform: `translateY(${item().start}px)`,
|
transform: `translateY(${item().start}px)`,
|
||||||
|
|||||||
@@ -146,13 +146,13 @@ const FileTreeNode = (
|
|||||||
<Dynamic
|
<Dynamic
|
||||||
component={local.as ?? "div"}
|
component={local.as ?? "div"}
|
||||||
classList={{
|
classList={{
|
||||||
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-start hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
|
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
|
||||||
"bg-surface-base-active": local.node.path === local.active,
|
"bg-surface-base-active": local.node.path === local.active,
|
||||||
...local.classList,
|
...local.classList,
|
||||||
[local.class ?? ""]: !!local.class,
|
[local.class ?? ""]: !!local.class,
|
||||||
[local.nodeClass ?? ""]: !!local.nodeClass,
|
[local.nodeClass ?? ""]: !!local.nodeClass,
|
||||||
}}
|
}}
|
||||||
style={`padding-inline-start: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
|
style={`padding-left: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
|
||||||
draggable={local.draggable}
|
draggable={local.draggable}
|
||||||
onDragStart={(event: DragEvent) => {
|
onDragStart={(event: DragEvent) => {
|
||||||
if (!local.draggable) return
|
if (!local.draggable) return
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { createSignal, Show } from "solid-js"
|
||||||
|
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
|
import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4"
|
||||||
|
import homeImage from "@/assets/help/home.png"
|
||||||
|
import tabsImage from "@/assets/help/tabs.png"
|
||||||
|
|
||||||
|
// TODO: wire to changelog / seen-state when available
|
||||||
|
const showPopover = () => true
|
||||||
|
|
||||||
|
// can remove this after the tabs rollout has been out for a while
|
||||||
|
export function TabsInfoPopup() {
|
||||||
|
const settings = useSettings()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const [drawerOpen, setDrawerOpen] = createSignal(false)
|
||||||
|
const windows = () => platform.platform === "desktop" && platform.os === "windows"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
|
||||||
|
<Show when={settings.general.shouldDisplayTabsToast()}>
|
||||||
|
<div
|
||||||
|
class="fixed bottom-5 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]"
|
||||||
|
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Dismiss Tabs information"
|
||||||
|
class="absolute top-3 right-3 z-10 size-5 flex items-center justify-center rounded-[4px] bg-[rgba(0,0,0,0.4)]"
|
||||||
|
onClick={settings.general.dismissTabsToast}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M4.25 11.75L11.75 4.25M11.75 11.75L4.25 4.25" stroke="white" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="relative block h-[232px] w-[184px] cursor-pointer overflow-hidden rounded-[4px] text-left"
|
||||||
|
onClick={() => {
|
||||||
|
settings.general.dismissTabsToast()
|
||||||
|
setDrawerOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
src={introducingTabsVideo}
|
||||||
|
class="absolute inset-0 h-full w-full object-cover"
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
autoplay
|
||||||
|
playsinline
|
||||||
|
aria-hidden="true"
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-x-0 bottom-0 flex w-full flex-col items-start gap-1.5 bg-[linear-gradient(180deg,rgba(0,0,0,0)_0%,#000000_100%)] px-3 py-5">
|
||||||
|
<p class="w-full select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-[#FFFFFF]">
|
||||||
|
Introducing Tabs
|
||||||
|
</p>
|
||||||
|
<p class="w-full select-none text-[13px] font-[440] leading-[140%] tracking-[-0.04px] text-[#808080]">
|
||||||
|
Organize your work and active sessions with tabs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<DrawerContent
|
||||||
|
style={
|
||||||
|
windows()
|
||||||
|
? {
|
||||||
|
inset: "0 0 0 auto",
|
||||||
|
"max-height": "100vh",
|
||||||
|
"max-width": "100vw",
|
||||||
|
"border-radius": "0",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show when={windows()}>
|
||||||
|
<DrawerClose
|
||||||
|
as={IconButtonV2}
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
variant="neutral"
|
||||||
|
aria-label="Close"
|
||||||
|
icon={<IconV2 name="xmark-small" />}
|
||||||
|
class="absolute top-[10px] left-[-36px]"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class="flex w-full shrink-0 items-center gap-4 self-stretch border-b border-v2-border-border-muted"
|
||||||
|
classList={{
|
||||||
|
"h-[40px] px-4": windows(),
|
||||||
|
"h-[52px] p-4": !windows(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p class="min-h-0 min-w-0 flex-1 text-[13px] font-[530] leading-5 tracking-[-0.04px] tabular-nums text-v2-text-text-muted">
|
||||||
|
July 14
|
||||||
|
</p>
|
||||||
|
<Show when={!windows()}>
|
||||||
|
<DrawerClose
|
||||||
|
as={IconButtonV2}
|
||||||
|
type="button"
|
||||||
|
size="small"
|
||||||
|
variant="ghost-muted"
|
||||||
|
aria-label="Close"
|
||||||
|
icon={<IconV2 name="xmark-small" />}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="relative flex min-h-0 w-full flex-1 flex-col items-start gap-6 overflow-y-auto p-8">
|
||||||
|
<p class="w-full shrink-0 self-stretch text-[21px] font-[610] leading-6 tracking-[-0.37px] tabular-nums text-v2-text-text-base">
|
||||||
|
Introducing Tabs
|
||||||
|
</p>
|
||||||
|
<div class="flex w-full flex-1 flex-col gap-4 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base">
|
||||||
|
<p>OpenCode Desktop is now built around tabs.</p>
|
||||||
|
<img src={tabsImage} alt="" class="aspect-video w-full rounded-[6px] object-cover" />
|
||||||
|
<p>
|
||||||
|
Start a new session in a tab, or open an existing session from any of your projects. Open a new tab when
|
||||||
|
you're starting something new, and close it when you're done.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Keeping a few tabs open makes it easier to organize your active sessions. Rename tabs to something
|
||||||
|
memorable if you plan to keep them around.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You'll find all your sessions and projects on the new Home screen. Selecting a session opens it in a tab.
|
||||||
|
</p>
|
||||||
|
<img src={homeImage} alt="" class="aspect-video w-full rounded-[6px] object-cover" />
|
||||||
|
<p>When you reopen the app, your tabs are still open.</p>
|
||||||
|
<p>
|
||||||
|
The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using
|
||||||
|
the previous layout, you can switch between layouts in Settings. Just keep in mind that the new layout
|
||||||
|
will become permanent in a few weeks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ComponentProps, splitProps } from "solid-js"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
|
||||||
|
export interface LinkProps extends Omit<ComponentProps<"a">, "href"> {
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link(props: LinkProps) {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const [local, rest] = splitProps(props, ["href", "children", "class"])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={local.href}
|
||||||
|
class={`text-text-strong underline ${local.class ?? ""}`}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!local.href) return
|
||||||
|
event.preventDefault()
|
||||||
|
platform.openLink(local.href)
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{local.children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user