mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12395c37ad | |||
| c86066efef | |||
| fa9ba2938e | |||
| 63e24880e5 | |||
| eaf42c4ab0 | |||
| 8e10ab0aa7 | |||
| b329fcceb2 | |||
| bd83177c48 | |||
| 06cb0de0b1 | |||
| 3ca89ac796 | |||
| 3aa2860058 | |||
| ad63cf1590 | |||
| aa56750f8b | |||
| 4aaed42640 | |||
| f69f677a32 | |||
| 8289883de8 | |||
| 003c22b4a3 | |||
| 1982d9832b | |||
| 90f0576222 | |||
| e34822db6b | |||
| b19ae6547f | |||
| 6b074ed718 | |||
| e687eb936b | |||
| 797cb530e5 | |||
| 0a5bed2bc2 | |||
| ba41dadc91 | |||
| 451876b0bd | |||
| 91561bbdfb | |||
| 018ce34b39 | |||
| ce82b21073 | |||
| 1f37c26574 | |||
| cb3e28d207 | |||
| 8f1e13f299 | |||
| 2fe68b5e91 | |||
| 2ace237938 | |||
| 5b62211167 | |||
| 60b6229c4e | |||
| 4caec80e67 | |||
| e9d4ca7e0f | |||
| d71454c701 | |||
| 884c256033 | |||
| 78235385dd | |||
| fd213e6df6 | |||
| 3726052307 | |||
| 7d33a6f7c9 | |||
| 7a035d7fc0 | |||
| 9151af7045 | |||
| 15bcbb1d7a | |||
| 4d3294727c | |||
| aae0e89519 | |||
| 93a7b4ab76 | |||
| 0ebe74b625 | |||
| b9dae8593c | |||
| d8f9388610 | |||
| be14739cce | |||
| 762588c251 | |||
| d6e54e9042 | |||
| d4fd528152 | |||
| de185559dd | |||
| bc5ce5eab1 | |||
| b10d617c80 | |||
| 18466b8020 | |||
| 71ec022b47 | |||
| f7eeb08942 | |||
| 9205dfe724 | |||
| a3776429aa | |||
| c0e43c0c65 | |||
| 08c5a2a5e8 | |||
| 7077c70d60 | |||
| 1fd8bf526d | |||
| 6d9539f469 | |||
| e5101d9651 | |||
| b0151e1d02 | |||
| c3637753bc | |||
| 48fc9e3cc3 | |||
| 82a482b36d | |||
| 7fac84319d | |||
| 0a5e617da8 | |||
| b5f92c9f48 | |||
| 2070fd9bc2 | |||
| 48dc05eee7 | |||
| 84bb706537 | |||
| be8cfa7e08 | |||
| 56a789d926 | |||
| 5409151ad4 | |||
| f90d154465 | |||
| 078385d386 | |||
| beaaa174ea | |||
| fb59606bb4 | |||
| 846d548154 | |||
| 84336e4f91 | |||
| 01a5c69244 | |||
| d78f91afeb | |||
| 33762292f7 | |||
| 5d6aa3b41a | |||
| 683aca5dbe | |||
| 92025e9a47 | |||
| 411e053572 | |||
| b862d178bf | |||
| bda0ddc207 | |||
| 58ba99e505 | |||
| 6ee817d041 | |||
| dfeb1b5051 | |||
| 61a7f6db35 | |||
| 6446b8ae3b | |||
| ae53163cad | |||
| 41202819a4 | |||
| a31698f99b | |||
| 6248542c49 | |||
| d25c91e5eb | |||
| 5d63020dcd | |||
| 062f54590e | |||
| a76c6918d2 | |||
| 2b91a6f210 | |||
| 10579cceb2 | |||
| 25702e01ce | |||
| ecc5c44d9a | |||
| f5a0b920a2 | |||
| 2caa016fe1 | |||
| 6861fedd09 | |||
| 3d072112ce | |||
| bdfea046db | |||
| a2d08fb63b | |||
| 5a55135d89 | |||
| 8870d36e0f | |||
| eb923c27ca | |||
| 9903abc704 | |||
| 225a1fbf35 | |||
| 93159bccbf | |||
| fab8ec4f54 | |||
| a3035c53ea | |||
| 36c416e143 | |||
| e1e0304a96 | |||
| 71c3a7c8f2 | |||
| ecdfff5a42 | |||
| 4b948c5d74 | |||
| cd56c51e2d | |||
| 43e39d7f68 | |||
| 7a17925495 | |||
| 5acb2530b4 | |||
| 44a6787359 | |||
| 42e6b7db32 | |||
| 2c02f8bace | |||
| 2ec20e576b | |||
| 20f47fec7a | |||
| 65210f2d97 | |||
| af0b7ffae7 |
@@ -6,7 +6,6 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- ci
|
- ci
|
||||||
- dev
|
- dev
|
||||||
- v2
|
|
||||||
- beta
|
- beta
|
||||||
- fix/npm-native-binary-install
|
- fix/npm-native-binary-install
|
||||||
- snapshot-*
|
- snapshot-*
|
||||||
@@ -32,9 +31,6 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
env:
|
|
||||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'v2' && 'next') || '' }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version:
|
version:
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
@@ -126,7 +122,7 @@ jobs:
|
|||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
runs-on: blacksmith-4vcpu-windows-2025
|
runs-on: blacksmith-4vcpu-windows-2025
|
||||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
|
if: github.repository == 'anomalyco/opencode'
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
@@ -225,7 +221,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'v2'
|
if: github.repository == 'anomalyco/opencode'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -451,7 +447,6 @@ jobs:
|
|||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
if: github.ref_name != 'v2'
|
|
||||||
with:
|
with:
|
||||||
name: opencode-cli-signed-windows
|
name: opencode-cli-signed-windows
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- v2
|
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -75,9 +74,13 @@ jobs:
|
|||||||
working-directory: packages/client
|
working-directory: packages/client
|
||||||
run: bun run check:generated
|
run: bun run check:generated
|
||||||
|
|
||||||
|
- name: Run HttpApi exerciser gates
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
working-directory: packages/opencode
|
||||||
|
run: bun run test:httpapi
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
name: e2e (${{ matrix.settings.name }})
|
name: e2e (${{ matrix.settings.name }})
|
||||||
if: github.ref_name != 'v2' && github.head_ref != 'v2'
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
export default {
|
|
||||||
id: "Orchestrator",
|
|
||||||
setup: async (ctx) => {
|
|
||||||
await ctx.agent.transform((agents) => {
|
|
||||||
agents.update("orchestrator", (agent) => {
|
|
||||||
agent.description = "Coordinates work by delegating implementation tasks to the minion subagent."
|
|
||||||
agent.mode = "primary"
|
|
||||||
agent.system = [
|
|
||||||
"You are Orchestrator, the primary coordinating agent for this repository. You do meta work only: you coordinate, brief, and synthesize — you do not perform the work itself.",
|
|
||||||
"Delegate ALL actual work to the minion subagent — implementation, exploration, discovery, searching the codebase, reading files to understand a problem, and even trivial one-line edits. Task size is never a reason to do it yourself, and there is no 'final integration' exception.",
|
|
||||||
"You are not hard-banned from tools, but direct tool use is reserved for coordination overhead: a quick peek to phrase a better brief, a fast read-only check to verify a minion's reported result, or answering a question about coordination state. If a tool call is producing the answer or the artifact the user asked for, that call belongs to a minion, not you.",
|
|
||||||
"Exploration is work. If the user asks how something works or where something lives, delegate the investigation to a minion rather than exploring yourself.",
|
|
||||||
"Always start minion subagents in the background. Even if you have nothing else to coordinate right now, the user may assign you new work while a Minion runs, and you must stay free to receive it. Never poll; you will be notified when they finish.",
|
|
||||||
"Give each minion a clear, self-contained brief: the goal, constraints, expected output, and any files or context already known from the user or previous minion reports.",
|
|
||||||
"Synthesize minion results, decide next steps, and report back concisely.",
|
|
||||||
].join("\n")
|
|
||||||
})
|
|
||||||
|
|
||||||
agents.update("minion", (agent) => {
|
|
||||||
agent.description = "Subagent that executes focused tasks delegated by Orchestrator."
|
|
||||||
agent.mode = "subagent"
|
|
||||||
agent.model = { providerID: "opencode", id: "glm-5.2" }
|
|
||||||
agent.system = [
|
|
||||||
"You are minion, a focused execution subagent for this repository.",
|
|
||||||
"Complete the specific task delegated to you by Orchestrator using the available tools.",
|
|
||||||
"Inspect the codebase before making assumptions, make targeted changes when requested, and verify your work when feasible.",
|
|
||||||
"Follow the repository's AGENTS.md conventions: respect the style guide, run `bun typecheck` from the affected package directory after code changes, never run tests from the repo root, and do not modify packages/opencode unless the task explicitly says V1 work.",
|
|
||||||
"If the task is ambiguous or you hit a blocker, stop and report your findings instead of guessing.",
|
|
||||||
"Keep your final response concise: summarize what you did, list important files changed or findings, and call out blockers or verification gaps.",
|
|
||||||
"Do not delegate to other subagents; execute the assigned work yourself.",
|
|
||||||
].join("\n")
|
|
||||||
agent.permissions.push({ action: "subagent", resource: "*", effect: "deny" })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
---
|
|
||||||
name: debug-opencode
|
|
||||||
description: Use when interactively running, debugging, or verifying opencode's own V2 CLI/TUI or server during development in this repo — starting the dev TUI, driving it with termctrl, comparing V2 against the legacy TUI, hitting the V2 server/API directly, reading log files, or attaching Bun's inspector.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Debugging opencode itself
|
|
||||||
|
|
||||||
Workflow for interactively exercising the V2 CLI/TUI and server while developing in this repo. All commands below run from `packages/cli` unless noted otherwise.
|
|
||||||
|
|
||||||
## Migration context
|
|
||||||
|
|
||||||
- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state.
|
|
||||||
- Preserve established TUI behavior unless the task intentionally changes it. When behavior, copy, keyboard interaction, or layout is unclear, compare the local V2 TUI with the latest released legacy TUI (see "Comparing V2 against the legacy TUI" below) rather than guessing.
|
|
||||||
|
|
||||||
## Server/client model
|
|
||||||
|
|
||||||
opencode V2 is a client/server system, not a single monolithic process:
|
|
||||||
|
|
||||||
- **Server process** runs the Effect HTTP API (`packages/server`) and owns all domain state: sessions, database, plugins, permissions, Location services. It's started by the `serve` command (`packages/cli/src/commands/handlers/serve.ts`).
|
|
||||||
- **TUI process** is a separate process that runs no application logic itself — it's an HTTP/SSE client of the server via the generated SDK (`createOpencodeClient` / `sdk.client.v2`).
|
|
||||||
- **Discovery**: CLI processes find the shared server through a JSON registration file at `~/.local/state/opencode/service.json` (or `service-local.json` for the local/dev channel) containing `{id, version, url, pid}`. A separate password file under `~/.config/opencode/service.json` provides HTTP Basic auth. Before reusing a registration, the client calls `GET /health` to confirm the server is alive, authenticated, and version-compatible.
|
|
||||||
- **Sharing**: because of this registration/health-check dance, many concurrent `opencode`/TUI invocations converge on one shared background daemon rather than each spawning their own. If no compatible healthy daemon is found, a new one is spawned detached (`serve --service`) and registers itself.
|
|
||||||
- **`bun dev service start|status|stop|restart`** manages this shared background daemon's lifecycle directly — useful when you need to force a fresh server, confirm one is running, or kill a stuck one.
|
|
||||||
- **Standalone mode** (`--standalone`) opts a single invocation out of the shared daemon: it spawns a private one-off `serve --stdio --port 0` child tied to that invocation's lifetime, with its own random password. Use this to isolate a debugging session from your other running opencode sessions.
|
|
||||||
- Every log line is tagged `role=server` or `role=cli` and a per-process `run=<id>`, so you can distinguish server-side and client-side activity in one shared log file (see "Logs" below) even when both roles are interleaved from concurrent processes.
|
|
||||||
|
|
||||||
## Starting the dev TUI
|
|
||||||
|
|
||||||
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
|
|
||||||
- Run commands from `packages/cli`. Use `bun dev` for most debugging so the TUI starts with a private V2 server.
|
|
||||||
|
|
||||||
## Interactive debugging with termctrl
|
|
||||||
|
|
||||||
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
|
|
||||||
- Use a dedicated session name and do not reuse or kill an unrelated session.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Wait for visible text before interacting instead of relying on fixed sleeps. Use the text expected from the screen under test, such as `Ask anything` or `Connect a provider`.
|
|
||||||
- Drive the running TUI with `termctrl send`. Prefix typed input with `text:` and send control keys separately so the interaction matches real terminal input.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl send opencode-v2-dev 'text:example prompt' enter
|
|
||||||
termctrl send opencode-v2-dev ctrl-c
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `termctrl show` after each meaningful interaction and inspect the full visible screen for rendering errors, stale state, error toasts, and unexpected exits.
|
|
||||||
- Save PNG evidence for every user-visible bug and fix. Do not save text captures; inspect the rendered PNG. Write temporary captures outside the repository unless the artifact is intended to be committed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2-tui.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- For resize-sensitive changes, resize the viewport, wait for the expected content, and capture the screen again:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl resize opencode-v2-dev --cols 100 --rows 30
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
|
|
||||||
- To exercise background-service behavior, use `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
|
|
||||||
- Always clean up the Terminal Control session when the check is complete:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl stop opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Comparing V2 against the legacy TUI
|
|
||||||
|
|
||||||
Run both versions in separate Terminal Control sessions and save PNG-only captures at equivalent states:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From packages/cli: local V2 TUI
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
|
|
||||||
# Released legacy TUI behavior reference
|
|
||||||
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
|
|
||||||
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2.png
|
|
||||||
termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use the same viewport and send equivalent inputs to both sessions before comparing screenshots. The released CLI is a behavioral reference, not a source of V2 API design; keep the local implementation on V2 endpoints.
|
|
||||||
- Stop both sessions after comparison: `termctrl stop opencode-v2-dev` and `termctrl stop opencode-legacy`.
|
|
||||||
|
|
||||||
## Server/API debugging
|
|
||||||
|
|
||||||
- Use `bun dev api --help` from `packages/cli` to inspect the API debugging command. It sends one request to the V2 server using the same daemon discovery/auth path as the CLI.
|
|
||||||
- Use `bun dev api` to introspect the server-side data backing the TUI. This is useful when debugging UI bugs: compare what the screen renders with the raw session, message, event, agent, or health data returned by the API to determine whether the bug is in the server state, the client data layer, or the TUI rendering.
|
|
||||||
- `bun dev api` accepts either an OpenAPI operation ID or a raw HTTP method plus path:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun dev api get /health
|
|
||||||
bun dev api get /openapi.json
|
|
||||||
bun dev api <operationId> --param key=value
|
|
||||||
```
|
|
||||||
|
|
||||||
- Pass JSON request bodies with `--data`/`-d`; the command sets `content-type: application/json` automatically unless you provide a header. Add extra headers with `--header`/`-H name:value`.
|
|
||||||
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
|
||||||
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
- Log files live under `~/.local/share/opencode/log/`. In a local/dev checkout the active file is `opencode-local.log`; `opencode.log` is used for non-local (released) channel installs. Both are append-only, shared across every CLI and server process on the machine.
|
|
||||||
- Each line is structured `key=value` text: `timestamp`, `level`, `run=<id>` (per-process run ID), `message`, and a `role=cli` or `role=server` tag. Use `run=` to isolate one process's activity and `role=` to separate client-side from server-side log lines, since a shared daemon interleaves many processes' output in one file.
|
|
||||||
- Tail the live file while reproducing an issue instead of guessing from stale output:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f ~/.local/share/opencode/log/opencode-local.log
|
|
||||||
```
|
|
||||||
|
|
||||||
- Filter to one run or role when the file is noisy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grep 'run=8fc3b1d5' ~/.local/share/opencode/log/opencode-local.log
|
|
||||||
grep 'role=server' ~/.local/share/opencode/log/opencode-local.log
|
|
||||||
```
|
|
||||||
|
|
||||||
- `OPENCODE_LOG_LEVEL` controls verbosity (default `INFO`); set it before starting `bun dev` or `serve` to get `DEBUG` output for a specific repro.
|
|
||||||
- `OPENCODE_PRINT_LOGS=1` additionally tees log output to stderr of the process that emitted it, which is useful when a process fails before you'd think to check the shared log file.
|
|
||||||
- `termctrl logs <session>` surfaces stdout/stderr for a Terminal Control session specifically (e.g. inspector output or startup failures before the TUI renderer starts) — use the log file above for anything emitted by a separate server/daemon process instead.
|
|
||||||
|
|
||||||
## Debugger
|
|
||||||
|
|
||||||
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
|
|
||||||
bun run --inspect=ws://localhost:6499/ src/index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until the debugger attaches.
|
|
||||||
- Use `termctrl logs opencode-v2-debug` for inspector output or startup failures emitted before the TUI renderer starts. Use `termctrl show` for the visible full-screen TUI.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- Run `bun typecheck` from `packages/cli` after CLI adapter changes.
|
|
||||||
- Run `bun typecheck` and `bun test` from `packages/tui` after shared TUI changes. Do not run tests from the repository root.
|
|
||||||
- Treat automated checks and Terminal Control smoke tests as complementary. For user-visible changes, verify initial render, the changed interaction, Ctrl-C exit behavior, and save a screenshot of the corrected state.
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
name: sample-skill
|
|
||||||
description: Use when the user says sample skill, skill demo, or asks how an opencode SKILL.md should be structured; demonstrates a tiny project-local skill with practical assistant workflow guidance.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Sample Skill
|
|
||||||
|
|
||||||
This is a minimal project-local opencode skill. It exists as a reference for how a skill is structured and as a tiny reusable workflow the assistant can load when the user asks for a skill example.
|
|
||||||
|
|
||||||
## When To Use
|
|
||||||
|
|
||||||
- Use when the user asks for a sample skill or skill template.
|
|
||||||
- Use when demonstrating the required `SKILL.md` frontmatter and body format.
|
|
||||||
- Do not use for unrelated coding tasks just because a skill exists.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
- Confirm the specific outcome the user wants if the request is ambiguous.
|
|
||||||
- Inspect the relevant files before changing anything.
|
|
||||||
- Make the smallest correct change.
|
|
||||||
- Verify the result with a focused read, typecheck, test, or other lightweight check when available.
|
|
||||||
- Summarize the changed files and any required restart or reload step.
|
|
||||||
|
|
||||||
## Example Response Style
|
|
||||||
|
|
||||||
When this skill is relevant, keep responses direct and actionable:
|
|
||||||
|
|
||||||
```text
|
|
||||||
I created a project-local skill at .opencode/skills/sample-skill/SKILL.md.
|
|
||||||
Restart opencode for the new skill to be discovered by future sessions.
|
|
||||||
```
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
- To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
||||||
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
|
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
|
||||||
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
|
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
|
||||||
- Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
|
|
||||||
- The default branch in this repo is `dev`.
|
- The default branch in this repo is `dev`.
|
||||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||||
|
|
||||||
@@ -159,5 +158,4 @@ const table = sqliteTable("session", {
|
|||||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
||||||
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
|
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
|
||||||
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
||||||
- Keep the System Context algebra and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Checkpoint persistence Session-owned. The runner composes all context producers explicitly in `loadSystemContext`; there is no context registry.
|
- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.
|
||||||
- The durable Applied record is what the model was last told, per source. Reconcile narrates drift as chronological System updates and never rewrites the baseline; only completed compaction rebaselines, and move or committed revert resets the checkpoint. Unavailable sources keep the model's prior belief, blocking only a session's first baseline.
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
"@dnd-kit/dom": "0.5.0",
|
"@dnd-kit/dom": "0.5.0",
|
||||||
@@ -92,13 +92,12 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode2": "./bin/opencode2.cjs",
|
"lildax": "./bin/lildax.cjs",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
@@ -107,15 +106,12 @@
|
|||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"jsonc-parser": "3.3.1",
|
|
||||||
"semver": "catalog:",
|
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/semver": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -144,7 +140,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -180,7 +176,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"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",
|
||||||
@@ -207,7 +203,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -229,7 +225,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"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",
|
||||||
@@ -253,7 +249,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -273,7 +269,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -304,7 +300,6 @@
|
|||||||
"@effect/sql-sqlite-bun": "catalog:",
|
"@effect/sql-sqlite-bun": "catalog:",
|
||||||
"@ff-labs/fff-bun": "0.9.4",
|
"@ff-labs/fff-bun": "0.9.4",
|
||||||
"@lydell/node-pty": "catalog:",
|
"@lydell/node-pty": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.29.0",
|
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@npmcli/config": "10.8.1",
|
"@npmcli/config": "10.8.1",
|
||||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||||
@@ -326,7 +321,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.10.0",
|
"gitlab-ai-provider": "6.9.3",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -368,7 +363,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -422,7 +417,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -436,7 +431,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -448,7 +443,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -480,7 +475,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -496,7 +491,7 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.83",
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||||
@@ -527,7 +522,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
@@ -546,7 +541,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -583,7 +578,6 @@
|
|||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -619,7 +613,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.10.0",
|
"gitlab-ai-provider": "6.9.3",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -677,12 +671,9 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
|
||||||
"@opencode-ai/schema": "workspace:*",
|
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -691,7 +682,6 @@
|
|||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "catalog:",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@tsconfig/bun": "catalog:",
|
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
@@ -710,7 +700,6 @@
|
|||||||
},
|
},
|
||||||
"packages/protocol": {
|
"packages/protocol": {
|
||||||
"name": "@opencode-ai/protocol",
|
"name": "@opencode-ai/protocol",
|
||||||
"version": "1.17.11",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -719,12 +708,10 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/schema": {
|
"packages/schema": {
|
||||||
"name": "@opencode-ai/schema",
|
"name": "@opencode-ai/schema",
|
||||||
"version": "1.17.11",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -732,7 +719,6 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/script": {
|
"packages/script": {
|
||||||
@@ -750,7 +736,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -762,7 +747,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -777,7 +762,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
@@ -792,7 +777,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -836,7 +821,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -849,7 +834,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@opencode-ai/stats-core": "workspace:*",
|
"@opencode-ai/stats-core": "workspace:*",
|
||||||
@@ -882,7 +867,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"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",
|
||||||
@@ -901,7 +886,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -942,9 +927,8 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/client": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -952,7 +936,6 @@
|
|||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/keymap": "catalog:",
|
"@opentui/keymap": "catalog:",
|
||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -971,7 +954,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -1022,7 +1005,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -3024,7 +3007,7 @@
|
|||||||
|
|
||||||
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
|
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
@@ -3194,7 +3177,7 @@
|
|||||||
|
|
||||||
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
"body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="],
|
||||||
|
|
||||||
"bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
|
"bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
|
||||||
|
|
||||||
@@ -3362,7 +3345,7 @@
|
|||||||
|
|
||||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
|
|
||||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||||
|
|
||||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
@@ -3372,7 +3355,7 @@
|
|||||||
|
|
||||||
"cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="],
|
"cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="],
|
||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
||||||
|
|
||||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||||
|
|
||||||
@@ -3680,7 +3663,7 @@
|
|||||||
|
|
||||||
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||||
|
|
||||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
"express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
|
||||||
|
|
||||||
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
|
"express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="],
|
||||||
|
|
||||||
@@ -3738,7 +3721,7 @@
|
|||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
||||||
|
|
||||||
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
||||||
|
|
||||||
@@ -3776,7 +3759,7 @@
|
|||||||
|
|
||||||
"framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="],
|
"framer-motion": ["framer-motion@8.5.5", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="],
|
||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||||
|
|
||||||
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||||
|
|
||||||
@@ -3828,7 +3811,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.10.0", "", { "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-oWEZ06rDO6JjB7INHO882wyBAQqCZVHiDHwCs5M+VPmdDj8TzhGXcYesA2CcV5RoI5lfHLKwGp5uKFB62VWpqw=="],
|
"gitlab-ai-provider": ["gitlab-ai-provider@6.9.3", "", { "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-lWo6b6es5+k9iXaDIvE9ECzyK4zfEza4+dQ5FN8SJpEuVRi3ZBCpHIOTa32QoYEDCBaiPh+tcyca86PfNodmlg=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -4340,11 +4323,11 @@
|
|||||||
|
|
||||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||||
|
|
||||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||||
|
|
||||||
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
|
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
|
||||||
|
|
||||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
||||||
|
|
||||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||||
|
|
||||||
@@ -4830,7 +4813,7 @@
|
|||||||
|
|
||||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||||
|
|
||||||
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||||
|
|
||||||
@@ -5016,7 +4999,7 @@
|
|||||||
|
|
||||||
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
|
||||||
|
|
||||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
||||||
|
|
||||||
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
||||||
|
|
||||||
@@ -5026,7 +5009,7 @@
|
|||||||
|
|
||||||
"seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="],
|
"seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="],
|
||||||
|
|
||||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||||
|
|
||||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
@@ -5342,7 +5325,7 @@
|
|||||||
|
|
||||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||||
|
|
||||||
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||||
|
|
||||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||||
|
|
||||||
@@ -5920,10 +5903,14 @@
|
|||||||
|
|
||||||
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
|
"@modelcontextprotocol/sdk/hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
|
"@modelcontextprotocol/sdk/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"@modelcontextprotocol/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|
||||||
"@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
|
"@npmcli/config/ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
|
||||||
@@ -5992,14 +5979,10 @@
|
|||||||
|
|
||||||
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||||
|
|
||||||
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||||
|
|
||||||
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"@opencode-ai/core/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
|
||||||
|
|
||||||
"@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/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
|
||||||
@@ -6010,8 +5993,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
"@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||||
|
|
||||||
"@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
||||||
|
|
||||||
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
|
||||||
@@ -6060,12 +6041,8 @@
|
|||||||
|
|
||||||
"@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
"@shikijs/themes/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
||||||
|
|
||||||
"@slack/bolt/express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
|
|
||||||
|
|
||||||
"@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
"@slack/bolt/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||||
|
|
||||||
"@slack/bolt/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
|
||||||
|
|
||||||
"@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
|
"@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
|
||||||
|
|
||||||
"@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
|
"@slack/socket-mode/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
|
||||||
@@ -6132,6 +6109,10 @@
|
|||||||
|
|
||||||
"@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
"@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
||||||
|
|
||||||
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
|
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
|
||||||
@@ -6186,6 +6167,10 @@
|
|||||||
|
|
||||||
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||||
|
|
||||||
|
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||||
|
|
||||||
"builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"builder-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"builder-util/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"builder-util/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
@@ -6264,10 +6249,16 @@
|
|||||||
|
|
||||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
||||||
|
|
||||||
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
"filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
"filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||||
|
|
||||||
|
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
"fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||||
@@ -6350,8 +6341,6 @@
|
|||||||
|
|
||||||
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||||
|
|
||||||
"opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
|
||||||
|
|
||||||
"opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
"opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||||
|
|
||||||
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
||||||
@@ -6392,6 +6381,8 @@
|
|||||||
|
|
||||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||||
|
|
||||||
"readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
"readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||||
|
|
||||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
@@ -6400,6 +6391,10 @@
|
|||||||
|
|
||||||
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||||
|
|
||||||
|
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||||
|
|
||||||
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
|
||||||
|
|
||||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
@@ -6444,7 +6439,7 @@
|
|||||||
|
|
||||||
"tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
|
"tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
|
||||||
|
|
||||||
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||||
|
|
||||||
@@ -6714,6 +6709,28 @@
|
|||||||
|
|
||||||
"@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
"@malept/flatpak-bundler/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||||
|
|
||||||
"@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
|
"@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="],
|
||||||
|
|
||||||
"@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
"@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
||||||
@@ -6836,34 +6853,6 @@
|
|||||||
|
|
||||||
"@shikijs/stream/@shikijs/core/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
"@shikijs/stream/@shikijs/core/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
||||||
|
|
||||||
"@slack/bolt/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/body-parser": ["body-parser@1.20.5", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
|
||||||
|
|
||||||
"@slack/bolt/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
|
||||||
|
|
||||||
"@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
"@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||||
@@ -6894,6 +6883,8 @@
|
|||||||
|
|
||||||
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||||
|
|
||||||
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
|
||||||
@@ -6958,6 +6949,8 @@
|
|||||||
|
|
||||||
"babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
"babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
|
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||||
@@ -7016,8 +7009,12 @@
|
|||||||
|
|
||||||
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="],
|
||||||
|
|
||||||
|
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
|
"iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
|
||||||
@@ -7054,6 +7051,8 @@
|
|||||||
|
|
||||||
"rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
"rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
|
|
||||||
|
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
"storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||||
|
|
||||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
@@ -7068,6 +7067,8 @@
|
|||||||
|
|
||||||
"tw-to-css/tailwindcss/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
"tw-to-css/tailwindcss/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||||
|
|
||||||
|
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
"unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
"unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||||
@@ -7260,6 +7261,10 @@
|
|||||||
|
|
||||||
"@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
"@jsx-email/cli/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
|
|
||||||
"@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
"@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||||
|
|
||||||
"@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
"@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||||
@@ -7292,20 +7297,6 @@
|
|||||||
|
|
||||||
"@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
"@sentry/bundler-plugin-core/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
||||||
"@slack/bolt/express/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
|
||||||
|
|
||||||
"@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||||
@@ -7434,10 +7425,6 @@
|
|||||||
|
|
||||||
"@sentry/bundler-plugin-core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"@sentry/bundler-plugin-core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"@slack/bolt/express/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
|
||||||
|
|
||||||
"@slack/bolt/express/type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
|
||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
|
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
|
||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
|
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-zjYaBa+TKuHVBO0z0YqOOFLOxpxj2hB6Iq1o+BXBeVc=",
|
"x86_64-linux": "sha256-An7vZmA+sVlxoSOMK4vQvkq3KJOF6Rm9IBU62LN3Hkg=",
|
||||||
"aarch64-linux": "sha256-S2QDDoKcg8QCqjxCnMmLp/3xmiVHAbCJ0CuHQRjDBbE=",
|
"aarch64-linux": "sha256-Hx4trrBLxRDyY4HQc96EQZUa8abIeetNvE8GtRFvrrc=",
|
||||||
"aarch64-darwin": "sha256-GtdfHbk4fhvosgo6iEjXrWSxteK+eNrT0P9swEccbew=",
|
"aarch64-darwin": "sha256-/eXdkybUkDCrfQRCkZHsgCa3BM/OfaQzH/z7OxwlpVM=",
|
||||||
"x86_64-darwin": "sha256-zmWFdPSTS/DpU40CgHA/KKiSIs0PlI092TzLv/cG0jE="
|
"x86_64-darwin": "sha256-flhsuFXikpI8tzK7nTwIVF/jzpmb5p+BWxGa/5vhN54="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -2,12 +2,11 @@
|
|||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.14",
|
"packageManager": "bun@1.3.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/cli --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||||
"dev:web": "bun --cwd packages/app dev",
|
"dev:web": "bun --cwd packages/app dev",
|
||||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export async function installStressSessionTabs(page: Page, input?: { draftID?: s
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.window.browser.dat:tabs",
|
"opencode.global.dat:tabs",
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
...sessionIDs.map((sessionId) => ({
|
...sessionIDs.map((sessionId) => ({
|
||||||
type: "session",
|
type: "session",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn
|
|||||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
|
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.window.browser.dat:tabs",
|
"opencode.global.dat:tabs",
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
{ type: "session", server: "http://127.0.0.1:4096", sessionId: sessionA },
|
{ type: "session", server: "http://127.0.0.1:4096", sessionId: sessionA },
|
||||||
{ type: "session", server: serverB, sessionId: sessionB },
|
{ type: "session", server: serverB, sessionId: sessionB },
|
||||||
@@ -52,7 +52,7 @@ test("legacy session routes preserve an existing tab's server", async ({ page })
|
|||||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
|
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.window.browser.dat:tabs",
|
"opencode.global.dat:tabs",
|
||||||
JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]),
|
JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => {
|
|||||||
await expect(control).toBeVisible()
|
await expect(control).toBeVisible()
|
||||||
|
|
||||||
await control.locator('[data-action="prompt-model-variant"]').click()
|
await control.locator('[data-action="prompt-model-variant"]').click()
|
||||||
const high = page.getByRole("menuitemradio", { name: "high" })
|
const high = page.getByRole("option", { name: "high" })
|
||||||
await expect(high).toBeVisible()
|
await expect(high).toBeVisible()
|
||||||
await page.mouse.move(0, 0)
|
await page.mouse.move(0, 0)
|
||||||
await expect(control).toBeVisible()
|
await expect(control).toBeVisible()
|
||||||
|
|||||||
@@ -38,29 +38,6 @@ test("shows a pending question dock", async ({ page }) => {
|
|||||||
await expect(question.getByRole("radio", { name: /Extended/ })).toBeVisible()
|
await expect(question.getByRole("radio", { name: /Extended/ })).toBeVisible()
|
||||||
await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
|
await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
|
||||||
|
|
||||||
const rejectRequests: string[] = []
|
|
||||||
page.on("request", (request) => {
|
|
||||||
if (request.method() !== "POST") return
|
|
||||||
if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url())
|
|
||||||
})
|
|
||||||
|
|
||||||
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
|
|
||||||
await expect(question).toBeVisible()
|
|
||||||
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
|
|
||||||
await expect(question.getByText("Select one answer")).toBeHidden()
|
|
||||||
await expect(question.getByRole("radio", { name: /Minimal/ })).toBeHidden()
|
|
||||||
await expect(question.getByRole("radio", { name: /Extended/ })).toBeHidden()
|
|
||||||
await expect(question.getByRole("button", { name: "Dismiss" })).toBeVisible()
|
|
||||||
await expect(question.getByRole("button", { name: "Submit" })).toBeVisible()
|
|
||||||
await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0)
|
|
||||||
expect(rejectRequests).toEqual([])
|
|
||||||
|
|
||||||
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
|
|
||||||
await expect(question).toBeVisible()
|
|
||||||
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
|
|
||||||
await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible()
|
|
||||||
expect(rejectRequests).toEqual([])
|
|
||||||
|
|
||||||
await question.getByRole("radio", { name: /Minimal/ }).click()
|
await question.getByRole("radio", { name: /Minimal/ }).click()
|
||||||
const reply = page.waitForRequest(
|
const reply = page.waitForRequest(
|
||||||
(request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply",
|
(request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply",
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ async function configurePage(page: Page) {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.window.browser.dat:tabs",
|
"opencode.global.dat:tabs",
|
||||||
JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))),
|
JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ test.describe("smoke: session timeline", () => {
|
|||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
({ dirBase64, sourceID, targetID }) => {
|
({ dirBase64, sourceID, targetID }) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.window.browser.dat:tabs",
|
"opencode.global.dat:tabs",
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
[sourceID, targetID].map((sessionId) => ({
|
[sourceID, targetID].map((sessionId) => ({
|
||||||
type: "session",
|
type: "session",
|
||||||
@@ -253,7 +253,7 @@ test.describe("smoke: session timeline", () => {
|
|||||||
await page.addInitScript(
|
await page.addInitScript(
|
||||||
({ dirBase64, sourceID, targetID }) => {
|
({ dirBase64, sourceID, targetID }) => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"opencode.window.browser.dat:tabs",
|
"opencode.global.dat:tabs",
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
[sourceID, targetID].map((sessionId) => ({
|
[sourceID, targetID].map((sessionId) => ({
|
||||||
type: "session",
|
type: "session",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+109
-10
@@ -31,19 +31,20 @@ import { Dynamic } from "solid-js/web"
|
|||||||
import { CommandProvider, useCommand, type CommandOption } from "@/context/command"
|
import { CommandProvider, useCommand, type CommandOption } from "@/context/command"
|
||||||
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, useServerSDK } 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"
|
||||||
import { LayoutProvider } from "@/context/layout"
|
import { LayoutProvider } from "@/context/layout"
|
||||||
import { ModelsProvider } from "@/context/models"
|
import { ModelsProvider } from "@/context/models"
|
||||||
import { NotificationProvider } from "@/context/notification"
|
import { NotificationProvider, useNotification } from "@/context/notification"
|
||||||
import { PermissionProvider } from "@/context/permission"
|
import { PermissionProvider } from "@/context/permission"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { PromptProvider } from "@/context/prompt"
|
import { PromptProvider } from "@/context/prompt"
|
||||||
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
import { SettingsProvider, useSettings } from "@/context/settings"
|
import { SettingsProvider, useSettings } from "@/context/settings"
|
||||||
|
import { TerminalProvider } from "@/context/terminal"
|
||||||
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
||||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||||
import { WslServersProvider } from "@/wsl/context"
|
import { WslServersProvider } from "@/wsl/context"
|
||||||
@@ -52,9 +53,16 @@ import LegacyLayout from "@/pages/layout"
|
|||||||
import NewLayout from "@/pages/layout-new"
|
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,
|
||||||
|
selectSessionLineage,
|
||||||
|
sessionHref,
|
||||||
|
} from "./utils/session-route"
|
||||||
|
import { isSessionNotFoundError } from "./utils/server-errors"
|
||||||
|
|
||||||
import { SessionPage, TargetSessionRoute as TargetSessionRouteContent } from "@/pages/session"
|
import Session from "@/pages/session"
|
||||||
import { NewHome, LegacyHome } from "@/pages/home"
|
import { NewHome, LegacyHome } from "@/pages/home"
|
||||||
|
|
||||||
const NewSession = lazy(() => import("@/pages/new-session"))
|
const NewSession = lazy(() => import("@/pages/new-session"))
|
||||||
@@ -88,7 +96,11 @@ const SessionRoute = () => {
|
|||||||
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
|
||||||
})
|
})
|
||||||
|
|
||||||
return <SessionPage />
|
return (
|
||||||
|
<SessionProviders>
|
||||||
|
<Session />
|
||||||
|
</SessionProviders>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TargetSessionRoute = () => {
|
const TargetSessionRoute = () => {
|
||||||
@@ -103,13 +115,76 @@ const TargetSessionRoute = () => {
|
|||||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||||
<ServerSDKProvider server={conn}>
|
<ServerSDKProvider server={conn}>
|
||||||
<ServerSyncProvider server={conn}>
|
<ServerSyncProvider server={conn}>
|
||||||
<TargetSessionRouteContent />
|
<ResolvedTargetSessionRoute />
|
||||||
</ServerSyncProvider>
|
</ServerSyncProvider>
|
||||||
</ServerSDKProvider>
|
</ServerSDKProvider>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResolvedTargetSessionRoute() {
|
||||||
|
const params = useParams<{ serverKey: string; id: string }>()
|
||||||
|
const settings = useSettings()
|
||||||
|
const tabs = useTabs()
|
||||||
|
const sync = useServerSync()
|
||||||
|
const serverKey = createMemo(() => requireServerKey(params.serverKey))
|
||||||
|
const cached = createMemo(() => sync().session.lineage.peek(params.id))
|
||||||
|
const [resolved] = createResource(
|
||||||
|
() => {
|
||||||
|
if (cached()) return
|
||||||
|
return { id: params.id, server: serverKey(), sync: sync() }
|
||||||
|
},
|
||||||
|
({ id, server, sync }) =>
|
||||||
|
sync.session.lineage.resolve(id).catch((error) => {
|
||||||
|
if (isSessionNotFoundError(error, id)) tabs.removeSessionTab({ server, sessionId: id })
|
||||||
|
throw error
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const current = createMemo(() => selectSessionLineage(params.id, cached(), resolved()))
|
||||||
|
const directory = createMemo(() => current()?.session.directory)
|
||||||
|
const targetDirectory = () => directory()!
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const session = current()
|
||||||
|
if (!session) return
|
||||||
|
tabs.addSessionTab({
|
||||||
|
server: serverKey(),
|
||||||
|
sessionId: session.root.id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TargetServerScopedProviders directory={directory} sessionID={() => params.id}>
|
||||||
|
<Show when={!!current() || resolved.state !== "errored"} fallback={<ErrorPage error={resolved.error} />}>
|
||||||
|
<Show when={directory()}>
|
||||||
|
<Show
|
||||||
|
when={settings.general.newLayoutDesigns()}
|
||||||
|
fallback={<Navigate href={legacySessionHref(directory()!, params.id)} />}
|
||||||
|
>
|
||||||
|
<SDKProvider directory={targetDirectory}>
|
||||||
|
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
|
||||||
|
<TargetSessionPage />
|
||||||
|
</DirectoryDataProvider>
|
||||||
|
</SDKProvider>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</TargetServerScopedProviders>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TargetSessionPage() {
|
||||||
|
const sdk = useSDK()
|
||||||
|
const serverSDK = useServerSDK()
|
||||||
|
return (
|
||||||
|
<Show when={`${serverSDK().scope}\0${sdk().directory}`} keyed>
|
||||||
|
<SessionProviders>
|
||||||
|
<Session />
|
||||||
|
</SessionProviders>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 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 (Permission/Layout/
|
// server via ServerKey, then provide the server-scoped shell (Permission/Layout/
|
||||||
// Notification/Models + the visual Layout) for that server.
|
// Notification/Models + the visual Layout) for that server.
|
||||||
@@ -157,7 +232,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
|||||||
<Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
|
<Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
|
||||||
<ServerSDKProvider server={conn}>
|
<ServerSDKProvider server={conn}>
|
||||||
<ServerSyncProvider server={conn}>
|
<ServerSyncProvider server={conn}>
|
||||||
<DraftServerScopedProviders directory={directory}>
|
<TargetServerScopedProviders directory={directory}>
|
||||||
<SDKProvider directory={directory}>
|
<SDKProvider directory={directory}>
|
||||||
<DirectoryDataProvider directory={directory} server={serverKey}>
|
<DirectoryDataProvider directory={directory} server={serverKey}>
|
||||||
<DraftProviders>
|
<DraftProviders>
|
||||||
@@ -165,7 +240,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
|||||||
</DraftProviders>
|
</DraftProviders>
|
||||||
</DirectoryDataProvider>
|
</DirectoryDataProvider>
|
||||||
</SDKProvider>
|
</SDKProvider>
|
||||||
</DraftServerScopedProviders>
|
</TargetServerScopedProviders>
|
||||||
</ServerSyncProvider>
|
</ServerSyncProvider>
|
||||||
</ServerSDKProvider>
|
</ServerSDKProvider>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -290,14 +365,38 @@ function NewAppLayout(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DraftServerScopedProviders(props: ParentProps<{ directory?: () => string | undefined }>) {
|
function TargetServerScopedProviders(props: ServerScopedShellProps) {
|
||||||
return (
|
return (
|
||||||
<PermissionProvider directory={props.directory}>
|
<PermissionProvider directory={props.directory}>
|
||||||
|
<MarkSessionNotificationsViewed sessionID={props.sessionID} />
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) {
|
||||||
|
const notification = useNotification()
|
||||||
|
createEffect(() => {
|
||||||
|
const sessionID = props.sessionID?.()
|
||||||
|
if (!notification.ready() || !sessionID) return
|
||||||
|
if (notification.session.unseenCount(sessionID) === 0) return
|
||||||
|
notification.session.markViewed(sessionID)
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionProviders(props: ParentProps) {
|
||||||
|
return (
|
||||||
|
<TerminalProvider>
|
||||||
|
<FileProvider>
|
||||||
|
<PromptProvider>
|
||||||
|
<CommentsProvider>{props.children}</CommentsProvider>
|
||||||
|
</PromptProvider>
|
||||||
|
</FileProvider>
|
||||||
|
</TerminalProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// The draft page only renders the prompt composer, so it drops TerminalProvider.
|
// The draft page only renders the prompt composer, so it drops TerminalProvider.
|
||||||
// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context.
|
// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context.
|
||||||
function DraftProviders(props: ParentProps) {
|
function DraftProviders(props: ParentProps) {
|
||||||
|
|||||||
@@ -3,26 +3,13 @@ import { List } from "@opencode-ai/ui/list"
|
|||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import type { Component } from "solid-js"
|
||||||
import { Dialog as DialogV2, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2"
|
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
|
||||||
import { Switch as SwitchV2 } from "@opencode-ai/ui/v2/switch-v2"
|
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
|
||||||
import { For, Show, type Component } from "solid-js"
|
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { SettingsListV2 } from "./settings-v2/parts/list"
|
|
||||||
import { SettingsRowV2 } from "./settings-v2/parts/row"
|
|
||||||
import "./settings-v2/settings-v2.css"
|
|
||||||
|
|
||||||
type ModelItem = ReturnType<ReturnType<typeof useLocal>["model"]["list"]>[number]
|
|
||||||
|
|
||||||
export const DialogManageModels: Component = () => {
|
export const DialogManageModels: Component = () => {
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
@@ -115,151 +102,3 @@ export const DialogManageModels: Component = () => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DialogManageModelsV2: Component = () => {
|
|
||||||
const local = useLocal()
|
|
||||||
const language = useLanguage()
|
|
||||||
const dialog = useDialog()
|
|
||||||
const directory = () => decode64(local.slug())
|
|
||||||
|
|
||||||
const handleConnectProvider = () => {
|
|
||||||
dialog.show(() => <DialogSelectProvider directory={directory} />)
|
|
||||||
}
|
|
||||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
|
||||||
const providerVisible = (providerID: string) =>
|
|
||||||
providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id }))
|
|
||||||
const setProviderVisibility = (providerID: string, checked: boolean) => {
|
|
||||||
providerList(providerID).forEach((x) => {
|
|
||||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const setModelVisibility = (item: ModelItem, checked: boolean) => {
|
|
||||||
local.model.setVisibility({ modelID: item.id, providerID: item.provider.id }, checked)
|
|
||||||
}
|
|
||||||
const list = useFilteredList<ModelItem>({
|
|
||||||
items: () => local.model.list(),
|
|
||||||
key: (x) => `${x.provider.id}:${x.id}`,
|
|
||||||
filterKeys: ["provider.name", "name", "id"],
|
|
||||||
sortBy: (a, b) => a.name.localeCompare(b.name),
|
|
||||||
groupBy: (x) => x.provider.id,
|
|
||||||
sortGroupsBy: (a, b) => {
|
|
||||||
const aRank = popularProviders.indexOf(a.category)
|
|
||||||
const bRank = popularProviders.indexOf(b.category)
|
|
||||||
const aPopular = aRank >= 0
|
|
||||||
const bPopular = bRank >= 0
|
|
||||||
if (aPopular && !bPopular) return -1
|
|
||||||
if (!aPopular && bPopular) return 1
|
|
||||||
return aRank - bRank
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogV2 size="large" variant="settings" class="settings-v2-manage-models-dialog">
|
|
||||||
<DialogHeader hideClose={true} closeLabel={language.t("common.close")}>
|
|
||||||
<DialogTitleGroup
|
|
||||||
title={language.t("dialog.model.manage")}
|
|
||||||
description={language.t("dialog.model.manage.description")}
|
|
||||||
/>
|
|
||||||
<ButtonV2 variant="neutral" icon="plus" onClick={handleConnectProvider}>
|
|
||||||
{language.t("command.provider.connect")}
|
|
||||||
</ButtonV2>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogBody class="flex min-h-0 flex-1 flex-col">
|
|
||||||
<div class="px-4 pt-px pb-3">
|
|
||||||
<div class="relative">
|
|
||||||
<TextInputV2
|
|
||||||
type="search"
|
|
||||||
appearance="base"
|
|
||||||
class="!w-full self-stretch"
|
|
||||||
value={list.filter()}
|
|
||||||
onInput={(event) => list.onInput(event.currentTarget.value)}
|
|
||||||
placeholder={language.t("dialog.model.search.placeholder")}
|
|
||||||
spellcheck={false}
|
|
||||||
autocorrect="off"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
autofocus
|
|
||||||
aria-label={language.t("dialog.model.search.placeholder")}
|
|
||||||
/>
|
|
||||||
<Show when={list.filter()}>
|
|
||||||
<IconButtonV2
|
|
||||||
type="button"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
class="settings-v2-tab-search-clear"
|
|
||||||
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
|
||||||
onClick={() => list.clear()}
|
|
||||||
aria-label={language.t("common.clear")}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-slot="manage-models-scroll" class="relative min-h-0 flex-1">
|
|
||||||
<div class="settings-v2-panel settings-v2-models h-full px-4 pt-4 pb-4">
|
|
||||||
<Show
|
|
||||||
when={!list.grouped.loading}
|
|
||||||
fallback={
|
|
||||||
<div class="settings-v2-models-status">
|
|
||||||
{language.t("common.loading")}
|
|
||||||
{language.t("common.loading.ellipsis")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={list.flat().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="settings-v2-models-status">
|
|
||||||
<span>{language.t("dialog.model.empty")}</span>
|
|
||||||
<Show when={list.filter()}>
|
|
||||||
<span class="settings-v2-models-status-filter">"{list.filter()}"</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={list.grouped.latest}>
|
|
||||||
{(group) => (
|
|
||||||
<div class="settings-v2-section" data-component="settings-models-provider">
|
|
||||||
<div class="settings-v2-models-group-header justify-between">
|
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
|
||||||
<ProviderIcon id={group.category} width={16} height={16} class="ml-4 shrink-0" />
|
|
||||||
<h3 class="settings-v2-section-title">{group.items[0].provider.name}</h3>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SwitchV2
|
|
||||||
class="mr-6"
|
|
||||||
checked={providerVisible(group.category)}
|
|
||||||
onChange={(checked) => setProviderVisibility(group.category, checked)}
|
|
||||||
hideLabel
|
|
||||||
>
|
|
||||||
{group.items[0].provider.name}
|
|
||||||
</SwitchV2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SettingsListV2>
|
|
||||||
<For each={group.items}>
|
|
||||||
{(item) => (
|
|
||||||
<SettingsRowV2 title={item.name} description="">
|
|
||||||
<div>
|
|
||||||
<SwitchV2
|
|
||||||
checked={local.model.visible({ modelID: item.id, providerID: item.provider.id })}
|
|
||||||
onChange={(checked) => setModelVisibility(item, checked)}
|
|
||||||
hideLabel
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</SwitchV2>
|
|
||||||
</div>
|
|
||||||
</SettingsRowV2>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</SettingsListV2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogBody>
|
|
||||||
</DialogV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||||
import { Component, ComponentProps, createMemo, For, JSX, Show, ValidComponent } from "solid-js"
|
import { Component, ComponentProps, createMemo, JSX, Show, ValidComponent } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
|
||||||
import { Tag } from "@opencode-ai/ui/tag"
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2"
|
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
||||||
import { ModelTooltip } from "./model-tooltip"
|
import { ModelTooltip } from "./model-tooltip"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
@@ -22,22 +18,6 @@ const isFree = (provider: string, cost: { input: number } | undefined) =>
|
|||||||
provider === "opencode" && (!cost || cost.input === 0)
|
provider === "opencode" && (!cost || cost.input === 0)
|
||||||
|
|
||||||
type ModelState = ReturnType<typeof useLocal>["model"]
|
type ModelState = ReturnType<typeof useLocal>["model"]
|
||||||
type ModelItem = ReturnType<ModelState["list"]>[number]
|
|
||||||
|
|
||||||
const modelKey = (model: ModelItem) => `${model.provider.id}:${model.id}`
|
|
||||||
const manageKey = "action:manage"
|
|
||||||
|
|
||||||
const sortModelGroups = (a: { category: string; items: ModelItem[] }, b: { category: string; items: ModelItem[] }) => {
|
|
||||||
const aIndex = popularProviders.indexOf(a.category)
|
|
||||||
const bIndex = popularProviders.indexOf(b.category)
|
|
||||||
const aPopular = aIndex >= 0
|
|
||||||
const bPopular = bIndex >= 0
|
|
||||||
|
|
||||||
if (aPopular && !bPopular) return -1
|
|
||||||
if (!aPopular && bPopular) return 1
|
|
||||||
if (aPopular && bPopular) return aIndex - bIndex
|
|
||||||
return a.items[0].provider.name.localeCompare(b.items[0].provider.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModelList: Component<{
|
const ModelList: Component<{
|
||||||
provider?: string
|
provider?: string
|
||||||
@@ -220,271 +200,6 @@ export function ModelSelectorPopover(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelSelectorPopoverV2(props: {
|
|
||||||
provider?: string
|
|
||||||
model?: ModelState
|
|
||||||
children?: JSX.Element
|
|
||||||
triggerAs?: ValidComponent
|
|
||||||
triggerProps?: ModelSelectorTriggerProps
|
|
||||||
onClose?: () => void
|
|
||||||
}) {
|
|
||||||
const model = props.model ?? useLocal().model
|
|
||||||
const language = useLanguage()
|
|
||||||
const dialog = useDialog()
|
|
||||||
const [store, setStore] = createStore({ open: false, search: "", active: "" })
|
|
||||||
let searchRef: HTMLInputElement | undefined
|
|
||||||
let contentRef: HTMLDivElement | undefined
|
|
||||||
let restoreTrigger = true
|
|
||||||
|
|
||||||
const allModels = createMemo(() =>
|
|
||||||
model
|
|
||||||
.list()
|
|
||||||
.filter((item) => model.visible({ modelID: item.id, providerID: item.provider.id }))
|
|
||||||
.filter((item) => (props.provider ? item.provider.id === props.provider : true)),
|
|
||||||
)
|
|
||||||
const models = createMemo(() => {
|
|
||||||
const search = store.search.trim().toLowerCase()
|
|
||||||
const filtered = search
|
|
||||||
? allModels().filter(
|
|
||||||
(item) =>
|
|
||||||
item.name.toLowerCase().includes(search) ||
|
|
||||||
item.id.toLowerCase().includes(search) ||
|
|
||||||
item.provider.name.toLowerCase().includes(search),
|
|
||||||
)
|
|
||||||
: allModels()
|
|
||||||
|
|
||||||
return [...filtered].sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
})
|
|
||||||
const groups = createMemo(() => {
|
|
||||||
const byProvider = new Map<string, ModelItem[]>()
|
|
||||||
for (const item of models()) {
|
|
||||||
byProvider.set(item.provider.id, [...(byProvider.get(item.provider.id) ?? []), item])
|
|
||||||
}
|
|
||||||
return Array.from(byProvider, ([category, items]) => ({ category, items })).sort(sortModelGroups)
|
|
||||||
})
|
|
||||||
const keys = () => [...models().map(modelKey), manageKey]
|
|
||||||
const current = () => {
|
|
||||||
const value = model.current()
|
|
||||||
return value ? `${value.provider.id}:${value.id}` : undefined
|
|
||||||
}
|
|
||||||
const initialActive = () => {
|
|
||||||
const selected = current()
|
|
||||||
const options = keys()
|
|
||||||
if (selected && options.includes(selected)) return selected
|
|
||||||
return options[0] ?? ""
|
|
||||||
}
|
|
||||||
const activeItem = () =>
|
|
||||||
store.active ? contentRef?.querySelector<HTMLElement>(`[data-option-key="${CSS.escape(store.active)}"]`) : undefined
|
|
||||||
const afterClose = (callback: () => void) => {
|
|
||||||
const complete = () => {
|
|
||||||
if (contentRef?.isConnected) {
|
|
||||||
requestAnimationFrame(complete)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
requestAnimationFrame(() => requestAnimationFrame(callback))
|
|
||||||
}
|
|
||||||
requestAnimationFrame(complete)
|
|
||||||
}
|
|
||||||
const setOpen = (open: boolean) => {
|
|
||||||
if (open) {
|
|
||||||
restoreTrigger = true
|
|
||||||
setStore({ open: true, active: initialActive() })
|
|
||||||
setTimeout(() =>
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
searchRef?.focus()
|
|
||||||
activeItem()?.scrollIntoView({ block: "nearest" })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setStore({ open: false, search: "", active: "" })
|
|
||||||
}
|
|
||||||
const select = (item: ModelItem) => {
|
|
||||||
model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true })
|
|
||||||
props.onClose?.()
|
|
||||||
}
|
|
||||||
const selectModel = (item: ModelItem) => {
|
|
||||||
restoreTrigger = false
|
|
||||||
setOpen(false)
|
|
||||||
afterClose(() => select(item))
|
|
||||||
}
|
|
||||||
const manage = () => {
|
|
||||||
restoreTrigger = false
|
|
||||||
setOpen(false)
|
|
||||||
afterClose(() => {
|
|
||||||
void import("./dialog-manage-models").then((x) => {
|
|
||||||
dialog.show(() => <x.DialogManageModelsV2 />)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const selectActive = () => {
|
|
||||||
const item = models().find((item) => modelKey(item) === store.active)
|
|
||||||
if (item) {
|
|
||||||
selectModel(item)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (store.active === manageKey) manage()
|
|
||||||
}
|
|
||||||
const moveActive = (delta: number) => {
|
|
||||||
const options = keys()
|
|
||||||
if (options.length === 0) return
|
|
||||||
const index = options.indexOf(store.active)
|
|
||||||
const start = index === -1 ? 0 : index
|
|
||||||
setStore("active", options[(start + delta + options.length) % options.length])
|
|
||||||
queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" }))
|
|
||||||
}
|
|
||||||
const setSearch = (value: string) => {
|
|
||||||
const search = value.trim().toLowerCase()
|
|
||||||
const first = [...allModels()]
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.find(
|
|
||||||
(item) =>
|
|
||||||
!search ||
|
|
||||||
item.name.toLowerCase().includes(search) ||
|
|
||||||
item.id.toLowerCase().includes(search) ||
|
|
||||||
item.provider.name.toLowerCase().includes(search),
|
|
||||||
)
|
|
||||||
setStore({ search: value, active: first ? modelKey(first) : manageKey })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuV2 open={store.open} modal={false} placement="top-start" gutter={6} onOpenChange={setOpen}>
|
|
||||||
<MenuV2.Trigger as={props.triggerAs ?? "div"} {...props.triggerProps}>
|
|
||||||
{props.children}
|
|
||||||
</MenuV2.Trigger>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content
|
|
||||||
ref={(el: HTMLDivElement) => (contentRef = el)}
|
|
||||||
class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none"
|
|
||||||
onPointerDownOutside={() => (restoreTrigger = false)}
|
|
||||||
onFocusOutside={() => (restoreTrigger = false)}
|
|
||||||
onCloseAutoFocus={(event) => {
|
|
||||||
if (!restoreTrigger) event.preventDefault()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col p-0.5">
|
|
||||||
<div class="flex h-7 items-center gap-2 rounded-sm pl-3 pr-2.5 text-v2-icon-icon-muted">
|
|
||||||
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
|
||||||
<input
|
|
||||||
ref={(el) => (searchRef = el)}
|
|
||||||
value={store.search}
|
|
||||||
placeholder={language.t("dialog.model.search.placeholder")}
|
|
||||||
class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint"
|
|
||||||
spellcheck={false}
|
|
||||||
autocorrect="off"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
onInput={(event) => setSearch(event.currentTarget.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Tab") return
|
|
||||||
event.stopPropagation()
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
event.preventDefault()
|
|
||||||
restoreTrigger = false
|
|
||||||
setOpen(false)
|
|
||||||
afterClose(() => props.onClose?.())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.altKey || event.metaKey) return
|
|
||||||
if (event.key === "ArrowDown") {
|
|
||||||
event.preventDefault()
|
|
||||||
moveActive(1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.key === "ArrowUp") {
|
|
||||||
event.preventDefault()
|
|
||||||
moveActive(-1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.key === "Enter" && !event.isComposing) {
|
|
||||||
event.preventDefault()
|
|
||||||
selectActive()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Show when={store.search.trim()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex size-5 items-center justify-center rounded-sm text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
|
|
||||||
onPointerDown={(event) => event.preventDefault()}
|
|
||||||
onClick={() => setSearch("")}
|
|
||||||
aria-label={language.t("common.clear")}
|
|
||||||
>
|
|
||||||
<Icon name="close" size="small" />
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-px bg-v2-border-border-muted" />
|
|
||||||
<ScrollView data-slot="model-selector-scroll" class="max-h-[220px] min-h-0">
|
|
||||||
<div class="flex flex-col p-0.5 pt-0">
|
|
||||||
<Show
|
|
||||||
when={models().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="flex h-12 items-center px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint">
|
|
||||||
{language.t("dialog.model.empty")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={groups()}>
|
|
||||||
{(group) => (
|
|
||||||
<MenuV2.Group>
|
|
||||||
<MenuV2.GroupLabel class="gap-2 px-3">
|
|
||||||
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
|
|
||||||
</MenuV2.GroupLabel>
|
|
||||||
<MenuV2.RadioGroup value={current()}>
|
|
||||||
<For each={group.items}>
|
|
||||||
{(item) => (
|
|
||||||
<MenuV2.RadioItem
|
|
||||||
value={modelKey(item)}
|
|
||||||
data-option-key={modelKey(item)}
|
|
||||||
data-selected-model={current() === modelKey(item) ? true : undefined}
|
|
||||||
class="scroll-my-6"
|
|
||||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setStore("active", modelKey(item))
|
|
||||||
setTimeout(() => searchRef?.focus())
|
|
||||||
}}
|
|
||||||
onSelect={() => selectModel(item)}
|
|
||||||
>
|
|
||||||
<span class="min-w-0 truncate">{item.name}</span>
|
|
||||||
<Show when={isFree(item.provider.id, item.cost)}>
|
|
||||||
<TagV2 class="shrink-0">{language.t("model.tag.free")}</TagV2>
|
|
||||||
</Show>
|
|
||||||
<Show when={item.latest}>
|
|
||||||
<TagV2 class="shrink-0">{language.t("model.tag.latest")}</TagV2>
|
|
||||||
</Show>
|
|
||||||
</MenuV2.RadioItem>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</MenuV2.RadioGroup>
|
|
||||||
</MenuV2.Group>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</ScrollView>
|
|
||||||
<div class="h-px bg-v2-border-border-muted" />
|
|
||||||
<div class="flex flex-col p-0.5">
|
|
||||||
<MenuV2.Item
|
|
||||||
data-option-key={manageKey}
|
|
||||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === manageKey }}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setStore("active", manageKey)
|
|
||||||
setTimeout(() => searchRef?.focus())
|
|
||||||
}}
|
|
||||||
onSelect={manage}
|
|
||||||
>
|
|
||||||
<Icon name="outline-sliders" size="small" />
|
|
||||||
<span class="min-w-0 flex-1 truncate leading-5">{language.t("dialog.model.manage")}</span>
|
|
||||||
</MenuV2.Item>
|
|
||||||
</div>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DialogSelectModel: Component<{ provider?: string; model?: ModelState }> = (props) => {
|
export const DialogSelectModel: Component<{ provider?: string; model?: ModelState }> = (props) => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|||||||
@@ -35,14 +35,12 @@ import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
|
||||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ModelSelectorPopover, ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
|
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
@@ -1482,7 +1480,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
model: props.controls.model.selection,
|
model: props.controls.model.selection,
|
||||||
providerID: props.controls.model.selection.current()?.provider?.id,
|
providerID: props.controls.model.selection.current()?.provider?.id,
|
||||||
modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"),
|
modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"),
|
||||||
newLayoutDesigns: props.controls.newLayoutDesigns,
|
|
||||||
style: control(),
|
style: control(),
|
||||||
onClose: restoreFocus,
|
onClose: restoreFocus,
|
||||||
onUnpaidClick: () => {
|
onUnpaidClick: () => {
|
||||||
@@ -1649,7 +1646,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<Show when={!providersLoading() && store.mode !== "shell" && showVariantControl()}>
|
<Show when={!providersLoading() && store.mode !== "shell" && showVariantControl()}>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-variant-control"
|
data-component="prompt-variant-control"
|
||||||
class="[&_[data-action=prompt-model-variant]]:![font-weight:440]"
|
|
||||||
classList={{
|
classList={{
|
||||||
"animate-in fade-in": providersShouldFadeIn(),
|
"animate-in fade-in": providersShouldFadeIn(),
|
||||||
"hidden group-hover/prompt-input:block group-focus-within/prompt-input:block":
|
"hidden group-hover/prompt-input:block group-focus-within/prompt-input:block":
|
||||||
@@ -1666,45 +1662,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuV2
|
<Select
|
||||||
gutter={6}
|
size="normal"
|
||||||
modal={false}
|
options={variants()}
|
||||||
placement="top-start"
|
current={props.controls.model.selection.variant.current() ?? "default"}
|
||||||
|
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||||
onOpenChange={(open) => setStore("variantOpen", open)}
|
onOpenChange={(open) => setStore("variantOpen", open)}
|
||||||
>
|
onSelect={(value) => {
|
||||||
<MenuV2.Trigger
|
props.controls.model.selection.variant.set(value === "default" ? undefined : value)
|
||||||
as={ButtonV2}
|
restoreFocus()
|
||||||
data-action="prompt-model-variant"
|
}}
|
||||||
variant="ghost-muted"
|
class="capitalize max-w-[160px] justify-start text-v2-text-text-faint"
|
||||||
size="normal"
|
valueClass="truncate text-[13px] font-[440] leading-5 text-v2-text-text-faint"
|
||||||
class="max-w-[160px] justify-start capitalize"
|
triggerStyle={control()}
|
||||||
style={control()}
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
>
|
variant="ghost"
|
||||||
<span class="truncate">
|
/>
|
||||||
{props.controls.model.selection.variant.current() ?? language.t("common.default")}
|
|
||||||
</span>
|
|
||||||
<span class="-ml-0.5 -mr-1 flex shrink-0">
|
|
||||||
<Icon name="chevron-down" size="small" />
|
|
||||||
</span>
|
|
||||||
</MenuV2.Trigger>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content>
|
|
||||||
<MenuV2.RadioGroup
|
|
||||||
value={props.controls.model.selection.variant.current() ?? "default"}
|
|
||||||
onChange={(value) => {
|
|
||||||
props.controls.model.selection.variant.set(value === "default" ? undefined : value)
|
|
||||||
restoreFocus()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{variants().map((value) => (
|
|
||||||
<MenuV2.RadioItem value={value} class="capitalize">
|
|
||||||
{value === "default" ? language.t("common.default") : value}
|
|
||||||
</MenuV2.RadioItem>
|
|
||||||
))}
|
|
||||||
</MenuV2.RadioGroup>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
</TooltipV2>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -2084,7 +2057,6 @@ type ComposerModelControlState = {
|
|||||||
model: ReturnType<typeof useLocal>["model"]
|
model: ReturnType<typeof useLocal>["model"]
|
||||||
providerID?: string
|
providerID?: string
|
||||||
modelName: string
|
modelName: string
|
||||||
newLayoutDesigns: boolean
|
|
||||||
style: JSX.CSSProperties | undefined
|
style: JSX.CSSProperties | undefined
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onUnpaidClick: () => void
|
onUnpaidClick: () => void
|
||||||
@@ -2175,65 +2147,36 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show
|
<ModelSelectorPopover
|
||||||
when={props.state.newLayoutDesigns}
|
model={props.state.model}
|
||||||
fallback={
|
triggerAs={Button}
|
||||||
<ModelSelectorPopover
|
triggerProps={{
|
||||||
model={props.state.model}
|
variant: "ghost",
|
||||||
triggerAs={Button}
|
size: "normal",
|
||||||
triggerProps={{
|
style: props.state.style,
|
||||||
variant: "ghost",
|
class:
|
||||||
size: "normal",
|
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group",
|
||||||
style: props.state.style,
|
classList: { "animate-in fade-in": props.state.shouldAnimate },
|
||||||
class:
|
"data-action": "prompt-model",
|
||||||
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group",
|
}}
|
||||||
classList: { "animate-in fade-in": props.state.shouldAnimate },
|
onClose={props.state.onClose}
|
||||||
"data-action": "prompt-model",
|
|
||||||
}}
|
|
||||||
onClose={props.state.onClose}
|
|
||||||
>
|
|
||||||
<ModelControlContent state={props.state} />
|
|
||||||
</ModelSelectorPopover>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<ModelSelectorPopoverV2
|
<Show when={props.state.providerID}>
|
||||||
model={props.state.model}
|
{(providerID) => (
|
||||||
triggerAs={ButtonV2}
|
<ProviderIcon
|
||||||
triggerProps={{
|
id={providerID()}
|
||||||
variant: "ghost-muted",
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
size: "normal",
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
style: props.state.style,
|
/>
|
||||||
class: "min-w-0 max-w-[220px] justify-start ![font-weight:440] group",
|
)}
|
||||||
classList: { "animate-in fade-in": props.state.shouldAnimate },
|
</Show>
|
||||||
"data-action": "prompt-model",
|
<span class="truncate">{props.state.modelName}</span>
|
||||||
}}
|
<span class="-ml-1 shrink-0 flex size-fit">
|
||||||
onClose={props.state.onClose}
|
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
||||||
>
|
</span>
|
||||||
<ModelControlContent state={props.state} v2 />
|
</ModelSelectorPopover>
|
||||||
</ModelSelectorPopoverV2>
|
|
||||||
</Show>
|
|
||||||
</TooltipV2>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelControlContent(props: { state: ComposerModelControlState; v2?: boolean }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Show when={props.state.providerID}>
|
|
||||||
{(providerID) => (
|
|
||||||
<ProviderIcon
|
|
||||||
id={providerID()}
|
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<span class="truncate">{props.state.modelName}</span>
|
|
||||||
<span class={props.v2 ? "-ml-0.5 -mr-1 flex shrink-0" : "-ml-1 shrink-0 flex size-fit"}>
|
|
||||||
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
openDelay={800}
|
openDelay={2000}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
|
|||||||
@@ -92,17 +92,11 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
</MenuV2.Content>
|
</MenuV2.Content>
|
||||||
</MenuV2.Portal>
|
</MenuV2.Portal>
|
||||||
</MenuV2>
|
</MenuV2>
|
||||||
<Show when={props.branch}>
|
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
||||||
{(branch) => (
|
<div class="flex h-7 min-w-0 max-w-[220px] items-center gap-1.5 px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px]">
|
||||||
<>
|
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
<span class="min-w-0 truncate">{props.branch || "main"}</span>
|
||||||
<div class="flex h-7 min-w-0 max-w-[220px] items-center gap-1.5 px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px]">
|
</div>
|
||||||
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
<span class="min-w-0 truncate">{branch()}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Match, Show, Switch, createMemo, type ComponentProps, type JSX } from "solid-js"
|
import { Match, Show, Switch, createMemo } from "solid-js"
|
||||||
|
import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
|
||||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
|
||||||
|
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
@@ -18,16 +18,7 @@ import { createSessionTabs } from "@/pages/session/helpers"
|
|||||||
interface SessionContextUsageProps {
|
interface SessionContextUsageProps {
|
||||||
variant?: "button" | "indicator"
|
variant?: "button" | "indicator"
|
||||||
buttonAppearance?: "default" | "v2"
|
buttonAppearance?: "default" | "v2"
|
||||||
placement?: ComponentProps<typeof TooltipV2>["placement"]
|
placement?: TooltipProps["placement"]
|
||||||
}
|
|
||||||
|
|
||||||
function ContextTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
|
|
||||||
return (
|
|
||||||
<div class="flex min-w-0 items-center gap-4">
|
|
||||||
<span class="shrink-0 text-v2-text-text-muted">{props.name}</span>
|
|
||||||
<span class="ml-auto min-w-0 truncate text-right text-v2-text-text-base">{props.value}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSessionContext(args: {
|
function openSessionContext(args: {
|
||||||
@@ -122,22 +113,38 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const tooltipValue = () => (
|
const tooltipValue = () => (
|
||||||
<div class="flex w-[120px] flex-col gap-2">
|
<div>
|
||||||
<ContextTooltipRow name={language.t("context.usage.cost")} value={cost()} />
|
<Show when={tokens()}>
|
||||||
<ContextTooltipRow name={language.t("context.usage.usage")} value={`${context()?.usage ?? 0}%`} />
|
{(value) => (
|
||||||
<ContextTooltipRow
|
<div class="flex items-center gap-2">
|
||||||
name={language.t("context.usage.tokens")}
|
<span class="text-text-invert-strong">
|
||||||
value={getSessionTokenTotal(tokens())?.toLocaleString(language.intl()) ?? "0"}
|
{getSessionTokenTotal(value())?.toLocaleString(language.intl())}
|
||||||
/>
|
</span>
|
||||||
|
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={context()}>
|
||||||
|
{(ctx) => (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
|
||||||
|
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-text-invert-strong">{cost()}</span>
|
||||||
|
<span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={params.id}>
|
<Show when={params.id}>
|
||||||
<TooltipV2 value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
|
<Switch>
|
||||||
<Switch>
|
<Match when={variant() === "indicator"}>{circle()}</Match>
|
||||||
<Match when={variant() === "indicator"}>{circle()}</Match>
|
<Match when={buttonAppearance() === "v2"}>
|
||||||
<Match when={buttonAppearance() === "v2"}>
|
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
@@ -146,8 +153,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
onClick={openContext}
|
onClick={openContext}
|
||||||
aria-label={language.t("context.usage.view")}
|
aria-label={language.t("context.usage.view")}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Tooltip>
|
||||||
<Match when={true}>
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -157,9 +166,9 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
|||||||
>
|
>
|
||||||
{circle()}
|
{circle()}
|
||||||
</Button>
|
</Button>
|
||||||
</Match>
|
</Tooltip>
|
||||||
</Switch>
|
</Match>
|
||||||
</TooltipV2>
|
</Switch>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,7 +541,6 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={props.state.reviewVisible}>
|
<Show when={props.state.reviewVisible}>
|
||||||
<TooltipV2
|
<TooltipV2
|
||||||
class="shrink-0"
|
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
value={
|
value={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection, serverName } from "@/context/server"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
import { projectForSession } from "@/pages/layout/helpers"
|
||||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import type { Session } from "@opencode-ai/sdk/v2"
|
import type { Session } from "@opencode-ai/sdk/v2"
|
||||||
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
|
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
|
||||||
import { TabPreviewPopover } from "./titlebar-tab-popover"
|
|
||||||
import "./titlebar-tab-nav.css"
|
import "./titlebar-tab-nav.css"
|
||||||
|
|
||||||
// MouseEvent.button uses 1 for the middle/wheel button.
|
// MouseEvent.button uses 1 for the middle/wheel button.
|
||||||
@@ -22,7 +21,6 @@ export function TabNavItem(props: {
|
|||||||
href: string
|
href: string
|
||||||
server: ServerConnection.Key
|
server: ServerConnection.Key
|
||||||
session: () => Session | undefined
|
session: () => Session | undefined
|
||||||
fallbackTitle?: string
|
|
||||||
onTitleChange?: (title: string) => void
|
onTitleChange?: (title: string) => void
|
||||||
onTitleChangeFailed?: (title: string) => void
|
onTitleChangeFailed?: (title: string) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -58,33 +56,6 @@ export function TabNavItem(props: {
|
|||||||
if (!session) return
|
if (!session) return
|
||||||
return projectForSession(session, serverCtx()?.projects.list() ?? [])
|
return projectForSession(session, serverCtx()?.projects.list() ?? [])
|
||||||
})
|
})
|
||||||
const title = createMemo(() => props.session()?.title ?? props.fallbackTitle)
|
|
||||||
|
|
||||||
const projectName = createMemo(() => {
|
|
||||||
const session = props.session()
|
|
||||||
if (!session) return
|
|
||||||
return displayName(project() ?? { worktree: session.directory })
|
|
||||||
})
|
|
||||||
const previewPath = createMemo(() => {
|
|
||||||
const session = props.session()
|
|
||||||
if (!session) return
|
|
||||||
const home = serverCtx()?.sync.data.path.home
|
|
||||||
return home ? session.directory.replace(home, "~") : session.directory
|
|
||||||
})
|
|
||||||
const branch = createMemo(() => {
|
|
||||||
const session = props.session()
|
|
||||||
if (!session) return
|
|
||||||
return serverCtx()?.sync.child(session.directory, { bootstrap: false })[0].vcs?.branch
|
|
||||||
})
|
|
||||||
// Only label the server when multiple servers are connected.
|
|
||||||
const serverLabel = createMemo(() => {
|
|
||||||
if (global.servers.list().length <= 1) return
|
|
||||||
const conn = global.servers.list().find((item) => ServerConnection.key(item) === props.server)
|
|
||||||
return conn ? serverName(conn) : undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const [popoverOpen, setPopoverOpen] = createSignal(false)
|
|
||||||
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session()
|
|
||||||
|
|
||||||
const measureTitleOverflow = () => {
|
const measureTitleOverflow = () => {
|
||||||
if (!titleEl || editing()) {
|
if (!titleEl || editing()) {
|
||||||
@@ -103,7 +74,7 @@ export function TabNavItem(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
title()
|
props.session()?.title
|
||||||
props.forceTruncate
|
props.forceTruncate
|
||||||
editing()
|
editing()
|
||||||
scheduleTitleOverflow()
|
scheduleTitleOverflow()
|
||||||
@@ -162,9 +133,9 @@ export function TabNavItem(props: {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (editing()) return
|
if (editing()) return
|
||||||
if (!titleEl) return
|
if (!titleEl) return
|
||||||
const value = title()
|
const title = props.session()?.title
|
||||||
if (value === undefined) return
|
if (title === undefined) return
|
||||||
titleEl.textContent = value
|
titleEl.textContent = title
|
||||||
})
|
})
|
||||||
|
|
||||||
const openRename = (event: MouseEvent) => {
|
const openRename = (event: MouseEvent) => {
|
||||||
@@ -200,7 +171,7 @@ export function TabNavItem(props: {
|
|||||||
onCleanup(cleanup)
|
onCleanup(cleanup)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tab = (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
tabRoot = el
|
tabRoot = el
|
||||||
@@ -225,26 +196,26 @@ export function TabNavItem(props: {
|
|||||||
closeTab(event)
|
closeTab(event)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={title() !== undefined}>
|
<Show when={props.session()}>
|
||||||
<a
|
{(session) => {
|
||||||
data-slot="tab-link"
|
return (
|
||||||
data-titlebar-tab-link
|
<a
|
||||||
href={props.href}
|
data-slot="tab-link"
|
||||||
draggable={false}
|
data-titlebar-tab-link
|
||||||
onDragStart={(event) => {
|
href={props.href}
|
||||||
event.preventDefault()
|
draggable={false}
|
||||||
event.stopPropagation()
|
onDragStart={(event) => {
|
||||||
}}
|
event.preventDefault()
|
||||||
onClick={(event) => {
|
event.stopPropagation()
|
||||||
event.preventDefault()
|
}}
|
||||||
if (editing()) return
|
onClick={(event) => {
|
||||||
if (props.suppressNavigation?.()) return
|
event.preventDefault()
|
||||||
props.onNavigate()
|
if (editing()) return
|
||||||
}}
|
if (props.suppressNavigation?.()) return
|
||||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
props.onNavigate()
|
||||||
>
|
}}
|
||||||
<Show when={props.session()}>
|
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
||||||
{(session) => (
|
>
|
||||||
<span data-slot="project-avatar-slot">
|
<span data-slot="project-avatar-slot">
|
||||||
<SessionTabAvatar
|
<SessionTabAvatar
|
||||||
project={project()}
|
project={project()}
|
||||||
@@ -253,45 +224,45 @@ export function TabNavItem(props: {
|
|||||||
activeServer={props.activeServer}
|
activeServer={props.activeServer}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span
|
||||||
</Show>
|
ref={(el) => {
|
||||||
<span
|
titleEl = el
|
||||||
ref={(el) => {
|
titleEl.textContent = session().title
|
||||||
titleEl = el
|
}}
|
||||||
titleEl.textContent = title() ?? ""
|
data-slot="tab-title"
|
||||||
}}
|
data-titlebar-tab-title
|
||||||
data-slot="tab-title"
|
class="min-w-0 flex-1 outline-none leading-4"
|
||||||
data-titlebar-tab-title
|
classList={{
|
||||||
class="min-w-0 flex-1 outline-none leading-4"
|
"overflow-hidden text-clip whitespace-nowrap": !editing(),
|
||||||
classList={{
|
"select-text": editing(),
|
||||||
"overflow-hidden text-clip whitespace-nowrap": !editing(),
|
}}
|
||||||
"select-text": editing(),
|
contenteditable={editing() ? true : undefined}
|
||||||
}}
|
onDblClick={openRename}
|
||||||
contenteditable={editing() ? true : undefined}
|
onKeyDown={(event) => {
|
||||||
onDblClick={openRename}
|
event.stopPropagation()
|
||||||
onKeyDown={(event) => {
|
if (event.key === "Enter") {
|
||||||
event.stopPropagation()
|
event.preventDefault()
|
||||||
if (event.key === "Enter") {
|
void closeRename(true)
|
||||||
event.preventDefault()
|
return
|
||||||
void closeRename(true)
|
}
|
||||||
return
|
if (event.key !== "Escape") return
|
||||||
}
|
event.preventDefault()
|
||||||
if (event.key !== "Escape") return
|
titleEl.textContent = session().title
|
||||||
event.preventDefault()
|
void closeRename(false)
|
||||||
titleEl.textContent = props.session()?.title ?? ""
|
}}
|
||||||
void closeRename(false)
|
onBlur={() => void closeRename(true)}
|
||||||
}}
|
onPointerDown={(event) => {
|
||||||
onBlur={() => void closeRename(true)}
|
if (!editing()) return
|
||||||
onPointerDown={(event) => {
|
event.stopPropagation()
|
||||||
if (!editing()) return
|
}}
|
||||||
event.stopPropagation()
|
onClick={(event) => {
|
||||||
}}
|
if (!editing()) return
|
||||||
onClick={(event) => {
|
event.preventDefault()
|
||||||
if (!editing()) return
|
}}
|
||||||
event.preventDefault()
|
/>
|
||||||
}}
|
</a>
|
||||||
/>
|
)
|
||||||
</a>
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
|
<div data-slot="tab-close" class="group-hover:bg-[var(--tab-bg)] group-data-[active=true]:bg-[var(--tab-bg)]">
|
||||||
@@ -309,24 +280,6 @@ export function TabNavItem(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
|
||||||
<TabPreviewPopover
|
|
||||||
trigger={tab}
|
|
||||||
open={popoverOpen() && !previewBlocked()}
|
|
||||||
onOpenChange={(value) => {
|
|
||||||
if (value && previewBlocked()) return
|
|
||||||
setPopoverOpen(value)
|
|
||||||
}}
|
|
||||||
data={{
|
|
||||||
projectName: projectName(),
|
|
||||||
title: props.session()?.title,
|
|
||||||
path: previewPath(),
|
|
||||||
branch: branch(),
|
|
||||||
serverName: serverLabel(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DraftTabItem(props: {
|
export function DraftTabItem(props: {
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
[data-component="session-tab-popover-trigger"] {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="session-tab-popover"] {
|
|
||||||
z-index: 50;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 256px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
background: var(--v2-background-bg-base);
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: var(--v2-elevation-floating);
|
|
||||||
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
transform-origin: var(--kb-hovercard-content-transform-origin);
|
|
||||||
/* Entrance: fade in while scaling up from 0.96 to 1.0. */
|
|
||||||
animation: sessionTabPopoverIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
|
|
||||||
&:focus-visible,
|
|
||||||
&:focus-within {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Exit: subtle fade + scale down, using the same ease-out curve as entry. */
|
|
||||||
&[data-closed] {
|
|
||||||
animation: sessionTabPopoverOut 100ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="header"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="project"] {
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="title"] {
|
|
||||||
font-weight: 530;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.2;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="row"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="icon"] {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: var(--v2-icon-icon-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="detail"] {
|
|
||||||
flex: 1 0 0;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="server"] {
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sessionTabPopoverIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sessionTabPopoverOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
[data-component="session-tab-popover"],
|
|
||||||
[data-component="session-tab-popover"][data-closed] {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { HoverCard as Kobalte } from "@kobalte/core/hover-card"
|
|
||||||
import { Show, type JSXElement } from "solid-js"
|
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import "./titlebar-tab-popover.css"
|
|
||||||
|
|
||||||
// Initial hover delay before the preview appears, per design.
|
|
||||||
const OPEN_DELAY = 200
|
|
||||||
// Mouse-out delay: begin closing immediately (a brief exit animation plays).
|
|
||||||
const CLOSE_DELAY = 0
|
|
||||||
// After a preview closes, hovering a neighbouring tab within this window skips
|
|
||||||
// the open delay — mirrors the tooltip's skipDelayDuration so moving across
|
|
||||||
// tabs doesn't re-wait the full delay each time.
|
|
||||||
const SKIP_WINDOW = 300
|
|
||||||
let lastClosedAt = 0
|
|
||||||
|
|
||||||
export interface TabPreviewData {
|
|
||||||
projectName?: string
|
|
||||||
title?: string
|
|
||||||
path?: string
|
|
||||||
branch?: string
|
|
||||||
serverName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TabPreviewPopover(props: {
|
|
||||||
trigger: JSXElement
|
|
||||||
open: boolean
|
|
||||||
onOpenChange: (open: boolean) => void
|
|
||||||
data: TabPreviewData
|
|
||||||
}) {
|
|
||||||
let triggerEl: HTMLDivElement | undefined
|
|
||||||
|
|
||||||
// Kobalte reads openDelay lazily when the pointer enters the trigger, so this
|
|
||||||
// resolves the skip window per-hover.
|
|
||||||
const resolveOpenDelay = () => (Date.now() - lastClosedAt < SKIP_WINDOW ? 0 : OPEN_DELAY)
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
|
||||||
if (!open) lastClosedAt = Date.now()
|
|
||||||
props.onOpenChange(open)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Kobalte
|
|
||||||
open={props.open}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
openDelay={resolveOpenDelay()}
|
|
||||||
closeDelay={CLOSE_DELAY}
|
|
||||||
// The preview is non-interactive (pointer-events: none), so there is no
|
|
||||||
// safe area to traverse — leaving the tab hides it immediately.
|
|
||||||
ignoreSafeArea
|
|
||||||
placement="bottom-start"
|
|
||||||
gutter={6}
|
|
||||||
>
|
|
||||||
<Kobalte.Trigger ref={triggerEl} as="div" data-component="session-tab-popover-trigger" tabIndex={-1}>
|
|
||||||
{props.trigger}
|
|
||||||
</Kobalte.Trigger>
|
|
||||||
<Kobalte.Portal>
|
|
||||||
<Kobalte.Content
|
|
||||||
ref={(el) => {
|
|
||||||
// Portalled content lives outside the themed subtree, so mirror the
|
|
||||||
// active theme like the v2 tooltip does.
|
|
||||||
const theme = triggerEl?.closest("[data-theme]")?.getAttribute("data-theme")
|
|
||||||
if (theme) el.setAttribute("data-theme", theme)
|
|
||||||
}}
|
|
||||||
data-component="session-tab-popover"
|
|
||||||
>
|
|
||||||
<div data-slot="header">
|
|
||||||
<Show when={props.data.projectName}>
|
|
||||||
<span data-slot="project">{props.data.projectName}</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={props.data.title}>
|
|
||||||
<span data-slot="title">{props.data.title}</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={props.data.path}>
|
|
||||||
<div data-slot="row">
|
|
||||||
<span data-slot="icon">
|
|
||||||
<IconV2 name="folder" />
|
|
||||||
</span>
|
|
||||||
<span data-slot="detail">{props.data.path}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.data.branch}>
|
|
||||||
<div data-slot="row">
|
|
||||||
<span data-slot="icon">
|
|
||||||
<IconV2 name="branch" />
|
|
||||||
</span>
|
|
||||||
<span data-slot="detail">{props.data.branch}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.data.serverName}>
|
|
||||||
<div data-slot="server">{props.data.serverName}</div>
|
|
||||||
</Show>
|
|
||||||
</Kobalte.Content>
|
|
||||||
</Kobalte.Portal>
|
|
||||||
</Kobalte>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,6 @@ function SessionTabSlot(props: {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const language = useLanguage()
|
|
||||||
const sortable = useSortable({
|
const sortable = useSortable({
|
||||||
get id() {
|
get id() {
|
||||||
return props.id
|
return props.id
|
||||||
@@ -51,7 +50,6 @@ function SessionTabSlot(props: {
|
|||||||
({ id, ctx }) => ctx.sync.session.resolve(id).catch(() => undefined),
|
({ id, ctx }) => ctx.sync.session.resolve(id).catch(() => undefined),
|
||||||
)
|
)
|
||||||
const session = createMemo(() => cachedSession() ?? loadedSession())
|
const session = createMemo(() => cachedSession() ?? loadedSession())
|
||||||
const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session())
|
|
||||||
let prefetched = false
|
let prefetched = false
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -89,7 +87,7 @@ function SessionTabSlot(props: {
|
|||||||
data-tab-key={props.id}
|
data-tab-key={props.id}
|
||||||
data-active={props.active()}
|
data-active={props.active()}
|
||||||
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
||||||
classList={{ hidden: !session() && !missingSession() }}
|
classList={{ hidden: !session() }}
|
||||||
>
|
>
|
||||||
<TabNavItem
|
<TabNavItem
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -98,7 +96,6 @@ function SessionTabSlot(props: {
|
|||||||
href={tabHref(props.tab)}
|
href={tabHref(props.tab)}
|
||||||
server={props.tab.server}
|
server={props.tab.server}
|
||||||
session={session}
|
session={session}
|
||||||
fallbackTitle={missingSession() ? language.t("session.tab.unknown") : undefined}
|
|
||||||
onTitleChange={(title) => {
|
onTitleChange={(title) => {
|
||||||
const value = session()
|
const value = session()
|
||||||
const ctx = props.serverCtx()
|
const ctx = props.serverCtx()
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
placement="bottom"
|
placement="bottom"
|
||||||
title={language.t("command.session.new")}
|
title={language.t("command.session.new")}
|
||||||
keybind={command.keybind("session.new")}
|
keybind={command.keybind("session.new")}
|
||||||
openDelay={800}
|
openDelay={2000}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -602,7 +602,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
>
|
>
|
||||||
<Show when={hasProjects() && nav()}>
|
<Show when={hasProjects() && nav()}>
|
||||||
<div class="flex items-center gap-0 transition-transform">
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={800}>
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
@@ -612,7 +612,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
|
|||||||
aria-label={language.t("common.goBack")}
|
aria-label={language.t("common.goBack")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={800}>
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
|
|||||||
@@ -64,9 +64,6 @@ type PlatformBase = {
|
|||||||
/** Storage mechanism, defaults to localStorage */
|
/** Storage mechanism, defaults to localStorage */
|
||||||
storage?: (name?: string) => SyncStorage | AsyncStorage
|
storage?: (name?: string) => SyncStorage | AsyncStorage
|
||||||
|
|
||||||
/** Stable platform window identity for window-scoped persistence */
|
|
||||||
windowID?: string
|
|
||||||
|
|
||||||
/** Application-global desktop updater */
|
/** Application-global desktop updater */
|
||||||
updater?: UpdaterPlatform
|
updater?: UpdaterPlatform
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
const fallback = server.key
|
const fallback = server.key
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
{
|
{
|
||||||
...Persist.window("tabs"),
|
...Persist.global("tabs"),
|
||||||
migrate: (value: unknown) => {
|
migrate: (value: unknown) => {
|
||||||
if (!Array.isArray(value)) return value
|
if (!Array.isArray(value)) return value
|
||||||
return value.map((tab) => {
|
return value.map((tab) => {
|
||||||
@@ -62,7 +62,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
createStore<Tab[]>([]),
|
createStore<Tab[]>([]),
|
||||||
)
|
)
|
||||||
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
|
const [recent, setRecent, , recentReady] = persisted(Persist.global("tabs.recent"), createStore<RecentTab>({}))
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|||||||
@@ -388,20 +388,6 @@ export const dict = {
|
|||||||
"wsl.onboarding.chooseDistroFirst": "Choose a distro first.",
|
"wsl.onboarding.chooseDistroFirst": "Choose a distro first.",
|
||||||
"wsl.onboarding.loadFailed": "Failed to load WSL state.",
|
"wsl.onboarding.loadFailed": "Failed to load WSL state.",
|
||||||
"wsl.onboarding.loading": "Loading...",
|
"wsl.onboarding.loading": "Loading...",
|
||||||
"wsl.onboarding.installedDistros": "Installed distros",
|
|
||||||
"wsl.onboarding.checkAgain": "Check again",
|
|
||||||
"wsl.onboarding.distroStatus.ready": "Ready",
|
|
||||||
"wsl.onboarding.distroStatus.checking": "Checking...",
|
|
||||||
"wsl.onboarding.distroStatus.opencodeMissing": "OpenCode not installed",
|
|
||||||
"wsl.onboarding.distroStatus.missingTools": "Missing bash, curl",
|
|
||||||
"wsl.onboarding.distroStatus.unsupported": "Unsupported · Use WSL 2",
|
|
||||||
"wsl.onboarding.needAnotherDistro": "Need another distro?",
|
|
||||||
"wsl.onboarding.needAnotherDistroHint": "Install a Linux distribution from the WSL catalog",
|
|
||||||
"wsl.onboarding.wslNotInstalled.title": "WSL not installed",
|
|
||||||
"wsl.onboarding.wslNotInstalled.description":
|
|
||||||
"WSL (Windows Subsystem for Linux) is required before OpenCode can add a WSL server",
|
|
||||||
"wsl.onboarding.wslUnavailable.title": "WSL unavailable",
|
|
||||||
"wsl.onboarding.wslUnavailable.description": "OpenCode could not verify WSL on this machine.",
|
|
||||||
"wsl.onboarding.installWsl": "Install WSL",
|
"wsl.onboarding.installWsl": "Install WSL",
|
||||||
"wsl.onboarding.windowsRestartRequired": "Restart Windows to finish installing WSL, then reopen OpenCode.",
|
"wsl.onboarding.windowsRestartRequired": "Restart Windows to finish installing WSL, then reopen OpenCode.",
|
||||||
"wsl.onboarding.next": "Next",
|
"wsl.onboarding.next": "Next",
|
||||||
@@ -411,7 +397,6 @@ export const dict = {
|
|||||||
"wsl.onboarding.install": "Install",
|
"wsl.onboarding.install": "Install",
|
||||||
"wsl.onboarding.installing": "Installing...",
|
"wsl.onboarding.installing": "Installing...",
|
||||||
"wsl.onboarding.installDistro": "Install distro",
|
"wsl.onboarding.installDistro": "Install distro",
|
||||||
"wsl.onboarding.searchDistros": "Search distros",
|
|
||||||
"wsl.onboarding.wsl2Required": "WSL 2 is required.",
|
"wsl.onboarding.wsl2Required": "WSL 2 is required.",
|
||||||
"wsl.onboarding.toolsRequired": "This distro needs bash and curl.",
|
"wsl.onboarding.toolsRequired": "This distro needs bash and curl.",
|
||||||
"wsl.onboarding.openTerminal": "Open terminal",
|
"wsl.onboarding.openTerminal": "Open terminal",
|
||||||
@@ -619,12 +604,7 @@ export const dict = {
|
|||||||
"session.tab.session": "Session",
|
"session.tab.session": "Session",
|
||||||
"session.tab.review": "Review",
|
"session.tab.review": "Review",
|
||||||
"session.tab.context": "Context",
|
"session.tab.context": "Context",
|
||||||
"session.tab.unknown": "Unknown Session",
|
|
||||||
"session.panel.reviewAndFiles": "Review and files",
|
"session.panel.reviewAndFiles": "Review and files",
|
||||||
"session.error.notFound": "This session cannot be found",
|
|
||||||
"session.error.notFound.description": "This tab points to a session that no longer exists on this server.",
|
|
||||||
"session.error.notFound.closeTab": "Close Tab",
|
|
||||||
"session.error.serverConnection": "Can't connect to this server",
|
|
||||||
"session.review.filesChanged": "{{count}} Files Changed",
|
"session.review.filesChanged": "{{count}} Files Changed",
|
||||||
"session.review.change.one": "Change",
|
"session.review.change.one": "Change",
|
||||||
"session.review.change.other": "Changes",
|
"session.review.change.other": "Changes",
|
||||||
@@ -657,10 +637,6 @@ export const dict = {
|
|||||||
"session.todo.expand": "Expand",
|
"session.todo.expand": "Expand",
|
||||||
"session.todo.progress": "{{done}} of {{total}} todos completed",
|
"session.todo.progress": "{{done}} of {{total}} todos completed",
|
||||||
"session.question.progress": "{{current}} of {{total}} questions",
|
"session.question.progress": "{{current}} of {{total}} questions",
|
||||||
"session.question.minimize": "Minimize question",
|
|
||||||
"session.question.restore": "Restore question",
|
|
||||||
"session.question.pending.one": "{{count}} pending question",
|
|
||||||
"session.question.pending.other": "{{count}} pending questions",
|
|
||||||
"session.followupDock.summary.one": "{{count}} queued message",
|
"session.followupDock.summary.one": "{{count}} queued message",
|
||||||
"session.followupDock.summary.other": "{{count}} queued messages",
|
"session.followupDock.summary.other": "{{count}} queued messages",
|
||||||
"session.followupDock.sendNow": "Send now",
|
"session.followupDock.sendNow": "Send now",
|
||||||
|
|||||||
@@ -194,19 +194,8 @@
|
|||||||
timeline-scope: --home-projects-scroll;
|
timeline-scope: --home-projects-scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="model-selector-scroll"] {
|
|
||||||
timeline-scope: --model-selector-scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="manage-models-scroll"] {
|
|
||||||
timeline-scope: --manage-models-scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"]::before,
|
[data-slot="home-projects-scroll"]::before,
|
||||||
[data-slot="home-projects-scroll"]::after,
|
[data-slot="home-projects-scroll"]::after {
|
||||||
[data-slot="model-selector-scroll"]::before,
|
|
||||||
[data-slot="model-selector-scroll"]::after,
|
|
||||||
[data-slot="manage-models-scroll"]::before {
|
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -222,26 +211,11 @@
|
|||||||
background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent);
|
background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="model-selector-scroll"]::before {
|
|
||||||
top: 0;
|
|
||||||
background: linear-gradient(to bottom, var(--v2-background-bg-layer-01), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="manage-models-scroll"]::before {
|
|
||||||
top: 0;
|
|
||||||
background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="home-projects-scroll"]::after {
|
[data-slot="home-projects-scroll"]::after {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: linear-gradient(to top, var(--v2-background-bg-base), transparent);
|
background: linear-gradient(to top, var(--v2-background-bg-base), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="model-selector-scroll"]::after {
|
|
||||||
bottom: 0;
|
|
||||||
background: linear-gradient(to top, var(--v2-background-bg-layer-01), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (animation-timeline: --home-projects-scroll) and (timeline-scope: --home-projects-scroll) {
|
@supports (animation-timeline: --home-projects-scroll) and (timeline-scope: --home-projects-scroll) {
|
||||||
[data-slot="home-projects-scroll"] .scroll-view__viewport {
|
[data-slot="home-projects-scroll"] .scroll-view__viewport {
|
||||||
scroll-timeline: --home-projects-scroll y;
|
scroll-timeline: --home-projects-scroll y;
|
||||||
@@ -259,34 +233,4 @@
|
|||||||
animation-range: calc(100% - 1.1px) calc(100% - 1px);
|
animation-range: calc(100% - 1.1px) calc(100% - 1px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@supports (animation-timeline: --model-selector-scroll) and (timeline-scope: --model-selector-scroll) {
|
|
||||||
[data-slot="model-selector-scroll"] .scroll-view__viewport {
|
|
||||||
scroll-timeline: --model-selector-scroll y;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="model-selector-scroll"]::before {
|
|
||||||
animation: home-projects-fade-top linear both;
|
|
||||||
animation-timeline: --model-selector-scroll;
|
|
||||||
animation-range: 0 0.1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="model-selector-scroll"]::after {
|
|
||||||
animation: home-projects-fade-bottom linear both;
|
|
||||||
animation-timeline: --model-selector-scroll;
|
|
||||||
animation-range: calc(100% - 1.1px) calc(100% - 1px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (animation-timeline: --manage-models-scroll) and (timeline-scope: --manage-models-scroll) {
|
|
||||||
[data-slot="manage-models-scroll"] .settings-v2-panel {
|
|
||||||
scroll-timeline: --manage-models-scroll y;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="manage-models-scroll"]::before {
|
|
||||||
animation: home-projects-fade-top linear both;
|
|
||||||
animation-timeline: --manage-models-scroll;
|
|
||||||
animation-range: 0 0.1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center font-sans"
|
class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans"
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
|
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
|
||||||
|
|||||||
@@ -671,7 +671,7 @@ function HomeProjectColumn(props: {
|
|||||||
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:mt-14 lg:pt-[52px]"
|
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:mt-14 lg:pt-[52px]"
|
||||||
aria-label={props.language.t("home.projects")}
|
aria-label={props.language.t("home.projects")}
|
||||||
>
|
>
|
||||||
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5">
|
||||||
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
|
||||||
<Show when={global.servers.list().length === 1}>
|
<Show when={global.servers.list().length === 1}>
|
||||||
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
||||||
@@ -697,7 +697,7 @@ function HomeProjectColumn(props: {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="flex min-w-0 flex-col gap-4 pr-3">
|
<div class="flex min-w-0 flex-col gap-1 pr-3">
|
||||||
<For each={global.servers.list()}>
|
<For each={global.servers.list()}>
|
||||||
{(item) => {
|
{(item) => {
|
||||||
const key = ServerConnection.key(item)
|
const key = ServerConnection.key(item)
|
||||||
@@ -1006,8 +1006,8 @@ function HomeSessionLeading(props: {
|
|||||||
<Show when={hasOpenTab()}>
|
<Show when={hasOpenTab()}>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="pointer-events-none absolute top-1/2 h-3 w-0.5 -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
|
class="pointer-events-none absolute top-1/2 h-[7px] w-[3px] -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
|
||||||
style={{ right: "calc(100% + 4px)" }}
|
style={{ right: "calc(100% + 5px)" }}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<SessionTabAvatar
|
<SessionTabAvatar
|
||||||
@@ -1163,7 +1163,13 @@ function HomeSessionSearch(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<label class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02/60 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02">
|
<label
|
||||||
|
class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out"
|
||||||
|
classList={{
|
||||||
|
"focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]": !props.open,
|
||||||
|
"shadow-[0_0_0_0.5px_var(--v2-border-border-focus)]": props.open,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<IconV2 name="magnifying-glass" />
|
<IconV2 name="magnifying-glass" />
|
||||||
<input
|
<input
|
||||||
ref={input}
|
ref={input}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
|
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||||
import {
|
import {
|
||||||
batch,
|
batch,
|
||||||
ErrorBoundary,
|
|
||||||
onCleanup,
|
onCleanup,
|
||||||
Show,
|
Show,
|
||||||
Match,
|
Match,
|
||||||
@@ -11,10 +10,8 @@ import {
|
|||||||
createMemo,
|
createMemo,
|
||||||
createEffect,
|
createEffect,
|
||||||
createComputed,
|
createComputed,
|
||||||
createResource,
|
|
||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
type ParentProps,
|
|
||||||
untrack,
|
untrack,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
@@ -22,37 +19,29 @@ import { createMediaQuery } from "@solid-primitives/media"
|
|||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { debounce } from "@solid-primitives/scheduled"
|
import { debounce } from "@solid-primitives/scheduled"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { FileProvider, selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
|
||||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
|
import { base64Encode, checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { Navigate, useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useSearchParams } from "@solidjs/router"
|
||||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||||
import { ErrorPage } from "@/pages/error"
|
import { useComments } from "@/context/comments"
|
||||||
import { CommentsProvider, useComments } from "@/context/comments"
|
|
||||||
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { ModelsProvider } from "@/context/models"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useNotification } from "@/context/notification"
|
|
||||||
import { PermissionProvider } from "@/context/permission"
|
|
||||||
import { PromptProvider, usePrompt } from "@/context/prompt"
|
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { ServerConnection, serverName, useServer } from "@/context/server"
|
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { TerminalProvider, useTerminal } from "@/context/terminal"
|
|
||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
import { useSettingsCommand } from "@/components/settings-dialog"
|
import { useSettingsCommand } from "@/components/settings-dialog"
|
||||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||||
@@ -84,8 +73,8 @@ import { Identifier } from "@/utils/id"
|
|||||||
import { diffs as list } from "@/utils/diffs"
|
import { diffs as list } from "@/utils/diffs"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import { formatServerError, isSessionNotFoundError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { legacySessionHref, requireServerKey, selectSessionLineage, sessionHref } from "@/utils/session-route"
|
import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
|
||||||
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
||||||
import { createSessionOwnership } from "./session/session-ownership"
|
import { createSessionOwnership } from "./session/session-ownership"
|
||||||
|
|
||||||
@@ -102,15 +91,6 @@ const sessionViewState = () => ({
|
|||||||
changes: "git" as ChangeMode,
|
changes: "git" as ChangeMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
function isLocalSessionNotFoundError(error: unknown, sessionID: string) {
|
|
||||||
return error instanceof Error && error.message === `Session not found: ${sessionID}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCurrentSessionNotFoundError(error: unknown, sessionID: string | undefined) {
|
|
||||||
if (!sessionID) return false
|
|
||||||
return isSessionNotFoundError(error, sessionID) || isLocalSessionNotFoundError(error, sessionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runPromptRollbackMutation<T, R>(input: {
|
async function runPromptRollbackMutation<T, R>(input: {
|
||||||
capturePrompt: () => { current: () => T[]; set: (value: T[]) => void; reset: () => void }
|
capturePrompt: () => { current: () => T[]; set: (value: T[]) => void; reset: () => void }
|
||||||
optimistic: (prompt: { set: (value: T[]) => void; reset: () => void }) => void
|
optimistic: (prompt: { set: (value: T[]) => void; reset: () => void }) => void
|
||||||
@@ -134,204 +114,6 @@ async function runPromptRollbackMutation<T, R>(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionPage() {
|
|
||||||
return (
|
|
||||||
<SessionProviders>
|
|
||||||
<Page />
|
|
||||||
</SessionProviders>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TargetSessionRoute() {
|
|
||||||
const params = useParams<{ serverKey: string; id: string }>()
|
|
||||||
return (
|
|
||||||
<Show when={`${params.serverKey}\0${params.id}`} keyed>
|
|
||||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)} padded>
|
|
||||||
<ResolvedTargetSessionRoute />
|
|
||||||
</SessionRouteErrorBoundary>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionRouteErrorBoundary(
|
|
||||||
props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key; padded?: boolean }>,
|
|
||||||
) {
|
|
||||||
const settings = useSettings()
|
|
||||||
return (
|
|
||||||
<ErrorBoundary
|
|
||||||
fallback={(error) =>
|
|
||||||
settings.general.newLayoutDesigns() ? (
|
|
||||||
<SessionRouteFrame padded={props.padded}>
|
|
||||||
<SessionPanelFrame newLayout raised={!!props.sessionID}>
|
|
||||||
<SessionErrorFallback error={error} sessionID={props.sessionID} serverKey={props.serverKey} />
|
|
||||||
</SessionPanelFrame>
|
|
||||||
</SessionRouteFrame>
|
|
||||||
) : (
|
|
||||||
<ErrorPage error={error} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</ErrorBoundary>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionErrorFallback(props: { error: unknown; sessionID?: string; serverKey?: ServerConnection.Key }) {
|
|
||||||
const language = useLanguage()
|
|
||||||
const server = useServer()
|
|
||||||
const tabs = useTabs()
|
|
||||||
const displayServer = createMemo(() => {
|
|
||||||
const key = props.serverKey ?? server.key
|
|
||||||
const conn = server.list.find((item) => ServerConnection.key(item) === key)
|
|
||||||
return conn ? serverName(conn) : key
|
|
||||||
})
|
|
||||||
const closeTab = () => {
|
|
||||||
if (!props.sessionID) return
|
|
||||||
tabs.removeSessionTab({ server: props.serverKey ?? server.key, sessionId: props.sessionID })
|
|
||||||
}
|
|
||||||
if (isCurrentSessionNotFoundError(props.error, props.sessionID)) {
|
|
||||||
return (
|
|
||||||
<div class="flex-1 min-h-0 overflow-hidden">
|
|
||||||
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-4">
|
|
||||||
<div class="flex flex-col items-center gap-2">
|
|
||||||
<div class="text-16-medium text-text max-w-md">{language.t("session.error.notFound")}</div>
|
|
||||||
<div class="text-13-regular text-text-weak max-w-md">
|
|
||||||
{language.t("session.error.notFound.description")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Show when={props.sessionID}>
|
|
||||||
{(sessionID) => (
|
|
||||||
<div class="max-w-full flex flex-col items-center gap-1">
|
|
||||||
<div class="max-w-full text-11-regular text-text-faint break-all">{displayServer()}</div>
|
|
||||||
<code class="max-w-full rounded-[4px] px-1 py-0.5 font-mono text-xs font-medium leading-4 text-text-base break-all bg-[color-mix(in_oklch,var(--v2-text-text-base)_8%,transparent)]">
|
|
||||||
{sessionID()}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<ButtonV2 variant="neutral" size="normal" icon="xmark-small" onClick={closeTab}>
|
|
||||||
{language.t("session.error.notFound.closeTab")}
|
|
||||||
</ButtonV2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return <ErrorPage error={props.error} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResolvedTargetSessionRoute() {
|
|
||||||
const params = useParams<{ serverKey: string; id: string }>()
|
|
||||||
const settings = useSettings()
|
|
||||||
const tabs = useTabs()
|
|
||||||
const sync = useServerSync()
|
|
||||||
const serverKey = createMemo(() => requireServerKey(params.serverKey))
|
|
||||||
const cached = createMemo(() => sync().session.lineage.peek(params.id))
|
|
||||||
const [resolved] = createResource(
|
|
||||||
() => {
|
|
||||||
if (cached()) return
|
|
||||||
return { id: params.id, sync: sync() }
|
|
||||||
},
|
|
||||||
({ id, sync }) => sync.session.lineage.resolve(id),
|
|
||||||
)
|
|
||||||
const current = createMemo(() => selectSessionLineage(params.id, cached(), resolved()))
|
|
||||||
const directory = createMemo(() => current()?.session.directory)
|
|
||||||
const targetDirectory = () => directory()!
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const session = current()
|
|
||||||
if (!session) return
|
|
||||||
tabs.addSessionTab({
|
|
||||||
server: serverKey(),
|
|
||||||
sessionId: session.root.id,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TargetServerScopedProviders directory={directory} sessionID={() => params.id}>
|
|
||||||
<Show when={directory()}>
|
|
||||||
<Show
|
|
||||||
when={settings.general.newLayoutDesigns()}
|
|
||||||
fallback={<Navigate href={legacySessionHref(directory()!, params.id)} />}
|
|
||||||
>
|
|
||||||
<SDKProvider directory={targetDirectory}>
|
|
||||||
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
|
|
||||||
<TargetSessionPage />
|
|
||||||
</DirectoryDataProvider>
|
|
||||||
</SDKProvider>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</TargetServerScopedProviders>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TargetSessionPage() {
|
|
||||||
const sdk = useSDK()
|
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
return (
|
|
||||||
<Show when={`${serverSDK().scope}\0${sdk().directory}`} keyed>
|
|
||||||
<SessionPage />
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TargetServerScopedProviders(
|
|
||||||
props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<PermissionProvider directory={props.directory}>
|
|
||||||
<MarkSessionNotificationsViewed sessionID={props.sessionID} />
|
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
|
||||||
</PermissionProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) {
|
|
||||||
const notification = useNotification()
|
|
||||||
createEffect(() => {
|
|
||||||
const sessionID = props.sessionID?.()
|
|
||||||
if (!notification.ready() || !sessionID) return
|
|
||||||
if (notification.session.unseenCount(sessionID) === 0) return
|
|
||||||
notification.session.markViewed(sessionID)
|
|
||||||
})
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionProviders(props: ParentProps) {
|
|
||||||
return (
|
|
||||||
<TerminalProvider>
|
|
||||||
<FileProvider>
|
|
||||||
<PromptProvider>
|
|
||||||
<CommentsProvider>{props.children}</CommentsProvider>
|
|
||||||
</PromptProvider>
|
|
||||||
</FileProvider>
|
|
||||||
</TerminalProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionRouteFrame(props: ParentProps<{ padded?: boolean }>) {
|
|
||||||
return (
|
|
||||||
<div class="relative size-full overflow-hidden flex flex-col" classList={{ "p-2": props.padded }}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SessionPanelFrame(props: ParentProps<{ newLayout: boolean; raised?: boolean }>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"flex-1 min-h-0 flex flex-col": true,
|
|
||||||
"bg-v2-background-bg-base": props.newLayout,
|
|
||||||
"bg-background-stronger": !props.newLayout,
|
|
||||||
"rounded-[10px] overflow-hidden": props.newLayout,
|
|
||||||
"shadow-[var(--v2-elevation-raised)]": props.newLayout && props.raised,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -1913,88 +1695,9 @@ export default function Page() {
|
|||||||
() => !isDesktop() && settings.general.newLayoutDesigns() && settings.general.mobileTitlebarPosition() === "bottom",
|
() => !isDesktop() && settings.general.newLayoutDesigns() && settings.general.mobileTitlebarPosition() === "bottom",
|
||||||
)
|
)
|
||||||
|
|
||||||
const sessionErrorFallback = (error: unknown, reset: () => void) => {
|
|
||||||
createEffect(on(sessionKey, reset, { defer: true }))
|
|
||||||
return <SessionErrorFallback error={error} sessionID={params.id} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionPanelContent = () => (
|
|
||||||
<>
|
|
||||||
{sessionSync() ?? ""}
|
|
||||||
<Show when={!isDesktop() && !!params.id && settings.general.newLayoutDesigns() && !mobileTabsBottom()}>
|
|
||||||
{mobileTabs(true)}
|
|
||||||
</Show>
|
|
||||||
<div class="flex-1 min-h-0 overflow-hidden">
|
|
||||||
<Switch>
|
|
||||||
<Match when={params.id && mobileChanges()}>
|
|
||||||
<div class="relative h-full overflow-hidden">
|
|
||||||
{reviewContent({
|
|
||||||
diffStyle: "unified",
|
|
||||||
classes: {
|
|
||||||
root: "pb-8 [&_[data-slot=session-review-list]]:pb-0",
|
|
||||||
header: "px-4 !h-16 !pb-4",
|
|
||||||
container: "px-4",
|
|
||||||
},
|
|
||||||
loadingClass: "px-4 py-4 text-text-weak",
|
|
||||||
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={params.id}>
|
|
||||||
<Show when={messagesReady() ? params.id : undefined} keyed>
|
|
||||||
{(_id) => (
|
|
||||||
<MessageTimeline
|
|
||||||
actions={actions}
|
|
||||||
scroll={ui.scroll}
|
|
||||||
onResumeScroll={resumeScroll}
|
|
||||||
setScrollRef={setScrollRef}
|
|
||||||
onScheduleScrollState={scheduleScrollState}
|
|
||||||
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
|
||||||
onMarkScrollGesture={markScrollGesture}
|
|
||||||
hasScrollGesture={hasScrollGesture}
|
|
||||||
onUserScroll={markUserScroll}
|
|
||||||
onHistoryScroll={onHistoryScroll}
|
|
||||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
|
||||||
shouldAnchorBottom={() =>
|
|
||||||
!location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled()
|
|
||||||
}
|
|
||||||
centered={centered()}
|
|
||||||
setContentRef={(el) => {
|
|
||||||
content = el
|
|
||||||
autoScroll.contentRef(el)
|
|
||||||
|
|
||||||
const root = scroller
|
|
||||||
if (root) scheduleScrollState(root)
|
|
||||||
}}
|
|
||||||
userMessages={visibleUserMessages()}
|
|
||||||
setHistoryAnchor={(handlers) => {
|
|
||||||
captureHistoryAnchor = handlers.capture
|
|
||||||
restoreHistoryAnchor = handlers.restore
|
|
||||||
}}
|
|
||||||
anchor={anchor}
|
|
||||||
setRevealMessage={(fn) => {
|
|
||||||
revealMessage = fn
|
|
||||||
}}
|
|
||||||
setScrollToEnd={(fn) => {
|
|
||||||
scrollToEnd = fn
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<NewSessionView worktree={newSessionWorktree()} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={(params.id || !newSessionDesign()) && !mobileChanges()}>{(_) => composerRegion()}</Show>
|
|
||||||
<Show when={!!params.id && mobileTabsBottom()}>{mobileTabs(true, true)}</Show>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionRouteFrame>
|
<div class="relative size-full overflow-hidden flex flex-col">
|
||||||
|
{sessionSync() ?? ""}
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div
|
<div
|
||||||
class="flex-1 min-h-0 flex flex-col md:flex-row"
|
class="flex-1 min-h-0 flex flex-col md:flex-row"
|
||||||
@@ -2014,13 +1717,85 @@ export default function Page() {
|
|||||||
width: sessionPanelWidth(),
|
width: sessionPanelWidth(),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SessionPanelFrame newLayout={settings.general.newLayoutDesigns()} raised={!!params.id}>
|
<div
|
||||||
{settings.general.newLayoutDesigns() ? (
|
classList={{
|
||||||
<ErrorBoundary fallback={sessionErrorFallback}>{sessionPanelContent()}</ErrorBoundary>
|
"flex-1 min-h-0 flex flex-col": true,
|
||||||
) : (
|
"bg-v2-background-bg-base": settings.general.newLayoutDesigns(),
|
||||||
sessionPanelContent()
|
"bg-background-stronger": !settings.general.newLayoutDesigns(),
|
||||||
)}
|
"rounded-[10px] overflow-hidden": settings.general.newLayoutDesigns(),
|
||||||
</SessionPanelFrame>
|
"shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={!isDesktop() && !!params.id && settings.general.newLayoutDesigns() && !mobileTabsBottom()}>
|
||||||
|
{mobileTabs(true)}
|
||||||
|
</Show>
|
||||||
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
|
<Switch>
|
||||||
|
<Match when={params.id && mobileChanges()}>
|
||||||
|
<div class="relative h-full overflow-hidden">
|
||||||
|
{reviewContent({
|
||||||
|
diffStyle: "unified",
|
||||||
|
classes: {
|
||||||
|
root: "pb-8 [&_[data-slot=session-review-list]]:pb-0",
|
||||||
|
header: "px-4 !h-16 !pb-4",
|
||||||
|
container: "px-4",
|
||||||
|
},
|
||||||
|
loadingClass: "px-4 py-4 text-text-weak",
|
||||||
|
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={params.id}>
|
||||||
|
<Show when={messagesReady() ? params.id : undefined} keyed>
|
||||||
|
{(_id) => (
|
||||||
|
<MessageTimeline
|
||||||
|
actions={actions}
|
||||||
|
scroll={ui.scroll}
|
||||||
|
onResumeScroll={resumeScroll}
|
||||||
|
setScrollRef={setScrollRef}
|
||||||
|
onScheduleScrollState={scheduleScrollState}
|
||||||
|
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
||||||
|
onMarkScrollGesture={markScrollGesture}
|
||||||
|
hasScrollGesture={hasScrollGesture}
|
||||||
|
onUserScroll={markUserScroll}
|
||||||
|
onHistoryScroll={onHistoryScroll}
|
||||||
|
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||||
|
shouldAnchorBottom={() =>
|
||||||
|
!location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled()
|
||||||
|
}
|
||||||
|
centered={centered()}
|
||||||
|
setContentRef={(el) => {
|
||||||
|
content = el
|
||||||
|
autoScroll.contentRef(el)
|
||||||
|
|
||||||
|
const root = scroller
|
||||||
|
if (root) scheduleScrollState(root)
|
||||||
|
}}
|
||||||
|
userMessages={visibleUserMessages()}
|
||||||
|
setHistoryAnchor={(handlers) => {
|
||||||
|
captureHistoryAnchor = handlers.capture
|
||||||
|
restoreHistoryAnchor = handlers.restore
|
||||||
|
}}
|
||||||
|
anchor={anchor}
|
||||||
|
setRevealMessage={(fn) => {
|
||||||
|
revealMessage = fn
|
||||||
|
}}
|
||||||
|
setScrollToEnd={(fn) => {
|
||||||
|
scrollToEnd = fn
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<NewSessionView worktree={newSessionWorktree()} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={(params.id || !newSessionDesign()) && !mobileChanges()}>{(_) => composerRegion()}</Show>
|
||||||
|
<Show when={!!params.id && mobileTabsBottom()}>{mobileTabs(true, true)}</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={desktopReviewOpen()}>
|
<Show when={desktopReviewOpen()}>
|
||||||
<div onPointerDown={() => size.start()}>
|
<div onPointerDown={() => size.start()}>
|
||||||
@@ -2057,6 +1832,6 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TerminalPanel />
|
<TerminalPanel />
|
||||||
</SessionRouteFrame>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { For, Show, createEffect, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { useMutation } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -78,12 +77,9 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
customOn: cached?.customOn ?? ([] as boolean[]),
|
customOn: cached?.customOn ?? ([] as boolean[]),
|
||||||
editing: false,
|
editing: false,
|
||||||
focus: 0,
|
focus: 0,
|
||||||
minimized: false,
|
|
||||||
optionsHeight: 180,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let root: HTMLDivElement | undefined
|
let root: HTMLDivElement | undefined
|
||||||
let optionsRef: HTMLDivElement | undefined
|
|
||||||
let customRef: HTMLButtonElement | undefined
|
let customRef: HTMLButtonElement | undefined
|
||||||
let optsRef: HTMLButtonElement[] = []
|
let optsRef: HTMLButtonElement[] = []
|
||||||
let replied = false
|
let replied = false
|
||||||
@@ -100,13 +96,11 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
const n = Math.min(store.tab + 1, total())
|
const n = Math.min(store.tab + 1, total())
|
||||||
return language.t("session.question.progress", { current: n, total: total() })
|
return language.t("session.question.progress", { current: n, total: total() })
|
||||||
})
|
})
|
||||||
|
|
||||||
const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer")
|
const customLabel = () => language.t("ui.messagePart.option.typeOwnAnswer")
|
||||||
const customPlaceholder = () => language.t("ui.question.custom.placeholder")
|
const customPlaceholder = () => language.t("ui.question.custom.placeholder")
|
||||||
|
|
||||||
const last = createMemo(() => store.tab >= total() - 1)
|
const last = createMemo(() => store.tab >= total() - 1)
|
||||||
const collapse = useSpring(() => (store.minimized ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
|
|
||||||
const hidden = createMemo(() => Math.max(0, Math.min(1, collapse())))
|
|
||||||
const optionsOff = createMemo(() => hidden() > 0.98)
|
|
||||||
|
|
||||||
const customUpdate = (value: string, selected: boolean = on()) => {
|
const customUpdate = (value: string, selected: boolean = on()) => {
|
||||||
const prev = input().trim()
|
const prev = input().trim()
|
||||||
@@ -198,14 +192,6 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
focus(pickFocus())
|
focus(pickFocus())
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const el = optionsRef
|
|
||||||
if (!el) return
|
|
||||||
const update = () => setStore("optionsHeight", (height) => Math.max(height, el.scrollHeight))
|
|
||||||
update()
|
|
||||||
createResizeObserver(el, update)
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (focusFrame !== undefined) cancelAnimationFrame(focusFrame)
|
if (focusFrame !== undefined) cancelAnimationFrame(focusFrame)
|
||||||
if (replied) return
|
if (replied) return
|
||||||
@@ -419,7 +405,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
const tab = store.tab + 1
|
const tab = store.tab + 1
|
||||||
setStore("tab", tab)
|
setStore("tab", tab)
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
if (!store.minimized) focus(pickFocus(tab))
|
focus(pickFocus(tab))
|
||||||
}
|
}
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
@@ -428,212 +414,163 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
const tab = store.tab - 1
|
const tab = store.tab - 1
|
||||||
setStore("tab", tab)
|
setStore("tab", tab)
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
if (!store.minimized) focus(pickFocus(tab))
|
focus(pickFocus(tab))
|
||||||
}
|
}
|
||||||
|
|
||||||
const jump = (tab: number) => {
|
const jump = (tab: number) => {
|
||||||
if (sending()) return
|
if (sending()) return
|
||||||
setStore("tab", tab)
|
setStore("tab", tab)
|
||||||
setStore("editing", false)
|
setStore("editing", false)
|
||||||
if (!store.minimized) focus(pickFocus(tab))
|
focus(pickFocus(tab))
|
||||||
}
|
|
||||||
|
|
||||||
const minimize = () => {
|
|
||||||
if (sending()) return
|
|
||||||
setStore("editing", false)
|
|
||||||
setStore("minimized", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const restore = () => {
|
|
||||||
if (sending()) return
|
|
||||||
setStore("minimized", false)
|
|
||||||
focus(pickFocus())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="session-question-dock">
|
<DockPrompt
|
||||||
<DockPrompt
|
kind="question"
|
||||||
kind="question"
|
ref={(el) => (root = el)}
|
||||||
ref={(el) => (root = el)}
|
onKeyDown={nav}
|
||||||
onKeyDown={nav}
|
header={
|
||||||
header={
|
<>
|
||||||
<>
|
<div data-slot="question-header-title">{summary()}</div>
|
||||||
<div data-slot="question-header-title">{summary()}</div>
|
<Show when={total() > 1}>
|
||||||
<div data-slot="question-header-actions">
|
<div data-slot="question-progress">
|
||||||
<Show when={total() > 1}>
|
<For each={questions()}>
|
||||||
<div data-slot="question-progress">
|
{(_, i) => (
|
||||||
<For each={questions()}>
|
<button
|
||||||
{(_, i) => (
|
type="button"
|
||||||
<button
|
data-slot="question-progress-segment"
|
||||||
type="button"
|
data-active={i() === store.tab}
|
||||||
data-slot="question-progress-segment"
|
data-answered={answered(i())}
|
||||||
data-active={i() === store.tab}
|
disabled={sending()}
|
||||||
data-answered={answered(i())}
|
onClick={() => jump(i())}
|
||||||
disabled={sending()}
|
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
||||||
onClick={() => jump(i())}
|
/>
|
||||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
)}
|
||||||
/>
|
</For>
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-component="icon-button"
|
|
||||||
data-icon="chevron-down"
|
|
||||||
data-size="normal"
|
|
||||||
data-variant="ghost"
|
|
||||||
disabled={sending()}
|
|
||||||
style={{ transform: `rotate(${hidden() * 180}deg)` }}
|
|
||||||
onClick={store.minimized ? restore : minimize}
|
|
||||||
aria-label={language.t(store.minimized ? "session.question.restore" : "session.question.minimize")}
|
|
||||||
>
|
|
||||||
<Icon name="chevron-down" size="small" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
}
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
<Button variant="ghost" size="large" disabled={sending()} onClick={reject} aria-keyshortcuts="Escape">
|
|
||||||
{language.t("ui.common.dismiss")}
|
|
||||||
</Button>
|
|
||||||
<div data-slot="question-footer-actions">
|
|
||||||
<Show when={store.tab > 0}>
|
|
||||||
<Button variant="secondary" size="large" disabled={sending()} onClick={back}>
|
|
||||||
{language.t("ui.common.back")}
|
|
||||||
</Button>
|
|
||||||
</Show>
|
|
||||||
<Button
|
|
||||||
variant={last() ? "primary" : "secondary"}
|
|
||||||
size="large"
|
|
||||||
disabled={sending()}
|
|
||||||
onClick={next}
|
|
||||||
aria-keyshortcuts="Meta+Enter Control+Enter"
|
|
||||||
>
|
|
||||||
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-slot="question-text"
|
|
||||||
style={{
|
|
||||||
display: store.minimized ? "-webkit-box" : undefined,
|
|
||||||
"-webkit-line-clamp": store.minimized ? "3" : undefined,
|
|
||||||
"-webkit-box-orient": store.minimized ? "vertical" : undefined,
|
|
||||||
overflow: store.minimized ? "hidden" : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{question()?.question}
|
|
||||||
</div>
|
|
||||||
<Show when={!store.minimized}>
|
|
||||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
|
||||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</>
|
||||||
<div
|
}
|
||||||
ref={(el) => (optionsRef = el)}
|
footer={
|
||||||
data-slot="question-options"
|
<>
|
||||||
aria-hidden={store.minimized || optionsOff() ? "true" : undefined}
|
<Button variant="ghost" size="large" disabled={sending()} onClick={reject} aria-keyshortcuts="Escape">
|
||||||
classList={{ "pointer-events-none": hidden() > 0.1 }}
|
{language.t("ui.common.dismiss")}
|
||||||
style={{
|
</Button>
|
||||||
"max-height": `${Math.max(0, store.optionsHeight * (1 - hidden()))}px`,
|
<div data-slot="question-footer-actions">
|
||||||
opacity: `${Math.max(0, Math.min(1, 1 - hidden()))}`,
|
<Show when={store.tab > 0}>
|
||||||
visibility: optionsOff() ? "hidden" : "visible",
|
<Button variant="secondary" size="large" disabled={sending()} onClick={back}>
|
||||||
}}
|
{language.t("ui.common.back")}
|
||||||
>
|
</Button>
|
||||||
<For each={options()}>
|
</Show>
|
||||||
{(opt, i) => (
|
<Button
|
||||||
<Option
|
variant={last() ? "primary" : "secondary"}
|
||||||
multi={multi()}
|
size="large"
|
||||||
picked={picked(opt.label)}
|
disabled={sending()}
|
||||||
label={opt.label}
|
onClick={next}
|
||||||
description={opt.description}
|
aria-keyshortcuts="Meta+Enter Control+Enter"
|
||||||
disabled={sending()}
|
>
|
||||||
ref={(el) => (optsRef[i()] = el)}
|
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
||||||
onFocus={() => setStore("focus", i())}
|
</Button>
|
||||||
onClick={() => selectOption(i())}
|
</div>
|
||||||
/>
|
</>
|
||||||
)}
|
}
|
||||||
</For>
|
>
|
||||||
|
<div data-slot="question-text" class="overflow-auto">
|
||||||
|
{question()?.question}
|
||||||
|
</div>
|
||||||
|
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||||
|
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||||
|
</Show>
|
||||||
|
<div data-slot="question-options">
|
||||||
|
<For each={options()}>
|
||||||
|
{(opt, i) => (
|
||||||
|
<Option
|
||||||
|
multi={multi()}
|
||||||
|
picked={picked(opt.label)}
|
||||||
|
label={opt.label}
|
||||||
|
description={opt.description}
|
||||||
|
disabled={sending()}
|
||||||
|
ref={(el) => (optsRef[i()] = el)}
|
||||||
|
onFocus={() => setStore("focus", i())}
|
||||||
|
onClick={() => selectOption(i())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={store.editing}
|
when={store.editing}
|
||||||
fallback={
|
fallback={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
ref={customRef}
|
ref={customRef}
|
||||||
data-slot="question-option"
|
|
||||||
data-custom="true"
|
|
||||||
data-picked={on()}
|
|
||||||
role={multi() ? "checkbox" : "radio"}
|
|
||||||
aria-checked={on()}
|
|
||||||
disabled={sending()}
|
|
||||||
onFocus={() => setStore("focus", options().length)}
|
|
||||||
onClick={customOpen}
|
|
||||||
>
|
|
||||||
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
|
||||||
<span data-slot="question-option-main">
|
|
||||||
<span data-slot="option-label">{customLabel()}</span>
|
|
||||||
<span data-slot="option-description">{input() || customPlaceholder()}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
data-slot="question-option"
|
data-slot="question-option"
|
||||||
data-custom="true"
|
data-custom="true"
|
||||||
data-picked={on()}
|
data-picked={on()}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={on()}
|
aria-checked={on()}
|
||||||
onMouseDown={(e) => {
|
disabled={sending()}
|
||||||
if (sending()) {
|
onFocus={() => setStore("focus", options().length)}
|
||||||
e.preventDefault()
|
onClick={customOpen}
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.target instanceof HTMLTextAreaElement) return
|
|
||||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
|
||||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
|
||||||
}}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
commitCustom()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
||||||
<span data-slot="question-option-main">
|
<span data-slot="question-option-main">
|
||||||
<span data-slot="option-label">{customLabel()}</span>
|
<span data-slot="option-label">{customLabel()}</span>
|
||||||
<textarea
|
<span data-slot="option-description">{input() || customPlaceholder()}</span>
|
||||||
ref={focusCustom}
|
|
||||||
data-slot="question-custom-input"
|
|
||||||
placeholder={customPlaceholder()}
|
|
||||||
value={input()}
|
|
||||||
rows={1}
|
|
||||||
disabled={sending()}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault()
|
|
||||||
setStore("editing", false)
|
|
||||||
focus(options().length)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ((e.metaKey || e.ctrlKey) && !e.altKey) return
|
|
||||||
if (e.key !== "Enter" || e.shiftKey) return
|
|
||||||
e.preventDefault()
|
|
||||||
commitCustom()
|
|
||||||
}}
|
|
||||||
onInput={(e) => {
|
|
||||||
customUpdate(e.currentTarget.value)
|
|
||||||
resizeInput(e.currentTarget)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</button>
|
||||||
</Show>
|
}
|
||||||
</div>
|
>
|
||||||
</DockPrompt>
|
<form
|
||||||
</div>
|
data-slot="question-option"
|
||||||
|
data-custom="true"
|
||||||
|
data-picked={on()}
|
||||||
|
role={multi() ? "checkbox" : "radio"}
|
||||||
|
aria-checked={on()}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (sending()) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.target instanceof HTMLTextAreaElement) return
|
||||||
|
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||||
|
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||||
|
}}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
|
||||||
|
<span data-slot="question-option-main">
|
||||||
|
<span data-slot="option-label">{customLabel()}</span>
|
||||||
|
<textarea
|
||||||
|
ref={focusCustom}
|
||||||
|
data-slot="question-custom-input"
|
||||||
|
placeholder={customPlaceholder()}
|
||||||
|
value={input()}
|
||||||
|
rows={1}
|
||||||
|
disabled={sending()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
setStore("editing", false)
|
||||||
|
focus(options().length)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ((e.metaKey || e.ctrlKey) && !e.altKey) return
|
||||||
|
if (e.key !== "Enter" || e.shiftKey) return
|
||||||
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
customUpdate(e.currentTarget.value)
|
||||||
|
resizeInput(e.currentTarget)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</DockPrompt>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1375,14 +1375,14 @@ export function MessageTimeline(props: {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-slot="session-title-parent"
|
data-slot="session-title-parent"
|
||||||
class="min-w-0 max-w-[40%] truncate px-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
|
class="min-w-0 max-w-[40%] truncate text-14-medium text-text-weak transition-colors hover:text-text-base"
|
||||||
onClick={navigateParent}
|
onClick={navigateParent}
|
||||||
>
|
>
|
||||||
{parentTitle()}
|
{parentTitle()}
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
data-slot="session-title-separator"
|
data-slot="session-title-separator"
|
||||||
class="-translate-y-[0.5px] px-1 text-[11px] font-medium text-v2-text-text-faint"
|
class="px-2 text-14-medium text-text-weak"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
/
|
/
|
||||||
@@ -1395,7 +1395,7 @@ export function MessageTimeline(props: {
|
|||||||
<h1
|
<h1
|
||||||
data-slot="session-title-child"
|
data-slot="session-title-child"
|
||||||
classList={{
|
classList={{
|
||||||
"truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
|
"text-14-medium text-text-strong truncate": true,
|
||||||
"w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
|
"w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
|
||||||
settings.general.newLayoutDesigns(),
|
settings.general.newLayoutDesigns(),
|
||||||
"grow-1 min-w-0": !settings.general.newLayoutDesigns(),
|
"grow-1 min-w-0": !settings.general.newLayoutDesigns(),
|
||||||
@@ -1414,7 +1414,7 @@ export function MessageTimeline(props: {
|
|||||||
value={title.draft}
|
value={title.draft}
|
||||||
disabled={titleMutation.isPending}
|
disabled={titleMutation.isPending}
|
||||||
classList={{
|
classList={{
|
||||||
"block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
|
"text-14-medium text-text-strong block": true,
|
||||||
"w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
|
"w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
|
||||||
"field-sizing-content self-start rounded-[6px] px-2 py-1 ":
|
"field-sizing-content self-start rounded-[6px] px-2 py-1 ":
|
||||||
settings.general.newLayoutDesigns(),
|
settings.general.newLayoutDesigns(),
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type PersistedWithReady<T> = [
|
|||||||
|
|
||||||
type PersistTarget = {
|
type PersistTarget = {
|
||||||
storage?: string
|
storage?: string
|
||||||
scope?: "window"
|
|
||||||
legacyStorageNames?: string[]
|
legacyStorageNames?: string[]
|
||||||
key: string
|
key: string
|
||||||
legacy?: string[]
|
legacy?: string[]
|
||||||
@@ -25,7 +24,6 @@ type PersistTarget = {
|
|||||||
|
|
||||||
const LEGACY_STORAGE = "default.dat"
|
const LEGACY_STORAGE = "default.dat"
|
||||||
const GLOBAL_STORAGE = "opencode.global.dat"
|
const GLOBAL_STORAGE = "opencode.global.dat"
|
||||||
const WINDOW_STORAGE = "opencode.window"
|
|
||||||
const LOCAL_PREFIX = "opencode."
|
const LOCAL_PREFIX = "opencode."
|
||||||
const fallback = new Map<string, boolean>()
|
const fallback = new Map<string, boolean>()
|
||||||
|
|
||||||
@@ -349,11 +347,6 @@ function draftStorage(draftID: string) {
|
|||||||
return `opencode.draft.${head}.${sum}.dat`
|
return `opencode.draft.${head}.${sum}.dat`
|
||||||
}
|
}
|
||||||
|
|
||||||
function windowStorage(windowID: string) {
|
|
||||||
const safe = (windowID || "browser").replace(/[^a-zA-Z0-9._-]/g, "-")
|
|
||||||
return `${WINDOW_STORAGE}.${safe}.dat`
|
|
||||||
}
|
|
||||||
|
|
||||||
function legacyWorkspaceStorage(dir: string) {
|
function legacyWorkspaceStorage(dir: string) {
|
||||||
const storage = workspaceStorage(pathKey(dir))
|
const storage = workspaceStorage(pathKey(dir))
|
||||||
const result = new Set<string>()
|
const result = new Set<string>()
|
||||||
@@ -474,8 +467,6 @@ export const PersistTesting = {
|
|||||||
localStorageWithPrefix,
|
localStorageWithPrefix,
|
||||||
migrateLegacy,
|
migrateLegacy,
|
||||||
normalize,
|
normalize,
|
||||||
resolveTarget,
|
|
||||||
windowStorage,
|
|
||||||
workspaceStorage,
|
workspaceStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,9 +474,6 @@ export const Persist = {
|
|||||||
global(key: string, legacy?: string[]): PersistTarget {
|
global(key: string, legacy?: string[]): PersistTarget {
|
||||||
return { storage: GLOBAL_STORAGE, key, legacy }
|
return { storage: GLOBAL_STORAGE, key, legacy }
|
||||||
},
|
},
|
||||||
window(key: string, legacy?: string[]): PersistTarget {
|
|
||||||
return { scope: "window", key, legacy }
|
|
||||||
},
|
|
||||||
draft(draftID: string, key: string, legacy?: string[]): PersistTarget {
|
draft(draftID: string, key: string, legacy?: string[]): PersistTarget {
|
||||||
return { storage: draftStorage(draftID), key: `draft:${key}`, legacy }
|
return { storage: draftStorage(draftID), key: `draft:${key}`, legacy }
|
||||||
},
|
},
|
||||||
@@ -515,16 +503,6 @@ export const Persist = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget {
|
|
||||||
if (target.scope !== "window") return target
|
|
||||||
if (platform.platform === "desktop" && !platform.windowID) return { ...target, storage: GLOBAL_STORAGE }
|
|
||||||
const windowID = platform.platform === "desktop" ? (platform.windowID ?? "browser") : "browser"
|
|
||||||
return {
|
|
||||||
...target,
|
|
||||||
storage: windowStorage(windowID),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removePersisted(
|
export function removePersisted(
|
||||||
target: { storage?: string; legacyStorageNames?: string[]; key: string },
|
target: { storage?: string; legacyStorageNames?: string[]; key: string },
|
||||||
platform?: Platform,
|
platform?: Platform,
|
||||||
@@ -555,7 +533,7 @@ export function persisted<T>(
|
|||||||
store: [Store<T>, SetStoreFunction<T>],
|
store: [Store<T>, SetStoreFunction<T>],
|
||||||
): PersistedWithReady<T> {
|
): PersistedWithReady<T> {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const config = resolveTarget(typeof target === "string" ? { key: target } : target, platform)
|
const config: PersistTarget = typeof target === "string" ? { key: target } : target
|
||||||
|
|
||||||
const defaults = snapshot(store[0])
|
const defaults = snapshot(store[0])
|
||||||
const legacy = config.legacy ?? []
|
const legacy = config.legacy ?? []
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { useMutation } from "@tanstack/solid-query"
|
|
||||||
import { createEffect } from "solid-js"
|
|
||||||
import type { Accessor } from "solid-js"
|
|
||||||
import {
|
|
||||||
addServerProbePlan,
|
|
||||||
createProbeFailureGate,
|
|
||||||
runAddableProbePlan,
|
|
||||||
type AddServerProbePlan,
|
|
||||||
type WslAddServerView,
|
|
||||||
} from "./settings-model"
|
|
||||||
import type { WslInstalledDistro, WslServersPlatform, WslServersState } from "./types"
|
|
||||||
|
|
||||||
export function useWslAddServerProbes(input: {
|
|
||||||
state: Accessor<WslServersState | undefined>
|
|
||||||
api: WslServersPlatform
|
|
||||||
view: Accessor<WslAddServerView>
|
|
||||||
adding: Accessor<boolean>
|
|
||||||
busy: Accessor<boolean>
|
|
||||||
selectedDistro: Accessor<string | null>
|
|
||||||
addableInstalledDistros: Accessor<WslInstalledDistro[]>
|
|
||||||
onError: (error: unknown) => void
|
|
||||||
}) {
|
|
||||||
const gate = createProbeFailureGate()
|
|
||||||
const probe = useMutation(() => ({
|
|
||||||
mutationFn: async (command: AddServerProbePlan) => {
|
|
||||||
if (command.kind === "addable") {
|
|
||||||
await runAddableProbePlan({ plan: command.plan, api: input.api })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (command.plan.action === "probeRuntime") await input.api.probeRuntime()
|
|
||||||
if (command.plan.action === "refreshDistros") await input.api.refreshDistros()
|
|
||||||
},
|
|
||||||
onError: input.onError,
|
|
||||||
onSettled: (_result, error, command) => {
|
|
||||||
if (command) gate.settle(command.key, error)
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (probe.isPending) return
|
|
||||||
const command = addServerProbePlan({
|
|
||||||
state: input.state(),
|
|
||||||
view: input.view(),
|
|
||||||
adding: input.adding(),
|
|
||||||
busy: input.busy(),
|
|
||||||
selectedDistro: input.selectedDistro(),
|
|
||||||
addableInstalledDistros: input.addableInstalledDistros(),
|
|
||||||
})
|
|
||||||
if (!command || !gate.accepts(command.key)) return
|
|
||||||
probe.mutate(command)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
probingAddable: () => probe.isPending && probe.variables?.kind === "addable",
|
|
||||||
resetProbeFailure: () => gate.reset(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,20 @@
|
|||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||||
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
|
||||||
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
|
||||||
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
|
|
||||||
import { RadioGroupV2, RadioItemV2 } from "@opencode-ai/ui/v2/radio-v2"
|
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
|
||||||
import { createMemo, For, Show } from "solid-js"
|
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useWslAddServerProbes } from "./add-server-probes"
|
|
||||||
import { useWslServers } from "./context"
|
import { useWslServers } from "./context"
|
||||||
import { addServerViewModel, type AddServerText } from "./settings-model"
|
import { enterWslOpencodeStep } from "./settings-model"
|
||||||
import "./dialog-add-wsl-server.css"
|
|
||||||
|
|
||||||
function isWslRuntimeMissing(error: string | null | undefined) {
|
type WslServerStep = "wsl" | "distro" | "opencode"
|
||||||
if (!error) return true
|
|
||||||
return /WSL is not installed|not been installed|wsl(?:\.exe)? --install/i.test(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
function translate(language: ReturnType<typeof useLanguage>, value: AddServerText) {
|
const STEPS: WslServerStep[] = ["wsl", "distro", "opencode"]
|
||||||
if (value.params) return language.t(value.key, value.params)
|
|
||||||
return language.t(value.key)
|
function isHiddenDistro(name: string) {
|
||||||
|
return /^docker-desktop(?:-data)?$/i.test(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DialogWslServerProps {
|
interface DialogWslServerProps {
|
||||||
@@ -30,272 +22,210 @@ interface DialogWslServerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
||||||
const language = useLanguage()
|
|
||||||
const controller = useWslAddServerController(props)
|
|
||||||
const model = controller.model
|
|
||||||
const primaryButton = () => model().primaryButton
|
|
||||||
const primaryButtonStyle = () => {
|
|
||||||
const width = primaryButton().width
|
|
||||||
if (!width) return undefined
|
|
||||||
return { width }
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show
|
|
||||||
when={!controller.wslServers.isPending && !controller.wslServers.isError}
|
|
||||||
fallback={
|
|
||||||
<Dialog fit class="settings-v2-wsl-dialog">
|
|
||||||
<Show
|
|
||||||
when={!controller.wslServers.isError}
|
|
||||||
fallback={<div class="settings-v2-wsl-loading">{controller.loadError()}</div>}
|
|
||||||
>
|
|
||||||
<div class="settings-v2-wsl-loading">
|
|
||||||
<LoaderV2 />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Dialog>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={model().runtimeState === "ready"}
|
|
||||||
fallback={
|
|
||||||
<Show
|
|
||||||
when={model().runtimeState === "checking" || model().runtimeState === "loading"}
|
|
||||||
fallback={
|
|
||||||
<DialogWslSetup
|
|
||||||
state={model().runtimeState}
|
|
||||||
error={controller.runtimeError()}
|
|
||||||
installable={isWslRuntimeMissing(controller.runtimeError())}
|
|
||||||
busy={model().busy}
|
|
||||||
onInstall={controller.installWsl}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Dialog fit class="settings-v2-wsl-dialog">
|
|
||||||
<div class="settings-v2-wsl-loading">
|
|
||||||
<LoaderV2 />
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Dialog fit class="settings-v2-wsl-dialog">
|
|
||||||
<DialogHeader hideClose={true}>
|
|
||||||
<DialogTitle>
|
|
||||||
{controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")}
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DividerV2 />
|
|
||||||
<Show
|
|
||||||
when={controller.view() === "main"}
|
|
||||||
fallback={
|
|
||||||
<>
|
|
||||||
<DialogBody class="settings-v2-wsl-dialog-body settings-v2-wsl-catalog-picker">
|
|
||||||
<TextInputV2
|
|
||||||
class="settings-v2-wsl-catalog-search"
|
|
||||||
appearance="large"
|
|
||||||
placeholder={language.t("wsl.onboarding.searchDistros")}
|
|
||||||
value={controller.catalogSearch()}
|
|
||||||
disabled={model().busy}
|
|
||||||
onInput={(event) => controller.setCatalogSearch(event.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<div class="settings-v2-wsl-catalog-list">
|
|
||||||
<RadioGroupV2
|
|
||||||
hideLabel
|
|
||||||
class="settings-v2-wsl-distro-group"
|
|
||||||
label={language.t("wsl.onboarding.installDistro")}
|
|
||||||
value={model().catalogTarget ?? undefined}
|
|
||||||
onChange={controller.setCatalogTarget}
|
|
||||||
disabled={model().busy}
|
|
||||||
>
|
|
||||||
<For each={model().filteredInstallableDistros}>
|
|
||||||
{(item) => (
|
|
||||||
<RadioItemV2
|
|
||||||
class="settings-v2-wsl-distro-row settings-v2-wsl-catalog-row"
|
|
||||||
value={item.name}
|
|
||||||
disabled={model().busy}
|
|
||||||
label={<span class="settings-v2-wsl-distro-label">{item.label}</span>}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</RadioGroupV2>
|
|
||||||
</div>
|
|
||||||
</DialogBody>
|
|
||||||
<DialogFooter>
|
|
||||||
<ButtonV2 variant="neutral" disabled={model().busy} onClick={controller.closeCatalog}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</ButtonV2>
|
|
||||||
<ButtonV2
|
|
||||||
variant={model().installingCatalogDistro ? "loading" : "contrast"}
|
|
||||||
disabled={!model().installingCatalogDistro && (model().busy || !model().catalogTarget)}
|
|
||||||
style={{ width: "99px" }}
|
|
||||||
onClick={controller.installCatalogDistro}
|
|
||||||
>
|
|
||||||
<Show when={model().installingCatalogDistro} fallback={language.t("wsl.onboarding.installDistro")}>
|
|
||||||
<LoaderV2 />
|
|
||||||
</Show>
|
|
||||||
</ButtonV2>
|
|
||||||
</DialogFooter>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DialogBody class="settings-v2-wsl-dialog-body">
|
|
||||||
<div class="settings-v2-wsl-section-header">
|
|
||||||
<span class="settings-v2-wsl-section-title">{language.t("wsl.onboarding.installedDistros")}</span>
|
|
||||||
<ButtonV2
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
disabled={model().busy}
|
|
||||||
onClick={controller.refreshDistros}
|
|
||||||
>
|
|
||||||
{language.t("wsl.onboarding.checkAgain")}
|
|
||||||
</ButtonV2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={model().addableInstalledDistros.length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="settings-v2-wsl-distro-list">
|
|
||||||
<div class="settings-v2-wsl-distro-empty">
|
|
||||||
{model().visibleInstalledDistros.length
|
|
||||||
? language.t("wsl.onboarding.allDistrosAdded")
|
|
||||||
: language.t("wsl.onboarding.noDistros")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="settings-v2-wsl-distro-list">
|
|
||||||
<RadioGroupV2
|
|
||||||
hideLabel
|
|
||||||
class="settings-v2-wsl-distro-group"
|
|
||||||
label={language.t("wsl.onboarding.installedDistros")}
|
|
||||||
value={model().selectedDistro ?? undefined}
|
|
||||||
onChange={controller.setSelectedDistro}
|
|
||||||
disabled={model().busy}
|
|
||||||
>
|
|
||||||
<For each={model().addableInstalledDistros}>
|
|
||||||
{(item) => {
|
|
||||||
const status = () => model().distroStatuses[item.name] ?? null
|
|
||||||
return (
|
|
||||||
<RadioItemV2
|
|
||||||
class={`settings-v2-wsl-distro-row${item.version === 1 ? " settings-v2-wsl-distro-row--unsupported" : ""}`}
|
|
||||||
value={item.name}
|
|
||||||
disabled={item.version === 1 || model().busy}
|
|
||||||
label={<span class="settings-v2-wsl-distro-label">{item.name}</span>}
|
|
||||||
description={
|
|
||||||
<Show when={status()}>
|
|
||||||
{(value) => (
|
|
||||||
<span class="settings-v2-wsl-distro-status" data-tone={value().tone}>
|
|
||||||
{translate(language, value().label)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</RadioGroupV2>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={model().installableDistros.length > 0}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="settings-v2-wsl-catalog-card"
|
|
||||||
disabled={model().busy}
|
|
||||||
onClick={controller.openCatalog}
|
|
||||||
>
|
|
||||||
<span class="settings-v2-wsl-catalog-icon" aria-hidden="true">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M13.5564 10.4443V13.5554H4.22309C3.24087 13.5554 2.44531 13.5554 2.44531 13.5554V10.4443M11.112 5.99989L8.00087 9.111L4.88976 5.99989M8.00087 9.111L8.00087 2.44434"
|
|
||||||
stroke="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span class="settings-v2-wsl-catalog-copy">
|
|
||||||
<span class="settings-v2-wsl-catalog-title">{language.t("wsl.onboarding.needAnotherDistro")}</span>
|
|
||||||
<span class="settings-v2-wsl-catalog-description">
|
|
||||||
{language.t("wsl.onboarding.needAnotherDistroHint")}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="settings-v2-wsl-catalog-chevron" aria-hidden="true">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M6 12L10 8L6 4" stroke="currentColor" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</DialogBody>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<ButtonV2 variant="neutral" disabled={controller.adding()} onClick={controller.close}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</ButtonV2>
|
|
||||||
<ButtonV2
|
|
||||||
variant={primaryButton().loading ? "loading" : primaryButton().variant}
|
|
||||||
disabled={!primaryButton().loading && primaryButton().disabled}
|
|
||||||
style={primaryButtonStyle()}
|
|
||||||
onClick={controller.runPrimary}
|
|
||||||
>
|
|
||||||
<Show when={primaryButton().loading} fallback={translate(language, primaryButton().label)}>
|
|
||||||
<LoaderV2 />
|
|
||||||
</Show>
|
|
||||||
</ButtonV2>
|
|
||||||
</DialogFooter>
|
|
||||||
</Show>
|
|
||||||
</Dialog>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useWslAddServerController(props: DialogWslServerProps) {
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const wslServers = useWslServers()
|
const wslServers = useWslServers()
|
||||||
const api = platform.wslServers!
|
const api = platform.wslServers!
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
view: "main" as "main" | "catalog",
|
step: undefined as WslServerStep | undefined,
|
||||||
selectedDistro: null as string | null,
|
selectedDistro: null as string | null,
|
||||||
catalogSearch: "",
|
installTarget: undefined as string | undefined,
|
||||||
catalogTarget: null as string | null,
|
|
||||||
adding: false,
|
adding: false,
|
||||||
})
|
})
|
||||||
const current = () => wslServers.data
|
const current = () => wslServers.data
|
||||||
const viewModel = (probingAddable: boolean) =>
|
let disposed = false
|
||||||
addServerViewModel({
|
onCleanup(() => {
|
||||||
state: current(),
|
disposed = true
|
||||||
view: store.view,
|
|
||||||
selectedDistro: store.selectedDistro,
|
|
||||||
catalogSearch: store.catalogSearch,
|
|
||||||
catalogTarget: store.catalogTarget,
|
|
||||||
adding: store.adding,
|
|
||||||
probingAddable,
|
|
||||||
})
|
|
||||||
const baseModel = createMemo(() => viewModel(false))
|
|
||||||
const probes = useWslAddServerProbes({
|
|
||||||
state: current,
|
|
||||||
api,
|
|
||||||
view: () => store.view,
|
|
||||||
adding: () => store.adding,
|
|
||||||
busy: () => baseModel().busy,
|
|
||||||
selectedDistro: () => baseModel().selectedDistro,
|
|
||||||
addableInstalledDistros: () => baseModel().addableInstalledDistros,
|
|
||||||
onError: (error) => requestError(language, error),
|
|
||||||
})
|
})
|
||||||
const model = createMemo(() => viewModel(probes.probingAddable()))
|
const busy = createMemo(() => !!current()?.job || store.adding)
|
||||||
|
const visibleInstalledDistros = createMemo(() =>
|
||||||
|
(current()?.installed ?? []).filter((item) => !isHiddenDistro(item.name)),
|
||||||
|
)
|
||||||
|
const visibleOnlineDistros = createMemo(() => (current()?.online ?? []).filter((item) => !isHiddenDistro(item.name)))
|
||||||
|
const defaultInstalledDistro = createMemo(() => visibleInstalledDistros().find((item) => item.isDefault) ?? null)
|
||||||
|
const existingServerDistros = createMemo(() => new Set((current()?.servers ?? []).map((item) => item.config.distro)))
|
||||||
|
const addableInstalledDistros = createMemo(() => {
|
||||||
|
return visibleInstalledDistros().filter((item) => !existingServerDistros().has(item.name))
|
||||||
|
})
|
||||||
|
const selectedDistro = createMemo(() => {
|
||||||
|
if (store.selectedDistro && addableInstalledDistros().some((item) => item.name === store.selectedDistro)) {
|
||||||
|
return store.selectedDistro
|
||||||
|
}
|
||||||
|
const distro = defaultInstalledDistro()
|
||||||
|
if (distro && !existingServerDistros().has(distro.name)) return distro.name
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
const selectedProbe = createMemo(() => {
|
||||||
|
const distro = selectedDistro()
|
||||||
|
if (!distro) return null
|
||||||
|
return current()?.distroProbes[distro] ?? null
|
||||||
|
})
|
||||||
|
const selectedInstalled = createMemo(() => {
|
||||||
|
const distro = selectedDistro()
|
||||||
|
if (!distro) return null
|
||||||
|
return (current()?.installed ?? []).find((item) => item.name === distro) ?? null
|
||||||
|
})
|
||||||
|
const opencodeCheck = createMemo(() => {
|
||||||
|
const distro = selectedDistro()
|
||||||
|
if (!distro) return null
|
||||||
|
return current()?.opencodeChecks[distro] ?? null
|
||||||
|
})
|
||||||
|
const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart)
|
||||||
|
const distroReady = createMemo(() => {
|
||||||
|
const probe = selectedProbe()
|
||||||
|
if (!probe || !selectedDistro()) return false
|
||||||
|
if (selectedInstalled()?.version === 1) return false
|
||||||
|
return probe.canExecute && probe.hasBash && probe.hasCurl
|
||||||
|
})
|
||||||
|
const opencodeReady = createMemo(() => {
|
||||||
|
const check = opencodeCheck()
|
||||||
|
return !!check?.resolvedPath && !check.error
|
||||||
|
})
|
||||||
|
const distroWarningProbe = createMemo(() => {
|
||||||
|
const probe = selectedProbe()
|
||||||
|
if (!probe) return null
|
||||||
|
if (distroReady()) return null
|
||||||
|
return probe
|
||||||
|
})
|
||||||
|
const distroUnavailableMessage = createMemo(() => {
|
||||||
|
const probe = distroWarningProbe()
|
||||||
|
const distro = selectedDistro()
|
||||||
|
if (!probe || probe.canExecute || !distro) return null
|
||||||
|
if (!selectedInstalled()) return language.t("wsl.onboarding.distroNotInstalled", { distro })
|
||||||
|
return language.t("wsl.onboarding.openDistroOnce", { distro })
|
||||||
|
})
|
||||||
|
const distroMissingTools = createMemo(() => {
|
||||||
|
const probe = distroWarningProbe()
|
||||||
|
if (!probe?.canExecute) return null
|
||||||
|
if (probe.hasBash && probe.hasCurl) return null
|
||||||
|
return probe
|
||||||
|
})
|
||||||
|
const installableDistros = createMemo(() => {
|
||||||
|
const online = visibleOnlineDistros()
|
||||||
|
const installed = new Set(visibleInstalledDistros().map((item) => item.name))
|
||||||
|
const hasVersionedUbuntu = online.some((item) => /^Ubuntu-\d/.test(item.name))
|
||||||
|
return online
|
||||||
|
.filter((item) => !installed.has(item.name))
|
||||||
|
.filter((item) => !(item.name === "Ubuntu" && hasVersionedUbuntu))
|
||||||
|
})
|
||||||
|
const installTarget = createMemo(
|
||||||
|
() => installableDistros().find((item) => item.name === store.installTarget) ?? installableDistros()[0] ?? null,
|
||||||
|
)
|
||||||
|
const installingDistro = createMemo(() => current()?.job?.kind === "install-distro")
|
||||||
|
const installingOpencode = createMemo(() => {
|
||||||
|
const job = current()?.job
|
||||||
|
return job?.kind === "install-opencode" && job.distro === selectedDistro()
|
||||||
|
})
|
||||||
|
const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady())
|
||||||
|
const addDisabled = createMemo(() => {
|
||||||
|
const job = current()?.job
|
||||||
|
if (!job) return store.adding
|
||||||
|
return store.adding || job.kind !== "probe-opencode"
|
||||||
|
})
|
||||||
|
const recommendedStep = createMemo<WslServerStep>(() => {
|
||||||
|
if (!wslReady()) return "wsl"
|
||||||
|
if (!distroReady()) return "distro"
|
||||||
|
return "opencode"
|
||||||
|
})
|
||||||
|
// activeStep falls back to recommendedStep when the user hasn't picked one.
|
||||||
|
// Once the user clicks a step tab we respect their choice rather than snapping
|
||||||
|
// them back when a probe result updates recommendedStep.
|
||||||
|
const activeStep = createMemo(() => store.step ?? recommendedStep())
|
||||||
|
|
||||||
const openCatalog = () => {
|
const autoProbe = createMemo(() => {
|
||||||
const first = model().installableDistros[0]
|
const state = current()
|
||||||
setStore({
|
if (!state || busy()) return null
|
||||||
view: "catalog",
|
if (state.pendingRestart) return null
|
||||||
catalogSearch: "",
|
if (!state.runtime) return { key: "runtime", run: () => api.probeRuntime() }
|
||||||
catalogTarget: first?.name ?? null,
|
if (!wslReady()) return null
|
||||||
})
|
if (!state.installed.length && !state.online.length) {
|
||||||
}
|
return { key: "distros", run: () => api.refreshDistros() }
|
||||||
|
}
|
||||||
|
const distro = selectedDistro()
|
||||||
|
if (distro && !state.distroProbes[distro]) {
|
||||||
|
return { key: `probe-distro:${distro}`, run: () => api.probeDistro(distro) }
|
||||||
|
}
|
||||||
|
if (!distro || !distroReady()) return null
|
||||||
|
if (!state.opencodeChecks[distro]) {
|
||||||
|
return { key: `probe-opencode:${distro}`, run: () => api.probeOpencode(distro) }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
let lastAutoProbe: string | null = null
|
||||||
|
createEffect(() => {
|
||||||
|
const probe = autoProbe()
|
||||||
|
if (!probe || probe.key === lastAutoProbe) return
|
||||||
|
const key = probe.key
|
||||||
|
lastAutoProbe = key
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await probe.run()
|
||||||
|
} catch (err) {
|
||||||
|
if (disposed) return
|
||||||
|
// Allow the same probe to run again when reactive inputs next change
|
||||||
|
// (e.g. user reselects a distro). Without this the user would be stuck
|
||||||
|
// on a transient wsl.exe failure until they pick a different distro.
|
||||||
|
if (lastAutoProbe === key) lastAutoProbe = null
|
||||||
|
requestError(language, err)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
|
||||||
|
const wslMessage = createMemo(() => {
|
||||||
|
const state = current()
|
||||||
|
if (!state || state.job?.kind === "runtime") return language.t("wsl.onboarding.checkingRuntime")
|
||||||
|
if (state.pendingRestart) return language.t("wsl.onboarding.restartRequired")
|
||||||
|
if (state.runtime?.available) return state.runtime.version ?? language.t("wsl.onboarding.ready")
|
||||||
|
return state.runtime?.error ?? language.t("wsl.onboarding.required")
|
||||||
|
})
|
||||||
|
|
||||||
|
const distroMessage = createMemo(() => {
|
||||||
|
const state = current()
|
||||||
|
if (!state) return language.t("wsl.onboarding.checkingDistros")
|
||||||
|
const distro = selectedDistro()
|
||||||
|
if (state.job?.kind === "install-distro")
|
||||||
|
return language.t("wsl.onboarding.installingDistro", { distro: state.job.distro })
|
||||||
|
if (state.job?.kind === "probe-distro")
|
||||||
|
return language.t("wsl.onboarding.checkingDistro", { distro: state.job.distro })
|
||||||
|
if (state.job?.kind === "distros") return language.t("wsl.onboarding.listingDistros")
|
||||||
|
if (distroUnavailableMessage()) return distroUnavailableMessage()!
|
||||||
|
if (selectedProbe() && distroReady())
|
||||||
|
return language.t("wsl.onboarding.distroReady", { distro: selectedProbe()!.name })
|
||||||
|
if (distro) return language.t("wsl.onboarding.finishingDistro", { distro })
|
||||||
|
return language.t("wsl.onboarding.pickDistro")
|
||||||
|
})
|
||||||
|
|
||||||
|
const opencodeMessage = createMemo(() => {
|
||||||
|
const state = current()
|
||||||
|
if (!state) return language.t("wsl.onboarding.checkingOpencode")
|
||||||
|
const distro = selectedDistro()
|
||||||
|
if (state.job?.kind === "install-opencode") {
|
||||||
|
return distro
|
||||||
|
? language.t("wsl.onboarding.updatingOpencodeIn", { distro })
|
||||||
|
: language.t("wsl.onboarding.updatingOpencode")
|
||||||
|
}
|
||||||
|
if (state.job?.kind === "probe-opencode") {
|
||||||
|
return distro
|
||||||
|
? language.t("wsl.onboarding.checkingOpencodeIn", { distro })
|
||||||
|
: language.t("wsl.onboarding.checkingOpencode")
|
||||||
|
}
|
||||||
|
if (opencodeCheck()?.error) return opencodeCheck()!.error
|
||||||
|
if (opencodeCheck()?.matchesDesktop === false) {
|
||||||
|
return distro
|
||||||
|
? language.t("wsl.onboarding.updateOpencodeIn", { distro })
|
||||||
|
: language.t("wsl.onboarding.updateOpencode")
|
||||||
|
}
|
||||||
|
if (opencodeReady()) {
|
||||||
|
return distro
|
||||||
|
? language.t("wsl.onboarding.opencodeReadyIn", { distro })
|
||||||
|
: language.t("wsl.onboarding.opencodeReady")
|
||||||
|
}
|
||||||
|
return distro
|
||||||
|
? language.t("wsl.onboarding.installOpencodeIn", { distro })
|
||||||
|
: language.t("wsl.onboarding.chooseDistroFirst")
|
||||||
|
})
|
||||||
|
|
||||||
const run = async (action: () => Promise<unknown>) => {
|
const run = async (action: () => Promise<unknown>) => {
|
||||||
try {
|
try {
|
||||||
@@ -305,43 +235,26 @@ function useWslAddServerController(props: DialogWslServerProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshDistros = () => {
|
const runSelectedDistro = (action: (distro: string) => Promise<unknown>) => {
|
||||||
void run(async () => {
|
const distro = selectedDistro()
|
||||||
probes.resetProbeFailure()
|
if (!distro) return
|
||||||
await api.refreshDistros()
|
void run(() => action(distro))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const installDistro = (name: string) => {
|
const selectDistro = (name: string) => {
|
||||||
void run(async () => {
|
setStore("selectedDistro", name)
|
||||||
probes.resetProbeFailure()
|
setStore("step", undefined)
|
||||||
await api.installDistro(name)
|
|
||||||
setStore("view", "main")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const installCatalogDistro = () => {
|
const openOpencodeStep = () => {
|
||||||
if (model().installingCatalogDistro) return
|
const distro = selectedDistro()
|
||||||
const name = model().catalogTarget
|
if (!distro) return
|
||||||
if (!name) return
|
void run(() => enterWslOpencodeStep(distro, api.probeOpencode, (step) => setStore("step", step)))
|
||||||
installDistro(name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeCatalog = () => {
|
const finish = async () => {
|
||||||
probes.resetProbeFailure()
|
const distro = selectedDistro()
|
||||||
setStore({ view: "main", catalogSearch: "", catalogTarget: null })
|
if (!distro) return
|
||||||
}
|
|
||||||
|
|
||||||
const runPrimary = async () => {
|
|
||||||
const button = model().primaryButton
|
|
||||||
if (button.loading) return
|
|
||||||
const distro = model().selectedDistro
|
|
||||||
const action = button.action
|
|
||||||
if (!distro || !action) return
|
|
||||||
if (action === "install-opencode") {
|
|
||||||
await run(() => api.installOpencode(distro))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setStore("adding", true)
|
setStore("adding", true)
|
||||||
try {
|
try {
|
||||||
await api.addServer(distro)
|
await api.addServer(distro)
|
||||||
@@ -357,99 +270,346 @@ function useWslAddServerController(props: DialogWslServerProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadError = () => {
|
const steps = createMemo(() => {
|
||||||
|
const active = activeStep()
|
||||||
|
const activeIndex = STEPS.indexOf(active)
|
||||||
|
const recommendedIndex = STEPS.indexOf(recommendedStep())
|
||||||
|
return STEPS.map((step) => {
|
||||||
|
const index = STEPS.indexOf(step)
|
||||||
|
return {
|
||||||
|
step,
|
||||||
|
title:
|
||||||
|
step === "wsl"
|
||||||
|
? language.t("wsl.server.label")
|
||||||
|
: step === "distro"
|
||||||
|
? language.t("wsl.onboarding.step.distro")
|
||||||
|
: language.t("wsl.onboarding.step.opencode"),
|
||||||
|
state:
|
||||||
|
active === step
|
||||||
|
? "current"
|
||||||
|
: step === "wsl"
|
||||||
|
? wslReady()
|
||||||
|
? "done"
|
||||||
|
: "warning"
|
||||||
|
: step === "distro"
|
||||||
|
? distroReady()
|
||||||
|
? "done"
|
||||||
|
: index > activeIndex
|
||||||
|
? "locked"
|
||||||
|
: "warning"
|
||||||
|
: opencodeCheck()?.matchesDesktop === false
|
||||||
|
? "warning"
|
||||||
|
: opencodeReady()
|
||||||
|
? "done"
|
||||||
|
: index > activeIndex
|
||||||
|
? "locked"
|
||||||
|
: "warning",
|
||||||
|
locked: index > recommendedIndex,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const loadError = createMemo(() => {
|
||||||
const error = wslServers.error
|
const error = wslServers.error
|
||||||
if (!error) return language.t("wsl.onboarding.loadFailed")
|
if (!error) return language.t("wsl.onboarding.loadFailed")
|
||||||
return error instanceof Error ? error.message : String(error)
|
return error instanceof Error ? error.message : String(error)
|
||||||
}
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
wslServers,
|
|
||||||
model,
|
|
||||||
loadError,
|
|
||||||
runtimeError: () => current()?.runtime?.error ?? null,
|
|
||||||
view: () => store.view,
|
|
||||||
catalogSearch: () => store.catalogSearch,
|
|
||||||
adding: () => store.adding,
|
|
||||||
setCatalogSearch: (value: string) => setStore("catalogSearch", value),
|
|
||||||
setCatalogTarget: (value: string) => setStore("catalogTarget", value),
|
|
||||||
setSelectedDistro: (value: string) => setStore("selectedDistro", value),
|
|
||||||
openCatalog,
|
|
||||||
closeCatalog,
|
|
||||||
refreshDistros,
|
|
||||||
installCatalogDistro,
|
|
||||||
installWsl: () => void run(() => api.installWsl()),
|
|
||||||
runPrimary: () => void runPrimary(),
|
|
||||||
close: () => dialog.close(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogWslSetup(props: {
|
|
||||||
state: string
|
|
||||||
error: string | null
|
|
||||||
installable: boolean
|
|
||||||
busy: boolean
|
|
||||||
onInstall: () => void
|
|
||||||
}) {
|
|
||||||
const language = useLanguage()
|
|
||||||
const dialog = useDialog()
|
|
||||||
const title = () =>
|
|
||||||
props.state === "pendingRestart"
|
|
||||||
? language.t("wsl.onboarding.restartRequired")
|
|
||||||
: props.installable
|
|
||||||
? language.t("wsl.onboarding.wslNotInstalled.title")
|
|
||||||
: language.t("wsl.onboarding.wslUnavailable.title")
|
|
||||||
const description = () => {
|
|
||||||
if (props.state === "pendingRestart") return language.t("wsl.onboarding.windowsRestartRequired")
|
|
||||||
if (!props.installable) return language.t("wsl.onboarding.wslUnavailable.description")
|
|
||||||
return language.t("wsl.onboarding.wslNotInstalled.description")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog fit class="settings-v2-wsl-not-installed-dialog">
|
<div class="px-5 pb-5 flex flex-col gap-4">
|
||||||
<div class="settings-v2-wsl-not-installed-content">
|
<Show
|
||||||
<div class="settings-v2-wsl-not-installed-message">
|
when={!wslServers.isPending}
|
||||||
<svg
|
fallback={<div class="px-1 py-6 text-14-regular text-text-weak">{language.t("wsl.onboarding.loading")}</div>}
|
||||||
class="settings-v2-wsl-not-installed-icon"
|
>
|
||||||
width="24"
|
<Show
|
||||||
height="24"
|
when={!wslServers.isError}
|
||||||
viewBox="0 0 24 24"
|
fallback={<div class="px-1 py-6 text-14-regular text-text-weak">{loadError()}</div>}
|
||||||
fill="none"
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<div class="flex gap-2 pb-1">
|
||||||
aria-hidden="true"
|
<For each={steps()}>
|
||||||
>
|
{(item) => (
|
||||||
<g clip-path="url(#settings-v2-wsl-warning-clip)">
|
<button
|
||||||
<path
|
type="button"
|
||||||
fill-rule="evenodd"
|
class="basis-0 flex-1 min-w-0 rounded-md border px-3 py-2 text-left transition-colors"
|
||||||
clip-rule="evenodd"
|
classList={{
|
||||||
d="M12 -0.00244141L23.6926 20.2498H0.308594L12 -0.00244141ZM12.7954 6.32932C12.5844 6.11834 12.2982 5.99982 11.9999 5.99982C11.7015 5.99982 11.4154 6.11834 11.2044 6.32932C10.9934 6.5403 10.8749 6.82645 10.8749 7.12482V11.6248C10.8749 11.9232 10.9934 12.2093 11.2044 12.4203C11.4154 12.6313 11.7015 12.7498 11.9999 12.7498C12.2982 12.7498 12.5844 12.6313 12.7954 12.4203C13.0064 12.2093 13.1249 11.9232 13.1249 11.6248V7.12482C13.1249 6.82645 13.0064 6.5403 12.7954 6.32932ZM13.0605 17.5605C12.7792 17.8418 12.3977 17.9998 11.9999 17.9998C11.6021 17.9998 11.2205 17.8418 10.9392 17.5605C10.6579 17.2792 10.4999 16.8976 10.4999 16.4998C10.4999 16.102 10.6579 15.7205 10.9392 15.4392C11.2205 15.1579 11.6021 14.9998 11.9999 14.9998C12.3977 14.9998 12.7792 15.1579 13.0605 15.4392C13.3418 15.7205 13.4999 16.102 13.4999 16.4998C13.4999 16.8976 13.3418 17.2792 13.0605 17.5605Z"
|
"border-border-strong-base bg-surface-base-hover": item.state === "current",
|
||||||
fill="#DBDBDB"
|
"border-icon-success-base/40 bg-surface-base": item.state === "done",
|
||||||
/>
|
"border-border-weak-base bg-background-base opacity-60": item.state === "locked",
|
||||||
</g>
|
"border-icon-warning-base/40 bg-surface-base": item.state === "warning",
|
||||||
<defs>
|
}}
|
||||||
<clipPath id="settings-v2-wsl-warning-clip">
|
disabled={item.locked}
|
||||||
<rect width="24" height="24" fill="white" />
|
onClick={() => setStore("step", item.step)}
|
||||||
</clipPath>
|
>
|
||||||
</defs>
|
<div class="text-13-medium text-text-strong">{item.title}</div>
|
||||||
</svg>
|
</button>
|
||||||
<h2 class="settings-v2-wsl-not-installed-title">{title()}</h2>
|
)}
|
||||||
<p class="settings-v2-wsl-not-installed-description">{description()}</p>
|
</For>
|
||||||
<Show when={!props.installable && props.error}>
|
</div>
|
||||||
<p class="settings-v2-wsl-unavailable-error">{props.error}</p>
|
|
||||||
|
<Switch>
|
||||||
|
<Match when={activeStep() === "wsl"}>
|
||||||
|
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="text-14-medium text-text-strong">{language.t("wsl.server.label")}</div>
|
||||||
|
<Show when={current()?.runtime && !wslReady() && !current()?.pendingRestart}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="large"
|
||||||
|
disabled={busy()}
|
||||||
|
onClick={() => void run(() => api.installWsl())}
|
||||||
|
>
|
||||||
|
{language.t("wsl.onboarding.installWsl")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">{wslMessage()}</div>
|
||||||
|
<Show when={current()?.pendingRestart}>
|
||||||
|
<div class="rounded-md border border-border-weak-base px-3 py-3">
|
||||||
|
<div class="text-12-regular text-text-warning-base">
|
||||||
|
{language.t("wsl.onboarding.windowsRestartRequired")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="large"
|
||||||
|
disabled={busy() || !wslReady()}
|
||||||
|
onClick={() => setStore("step", "distro")}
|
||||||
|
>
|
||||||
|
{language.t("wsl.onboarding.next")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<Match when={activeStep() === "distro"}>
|
||||||
|
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="text-14-medium text-text-strong">{language.t("wsl.onboarding.step.distro")}</div>
|
||||||
|
<Show when={selectedDistro()}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
disabled={busy()}
|
||||||
|
onClick={() => runSelectedDistro((distro) => api.probeDistro(distro))}
|
||||||
|
>
|
||||||
|
{language.t("wsl.onboarding.refresh")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">{distroMessage()}</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Show
|
||||||
|
when={addableInstalledDistros().length > 0}
|
||||||
|
fallback={
|
||||||
|
<div class="text-12-regular text-text-weak">
|
||||||
|
{visibleInstalledDistros().length
|
||||||
|
? language.t("wsl.onboarding.allDistrosAdded")
|
||||||
|
: current()?.runtime?.available
|
||||||
|
? language.t("wsl.onboarding.noDistros")
|
||||||
|
: language.t("wsl.onboarding.checkingDistros")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={addableInstalledDistros()}>
|
||||||
|
{(item) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border border-border-weak-base px-3 py-2 text-left transition-colors"
|
||||||
|
classList={{ "bg-surface-raised-base": selectedDistro() === item.name }}
|
||||||
|
onClick={() => selectDistro(item.name)}
|
||||||
|
>
|
||||||
|
<div class="text-13-medium text-text-strong">{item.name}</div>
|
||||||
|
<Show when={item.isDefault}>
|
||||||
|
<div class="text-12-regular text-text-weak">{language.t("common.default")}</div>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={installableDistros().length > 0}>
|
||||||
|
<div class="rounded-md border border-border-weak-base p-2 flex flex-col gap-2">
|
||||||
|
<div class="px-1 flex items-center justify-between gap-3">
|
||||||
|
<div class="text-12-medium text-text-weak">{language.t("wsl.onboarding.install")}</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<Show when={installingDistro()}>
|
||||||
|
<Spinner class="h-4 w-4 text-icon-info-base shrink-0" />
|
||||||
|
</Show>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
disabled={busy() || !installTarget()}
|
||||||
|
onClick={() => void run(() => api.installDistro(installTarget()!.name))}
|
||||||
|
>
|
||||||
|
{installingDistro()
|
||||||
|
? language.t("wsl.onboarding.installing")
|
||||||
|
: language.t("wsl.onboarding.install")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={language.t("wsl.onboarding.installDistro")}
|
||||||
|
class="max-h-52 overflow-y-auto rounded-md bg-background-base"
|
||||||
|
>
|
||||||
|
<For each={installableDistros()}>
|
||||||
|
{(item) => {
|
||||||
|
const selected = () => installTarget()?.name === item.name
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={selected()}
|
||||||
|
disabled={busy()}
|
||||||
|
class="w-full px-3 py-2 flex items-center gap-3 text-left border-b border-border-weak-base last:border-b-0 transition-colors"
|
||||||
|
classList={{
|
||||||
|
"bg-surface-raised-base": selected(),
|
||||||
|
"hover:bg-surface-base": !selected(),
|
||||||
|
}}
|
||||||
|
onClick={() => setStore("installTarget", item.name)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mt-0.5 h-4 w-4 rounded-full border border-border-strong-base flex items-center justify-center shrink-0"
|
||||||
|
classList={{ "border-text-strong": selected() }}
|
||||||
|
>
|
||||||
|
<div class="h-2 w-2 rounded-full bg-text-strong" classList={{ hidden: !selected() }} />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1 text-13-medium text-text-strong truncate">{item.label}</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={selectedInstalled()?.version === 1 || distroUnavailableMessage() || distroMissingTools()}>
|
||||||
|
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||||
|
<Show when={selectedInstalled()?.version === 1}>
|
||||||
|
<div class="text-12-regular text-text-warning-base">
|
||||||
|
{language.t("wsl.onboarding.wsl2Required")}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={distroUnavailableMessage()}>
|
||||||
|
{(message) => <div class="text-12-regular text-text-warning-base">{message()}</div>}
|
||||||
|
</Show>
|
||||||
|
<Show when={distroMissingTools()}>
|
||||||
|
<div class="text-12-regular text-text-warning-base">
|
||||||
|
{language.t("wsl.onboarding.toolsRequired")}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="large"
|
||||||
|
disabled={busy() || !selectedInstalled()}
|
||||||
|
onClick={() => runSelectedDistro((distro) => api.openTerminal(distro))}
|
||||||
|
>
|
||||||
|
{language.t("wsl.onboarding.openTerminal")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="large"
|
||||||
|
disabled={busy() || !selectedDistro()}
|
||||||
|
onClick={() => runSelectedDistro((distro) => api.probeDistro(distro))}
|
||||||
|
>
|
||||||
|
{language.t("wsl.onboarding.refresh")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="large"
|
||||||
|
disabled={busy() || !selectedDistro() || !distroReady()}
|
||||||
|
onClick={openOpencodeStep}
|
||||||
|
>
|
||||||
|
{language.t("wsl.onboarding.next")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
|
||||||
|
<Match when={activeStep() === "opencode"}>
|
||||||
|
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="text-14-medium text-text-strong">{language.t("wsl.onboarding.step.opencode")}</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Show when={selectedDistro()}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="large"
|
||||||
|
disabled={busy()}
|
||||||
|
onClick={() => runSelectedDistro((distro) => api.probeOpencode(distro))}
|
||||||
|
>
|
||||||
|
{language.t("wsl.onboarding.refresh")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
<Show when={!opencodeReady() || opencodeCheck()?.matchesDesktop === false}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="large"
|
||||||
|
disabled={busy()}
|
||||||
|
onClick={() => runSelectedDistro((distro) => api.installOpencode(distro))}
|
||||||
|
>
|
||||||
|
<Show when={installingOpencode()}>
|
||||||
|
<Spinner class="size-4 shrink-0" />
|
||||||
|
</Show>
|
||||||
|
{opencodeCheck()?.resolvedPath
|
||||||
|
? language.t("wsl.onboarding.updateOpencode")
|
||||||
|
: language.t("wsl.onboarding.installOpencode")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">{opencodeMessage()}</div>
|
||||||
|
<Show when={opencodeCheck()?.matchesDesktop === false ? opencodeCheck() : null}>
|
||||||
|
{(check) => (
|
||||||
|
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||||
|
<div class="text-12-regular text-text-weak">
|
||||||
|
{language.t("wsl.onboarding.path", {
|
||||||
|
path: check().resolvedPath ?? language.t("wsl.onboarding.notFound"),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-weak">
|
||||||
|
{language.t("wsl.onboarding.version", {
|
||||||
|
version: check().version ?? language.t("wsl.onboarding.unknown"),
|
||||||
|
})}
|
||||||
|
<Show when={check().expectedVersion}>
|
||||||
|
{(expected) => (
|
||||||
|
<span>{` · ${language.t("wsl.onboarding.desktopVersion", { version: expected() })}`}</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-warning-base">
|
||||||
|
{language.t("wsl.onboarding.versionMismatch")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
<Show when={activeStep() === "opencode" && allReady() && selectedDistro()}>
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<Button variant="ghost" size="large" disabled={store.adding} onClick={() => dialog.close()}>
|
||||||
|
{language.t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" size="large" disabled={addDisabled()} onClick={() => void finish()}>
|
||||||
|
{store.adding ? language.t("wsl.onboarding.adding") : language.t("wsl.server.add")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
<Show when={props.state === "unavailable" && props.installable}>
|
|
||||||
<ButtonV2 variant="neutral" disabled={props.busy} onClick={props.onInstall}>
|
|
||||||
{language.t("wsl.onboarding.installWsl")}
|
|
||||||
</ButtonV2>
|
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.state !== "unavailable"}>
|
</Show>
|
||||||
<ButtonV2 variant="neutral" onClick={() => dialog.close()}>
|
</div>
|
||||||
{language.t("common.close")}
|
|
||||||
</ButtonV2>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,392 +0,0 @@
|
|||||||
[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-container"] {
|
|
||||||
width: 480px;
|
|
||||||
max-width: calc(100vw - 32px);
|
|
||||||
height: auto;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-content"] {
|
|
||||||
align-items: stretch;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-header"] {
|
|
||||||
align-items: center;
|
|
||||||
padding: 24px 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-body"] {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 0;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-dialog-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
align-self: stretch;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 16px 16px 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-footer"] {
|
|
||||||
padding: 0 16px 24px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-container"] {
|
|
||||||
width: 480px;
|
|
||||||
max-width: calc(100vw - 32px);
|
|
||||||
min-height: 264px;
|
|
||||||
height: auto;
|
|
||||||
background: var(--v2-background-bg-layer-01);
|
|
||||||
box-shadow: var(--v2-elevation-overlay);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-content"] {
|
|
||||||
align-items: stretch;
|
|
||||||
min-height: 264px;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-body"] {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-not-installed-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
padding: 56px 0;
|
|
||||||
gap: 24px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-not-installed-message {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-not-installed-icon {
|
|
||||||
flex: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-not-installed-title {
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 530;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 20px;
|
|
||||||
letter-spacing: -0.13px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-not-installed-description {
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
margin: 0;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-unavailable-error {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 360px;
|
|
||||||
margin: 4px 0 0;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
color: var(--v2-text-text-faint);
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-section-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex: none;
|
|
||||||
align-self: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0 0 0 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-section-title {
|
|
||||||
flex: none;
|
|
||||||
user-select: none;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 16px;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex: none;
|
|
||||||
align-self: stretch;
|
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
max-height: 220px;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--v2-background-bg-base);
|
|
||||||
box-shadow: var(--v2-elevation-raised);
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-group {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-group [data-slot="radio-v2-items"] {
|
|
||||||
width: 100%;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-row {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 62px;
|
|
||||||
padding: 14px 14px 14px 16px;
|
|
||||||
border-bottom: 0.5px solid var(--v2-border-border-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-row:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-row [data-slot="radio-v2-item-label"] {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-row [data-slot="radio-v2-item-label"]::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: -14px -14px -14px -44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-row [data-slot="radio-v2-item-text"] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-label {
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 16px;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-status {
|
|
||||||
flex: none;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 12px;
|
|
||||||
letter-spacing: 0.05px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-status[data-tone="success"] {
|
|
||||||
color: var(--v2-state-fg-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-status[data-tone="warning"] {
|
|
||||||
color: var(--v2-state-fg-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-status[data-tone="muted"] {
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-row:where([data-disabled]) .settings-v2-wsl-distro-status {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-distro-empty {
|
|
||||||
padding: 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 440;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-card {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex: none;
|
|
||||||
align-self: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 64px;
|
|
||||||
padding: 14px 14px 16px 16px;
|
|
||||||
border: 0.5px solid var(--v2-border-border-base);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-card:focus-visible {
|
|
||||||
outline: 2px solid var(--v2-border-border-focus);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-card:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-icon {
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: var(--v2-icon-icon-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-copy {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-title {
|
|
||||||
user-select: none;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 16px;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-description {
|
|
||||||
user-select: none;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 440;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 12px;
|
|
||||||
letter-spacing: 0.05px;
|
|
||||||
font-variation-settings: "slnt" 0;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-chevron {
|
|
||||||
display: flex;
|
|
||||||
flex: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
color: var(--v2-icon-icon-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex: none;
|
|
||||||
align-self: stretch;
|
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
max-height: 220px;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong);
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-picker {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-search {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-catalog-row {
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-wsl-loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 48px 24px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 440;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import {
|
import { enterWslOpencodeStep, wslOpencodeAction, wslRuntimeRetryable } from "./settings-model"
|
||||||
addableProbePlan,
|
|
||||||
addServerProbePlan,
|
|
||||||
addServerViewModel,
|
|
||||||
autoProbePlan,
|
|
||||||
createProbeFailureGate,
|
|
||||||
runAddableProbePlan,
|
|
||||||
wslOpencodeAction,
|
|
||||||
wslRuntimeRetryable,
|
|
||||||
} from "./settings-model"
|
|
||||||
import type { WslServersState } from "./types"
|
|
||||||
|
|
||||||
const readyWslState = readyState()
|
|
||||||
|
|
||||||
function readyState(input: Partial<WslServersState> = {}): WslServersState {
|
|
||||||
return {
|
|
||||||
runtime: { available: true, version: "2.4.13.0", error: null },
|
|
||||||
installed: [],
|
|
||||||
online: [],
|
|
||||||
distroProbes: {},
|
|
||||||
opencodeChecks: {},
|
|
||||||
pendingRestart: false,
|
|
||||||
servers: [],
|
|
||||||
job: null,
|
|
||||||
...input,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("WSL server settings presentation", () => {
|
describe("WSL server settings presentation", () => {
|
||||||
test("retries only settled unsuccessful runtimes", () => {
|
test("retries only settled unsuccessful runtimes", () => {
|
||||||
@@ -71,161 +45,13 @@ describe("WSL server settings presentation", () => {
|
|||||||
).toBeUndefined()
|
).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("plans addable distro probes with the selected distro first", () => {
|
test("probes the selected distro before entering the OpenCode step", async () => {
|
||||||
const plan = addableProbePlan({
|
const calls: string[] = []
|
||||||
state: readyWslState,
|
await enterWslOpencodeStep(
|
||||||
view: "main",
|
"Debian",
|
||||||
adding: false,
|
async (distro) => calls.push(distro),
|
||||||
selectedDistro: "Ubuntu",
|
(step) => calls.push(step),
|
||||||
addableInstalledDistros: [
|
)
|
||||||
{ name: "Debian", version: 2, isDefault: true },
|
expect(calls).toEqual(["Debian", "opencode"])
|
||||||
{ name: "Ubuntu", version: 2, isDefault: false },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(plan?.key).toBe("distro:Ubuntu|distro:Debian")
|
|
||||||
expect(plan?.distros).toEqual(["Ubuntu", "Debian"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("plans bootstrap probes for missing runtime and initial distro lists", () => {
|
|
||||||
expect(autoProbePlan({ state: undefined, busy: false })).toBeUndefined()
|
|
||||||
expect(autoProbePlan({ state: { ...readyWslState, runtime: null }, busy: false })).toEqual({
|
|
||||||
key: "runtime",
|
|
||||||
action: "probeRuntime",
|
|
||||||
})
|
|
||||||
expect(autoProbePlan({ state: readyWslState, busy: false })).toEqual({
|
|
||||||
key: "distros",
|
|
||||||
action: "refreshDistros",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("uses one command plan for bootstrap before addable distro probing", () => {
|
|
||||||
expect(
|
|
||||||
addServerProbePlan({
|
|
||||||
state: { ...readyWslState, runtime: null },
|
|
||||||
view: "main",
|
|
||||||
adding: false,
|
|
||||||
busy: false,
|
|
||||||
selectedDistro: null,
|
|
||||||
addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }],
|
|
||||||
}),
|
|
||||||
).toEqual({ kind: "auto", key: "auto:runtime", plan: { key: "runtime", action: "probeRuntime" } })
|
|
||||||
|
|
||||||
expect(
|
|
||||||
addServerProbePlan({
|
|
||||||
state: {
|
|
||||||
...readyWslState,
|
|
||||||
installed: [{ name: "Debian", version: 2, isDefault: true }],
|
|
||||||
online: [{ name: "Ubuntu", label: "Ubuntu" }],
|
|
||||||
},
|
|
||||||
view: "main",
|
|
||||||
adding: false,
|
|
||||||
busy: false,
|
|
||||||
selectedDistro: "Debian",
|
|
||||||
addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }],
|
|
||||||
}),
|
|
||||||
).toEqual({ kind: "addable", key: "addable:distro:Debian", plan: { key: "distro:Debian", distros: ["Debian"] } })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not accept the same failed probe command until reset", () => {
|
|
||||||
const gate = createProbeFailureGate()
|
|
||||||
|
|
||||||
expect(gate.accepts("addable:distro:Debian")).toBe(true)
|
|
||||||
gate.settle("addable:distro:Debian", new Error("wsl failed"))
|
|
||||||
expect(gate.accepts("addable:distro:Debian")).toBe(false)
|
|
||||||
expect(gate.accepts("addable:distro:Ubuntu")).toBe(true)
|
|
||||||
gate.reset()
|
|
||||||
expect(gate.accepts("addable:distro:Debian")).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("keeps default distro selection stable while probes resolve", () => {
|
|
||||||
const model = addServerViewModel({
|
|
||||||
state: {
|
|
||||||
...readyWslState,
|
|
||||||
installed: [
|
|
||||||
{ name: "Debian", version: 2, isDefault: true },
|
|
||||||
{ name: "Ubuntu", version: 2, isDefault: false },
|
|
||||||
],
|
|
||||||
online: [{ name: "Alpine", label: "Alpine Linux" }],
|
|
||||||
distroProbes: {
|
|
||||||
Ubuntu: { name: "Ubuntu", canExecute: true, hasBash: true, hasCurl: true, error: null },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
view: "main",
|
|
||||||
selectedDistro: null,
|
|
||||||
catalogSearch: "",
|
|
||||||
catalogTarget: null,
|
|
||||||
adding: false,
|
|
||||||
probingAddable: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(model.selectedDistro).toBe("Debian")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("keeps the dialog busy across serial addable probe job gaps", () => {
|
|
||||||
const model = addServerViewModel({
|
|
||||||
state: readyState({
|
|
||||||
installed: [{ name: "Debian", version: 2, isDefault: true }],
|
|
||||||
online: [{ name: "Ubuntu", label: "Ubuntu" }],
|
|
||||||
}),
|
|
||||||
view: "main",
|
|
||||||
selectedDistro: null,
|
|
||||||
catalogSearch: "",
|
|
||||||
catalogTarget: null,
|
|
||||||
adding: false,
|
|
||||||
probingAddable: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(model.busy).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not report ready when OpenCode is present but cannot run", () => {
|
|
||||||
const model = addServerViewModel({
|
|
||||||
state: {
|
|
||||||
...readyWslState,
|
|
||||||
installed: [{ name: "Debian", version: 2, isDefault: true }],
|
|
||||||
online: [{ name: "Ubuntu", label: "Ubuntu" }],
|
|
||||||
distroProbes: {
|
|
||||||
Debian: { name: "Debian", canExecute: true, hasBash: true, hasCurl: true, error: null },
|
|
||||||
},
|
|
||||||
opencodeChecks: {
|
|
||||||
Debian: {
|
|
||||||
distro: "Debian",
|
|
||||||
resolvedPath: "/home/me/.opencode/bin/opencode",
|
|
||||||
version: null,
|
|
||||||
expectedVersion: "1.2.3",
|
|
||||||
matchesDesktop: null,
|
|
||||||
error: "opencode is installed but could not run",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
view: "main",
|
|
||||||
selectedDistro: null,
|
|
||||||
catalogSearch: "",
|
|
||||||
catalogTarget: null,
|
|
||||||
adding: false,
|
|
||||||
probingAddable: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(model.distroStatuses.Debian).toEqual({
|
|
||||||
label: { key: "wsl.onboarding.installOpencode" },
|
|
||||||
tone: "warning",
|
|
||||||
})
|
|
||||||
expect(model.primaryButton.action).toBe("install-opencode")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("delegates addable probe plans to one batch command", async () => {
|
|
||||||
const calls: string[][] = []
|
|
||||||
|
|
||||||
await runAddableProbePlan({
|
|
||||||
plan: { key: "distro:Debian|distro:Ubuntu", distros: ["Debian", "Ubuntu"] },
|
|
||||||
api: {
|
|
||||||
probeAddable: async (distros) => {
|
|
||||||
calls.push(distros)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(calls).toEqual([["Debian", "Ubuntu"]])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,341 +1,19 @@
|
|||||||
import fuzzysort from "fuzzysort"
|
import type { WslOpencodeCheck, WslServerRuntime } from "./types"
|
||||||
import type {
|
|
||||||
WslInstalledDistro,
|
|
||||||
WslOnlineDistro,
|
|
||||||
WslOpencodeCheck,
|
|
||||||
WslServersPlatform,
|
|
||||||
WslServerRuntime,
|
|
||||||
WslServersState,
|
|
||||||
} from "./types"
|
|
||||||
|
|
||||||
export type AddServerText = {
|
|
||||||
key: string
|
|
||||||
params?: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DistroStatusTone = "success" | "warning" | "muted"
|
|
||||||
|
|
||||||
export type DistroStatus = {
|
|
||||||
label: AddServerText
|
|
||||||
tone: DistroStatusTone
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddServerPrimaryButton = {
|
|
||||||
variant: "neutral" | "contrast"
|
|
||||||
label: AddServerText
|
|
||||||
disabled: boolean
|
|
||||||
action: "install-opencode" | "add" | null
|
|
||||||
loading: boolean
|
|
||||||
width: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddServerRuntimeState = "loading" | "pendingRestart" | "checking" | "unavailable" | "ready"
|
|
||||||
|
|
||||||
export type AddableProbePlan = {
|
|
||||||
key: string
|
|
||||||
distros: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AutoProbePlan = { key: "runtime"; action: "probeRuntime" } | { key: "distros"; action: "refreshDistros" }
|
|
||||||
|
|
||||||
export type AddServerProbePlan =
|
|
||||||
| { kind: "auto"; key: string; plan: AutoProbePlan }
|
|
||||||
| { kind: "addable"; key: string; plan: AddableProbePlan }
|
|
||||||
|
|
||||||
export type WslAddServerView = "main" | "catalog"
|
|
||||||
|
|
||||||
function isHiddenDistro(name: string) {
|
|
||||||
return /^docker-desktop(?:-data)?$/i.test(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const wslRuntimeRetryable = (runtime: WslServerRuntime) =>
|
export const wslRuntimeRetryable = (runtime: WslServerRuntime) =>
|
||||||
runtime.kind === "failed" || runtime.kind === "stopped"
|
runtime.kind === "failed" || runtime.kind === "stopped"
|
||||||
|
|
||||||
|
export async function enterWslOpencodeStep(
|
||||||
|
distro: string,
|
||||||
|
probe: (distro: string) => Promise<unknown>,
|
||||||
|
select: (step: "opencode") => void,
|
||||||
|
) {
|
||||||
|
await probe(distro)
|
||||||
|
select("opencode")
|
||||||
|
}
|
||||||
|
|
||||||
export function wslOpencodeAction(check?: WslOpencodeCheck) {
|
export function wslOpencodeAction(check?: WslOpencodeCheck) {
|
||||||
if (!check) return
|
if (!check) return
|
||||||
if (!check.resolvedPath) return "Install OpenCode"
|
if (!check.resolvedPath) return "Install OpenCode"
|
||||||
if (check.matchesDesktop === false) return "Update OpenCode"
|
if (check.matchesDesktop === false) return "Update OpenCode"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wslDistroReady(state: WslServersState | undefined, name: string) {
|
|
||||||
const installed = state?.installed.find((item) => item.name === name)
|
|
||||||
const probe = state?.distroProbes[name]
|
|
||||||
if (!probe || !installed) return false
|
|
||||||
if (installed.version === 1) return false
|
|
||||||
return probe.canExecute && probe.hasBash && probe.hasCurl
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addServerViewModel(input: {
|
|
||||||
state: WslServersState | undefined
|
|
||||||
view: WslAddServerView
|
|
||||||
selectedDistro: string | null
|
|
||||||
catalogSearch: string
|
|
||||||
catalogTarget: string | null
|
|
||||||
adding: boolean
|
|
||||||
probingAddable: boolean
|
|
||||||
}) {
|
|
||||||
const state = input.state
|
|
||||||
const visibleInstalledDistros = (state?.installed ?? []).filter((item) => !isHiddenDistro(item.name))
|
|
||||||
const visibleOnlineDistros = (state?.online ?? []).filter((item) => !isHiddenDistro(item.name))
|
|
||||||
const existingServerDistros = new Set((state?.servers ?? []).map((item) => item.config.distro))
|
|
||||||
const addableInstalledDistros = visibleInstalledDistros.filter((item) => !existingServerDistros.has(item.name))
|
|
||||||
const selectedDistro = addServerSelectedDistro(input.selectedDistro, visibleInstalledDistros, addableInstalledDistros)
|
|
||||||
const opencodeCheck = selectedDistro ? (state?.opencodeChecks[selectedDistro] ?? null) : null
|
|
||||||
const installableDistros = addServerInstallableDistros(visibleInstalledDistros, visibleOnlineDistros)
|
|
||||||
const filteredInstallableDistros = addServerFilteredInstallableDistros(installableDistros, input.catalogSearch)
|
|
||||||
const catalogTarget = addServerCatalogTarget(input.catalogTarget, filteredInstallableDistros)
|
|
||||||
const busy = !!state?.job || input.adding || input.probingAddable
|
|
||||||
|
|
||||||
return {
|
|
||||||
busy,
|
|
||||||
runtimeState: addServerRuntimeState(state),
|
|
||||||
visibleInstalledDistros,
|
|
||||||
visibleOnlineDistros,
|
|
||||||
addableInstalledDistros,
|
|
||||||
selectedDistro,
|
|
||||||
opencodeCheck,
|
|
||||||
wslReady: !!state?.runtime?.available && !state?.pendingRestart,
|
|
||||||
distroStatuses: Object.fromEntries(
|
|
||||||
addableInstalledDistros.flatMap((item) => {
|
|
||||||
const status = addServerDistroStatus({ state, name: item.name, probingAddable: input.probingAddable })
|
|
||||||
if (!status) return []
|
|
||||||
return [[item.name, status]]
|
|
||||||
}),
|
|
||||||
) as Record<string, DistroStatus | undefined>,
|
|
||||||
primaryButton: addServerPrimaryButton({
|
|
||||||
state,
|
|
||||||
selectedDistro,
|
|
||||||
opencodeCheck,
|
|
||||||
adding: input.adding,
|
|
||||||
probingAddable: input.probingAddable,
|
|
||||||
}),
|
|
||||||
installableDistros,
|
|
||||||
filteredInstallableDistros,
|
|
||||||
catalogTarget,
|
|
||||||
installingCatalogDistro: state?.job?.kind === "install-distro",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerSelectedDistro(
|
|
||||||
selected: string | null,
|
|
||||||
visibleInstalledDistros: WslInstalledDistro[],
|
|
||||||
addableInstalledDistros: WslInstalledDistro[],
|
|
||||||
) {
|
|
||||||
if (selected && addableInstalledDistros.some((item) => item.name === selected && item.version !== 1)) return selected
|
|
||||||
const defaultDistro = visibleInstalledDistros.find((item) => item.isDefault)
|
|
||||||
if (
|
|
||||||
defaultDistro &&
|
|
||||||
defaultDistro.version !== 1 &&
|
|
||||||
addableInstalledDistros.some((item) => item.name === defaultDistro.name)
|
|
||||||
) {
|
|
||||||
return defaultDistro.name
|
|
||||||
}
|
|
||||||
return addableInstalledDistros.find((item) => item.version !== 1)?.name ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerRuntimeState(state: WslServersState | undefined): AddServerRuntimeState {
|
|
||||||
if (!state) return "loading"
|
|
||||||
if (state.pendingRestart) return "pendingRestart"
|
|
||||||
if (!state.runtime) return "checking"
|
|
||||||
if (!state.runtime.available) return "unavailable"
|
|
||||||
return "ready"
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerDistroStatus(input: {
|
|
||||||
state: WslServersState | undefined
|
|
||||||
name: string
|
|
||||||
probingAddable: boolean
|
|
||||||
}): DistroStatus | undefined {
|
|
||||||
const installed = input.state?.installed.find((item) => item.name === input.name)
|
|
||||||
if (installed?.version === 1) return { label: { key: "wsl.onboarding.distroStatus.unsupported" }, tone: "muted" }
|
|
||||||
const job = input.state?.job
|
|
||||||
const probe = input.state?.distroProbes[input.name]
|
|
||||||
if (!probe) {
|
|
||||||
if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) {
|
|
||||||
return checkingStatus()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!probe.canExecute) {
|
|
||||||
if (!installed) {
|
|
||||||
return { label: { key: "wsl.onboarding.distroNotInstalled", params: { distro: input.name } }, tone: "warning" }
|
|
||||||
}
|
|
||||||
return { label: { key: "wsl.onboarding.openDistroOnce", params: { distro: input.name } }, tone: "warning" }
|
|
||||||
}
|
|
||||||
if (!probe.hasBash || !probe.hasCurl) {
|
|
||||||
return { label: { key: "wsl.onboarding.distroStatus.missingTools" }, tone: "warning" }
|
|
||||||
}
|
|
||||||
const check = input.state?.opencodeChecks[input.name]
|
|
||||||
if (!check) {
|
|
||||||
if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) {
|
|
||||||
return checkingStatus()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (check.matchesDesktop === false) return { label: { key: "wsl.onboarding.updateOpencode" }, tone: "warning" }
|
|
||||||
if (!check.resolvedPath) return { label: { key: "wsl.onboarding.distroStatus.opencodeMissing" }, tone: "warning" }
|
|
||||||
if (check.error) return { label: { key: "wsl.onboarding.installOpencode" }, tone: "warning" }
|
|
||||||
return { label: { key: "wsl.onboarding.distroStatus.ready" }, tone: "success" }
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkingStatus(): DistroStatus {
|
|
||||||
return { label: { key: "wsl.onboarding.distroStatus.checking" }, tone: "muted" }
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerPrimaryButton(input: {
|
|
||||||
state: WslServersState | undefined
|
|
||||||
selectedDistro: string | null
|
|
||||||
opencodeCheck: WslOpencodeCheck | null
|
|
||||||
adding: boolean
|
|
||||||
probingAddable: boolean
|
|
||||||
}): AddServerPrimaryButton {
|
|
||||||
const ready = !!input.selectedDistro && wslDistroReady(input.state, input.selectedDistro)
|
|
||||||
const probingSelected = input.probingAddable && !addServerSelectedDistroSettled(input.state, input.selectedDistro)
|
|
||||||
const probingOpencode =
|
|
||||||
probingSelected ||
|
|
||||||
(ready &&
|
|
||||||
(!input.opencodeCheck ||
|
|
||||||
(!!input.selectedDistro &&
|
|
||||||
input.state?.job?.kind === "probe-addable" &&
|
|
||||||
input.state.job.distros.includes(input.selectedDistro))))
|
|
||||||
const installingOpencode =
|
|
||||||
input.state?.job?.kind === "install-opencode" && input.state.job.distro === input.selectedDistro
|
|
||||||
if (!ready || probingOpencode) {
|
|
||||||
return {
|
|
||||||
variant: "contrast",
|
|
||||||
label: probingSelected ? { key: "wsl.onboarding.distroStatus.checking" } : { key: "wsl.server.add" },
|
|
||||||
disabled: true,
|
|
||||||
action: null,
|
|
||||||
loading: probingSelected,
|
|
||||||
width: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!addServerOpencodeReady(input.opencodeCheck)) {
|
|
||||||
const update = !!input.opencodeCheck?.resolvedPath && input.opencodeCheck.matchesDesktop === false
|
|
||||||
return {
|
|
||||||
variant: "neutral",
|
|
||||||
label: installingOpencode
|
|
||||||
? { key: "wsl.onboarding.updatingOpencode" }
|
|
||||||
: update
|
|
||||||
? { key: "wsl.onboarding.updateOpencode" }
|
|
||||||
: { key: "wsl.onboarding.installOpencode" },
|
|
||||||
disabled: !!input.state?.job || input.adding,
|
|
||||||
action: "install-opencode",
|
|
||||||
loading: installingOpencode,
|
|
||||||
width: update ? "138px" : "129px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
variant: "contrast",
|
|
||||||
label: input.adding ? { key: "wsl.onboarding.adding" } : { key: "wsl.server.add" },
|
|
||||||
disabled: input.adding || !!input.state?.job,
|
|
||||||
action: "add",
|
|
||||||
loading: input.adding,
|
|
||||||
width: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerOpencodeReady(check: WslOpencodeCheck | null) {
|
|
||||||
return !!check?.resolvedPath && check.matchesDesktop !== false && !check.error
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerSelectedDistroSettled(state: WslServersState | undefined, selectedDistro: string | null) {
|
|
||||||
if (!selectedDistro) return false
|
|
||||||
const installed = state?.installed.find((item) => item.name === selectedDistro)
|
|
||||||
if (installed?.version === 1) return false
|
|
||||||
if (!state?.distroProbes[selectedDistro]) return false
|
|
||||||
if (!wslDistroReady(state, selectedDistro)) return true
|
|
||||||
return !!state.opencodeChecks[selectedDistro]
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerInstallableDistros(installedDistros: WslInstalledDistro[], onlineDistros: WslOnlineDistro[]) {
|
|
||||||
const installed = new Set(installedDistros.map((item) => item.name))
|
|
||||||
const hasVersionedUbuntu = onlineDistros.some((item) => /^Ubuntu-\d/.test(item.name))
|
|
||||||
return onlineDistros
|
|
||||||
.filter((item) => !installed.has(item.name))
|
|
||||||
.filter((item) => item.name !== "Ubuntu" || !hasVersionedUbuntu)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerFilteredInstallableDistros(installableDistros: WslOnlineDistro[], search: string) {
|
|
||||||
const query = search.trim()
|
|
||||||
if (!query) return installableDistros
|
|
||||||
return fuzzysort.go(query, installableDistros, { keys: ["label", "name"] }).map((item) => item.obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addServerCatalogTarget(target: string | null, distros: WslOnlineDistro[]) {
|
|
||||||
if (target && distros.some((item) => item.name === target)) return target
|
|
||||||
return distros[0]?.name ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addableProbePlan(input: {
|
|
||||||
state: WslServersState | undefined
|
|
||||||
view: WslAddServerView
|
|
||||||
adding: boolean
|
|
||||||
selectedDistro: string | null
|
|
||||||
addableInstalledDistros: WslInstalledDistro[]
|
|
||||||
}) {
|
|
||||||
const state = input.state
|
|
||||||
if (!state?.runtime?.available || state.pendingRestart || input.view !== "main" || input.adding) return
|
|
||||||
if (state.job) return
|
|
||||||
const ordered = input.selectedDistro
|
|
||||||
? [
|
|
||||||
...input.addableInstalledDistros.filter((item) => item.name === input.selectedDistro),
|
|
||||||
...input.addableInstalledDistros.filter((item) => item.name !== input.selectedDistro),
|
|
||||||
]
|
|
||||||
: input.addableInstalledDistros
|
|
||||||
const pending = ordered.flatMap((item) => {
|
|
||||||
if (item.version === 1) return []
|
|
||||||
if (!state.distroProbes[item.name]) return [`distro:${item.name}`]
|
|
||||||
if (wslDistroReady(state, item.name) && !state.opencodeChecks[item.name]) return [`opencode:${item.name}`]
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
if (!pending.length) return
|
|
||||||
return {
|
|
||||||
key: pending.join("|"),
|
|
||||||
distros: ordered.filter((item) => item.version !== 1).map((item) => item.name),
|
|
||||||
} satisfies AddableProbePlan
|
|
||||||
}
|
|
||||||
|
|
||||||
export function autoProbePlan(input: { state: WslServersState | undefined; busy: boolean }) {
|
|
||||||
if (!input.state || input.busy || input.state.pendingRestart) return
|
|
||||||
if (!input.state.runtime) return { key: "runtime", action: "probeRuntime" } satisfies AutoProbePlan
|
|
||||||
if (!input.state.runtime.available) return
|
|
||||||
if (input.state.installed.length || input.state.online.length) return
|
|
||||||
return { key: "distros", action: "refreshDistros" } satisfies AutoProbePlan
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addServerProbePlan(input: {
|
|
||||||
state: WslServersState | undefined
|
|
||||||
view: WslAddServerView
|
|
||||||
adding: boolean
|
|
||||||
busy: boolean
|
|
||||||
selectedDistro: string | null
|
|
||||||
addableInstalledDistros: WslInstalledDistro[]
|
|
||||||
}) {
|
|
||||||
const auto = autoProbePlan({ state: input.state, busy: input.busy })
|
|
||||||
if (auto) return { kind: "auto", key: `auto:${auto.key}`, plan: auto } satisfies AddServerProbePlan
|
|
||||||
const addable = addableProbePlan(input)
|
|
||||||
if (addable) return { kind: "addable", key: `addable:${addable.key}`, plan: addable } satisfies AddServerProbePlan
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProbeFailureGate() {
|
|
||||||
let failed: string | undefined
|
|
||||||
return {
|
|
||||||
accepts(key: string) {
|
|
||||||
return key !== failed
|
|
||||||
},
|
|
||||||
settle(key: string, error?: unknown) {
|
|
||||||
if (error) failed = key
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
failed = undefined
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runAddableProbePlan(input: {
|
|
||||||
plan: AddableProbePlan
|
|
||||||
api: Pick<WslServersPlatform, "probeAddable">
|
|
||||||
}) {
|
|
||||||
await input.api.probeAddable(input.plan.distros)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
|
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
|
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||||
@@ -28,7 +30,17 @@ export function AddServerMenu(props: { onAddServer: () => void }) {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const openAddWsl = () => {
|
const openAddWsl = () => {
|
||||||
dialog.push(() => <DialogAddWslServer />)
|
dialog.push(() => (
|
||||||
|
<Dialog size="large" fit class="settings-v2-wsl-dialog">
|
||||||
|
<DialogHeader hideClose={true}>
|
||||||
|
<DialogTitle>{language.t("wsl.server.add")}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DividerV2 />
|
||||||
|
<DialogBody>
|
||||||
|
<DialogAddWslServer />
|
||||||
|
</DialogBody>
|
||||||
|
</Dialog>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ export type WslJob =
|
|||||||
| { kind: "distros"; startedAt: number }
|
| { kind: "distros"; startedAt: number }
|
||||||
| { kind: "install-wsl"; startedAt: number }
|
| { kind: "install-wsl"; startedAt: number }
|
||||||
| { kind: "install-distro"; distro: string; startedAt: number }
|
| { kind: "install-distro"; distro: string; startedAt: number }
|
||||||
| { kind: "probe-addable"; distros: string[]; startedAt: number }
|
| { kind: "probe-distro"; distro: string; startedAt: number }
|
||||||
|
| { kind: "probe-opencode"; distro: string; startedAt: number }
|
||||||
| { kind: "install-opencode"; distro: string; startedAt: number }
|
| { kind: "install-opencode"; distro: string; startedAt: number }
|
||||||
|
|
||||||
export type WslServersState = {
|
export type WslServersState = {
|
||||||
@@ -76,7 +77,8 @@ export type WslServersPlatform = {
|
|||||||
refreshDistros(): Promise<void>
|
refreshDistros(): Promise<void>
|
||||||
installWsl(): Promise<void>
|
installWsl(): Promise<void>
|
||||||
installDistro(name: string): Promise<void>
|
installDistro(name: string): Promise<void>
|
||||||
probeAddable(distros: string[]): Promise<void>
|
probeDistro(name: string): Promise<void>
|
||||||
|
probeOpencode(name: string): Promise<void>
|
||||||
installOpencode(name: string): Promise<void>
|
installOpencode(name: string): Promise<void>
|
||||||
openTerminal(name: string): Promise<void>
|
openTerminal(name: string): Promise<void>
|
||||||
addServer(distro: string): Promise<WslServerConfig>
|
addServer(distro: string): Promise<WslServerConfig>
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
# V2 CLI and TUI development guide
|
|
||||||
|
|
||||||
## Migration context
|
|
||||||
|
|
||||||
- The TUI is being ported from legacy APIs to the new V2 APIs. New and migrated TUI behavior should use `sdk.client.v2` and the location-scoped data in `packages/tui/src/context/data.tsx` instead of adding dependencies on legacy sync state.
|
|
||||||
- Preserve established TUI behavior unless the task intentionally changes it. When behavior, copy, keyboard interaction, or layout is unclear, compare the local V2 TUI with the latest released legacy TUI.
|
|
||||||
- Run both versions in separate Terminal Control sessions and save PNG-only captures at equivalent states:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From packages/cli: local V2 TUI
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
|
|
||||||
# Released legacy TUI behavior reference
|
|
||||||
termctrl start opencode-legacy --host opentui --cols 112 --rows 34 -- bunx opencode-ai@latest
|
|
||||||
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2.png
|
|
||||||
termctrl save opencode-legacy --format png --out /tmp/opencode/legacy.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use the same viewport and send equivalent inputs to both sessions before comparing screenshots. The released CLI is a behavioral reference, not a source of V2 API design; keep the local implementation on V2 endpoints.
|
|
||||||
- Stop both sessions after comparison: `termctrl stop opencode-v2-dev` and `termctrl stop opencode-legacy`.
|
|
||||||
|
|
||||||
## Interactive debugging
|
|
||||||
|
|
||||||
- This package is the V2 CLI adapter. Run its `dev` script when testing the TUI; do not use the repository-root `bun dev`, which launches the legacy `packages/opencode` CLI.
|
|
||||||
- Run commands from `packages/cli`. Use `bun dev` for most debugging so the TUI starts with a private V2 server.
|
|
||||||
- Use `termctrl` for interactive checks instead of starting the TUI as a blocking foreground process. It provides a real PTY, handles OpenTUI's host handshake, and can save reviewable screenshots.
|
|
||||||
- Use a dedicated session name and do not reuse or kill an unrelated session.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-dev --host opentui --cols 112 --rows 34 -- bun dev
|
|
||||||
termctrl wait opencode-v2-dev "Ask anything" --timeout 20000
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Wait for visible text before interacting instead of relying on fixed sleeps. Use the text expected from the screen under test, such as `Ask anything` or `Connect a provider`.
|
|
||||||
- Drive the running TUI with `termctrl send`. Prefix typed input with `text:` and send control keys separately so the interaction matches real terminal input.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl send opencode-v2-dev 'text:example prompt' enter
|
|
||||||
termctrl send opencode-v2-dev ctrl-c
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `termctrl show` after each meaningful interaction and inspect the full visible screen for rendering errors, stale state, error toasts, and unexpected exits.
|
|
||||||
- Save PNG evidence for every user-visible bug and fix. Do not save text captures; inspect the rendered PNG. Write temporary captures outside the repository unless the artifact is intended to be committed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl save opencode-v2-dev --format png --out /tmp/opencode/v2-tui.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- For resize-sensitive changes, resize the viewport, wait for the expected content, and capture the screen again:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl resize opencode-v2-dev --cols 100 --rows 30
|
|
||||||
termctrl show opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
- Source changes may require restarting the process. Use `termctrl restart opencode-v2-dev` rather than assuming the running TUI reloaded the change.
|
|
||||||
- To exercise background-service behavior, use `bun dev service start`, `bun dev service status`, and `bun dev service stop`.
|
|
||||||
- Always clean up the Terminal Control session when the check is complete:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl stop opencode-v2-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server/API debugging
|
|
||||||
|
|
||||||
- Use `bun dev api --help` from `packages/cli` to inspect the API debugging command. It sends one request to the V2 server using the same daemon discovery/auth path as the CLI.
|
|
||||||
- Use `bun dev api` to introspect the server-side data backing the TUI. This is useful when debugging UI bugs: compare what the screen renders with the raw session, message, event, agent, or health data returned by the API to determine whether the bug is in the server state, the client data layer, or the TUI rendering.
|
|
||||||
- `bun dev api` accepts either an OpenAPI operation ID or a raw HTTP method plus path:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun dev api get /health
|
|
||||||
bun dev api get /openapi.json
|
|
||||||
bun dev api <operationId> --param key=value
|
|
||||||
```
|
|
||||||
|
|
||||||
- Pass JSON request bodies with `--data`/`-d`; the command sets `content-type: application/json` automatically unless you provide a header. Add extra headers with `--header`/`-H name:value`.
|
|
||||||
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
|
|
||||||
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
|
|
||||||
|
|
||||||
## Debugger
|
|
||||||
|
|
||||||
- To debug the V2 CLI or TUI with Bun's inspector, launch the CLI entrypoint through Terminal Control with an inspector URL, then attach a debugger to that URL:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
termctrl start opencode-v2-debug --host opentui --cols 112 --rows 34 -- \
|
|
||||||
bun run --inspect=ws://localhost:6499/ src/index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
- Use `--inspect-wait` or `--inspect-brk` when execution must pause until the debugger attaches.
|
|
||||||
- Use `termctrl logs opencode-v2-debug` for inspector output or startup failures emitted before the TUI renderer starts. Use `termctrl show` for the visible full-screen TUI.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- Run `bun typecheck` from `packages/cli` after CLI adapter changes.
|
|
||||||
- Run `bun typecheck` and `bun test` from `packages/tui` after shared TUI changes. Do not run tests from the repository root.
|
|
||||||
- Treat automated checks and Terminal Control smoke tests as complementary. For user-visible changes, verify initial render, the changed interaction, Ctrl-C exit behavior, and save a screenshot of the corrected state.
|
|
||||||
@@ -31,11 +31,11 @@ function run(target) {
|
|||||||
|
|
||||||
const envPath = process.env.OPENCODE_BIN_PATH
|
const envPath = process.env.OPENCODE_BIN_PATH
|
||||||
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
||||||
const cached = path.join(scriptDir, ".opencode2")
|
const cached = path.join(scriptDir, ".lildax")
|
||||||
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
||||||
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
||||||
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
||||||
const binary = platform === "windows" ? "opencode2.exe" : "opencode2"
|
const binary = platform === "windows" ? "lildax.exe" : "lildax"
|
||||||
|
|
||||||
function supportsAvx2() {
|
function supportsAvx2() {
|
||||||
if (arch !== "x64") return false
|
if (arch !== "x64") return false
|
||||||
@@ -121,7 +121,7 @@ function findBinary(startDir) {
|
|||||||
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
console.error(
|
console.error(
|
||||||
"It seems that your package manager failed to install the right opencode2 CLI package. Try manually installing " +
|
"It seems that your package manager failed to install the right lildax CLI package. Try manually installing " +
|
||||||
names.map((name) => `"${name}"`).join(" or ") +
|
names.map((name) => `"${name}"`).join(" or ") +
|
||||||
" package",
|
" package",
|
||||||
)
|
)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode2": "./bin/opencode2.cjs"
|
"lildax": "./bin/lildax.cjs"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"bin"
|
"bin"
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
@@ -26,15 +25,12 @@
|
|||||||
"@opentui/solid": "catalog:",
|
"@opentui/solid": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"jsonc-parser": "3.3.1",
|
|
||||||
"semver": "catalog:",
|
|
||||||
"solid-js": "catalog:"
|
"solid-js": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/semver": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:"
|
"@typescript/native-preview": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import pkg from "../package.json"
|
|||||||
import { modelsData } from "./generate"
|
import { modelsData } from "./generate"
|
||||||
|
|
||||||
const dir = path.resolve(import.meta.dirname, "..")
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
const binary = "opencode2"
|
const binary = "lildax"
|
||||||
process.chdir(dir)
|
process.chdir(dir)
|
||||||
|
|
||||||
await rm("dist", { recursive: true, force: true })
|
await rm("dist", { recursive: true, force: true })
|
||||||
|
|||||||
@@ -25,15 +25,14 @@ for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" }
|
|||||||
}
|
}
|
||||||
console.log("binaries", binaries)
|
console.log("binaries", binaries)
|
||||||
const version = Object.values(binaries)[0]
|
const version = Object.values(binaries)[0]
|
||||||
const name = pkg.name
|
|
||||||
|
|
||||||
await $`mkdir -p ./dist/${name}/bin`
|
await $`mkdir -p ./dist/${pkg.name}/bin`
|
||||||
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
|
await $`cp ./bin/lildax.cjs ./dist/${pkg.name}/bin/lildax`
|
||||||
await Bun.file(`./dist/${name}/package.json`).write(
|
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
name,
|
name: pkg.name,
|
||||||
bin: { opencode2: "./bin/opencode2" },
|
bin: { lildax: "./bin/lildax" },
|
||||||
version,
|
version,
|
||||||
license: pkg.license,
|
license: pkg.license,
|
||||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||||
@@ -51,4 +50,4 @@ await Promise.all(
|
|||||||
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
|
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await publish(`./dist/${name}`, name, version)
|
await publish(`./dist/${pkg.name}`, pkg.name, version)
|
||||||
|
|||||||
@@ -5,26 +5,6 @@ declare const OPENCODE_CLI_NAME: string | undefined
|
|||||||
|
|
||||||
export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
|
export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
|
||||||
description: "OpenCode 2.0 preview command line interface",
|
description: "OpenCode 2.0 preview command line interface",
|
||||||
params: {
|
|
||||||
directory: Argument.string("directory").pipe(
|
|
||||||
Argument.withDescription("Directory to start OpenCode in"),
|
|
||||||
Argument.optional,
|
|
||||||
),
|
|
||||||
standalone: Flag.boolean("standalone").pipe(
|
|
||||||
Flag.withDescription("Run with a private server instead of the background service"),
|
|
||||||
Flag.withDefault(false),
|
|
||||||
),
|
|
||||||
continue: Flag.boolean("continue").pipe(
|
|
||||||
Flag.withAlias("c"),
|
|
||||||
Flag.withDescription("Continue the last session"),
|
|
||||||
Flag.withDefault(false),
|
|
||||||
),
|
|
||||||
session: Flag.string("session").pipe(
|
|
||||||
Flag.withAlias("s"),
|
|
||||||
Flag.withDescription("Session ID to continue"),
|
|
||||||
Flag.optional,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
commands: [
|
commands: [
|
||||||
Spec.make("api", {
|
Spec.make("api", {
|
||||||
description: "Make a request to the running server",
|
description: "Make a request to the running server",
|
||||||
@@ -46,43 +26,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
|||||||
description: "Debugging and troubleshooting tools",
|
description: "Debugging and troubleshooting tools",
|
||||||
commands: [Spec.make("agents", { description: "List all agents" })],
|
commands: [Spec.make("agents", { description: "List all agents" })],
|
||||||
}),
|
}),
|
||||||
Spec.make("mcp", {
|
|
||||||
description: "Manage MCP (Model Context Protocol) servers",
|
|
||||||
commands: [
|
|
||||||
Spec.make("list", { description: "List configured MCP servers and their status" }),
|
|
||||||
Spec.make("add", {
|
|
||||||
description: "Add an MCP server to your configuration",
|
|
||||||
params: {
|
|
||||||
name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")),
|
|
||||||
command: Argument.string("command").pipe(
|
|
||||||
Argument.withDescription("Command and arguments for a local server, passed after --"),
|
|
||||||
Argument.variadic({ min: 0 }),
|
|
||||||
),
|
|
||||||
url: Flag.string("url").pipe(Flag.withDescription("URL for a remote MCP server"), Flag.optional),
|
|
||||||
header: Flag.keyValuePair("header").pipe(
|
|
||||||
Flag.withDescription("HTTP header for a remote server, as name=value"),
|
|
||||||
Flag.optional,
|
|
||||||
),
|
|
||||||
env: Flag.keyValuePair("env").pipe(
|
|
||||||
Flag.withDescription("Environment variable for a local server, as name=value"),
|
|
||||||
Flag.optional,
|
|
||||||
),
|
|
||||||
global: Flag.boolean("global").pipe(
|
|
||||||
Flag.withDescription("Write to the global config instead of the project config"),
|
|
||||||
Flag.withDefault(false),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Spec.make("auth", {
|
|
||||||
description: "Authenticate with an OAuth-capable remote MCP server",
|
|
||||||
params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) },
|
|
||||||
}),
|
|
||||||
Spec.make("logout", {
|
|
||||||
description: "Remove stored OAuth credentials for an MCP server",
|
|
||||||
params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Spec.make("migrate", { description: "Migrate v1 data to v2" }),
|
Spec.make("migrate", { description: "Migrate v1 data to v2" }),
|
||||||
Spec.make("service", {
|
Spec.make("service", {
|
||||||
description: "Manage the background server",
|
description: "Manage the background server",
|
||||||
@@ -91,27 +34,18 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
|||||||
Spec.make("restart", { description: "Restart the background server" }),
|
Spec.make("restart", { description: "Restart the background server" }),
|
||||||
Spec.make("status", { description: "Show background server status" }),
|
Spec.make("status", { description: "Show background server status" }),
|
||||||
Spec.make("stop", { description: "Stop the background server" }),
|
Spec.make("stop", { description: "Stop the background server" }),
|
||||||
Spec.make("get", {
|
Spec.make("password", {
|
||||||
description: "Get service configuration",
|
description: "Get or set the server password",
|
||||||
params: { key: Argument.string("key").pipe(Argument.optional) },
|
params: { value: Argument.string("value").pipe(Argument.optional) },
|
||||||
}),
|
|
||||||
Spec.make("set", {
|
|
||||||
description: "Set service configuration",
|
|
||||||
params: { key: Argument.string("key"), value: Argument.string("value") },
|
|
||||||
}),
|
|
||||||
Spec.make("unset", {
|
|
||||||
description: "Unset service configuration",
|
|
||||||
params: { key: Argument.string("key") },
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
Spec.make("serve", {
|
Spec.make("serve", {
|
||||||
description: "Start the v2 API server",
|
description: "Start the v2 API server",
|
||||||
params: {
|
params: {
|
||||||
hostname: Flag.string("hostname").pipe(Flag.optional),
|
hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")),
|
||||||
port: Flag.integer("port").pipe(Flag.optional),
|
port: Flag.integer("port").pipe(Flag.optional),
|
||||||
service: Flag.boolean("service").pipe(Flag.withDefault(false)),
|
register: Flag.boolean("register").pipe(Flag.withDefault(false)),
|
||||||
stdio: Flag.boolean("stdio").pipe(Flag.withDefault(false)),
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Effect, Option } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Daemon } from "../../services/daemon"
|
import { Daemon } from "../../services/daemon"
|
||||||
import { Standalone } from "../../services/standalone"
|
|
||||||
import { Updater } from "../../services/updater"
|
|
||||||
|
|
||||||
export default Runtime.handler(Commands, (input) =>
|
export default Runtime.handler(Commands, () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const directory = Option.getOrUndefined(input.directory)
|
|
||||||
if (directory !== undefined) process.chdir(directory)
|
|
||||||
const updater = yield* Updater.Service
|
|
||||||
yield* updater.check().pipe(Effect.forkScoped)
|
|
||||||
const daemon = yield* Daemon.Service
|
const daemon = yield* Daemon.Service
|
||||||
const transport = yield* (input.standalone ? Standalone.transport() : daemon.transport())
|
const transport = yield* daemon.transport()
|
||||||
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
||||||
yield* runTui(
|
yield* runTui(transport)
|
||||||
transport,
|
|
||||||
{ continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
|
|
||||||
input.standalone
|
|
||||||
? undefined
|
|
||||||
: async () => {
|
|
||||||
await Effect.runPromise(daemon.stop())
|
|
||||||
return Effect.runPromise(daemon.transport())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { EOL } from "node:os"
|
|
||||||
import path from "node:path"
|
|
||||||
import { Effect, Option } from "effect"
|
|
||||||
import { applyEdits, modify } from "jsonc-parser"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
|
||||||
import { Commands } from "../../commands"
|
|
||||||
import { Runtime } from "../../../framework/runtime"
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.mcp.commands.add,
|
|
||||||
Effect.fn("cli.mcp.add")(function* (input) {
|
|
||||||
const url = Option.getOrUndefined(input.url)
|
|
||||||
const headers = Option.getOrUndefined(input.header)
|
|
||||||
const environment = Option.getOrUndefined(input.env)
|
|
||||||
// The CLI framework strands `--` operands on the root command, so read the local server command
|
|
||||||
// straight from argv after `--`. This also lets the command carry its own flags (e.g. `npx -y`).
|
|
||||||
const dash = process.argv.indexOf("--")
|
|
||||||
const command = dash === -1 ? [...input.command] : process.argv.slice(dash + 1)
|
|
||||||
|
|
||||||
const hasCommand = command.length > 0
|
|
||||||
if (url && hasCommand)
|
|
||||||
return yield* Effect.fail(new Error("Provide either --url <url> or a command after --, not both"))
|
|
||||||
if (!url && !hasCommand) return yield* Effect.fail(new Error("Provide either --url <url> or a command after --"))
|
|
||||||
if (url && !URL.canParse(url)) return yield* Effect.fail(new Error(`Invalid URL: ${url}`))
|
|
||||||
if (url && environment) return yield* Effect.fail(new Error("--env is only valid for local MCP servers"))
|
|
||||||
if (hasCommand && headers) return yield* Effect.fail(new Error("--header is only valid for remote MCP servers"))
|
|
||||||
|
|
||||||
const server = url
|
|
||||||
? { type: "remote" as const, url, ...(headers ? { headers } : {}) }
|
|
||||||
: { type: "local" as const, command, ...(environment ? { environment } : {}) }
|
|
||||||
|
|
||||||
const configPath = yield* Effect.promise(() => resolveConfigPath(input.global ? Global.Path.config : process.cwd()))
|
|
||||||
yield* Effect.promise(() => write(configPath, input.name, server))
|
|
||||||
process.stdout.write(`MCP server "${input.name}" added to ${configPath}` + EOL)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
async function resolveConfigPath(directory: string) {
|
|
||||||
const candidates = [
|
|
||||||
path.join(directory, "opencode.json"),
|
|
||||||
path.join(directory, "opencode.jsonc"),
|
|
||||||
path.join(directory, ".opencode", "opencode.json"),
|
|
||||||
path.join(directory, ".opencode", "opencode.jsonc"),
|
|
||||||
]
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (await Bun.file(candidate).exists()) return candidate
|
|
||||||
}
|
|
||||||
return candidates[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function write(configPath: string, name: string, server: unknown) {
|
|
||||||
const file = Bun.file(configPath)
|
|
||||||
const text = (await file.exists()) ? await file.text() : "{}"
|
|
||||||
const edits = modify(text, ["mcp", "servers", name], server, {
|
|
||||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
|
||||||
})
|
|
||||||
await Bun.write(configPath, applyEdits(text, edits))
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { EOL } from "node:os"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import type { IntegrationAttemptStatus, IntegrationOAuthMethod, OpencodeClient } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { Commands } from "../../commands"
|
|
||||||
import { Runtime } from "../../../framework/runtime"
|
|
||||||
import { Daemon } from "../../../services/daemon"
|
|
||||||
import { resolveIntegration } from "./resolve"
|
|
||||||
|
|
||||||
const location = { directory: process.cwd() }
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.mcp.commands.auth,
|
|
||||||
Effect.fn("cli.mcp.auth")(function* (input) {
|
|
||||||
const daemon = yield* Daemon.Service
|
|
||||||
const client = yield* daemon.client()
|
|
||||||
|
|
||||||
const integration = yield* resolveIntegration(client, input.name, location)
|
|
||||||
if (!integration)
|
|
||||||
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
|
|
||||||
const method = integration.methods.find(
|
|
||||||
(candidate): candidate is IntegrationOAuthMethod => candidate.type === "oauth",
|
|
||||||
)
|
|
||||||
if (!method)
|
|
||||||
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
|
|
||||||
|
|
||||||
const started = yield* Effect.promise(() =>
|
|
||||||
client.v2.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
|
|
||||||
)
|
|
||||||
const attempt = started.data?.data
|
|
||||||
if (!attempt) return yield* Effect.fail(new Error(started.error?.message ?? "Failed to start OAuth attempt"))
|
|
||||||
if (attempt.mode === "code")
|
|
||||||
return yield* Effect.fail(new Error("This server requires manual code entry, which the CLI does not support"))
|
|
||||||
|
|
||||||
process.stdout.write(attempt.instructions + EOL + attempt.url + EOL)
|
|
||||||
|
|
||||||
const result = yield* poll(client, attempt.attemptID)
|
|
||||||
if (result.status === "complete") {
|
|
||||||
process.stdout.write(`Authenticated with ${input.name}` + EOL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const reason = result.status === "failed" ? `: ${result.message}` : ""
|
|
||||||
return yield* Effect.fail(new Error(`Authentication ${result.status}${reason}`))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const poll = (
|
|
||||||
client: OpencodeClient,
|
|
||||||
attemptID: string,
|
|
||||||
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const response = yield* Effect.promise(() => client.v2.integration.attempt.status({ attemptID, location }))
|
|
||||||
const status = response.data?.data
|
|
||||||
if (!status || status.status === "pending") {
|
|
||||||
yield* Effect.sleep("1 second")
|
|
||||||
return yield* poll(client, attemptID)
|
|
||||||
}
|
|
||||||
return status
|
|
||||||
})
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { EOL } from "node:os"
|
|
||||||
import * as Effect from "effect/Effect"
|
|
||||||
import type { McpServer } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { Commands } from "../../commands"
|
|
||||||
import { Runtime } from "../../../framework/runtime"
|
|
||||||
import { Daemon } from "../../../services/daemon"
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.mcp.commands.list,
|
|
||||||
Effect.fn("cli.mcp.list")(function* () {
|
|
||||||
const daemon = yield* Daemon.Service
|
|
||||||
const client = yield* daemon.client()
|
|
||||||
const response = yield* Effect.promise(() => client.v2.mcp.list({ location: { directory: process.cwd() } }))
|
|
||||||
const servers = (response.data?.data ?? []).toSorted((a, b) => a.name.localeCompare(b.name))
|
|
||||||
if (servers.length === 0) {
|
|
||||||
process.stdout.write("No MCP servers configured" + EOL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const width = Math.max(...servers.map((server) => server.name.length))
|
|
||||||
const lines = servers.map(
|
|
||||||
(server) => `${icon(server.status)} ${server.name.padEnd(width)} ${describe(server.status)}`,
|
|
||||||
)
|
|
||||||
process.stdout.write(lines.join(EOL) + EOL)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
function icon(status: McpServer["status"]) {
|
|
||||||
switch (status.status) {
|
|
||||||
case "connected":
|
|
||||||
return "✓"
|
|
||||||
case "needs_auth":
|
|
||||||
return "⚠"
|
|
||||||
case "failed":
|
|
||||||
case "needs_client_registration":
|
|
||||||
return "✗"
|
|
||||||
default:
|
|
||||||
return "○"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function describe(status: McpServer["status"]) {
|
|
||||||
switch (status.status) {
|
|
||||||
case "needs_auth":
|
|
||||||
return "needs authentication"
|
|
||||||
case "needs_client_registration":
|
|
||||||
return `needs client registration: ${status.error}`
|
|
||||||
case "failed":
|
|
||||||
return `failed: ${status.error}`
|
|
||||||
default:
|
|
||||||
return status.status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { EOL } from "node:os"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import { Commands } from "../../commands"
|
|
||||||
import { Runtime } from "../../../framework/runtime"
|
|
||||||
import { Daemon } from "../../../services/daemon"
|
|
||||||
import { resolveIntegration } from "./resolve"
|
|
||||||
|
|
||||||
const location = { directory: process.cwd() }
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.mcp.commands.logout,
|
|
||||||
Effect.fn("cli.mcp.logout")(function* (input) {
|
|
||||||
const daemon = yield* Daemon.Service
|
|
||||||
const client = yield* daemon.client()
|
|
||||||
|
|
||||||
const integration = yield* resolveIntegration(client, input.name, location)
|
|
||||||
if (!integration) {
|
|
||||||
process.stdout.write(`No stored credentials for ${input.name}` + EOL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentials = integration.connections.filter((connection) => connection.type === "credential")
|
|
||||||
if (credentials.length === 0) {
|
|
||||||
process.stdout.write(`No stored credentials for ${input.name}` + EOL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* Effect.forEach(
|
|
||||||
credentials,
|
|
||||||
(connection) => Effect.promise(() => client.v2.credential.remove({ credentialID: connection.id, location })),
|
|
||||||
{ discard: true },
|
|
||||||
)
|
|
||||||
process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Effect } from "effect"
|
|
||||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
|
|
||||||
|
|
||||||
// Resolve through the MCP-owned integrationID rather than matching integration names: the shared
|
|
||||||
// integration registry also holds provider/plugin integrations, whose names could collide with a server.
|
|
||||||
// Fails when the server is unknown; returns undefined when the server has no integration (e.g. a local
|
|
||||||
// or anonymous server), leaving that case for the caller to interpret.
|
|
||||||
export const resolveIntegration = (client: OpencodeClient, name: string, location: { directory: string }) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const servers = yield* Effect.promise(() => client.v2.mcp.list({ location }))
|
|
||||||
const server = (servers.data?.data ?? []).find((entry) => entry.name === name)
|
|
||||||
if (!server) return yield* Effect.fail(new Error(`MCP server not found: ${name}`))
|
|
||||||
const integrationID = server.integrationID
|
|
||||||
if (!integrationID) return undefined
|
|
||||||
const found = yield* Effect.promise(() => client.v2.integration.get({ integrationID, location }))
|
|
||||||
return found.data?.data
|
|
||||||
})
|
|
||||||
@@ -1,76 +1,30 @@
|
|||||||
import { NodeHttpServer } from "@effect/platform-node"
|
import { NodeHttpServer } from "@effect/platform-node"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Context, Layer, Option } from "effect"
|
||||||
import { Context, Layer, Option, Schedule } from "effect"
|
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import { createRoutes } from "@opencode-ai/server/routes"
|
import { createRoutes } from "@opencode-ai/server/routes"
|
||||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
import { Runtime } from "../../framework/runtime"
|
import { Runtime } from "../../framework/runtime"
|
||||||
import { Daemon } from "../../services/daemon"
|
import { Daemon } from "../../services/daemon"
|
||||||
import { Updater } from "../../services/updater"
|
|
||||||
import { randomBytes } from "crypto"
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
export default Runtime.handler(
|
||||||
Commands.commands.serve,
|
Commands.commands.serve,
|
||||||
Effect.fn("cli.serve")(function* (input) {
|
Effect.fn("cli.serve")(function* (input) {
|
||||||
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
|
|
||||||
return yield* Effect.scoped(
|
return yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const daemon = yield* Daemon.Service
|
const daemon = yield* Daemon.Service
|
||||||
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
|
const address = yield* listen(input.hostname, input.port, yield* daemon.password())
|
||||||
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
|
if (input.register) yield* daemon.register(address)
|
||||||
const config = input.service ? yield* daemon.config() : {}
|
console.log(`server listening on ${HttpServer.formatAddress(address)}`)
|
||||||
const password = input.service
|
return yield* Effect.never
|
||||||
? yield* daemon.password()
|
}),
|
||||||
: standalonePassword || randomBytes(32).toString("base64url")
|
|
||||||
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
|
||||||
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
|
|
||||||
const port = Option.isSome(input.port)
|
|
||||||
? input.port
|
|
||||||
: config.port === undefined
|
|
||||||
? Option.none<number>()
|
|
||||||
: Option.some(config.port)
|
|
||||||
const address = yield* listen(hostname, port, password)
|
|
||||||
yield* Effect.tryPromise(() =>
|
|
||||||
createOpencodeClient({
|
|
||||||
baseUrl: HttpServer.formatAddress(address),
|
|
||||||
headers: ServerAuth.headers({ password }),
|
|
||||||
}).v2.health.get({}),
|
|
||||||
)
|
|
||||||
if (input.service) yield* daemon.register(address)
|
|
||||||
const url = HttpServer.formatAddress(address)
|
|
||||||
console.log(input.stdio ? JSON.stringify({ url }) : `server listening on ${url}`)
|
|
||||||
if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`)
|
|
||||||
const updater = yield* Updater.Service
|
|
||||||
yield* updater.check().pipe(Effect.schedule(Schedule.spaced("10 minutes")), Effect.forkScoped)
|
|
||||||
return yield* (input.stdio ? waitForStdinClose() : Effect.never)
|
|
||||||
}).pipe(Effect.annotateLogs({ role: "server" })),
|
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
function waitForStdinClose() {
|
|
||||||
return Effect.callback<void>((resume) => {
|
|
||||||
const close = () => resume(Effect.void)
|
|
||||||
process.stdin.once("end", close)
|
|
||||||
process.stdin.once("close", close)
|
|
||||||
process.stdin.resume()
|
|
||||||
if (process.stdin.readableEnded || process.stdin.destroyed) close()
|
|
||||||
return Effect.sync(() => {
|
|
||||||
process.stdin.off("end", close)
|
|
||||||
process.stdin.off("close", close)
|
|
||||||
process.stdin.pause()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function listen(hostname: string, port: Option.Option<number>, password: string) {
|
function listen(hostname: string, port: Option.Option<number>, password: string) {
|
||||||
if (Option.isSome(port)) return bind(hostname, port.value, password)
|
if (Option.isSome(port)) return bind(hostname, port.value, password)
|
||||||
const next = (port: number): ReturnType<typeof bind> =>
|
const next = (port: number): ReturnType<typeof bind> =>
|
||||||
@@ -81,14 +35,11 @@ function listen(hostname: string, port: Option.Option<number>, password: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bind(hostname: string, port: number, password: string) {
|
function bind(hostname: string, port: number, password: string) {
|
||||||
const server = createServer()
|
|
||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
||||||
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
|
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
|
||||||
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))),
|
Layer.provide(Credential.defaultLayer),
|
||||||
|
Layer.provide(PermissionSaved.defaultLayer),
|
||||||
),
|
),
|
||||||
).pipe(
|
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
||||||
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
|
|
||||||
Effect.map((context) => Context.get(context, HttpServer.HttpServer).address),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -6,9 +6,11 @@ import { Runtime } from "../../../framework/runtime"
|
|||||||
import { Daemon } from "../../../services/daemon"
|
import { Daemon } from "../../../services/daemon"
|
||||||
|
|
||||||
export default Runtime.handler(
|
export default Runtime.handler(
|
||||||
Commands.commands.service.commands.get,
|
Commands.commands.service.commands.password,
|
||||||
Effect.fn("cli.service.get")(function* (input) {
|
Effect.fn("cli.service.password")(function* (input) {
|
||||||
const daemon = yield* Daemon.Service
|
const daemon = yield* Daemon.Service
|
||||||
process.stdout.write((yield* daemon.get(Option.getOrUndefined(input.key))) + EOL)
|
const value = Option.getOrUndefined(input.value)
|
||||||
|
if (value !== undefined) yield* daemon.stop()
|
||||||
|
process.stdout.write((yield* daemon.password(value)) + EOL)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import * as Effect from "effect/Effect"
|
|
||||||
import { Commands } from "../../commands"
|
|
||||||
import { Runtime } from "../../../framework/runtime"
|
|
||||||
import { Daemon } from "../../../services/daemon"
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.service.commands.set,
|
|
||||||
Effect.fn("cli.service.set")(function* (input) {
|
|
||||||
yield* (yield* Daemon.Service).set(input.key, input.value)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -8,6 +8,6 @@ export default Runtime.handler(
|
|||||||
Commands.commands.service.commands.status,
|
Commands.commands.service.commands.status,
|
||||||
Effect.fn("cli.service.status")(function* () {
|
Effect.fn("cli.service.status")(function* () {
|
||||||
const url = yield* (yield* Daemon.Service).status()
|
const url = yield* (yield* Daemon.Service).status()
|
||||||
process.stdout.write((url ? url : "stopped") + EOL)
|
process.stdout.write((url ? `running ${url}` : "stopped") + EOL)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import * as Effect from "effect/Effect"
|
|
||||||
import { Commands } from "../../commands"
|
|
||||||
import { Runtime } from "../../../framework/runtime"
|
|
||||||
import { Daemon } from "../../../services/daemon"
|
|
||||||
|
|
||||||
export default Runtime.handler(
|
|
||||||
Commands.commands.service.commands.unset,
|
|
||||||
Effect.fn("cli.service.unset")(function* (input) {
|
|
||||||
yield* (yield* Daemon.Service).unset(input.key)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -2,8 +2,6 @@ import * as Effect from "effect/Effect"
|
|||||||
import * as Command from "effect/unstable/cli/Command"
|
import * as Command from "effect/unstable/cli/Command"
|
||||||
import { Spec } from "./spec"
|
import { Spec } from "./spec"
|
||||||
import { Daemon } from "../services/daemon"
|
import { Daemon } from "../services/daemon"
|
||||||
import { Updater } from "../services/updater"
|
|
||||||
import { Scope } from "effect"
|
|
||||||
|
|
||||||
export type Input<Value> =
|
export type Input<Value> =
|
||||||
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
||||||
@@ -12,11 +10,11 @@ export type Input<Value> =
|
|||||||
? Input
|
? Input
|
||||||
: never
|
: never
|
||||||
|
|
||||||
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service | Updater.Service | Scope.Scope>
|
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service>
|
||||||
type Loader<Node extends Spec.Any> = () => Promise<{
|
type Loader<Node extends Spec.Any> = () => Promise<{
|
||||||
default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service | Updater.Service | Scope.Scope>
|
default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service>
|
||||||
}>
|
}>
|
||||||
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service | Updater.Service | Scope.Scope>
|
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service>
|
||||||
|
|
||||||
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
||||||
? Loader<Node>
|
? Loader<Node>
|
||||||
|
|||||||
@@ -2,25 +2,10 @@
|
|||||||
|
|
||||||
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
||||||
import * as NodeServices from "@effect/platform-node/NodeServices"
|
import * as NodeServices from "@effect/platform-node/NodeServices"
|
||||||
import { NodeFileSystem } from "@effect/platform-node"
|
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
import { Layer, Logger, References } from "effect"
|
|
||||||
import { Commands } from "./commands/commands"
|
import { Commands } from "./commands/commands"
|
||||||
import { Runtime } from "./framework/runtime"
|
import { Runtime } from "./framework/runtime"
|
||||||
import { Daemon } from "./services/daemon"
|
import { Daemon } from "./services/daemon"
|
||||||
import { Logging } from "@opencode-ai/core/observability/logging"
|
|
||||||
import { Updater } from "./services/updater"
|
|
||||||
import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version"
|
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
|
||||||
import { AppProcess } from "@opencode-ai/core/process"
|
|
||||||
|
|
||||||
const LoggingLayer = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
|
|
||||||
Layer.provide(NodeFileSystem.layer),
|
|
||||||
Layer.orDie,
|
|
||||||
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
|
|
||||||
)
|
|
||||||
|
|
||||||
const Handlers = Runtime.handlers(Commands, {
|
const Handlers = Runtime.handlers(Commands, {
|
||||||
$: () => import("./commands/handlers/default"),
|
$: () => import("./commands/handlers/default"),
|
||||||
@@ -28,34 +13,20 @@ const Handlers = Runtime.handlers(Commands, {
|
|||||||
debug: {
|
debug: {
|
||||||
agents: () => import("./commands/handlers/debug/agents"),
|
agents: () => import("./commands/handlers/debug/agents"),
|
||||||
},
|
},
|
||||||
mcp: {
|
|
||||||
list: () => import("./commands/handlers/mcp/list"),
|
|
||||||
add: () => import("./commands/handlers/mcp/add"),
|
|
||||||
auth: () => import("./commands/handlers/mcp/auth"),
|
|
||||||
logout: () => import("./commands/handlers/mcp/logout"),
|
|
||||||
},
|
|
||||||
migrate: () => import("./commands/handlers/migrate"),
|
migrate: () => import("./commands/handlers/migrate"),
|
||||||
service: {
|
service: {
|
||||||
start: () => import("./commands/handlers/service/start"),
|
start: () => import("./commands/handlers/service/start"),
|
||||||
restart: () => import("./commands/handlers/service/restart"),
|
restart: () => import("./commands/handlers/service/restart"),
|
||||||
status: () => import("./commands/handlers/service/status"),
|
status: () => import("./commands/handlers/service/status"),
|
||||||
stop: () => import("./commands/handlers/service/stop"),
|
stop: () => import("./commands/handlers/service/stop"),
|
||||||
get: () => import("./commands/handlers/service/get"),
|
password: () => import("./commands/handlers/service/password"),
|
||||||
set: () => import("./commands/handlers/service/set"),
|
|
||||||
unset: () => import("./commands/handlers/service/unset"),
|
|
||||||
},
|
},
|
||||||
serve: () => import("./commands/handlers/serve"),
|
serve: () => import("./commands/handlers/serve"),
|
||||||
})
|
})
|
||||||
|
|
||||||
Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe(
|
Runtime.run(Commands, Handlers, { version: "local" }).pipe(
|
||||||
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
|
Effect.provide(Daemon.defaultLayer),
|
||||||
Effect.annotateLogs({ role: "cli" }),
|
|
||||||
Effect.provide(Daemon.layer),
|
|
||||||
Effect.provide(Updater.layer),
|
|
||||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
|
|
||||||
Effect.provide(LoggingLayer),
|
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
Effect.tap(() => Effect.sync(() => process.exit(0))),
|
|
||||||
NodeRuntime.runMain,
|
NodeRuntime.runMain,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||||
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
||||||
@@ -15,10 +15,6 @@ export interface Interface {
|
|||||||
readonly status: () => Effect.Effect<string | undefined>
|
readonly status: () => Effect.Effect<string | undefined>
|
||||||
readonly stop: () => Effect.Effect<void, unknown>
|
readonly stop: () => Effect.Effect<void, unknown>
|
||||||
readonly password: (value?: string) => Effect.Effect<string, unknown>
|
readonly password: (value?: string) => Effect.Effect<string, unknown>
|
||||||
readonly config: () => Effect.Effect<ServiceConfig, unknown>
|
|
||||||
readonly get: (key?: string) => Effect.Effect<string, unknown>
|
|
||||||
readonly set: (key: string, value: string) => Effect.Effect<void, unknown>
|
|
||||||
readonly unset: (key: string) => Effect.Effect<void, unknown>
|
|
||||||
readonly register: (address: HttpServer.Address) => Effect.Effect<void, unknown, Scope.Scope>
|
readonly register: (address: HttpServer.Address) => Effect.Effect<void, unknown, Scope.Scope>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,22 +28,6 @@ const Registration = Schema.Struct({
|
|||||||
})
|
})
|
||||||
type Registration = typeof Registration.Type
|
type Registration = typeof Registration.Type
|
||||||
|
|
||||||
const ServiceConfig = Schema.Struct({
|
|
||||||
hostname: Schema.optional(Schema.String),
|
|
||||||
port: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535))),
|
|
||||||
password: Schema.optional(Schema.String),
|
|
||||||
autostart: Schema.optional(Schema.Boolean),
|
|
||||||
})
|
|
||||||
export type ServiceConfig = typeof ServiceConfig.Type
|
|
||||||
|
|
||||||
const serviceConfigKeys = ["hostname", "port", "password", "autostart"] as const
|
|
||||||
type ServiceConfigKey = (typeof serviceConfigKeys)[number]
|
|
||||||
|
|
||||||
function serviceConfigKey(key: string): ServiceConfigKey {
|
|
||||||
if (serviceConfigKeys.includes(key as ServiceConfigKey)) return key as ServiceConfigKey
|
|
||||||
throw new Error(`Unknown service config key: ${key}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function sameRegistration(left: Registration, right: Registration) {
|
function sameRegistration(left: Registration, right: Registration) {
|
||||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||||
}
|
}
|
||||||
@@ -56,115 +36,23 @@ export const layer = Layer.effect(
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
const global = yield* Global.Service
|
const directory = Global.Path.state
|
||||||
const directory = global.state
|
const file = path.join(directory, "server.json")
|
||||||
const filename = InstallationChannel === "local" ? "service-local.json" : "service.json"
|
const passwordFile = path.join(directory, "password")
|
||||||
const file = path.join(directory, filename)
|
|
||||||
const configFile = path.join(global.config, filename)
|
|
||||||
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
||||||
const decodeServiceConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(ServiceConfig))
|
|
||||||
|
|
||||||
const config = Effect.fn("cli.daemon.config")(function* () {
|
|
||||||
return yield* fs.readFileString(configFile).pipe(
|
|
||||||
Effect.flatMap(decodeServiceConfig),
|
|
||||||
Effect.catch(() => Effect.succeed({} as ServiceConfig)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const writeConfig = Effect.fn("cli.daemon.writeConfig")(function* (value: ServiceConfig) {
|
|
||||||
const temp = configFile + ".tmp"
|
|
||||||
yield* fs.makeDirectory(path.dirname(configFile), { recursive: true })
|
|
||||||
yield* fs.writeFileString(temp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 })
|
|
||||||
yield* fs.rename(temp, configFile)
|
|
||||||
})
|
|
||||||
|
|
||||||
const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
|
const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
|
||||||
const existing = yield* config()
|
const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||||
if (value === undefined && existing.password) return existing.password
|
if (value === undefined && existing) return existing
|
||||||
const next = value ?? randomBytes(32).toString("base64url")
|
|
||||||
|
|
||||||
// Keep one private credential across server restarts so discovered clients
|
// Keep one private credential across server restarts so discovered clients
|
||||||
// can reconnect without exposing a password flag or environment variable.
|
// can reconnect without exposing a password flag or environment variable.
|
||||||
yield* writeConfig({ ...existing, password: next })
|
const generated = value ?? randomBytes(32).toString("base64url")
|
||||||
return next
|
const temp = passwordFile + ".tmp"
|
||||||
})
|
yield* fs.makeDirectory(directory, { recursive: true })
|
||||||
|
yield* fs.writeFileString(temp, generated, { mode: 0o600 })
|
||||||
const get = Effect.fn("cli.daemon.get")(function* (key?: string) {
|
yield* fs.rename(temp, passwordFile)
|
||||||
if (key === undefined) {
|
return generated
|
||||||
const { password: _password, ...safe } = yield* config()
|
|
||||||
return JSON.stringify(safe, null, 2)
|
|
||||||
}
|
|
||||||
switch (serviceConfigKey(key)) {
|
|
||||||
case "hostname": {
|
|
||||||
return (yield* config()).hostname ?? ""
|
|
||||||
}
|
|
||||||
case "port": {
|
|
||||||
const port = (yield* config()).port
|
|
||||||
return port === undefined ? "" : String(port)
|
|
||||||
}
|
|
||||||
case "password": {
|
|
||||||
return yield* password()
|
|
||||||
}
|
|
||||||
case "autostart": {
|
|
||||||
const autostart = (yield* config()).autostart
|
|
||||||
return autostart === undefined ? "" : String(autostart)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const set = Effect.fn("cli.daemon.set")(function* (key: string, value: string) {
|
|
||||||
switch (serviceConfigKey(key)) {
|
|
||||||
case "hostname": {
|
|
||||||
yield* stop()
|
|
||||||
yield* writeConfig({ ...(yield* config()), hostname: value })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "port": {
|
|
||||||
const port = Number(value)
|
|
||||||
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("Port must be between 1 and 65535")
|
|
||||||
yield* stop()
|
|
||||||
yield* writeConfig({ ...(yield* config()), port })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "password": {
|
|
||||||
yield* stop()
|
|
||||||
yield* password(value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "autostart": {
|
|
||||||
if (value !== "true" && value !== "false") throw new Error("Autostart must be true or false")
|
|
||||||
yield* writeConfig({ ...(yield* config()), autostart: value === "true" })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const unset = Effect.fn("cli.daemon.unset")(function* (key: string) {
|
|
||||||
switch (serviceConfigKey(key)) {
|
|
||||||
case "hostname": {
|
|
||||||
yield* stop()
|
|
||||||
const { hostname: _hostname, ...next } = yield* config()
|
|
||||||
yield* writeConfig(next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "port": {
|
|
||||||
yield* stop()
|
|
||||||
const { port: _port, ...next } = yield* config()
|
|
||||||
yield* writeConfig(next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "password": {
|
|
||||||
yield* stop()
|
|
||||||
const { password: _password, ...next } = yield* config()
|
|
||||||
yield* writeConfig(next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "autostart": {
|
|
||||||
const { autostart: _autostart, ...next } = yield* config()
|
|
||||||
yield* writeConfig(next)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const registration = Effect.fnUntraced(function* () {
|
const registration = Effect.fnUntraced(function* () {
|
||||||
@@ -183,16 +71,6 @@ export const layer = Layer.effect(
|
|||||||
return yield* Effect.fail(new Error("Registered server is not healthy"))
|
return yield* Effect.fail(new Error("Registered server is not healthy"))
|
||||||
})
|
})
|
||||||
|
|
||||||
const remoteTransport = Effect.fn("cli.daemon.remoteTransport")(function* (input: ServiceConfig) {
|
|
||||||
const url = serviceURL(input)
|
|
||||||
const headers = ServerAuth.headers({ password: input.password })
|
|
||||||
const response = yield* Effect.tryPromise(() =>
|
|
||||||
createOpencodeClient({ baseUrl: url, headers }).v2.health.get({ signal: AbortSignal.timeout(2_000) }),
|
|
||||||
)
|
|
||||||
if (response.data?.healthy === true) return { url, headers }
|
|
||||||
return yield* Effect.fail(new Error(`Server is not healthy: ${url}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
const compatible = Effect.fnUntraced(function* () {
|
const compatible = Effect.fnUntraced(function* () {
|
||||||
const info = yield* healthy()
|
const info = yield* healthy()
|
||||||
if (info.version === InstallationVersion) return info
|
if (info.version === InstallationVersion) return info
|
||||||
@@ -233,7 +111,7 @@ export const layer = Layer.effect(
|
|||||||
const existing = yield* healthy().pipe(Effect.option)
|
const existing = yield* healthy().pipe(Effect.option)
|
||||||
const found = Option.getOrUndefined(existing)
|
const found = Option.getOrUndefined(existing)
|
||||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||||
if (found?.version === InstallationVersion) return found.url
|
if (found?.version === InstallationVersion && compiled) return found.url
|
||||||
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
||||||
|
|
||||||
const entrypoint = compiled ? undefined : process.argv[1]
|
const entrypoint = compiled ? undefined : process.argv[1]
|
||||||
@@ -241,7 +119,7 @@ export const layer = Layer.effect(
|
|||||||
return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
|
return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
|
||||||
yield* Effect.try({
|
yield* Effect.try({
|
||||||
try: () => {
|
try: () => {
|
||||||
spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--service"], {
|
spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: "ignore",
|
stdio: "ignore",
|
||||||
}).unref()
|
}).unref()
|
||||||
@@ -257,8 +135,6 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const transport = Effect.fn("cli.daemon.transport")(function* () {
|
const transport = Effect.fn("cli.daemon.transport")(function* () {
|
||||||
const current = yield* config()
|
|
||||||
if (current.autostart === false) return yield* remoteTransport(current)
|
|
||||||
return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) }
|
return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -309,15 +185,10 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({ client, transport, start, status, stop, password, config, get, set, unset, register })
|
return Service.of({ client, transport, start, status, stop, password, register })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
function serviceURL(config: ServiceConfig) {
|
export const defaultLayer = layer
|
||||||
const hostname = config.hostname ?? "127.0.0.1"
|
|
||||||
const result = new URL(`http://${hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname}`)
|
|
||||||
result.port = String(config.port ?? 4096)
|
|
||||||
return result.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export * as Daemon from "./daemon"
|
export * as Daemon from "./daemon"
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { Effect, Schema, Stream } from "effect"
|
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|
||||||
import { randomBytes } from "node:crypto"
|
|
||||||
import path from "node:path"
|
|
||||||
|
|
||||||
const Ready = Schema.Struct({ url: Schema.String })
|
|
||||||
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
|
|
||||||
|
|
||||||
function command(password: string) {
|
|
||||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
|
||||||
const entrypoint = compiled ? [] : process.argv[1] ? [process.argv[1]] : []
|
|
||||||
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
|
|
||||||
return ChildProcess.make(process.execPath, [...entrypoint, "serve", "--stdio", "--port", "0"], {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
env: { OPENCODE_SERVER_PASSWORD: password },
|
|
||||||
extendEnv: true,
|
|
||||||
// The server treats EOF on this pipe as the end of its ownership lease.
|
|
||||||
// The OS closes it even when the TUI is killed before Effect finalizers run.
|
|
||||||
stdin: "pipe",
|
|
||||||
stderr: "ignore",
|
|
||||||
killSignal: "SIGTERM",
|
|
||||||
forceKillAfter: "3 seconds",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const transport = Effect.fn("cli.standalone.transport")(
|
|
||||||
function* () {
|
|
||||||
const password = randomBytes(32).toString("base64url")
|
|
||||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
||||||
const proc = yield* spawner.spawn(command(password))
|
|
||||||
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
|
|
||||||
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
|
|
||||||
const ready = yield* Effect.tryPromise(() => decodeReady(output))
|
|
||||||
return { url: ready.url, headers: ServerAuth.headers({ password }), pid: proc.pid }
|
|
||||||
},
|
|
||||||
Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)),
|
|
||||||
)
|
|
||||||
|
|
||||||
export * as Standalone from "./standalone"
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { action, decodePolicy } from "./updater"
|
|
||||||
|
|
||||||
describe("updater", () => {
|
|
||||||
test("reads autoupdate from JSONC", () => {
|
|
||||||
expect(decodePolicy('{ // preference\n "autoupdate": "notify",\n}')).toBe("notify")
|
|
||||||
expect(decodePolicy('{ "autoupdate": false }')).toBe(false)
|
|
||||||
expect(decodePolicy('{ "autoupdate": "invalid" }')).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("automatically updates patches and minors", () => {
|
|
||||||
expect(action("1.2.3", "1.2.4", true)).toBe("upgrade")
|
|
||||||
expect(action("1.2.3", "1.3.0", true)).toBe("upgrade")
|
|
||||||
expect(action("1.2.3", "1.2.4", "notify")).toBe("upgrade")
|
|
||||||
expect(action("1.2.3", "1.3.0", "notify")).toBe("upgrade")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("skips when autoupdate is disabled", () => {
|
|
||||||
expect(action("1.2.3", "1.2.4", false)).toBe("none")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("never automatically updates majors", () => {
|
|
||||||
expect(action("1.2.3", "2.0.0", true)).toBe("none")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("reports up-to-date only when versions match", () => {
|
|
||||||
expect(action("1.2.3", "1.2.3", true)).toBe("none")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("upgrades when latest is lower (rollback)", () => {
|
|
||||||
expect(action("1.2.4", "1.2.3", true)).toBe("upgrade")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import { Global } from "@opencode-ai/core/global"
|
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import { AppProcess } from "@opencode-ai/core/process"
|
|
||||||
import {
|
|
||||||
InstallationChannel,
|
|
||||||
InstallationLocal,
|
|
||||||
InstallationVersion,
|
|
||||||
} from "@opencode-ai/core/installation/version"
|
|
||||||
import { Context, Duration, Effect, FileSystem, Layer } from "effect"
|
|
||||||
import { ChildProcess } from "effect/unstable/process"
|
|
||||||
import { parse, type ParseError } from "jsonc-parser"
|
|
||||||
import path from "node:path"
|
|
||||||
import semver from "semver"
|
|
||||||
|
|
||||||
export type Policy = boolean | "notify"
|
|
||||||
export type Action = "none" | "upgrade"
|
|
||||||
type Method = "npm" | "pnpm" | "bun" | "yarn"
|
|
||||||
|
|
||||||
const packageName = "@opencode-ai/cli"
|
|
||||||
|
|
||||||
export interface Interface {
|
|
||||||
readonly check: () => Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Updater") {}
|
|
||||||
|
|
||||||
export function decodePolicy(text: string): Policy | undefined {
|
|
||||||
// The CLI only projects this host-level preference instead of initializing
|
|
||||||
// the location-scoped server configuration graph.
|
|
||||||
const errors: ParseError[] = []
|
|
||||||
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
|
||||||
if (errors.length || typeof input !== "object" || input === null || !("autoupdate" in input)) return
|
|
||||||
const value = input.autoupdate
|
|
||||||
if (typeof value === "boolean" || value === "notify") return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function action(current: string, latest: string, policy: Policy): Action {
|
|
||||||
if (policy === false) return "none"
|
|
||||||
if (!semver.valid(current) || !semver.valid(latest) || semver.eq(latest, current)) return "none"
|
|
||||||
// Major upgrades are never installed automatically.
|
|
||||||
if (semver.major(latest) !== semver.major(current)) return "none"
|
|
||||||
return "upgrade"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const fs = yield* FileSystem.FileSystem
|
|
||||||
const global = yield* Global.Service
|
|
||||||
const appProcess = yield* AppProcess.Service
|
|
||||||
const channel = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")
|
|
||||||
|
|
||||||
const readPolicy = Effect.fnUntraced(function* () {
|
|
||||||
const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) =>
|
|
||||||
fs
|
|
||||||
.readFileString(path.join(global.config, name))
|
|
||||||
.pipe(Effect.map(decodePolicy), Effect.catch(() => Effect.succeed(undefined))),
|
|
||||||
)
|
|
||||||
return values.findLast((value) => value !== undefined) ?? true
|
|
||||||
})
|
|
||||||
|
|
||||||
const run = Effect.fnUntraced(function* (command: string[], timeout: Duration.Input = "10 seconds") {
|
|
||||||
return yield* appProcess
|
|
||||||
.run(ChildProcess.make(command[0], command.slice(1)), {
|
|
||||||
timeout,
|
|
||||||
maxOutputBytes: 100_000,
|
|
||||||
maxErrorBytes: 100_000,
|
|
||||||
})
|
|
||||||
.pipe(
|
|
||||||
Effect.map((result) => ({
|
|
||||||
code: result.exitCode,
|
|
||||||
stdout: result.stdout.toString("utf8"),
|
|
||||||
stderr: result.stderr.toString("utf8"),
|
|
||||||
})),
|
|
||||||
Effect.catch(() => Effect.succeed({ code: 1, stdout: "", stderr: "" })),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const method = Effect.fnUntraced(function* () {
|
|
||||||
const checks: ReadonlyArray<{ method: Method; command: string[] }> = [
|
|
||||||
{ method: "npm", command: ["npm", "list", "-g", "--depth=0", packageName] },
|
|
||||||
{ method: "pnpm", command: ["pnpm", "list", "-g", "--depth=0", packageName] },
|
|
||||||
{ method: "bun", command: ["bun", "pm", "ls", "-g"] },
|
|
||||||
{ method: "yarn", command: ["yarn", "global", "list"] },
|
|
||||||
]
|
|
||||||
const results = yield* Effect.forEach(
|
|
||||||
checks,
|
|
||||||
(check) => run(check.command).pipe(Effect.map((result) => ({ check, result }))),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
return results.find((result) => result.result.stdout.includes(packageName))?.check.method
|
|
||||||
})
|
|
||||||
|
|
||||||
const latest = Effect.fnUntraced(function* () {
|
|
||||||
const response = yield* Effect.tryPromise({
|
|
||||||
try: () =>
|
|
||||||
fetch(
|
|
||||||
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/${encodeURIComponent(InstallationChannel)}`,
|
|
||||||
{ headers: { "User-Agent": `opencode/${InstallationVersion}` }, signal: AbortSignal.timeout(10_000) },
|
|
||||||
),
|
|
||||||
catch: (cause) => new Error("Failed to check for updates", { cause }),
|
|
||||||
})
|
|
||||||
if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`))
|
|
||||||
const data = yield* Effect.tryPromise({
|
|
||||||
try: () => response.json(),
|
|
||||||
catch: (cause) => new Error("Failed to read update information", { cause }),
|
|
||||||
})
|
|
||||||
if (typeof data !== "object" || data === null || !("version" in data) || typeof data.version !== "string") {
|
|
||||||
return yield* Effect.fail(new Error("Update information did not include a version"))
|
|
||||||
}
|
|
||||||
return data.version
|
|
||||||
})
|
|
||||||
|
|
||||||
const upgrade = Effect.fnUntraced(function* (method: Method, version: string) {
|
|
||||||
const target = `${packageName}@${version}`
|
|
||||||
const commands: Record<Method, string[]> = {
|
|
||||||
npm: ["npm", "install", "--global", target],
|
|
||||||
pnpm: ["pnpm", "install", "--global", target],
|
|
||||||
bun: ["bun", "install", "--global", target],
|
|
||||||
yarn: ["yarn", "global", "add", target],
|
|
||||||
}
|
|
||||||
const result = yield* run(commands[method], "5 minutes")
|
|
||||||
if (result.code === 0) return
|
|
||||||
return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
const check = Effect.fn("cli.updater.check")(function* () {
|
|
||||||
if (InstallationLocal || Flag.OPENCODE_DISABLE_AUTOUPDATE)
|
|
||||||
return yield* Effect.logInfo("update check skipped", {
|
|
||||||
reason: InstallationLocal ? "local-install" : "disabled",
|
|
||||||
version: InstallationVersion,
|
|
||||||
channel: InstallationChannel,
|
|
||||||
})
|
|
||||||
const policy = yield* readPolicy()
|
|
||||||
if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" })
|
|
||||||
|
|
||||||
return yield* Effect.gen(function* () {
|
|
||||||
const version = yield* latest()
|
|
||||||
yield* Effect.logInfo("update check", {
|
|
||||||
current: InstallationVersion,
|
|
||||||
latest: version,
|
|
||||||
})
|
|
||||||
const next = action(InstallationVersion, version, policy)
|
|
||||||
if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" })
|
|
||||||
const detected = yield* method()
|
|
||||||
if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found")
|
|
||||||
yield* upgrade(detected, version)
|
|
||||||
yield* Effect.logInfo("updated OpenCode", { from: InstallationVersion, to: version, method: detected })
|
|
||||||
})
|
|
||||||
}, Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })))
|
|
||||||
|
|
||||||
return Service.of({ check })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export * as Updater from "./updater"
|
|
||||||
+29
-42
@@ -1,49 +1,36 @@
|
|||||||
import { run } from "@opencode-ai/tui"
|
import { run } from "@opencode-ai/tui"
|
||||||
import { TuiConfig } from "@opencode-ai/tui/config"
|
import { TuiConfig } from "@opencode-ai/tui/config"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
|
||||||
import { OpenCode } from "@opencode-ai/client"
|
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import type { Args } from "@opencode-ai/tui/context/args"
|
|
||||||
|
|
||||||
type Transport = { url: string; headers: RequestInit["headers"] }
|
export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
|
||||||
|
|
||||||
export function runTui(transport: Transport, args: Args, reload?: () => Promise<Transport>) {
|
|
||||||
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
||||||
let disposeSlots: (() => void) | undefined
|
return run({
|
||||||
return Effect.gen(function* () {
|
...transport,
|
||||||
const options = { baseUrl: transport.url, headers: transport.headers }
|
args: {},
|
||||||
const api = OpenCode.make(options)
|
config,
|
||||||
const directory = yield* Effect.tryPromise(() => api.file.list({ location: { directory: process.cwd() } })).pipe(
|
fetch: gracefulFetch,
|
||||||
Effect.map((response) => response.location.directory),
|
pluginHost: {
|
||||||
Effect.catch(() =>
|
async start() {},
|
||||||
Effect.tryPromise(() => api.location.get()).pipe(Effect.map((response) => response.directory)),
|
async dispose() {},
|
||||||
),
|
},
|
||||||
)
|
}).pipe(Effect.provide(Global.defaultLayer))
|
||||||
return yield* run({
|
|
||||||
client: createOpencodeClient({ ...options, directory }),
|
|
||||||
api,
|
|
||||||
reload: reload
|
|
||||||
? async () => {
|
|
||||||
const next = await reload()
|
|
||||||
return {
|
|
||||||
client: createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory }),
|
|
||||||
api: OpenCode.make({ baseUrl: next.url, headers: next.headers }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
args,
|
|
||||||
config,
|
|
||||||
pluginHost: {
|
|
||||||
async start(input) {
|
|
||||||
disposeSlots = await loadBuiltinPlugins(input.api, input.runtime)
|
|
||||||
},
|
|
||||||
async dispose() {
|
|
||||||
disposeSlots?.()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyDefaults: Record<string, unknown> = {
|
||||||
|
"/config/providers": { providers: [], default: {} },
|
||||||
|
"/provider": { all: [], default: {}, connected: [] },
|
||||||
|
"/agent": [],
|
||||||
|
"/config": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const gracefulFetch = Object.assign(
|
||||||
|
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
|
const response = await fetch(input, init)
|
||||||
|
if (response.status !== 404) return response
|
||||||
|
const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname]
|
||||||
|
if (fallback === undefined) return response
|
||||||
|
return Response.json(fallback)
|
||||||
|
},
|
||||||
|
{ preconnect: fetch.preconnect },
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
|
||||||
import { expect, test } from "bun:test"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import fs from "node:fs/promises"
|
|
||||||
import os from "node:os"
|
|
||||||
import path from "node:path"
|
|
||||||
import { Daemon } from "../src/services/daemon"
|
|
||||||
|
|
||||||
test("local channel stores service config with the local service filename", async () => {
|
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-daemon-"))
|
|
||||||
try {
|
|
||||||
await Effect.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const daemon = yield* Daemon.Service
|
|
||||||
yield* daemon.set("autostart", "false")
|
|
||||||
}).pipe(
|
|
||||||
Effect.provide(Daemon.layer),
|
|
||||||
Effect.provide(Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })),
|
|
||||||
Effect.provide(NodeFileSystem.layer),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
expect(await Bun.file(path.join(root, "config", "service-local.json")).json()).toEqual({
|
|
||||||
autostart: false,
|
|
||||||
})
|
|
||||||
expect(await Bun.file(path.join(root, "config", "service.json")).exists()).toBe(false)
|
|
||||||
} finally {
|
|
||||||
await fs.rm(root, { recursive: true, force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Effect } from "effect"
|
|
||||||
import path from "node:path"
|
|
||||||
import { Standalone } from "../../src/services/standalone"
|
|
||||||
|
|
||||||
process.argv[1] = path.join(import.meta.dir, "../../src/index.ts")
|
|
||||||
|
|
||||||
await Effect.runPromise(
|
|
||||||
Effect.scoped(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const transport = yield* Standalone.transport()
|
|
||||||
console.log(`${transport.pid} ${transport.url}`)
|
|
||||||
return yield* Effect.never
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { expect, test } from "bun:test"
|
|
||||||
import path from "node:path"
|
|
||||||
|
|
||||||
test("standalone server exits when its owner is killed", async () => {
|
|
||||||
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "fixture/standalone-owner.ts")], {
|
|
||||||
cwd: path.join(import.meta.dir, ".."),
|
|
||||||
env: process.env,
|
|
||||||
stdin: "ignore",
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
})
|
|
||||||
const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)])
|
|
||||||
const [rawPID, url] = line?.split(" ") ?? []
|
|
||||||
const pid = Number(rawPID)
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(pid).toBeGreaterThan(0)
|
|
||||||
expect(url).toStartWith("http://127.0.0.1:")
|
|
||||||
expect(running(pid)).toBe(true)
|
|
||||||
|
|
||||||
owner.kill("SIGKILL")
|
|
||||||
await owner.exited
|
|
||||||
|
|
||||||
expect(await waitForExit(pid)).toBe(true)
|
|
||||||
} finally {
|
|
||||||
owner.kill("SIGKILL")
|
|
||||||
if (running(pid)) process.kill(pid, "SIGKILL")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function readLine(stream: ReadableStream<Uint8Array>) {
|
|
||||||
const reader = stream.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
const chunks: string[] = []
|
|
||||||
while (true) {
|
|
||||||
const result = await reader.read()
|
|
||||||
if (result.done) break
|
|
||||||
chunks.push(decoder.decode(result.value, { stream: true }))
|
|
||||||
const output = chunks.join("")
|
|
||||||
const newline = output.indexOf("\n")
|
|
||||||
if (newline !== -1) {
|
|
||||||
reader.releaseLock()
|
|
||||||
return output.slice(0, newline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.releaseLock()
|
|
||||||
return chunks.join("") + decoder.decode()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForExit(pid: number, attempts = 100): Promise<boolean> {
|
|
||||||
if (!running(pid)) return true
|
|
||||||
if (attempts === 0) return false
|
|
||||||
await Bun.sleep(50)
|
|
||||||
return waitForExit(pid, attempts - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function running(pid: number) {
|
|
||||||
if (!Number.isSafeInteger(pid) || pid <= 0) return false
|
|
||||||
try {
|
|
||||||
process.kill(pid, 0)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
|
||||||
import {
|
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
|
||||||
ClientApi,
|
|
||||||
effectOmitEndpoints,
|
|
||||||
endpointNames,
|
|
||||||
groupNames,
|
|
||||||
promiseOmitEndpoints,
|
|
||||||
} from "@opencode-ai/protocol/client"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
const promiseContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
|
||||||
const effectContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: effectOmitEndpoints })
|
|
||||||
|
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
Effect.all(
|
Effect.all(
|
||||||
[
|
[
|
||||||
write(
|
write(
|
||||||
emitPromise(promiseContract, {
|
emitPromise(contract, {
|
||||||
outputTypes: {
|
outputTypes: {
|
||||||
"events.subscribe": {
|
"events.subscribe": {
|
||||||
name: "OpenCodeEventEncoded",
|
name: "OpenCodeEventEncoded",
|
||||||
@@ -28,14 +21,10 @@ await Effect.runPromise(
|
|||||||
fileURLToPath(new URL("../src/generated", import.meta.url)),
|
fileURLToPath(new URL("../src/generated", import.meta.url)),
|
||||||
),
|
),
|
||||||
write(
|
write(
|
||||||
emitEffectImported(effectContract, { module: "../contract", api: "ClientApi" }),
|
emitEffectImported(contract, { module: "../contract", api: "ClientApi" }),
|
||||||
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
||||||
),
|
),
|
||||||
write(
|
|
||||||
emitEffectShape(effectContract, { module: "@opencode-ai/protocol/client", api: "ClientApi" }),
|
|
||||||
fileURLToPath(new URL("../../plugin/src/v2/effect/generated", import.meta.url)),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
{ concurrency: 3, discard: true },
|
{ concurrency: 2, discard: true },
|
||||||
).pipe(Effect.provide(NodeFileSystem.layer)),
|
).pipe(Effect.provide(NodeFileSystem.layer)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,53 @@
|
|||||||
export {
|
import { makeDefaultApi } from "@opencode-ai/protocol/api"
|
||||||
ClientApi,
|
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
effectOmitEndpoints,
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||||
endpointNames,
|
|
||||||
groupNames,
|
class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
|
||||||
promiseOmitEndpoints,
|
"@opencode-ai/client/LocationMiddleware",
|
||||||
} from "@opencode-ai/protocol/client"
|
) {}
|
||||||
|
|
||||||
|
class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
|
||||||
|
"@opencode-ai/client/SessionLocationMiddleware",
|
||||||
|
{ error: [InvalidRequestError, SessionNotFoundError] },
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export const ClientApi = makeDefaultApi({
|
||||||
|
locationMiddleware: LocationMiddleware,
|
||||||
|
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const groupNames = {
|
||||||
|
"server.health": "health",
|
||||||
|
"server.location": "location",
|
||||||
|
"server.agent": "agents",
|
||||||
|
"server.session": "sessions",
|
||||||
|
"server.message": "messages",
|
||||||
|
"server.model": "models",
|
||||||
|
"server.provider": "providers",
|
||||||
|
"server.integration": "integrations",
|
||||||
|
"server.credential": "credentials",
|
||||||
|
"server.permission": "permissions",
|
||||||
|
"server.fs": "files",
|
||||||
|
"server.command": "commands",
|
||||||
|
"server.skill": "skills",
|
||||||
|
"server.event": "events",
|
||||||
|
"server.pty": "ptys",
|
||||||
|
"server.question": "questions",
|
||||||
|
"server.reference": "references",
|
||||||
|
"server.projectCopy": "projectCopies",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const endpointNames = {
|
||||||
|
"session.messages": "list",
|
||||||
|
"integration.connect.key": "connectKey",
|
||||||
|
"integration.connect.oauth": "connectOauth",
|
||||||
|
"integration.attempt.status": "attemptStatus",
|
||||||
|
"integration.attempt.complete": "attemptComplete",
|
||||||
|
"integration.attempt.cancel": "attemptCancel",
|
||||||
|
"permission.request.list": "listRequests",
|
||||||
|
"permission.saved.list": "listSaved",
|
||||||
|
"permission.saved.remove": "removeSaved",
|
||||||
|
"question.request.list": "listRequests",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const omitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export * from "./generated-effect/index"
|
|||||||
export { Agent } from "@opencode-ai/schema/agent"
|
export { Agent } from "@opencode-ai/schema/agent"
|
||||||
export { Command } from "@opencode-ai/schema/command"
|
export { Command } from "@opencode-ai/schema/command"
|
||||||
export { Credential } from "@opencode-ai/schema/credential"
|
export { Credential } from "@opencode-ai/schema/credential"
|
||||||
export { Event } from "@opencode-ai/schema/event"
|
|
||||||
export { EventLog } from "@opencode-ai/schema/event-log"
|
|
||||||
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||||
export { Integration } from "@opencode-ai/schema/integration"
|
export { Integration } from "@opencode-ai/schema/integration"
|
||||||
export { Location } from "@opencode-ai/schema/location"
|
export { Location } from "@opencode-ai/schema/location"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1473
-3111
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,2 @@
|
|||||||
export * from "./generated/index"
|
export * from "./generated/index"
|
||||||
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
||||||
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Schema } from "effect"
|
|||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Location as CoreLocation } from "@opencode-ai/core/location"
|
import { Location as CoreLocation } from "@opencode-ai/core/location"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { SessionV2 } from "@opencode-ai/core/session"
|
||||||
import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input"
|
import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input"
|
||||||
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
|
||||||
@@ -20,21 +19,17 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
|
|||||||
import { Workspace } from "@opencode-ai/schema/workspace"
|
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||||
import { Api } from "@opencode-ai/server/api"
|
import { Api } from "@opencode-ai/server/api"
|
||||||
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
||||||
import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
|
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
|
||||||
|
|
||||||
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
|
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
|
||||||
expect(AgentV2.ID).toBe(Agent.ID)
|
expect(AgentV2.ID).toBe(Agent.ID)
|
||||||
expect(CoreLocation.Ref).toBe(Location.Ref)
|
expect(CoreLocation.Ref).toBe(Location.Ref)
|
||||||
expect(ModelV2.Ref).toBe(Model.Ref)
|
expect(ModelV2.Ref).toBe(Model.Ref)
|
||||||
expect(SessionV2.Info).toBe(Session.Info)
|
expect(SessionV2.Info).toBe(Session.Info)
|
||||||
expect(ProjectV2.Current).toBe(Project.Current)
|
|
||||||
expect(ProjectV2.Directory).toBe(Project.Directory)
|
|
||||||
expect(ProjectV2.Directories).toBe(Project.Directories)
|
|
||||||
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
|
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
|
||||||
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
|
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
|
||||||
expect(CorePrompt).toBe(Prompt)
|
expect(CorePrompt).toBe(Prompt)
|
||||||
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
expect(Api.groups["server.session"].identifier).toBe("server.session")
|
||||||
expect(Api.groups["server.project"].identifier).toBe("server.project")
|
|
||||||
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
|
||||||
expect(Session.ID.create()).toStartWith("ses_")
|
expect(Session.ID.create()).toStartWith("ses_")
|
||||||
expect(Project.ID.global).toBe("global")
|
expect(Project.ID.global).toBe("global")
|
||||||
@@ -43,8 +38,8 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("client and Server contracts generate identically", () => {
|
test("client and Server contracts generate identically", () => {
|
||||||
const server = compile(Api, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
const server = compile(Api, { groupNames, endpointNames, omitEndpoints })
|
||||||
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
|
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
|
||||||
|
|
||||||
expect(emitPromise(client)).toEqual(emitPromise(server))
|
expect(emitPromise(client)).toEqual(emitPromise(server))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { DateTime, Effect, Stream } from "effect"
|
import { DateTime, Effect, Stream } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { AbsolutePath, Agent, Event, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect"
|
import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect"
|
||||||
|
|
||||||
const caughtUp = { type: "log.caught_up" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
|
test("sessions.get returns the decoded Effect projection", async () => {
|
||||||
|
|
||||||
test("session.get returns the decoded Effect projection", async () => {
|
|
||||||
const httpClient = HttpClient.make((request) =>
|
const httpClient = HttpClient.make((request) =>
|
||||||
Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session))),
|
Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session))),
|
||||||
)
|
)
|
||||||
const result = await Effect.gen(function* () {
|
const result = await Effect.gen(function* () {
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||||
return yield* client.session.get({ sessionID: Session.ID.make("ses_test") })
|
return yield* client.sessions.get({ sessionID: Session.ID.make("ses_test") })
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||||
|
|
||||||
expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("event.subscribe exposes and decodes the native Effect event stream", async () => {
|
test("events.subscribe exposes and decodes the native Effect event stream", async () => {
|
||||||
const httpClient = HttpClient.make((request) =>
|
const httpClient = HttpClient.make((request) =>
|
||||||
Effect.succeed(
|
Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(
|
||||||
@@ -32,7 +30,7 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
|||||||
)
|
)
|
||||||
const events = await Effect.gen(function* () {
|
const events = await Effect.gen(function* () {
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||||
return yield* client.event.subscribe().pipe(Stream.runCollect)
|
return yield* client.events.subscribe().pipe(Stream.runCollect)
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||||
|
|
||||||
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"])
|
expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"])
|
||||||
@@ -42,7 +40,7 @@ test("event.subscribe exposes and decodes the native Effect event stream", async
|
|||||||
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("event.subscribe terminates on Effect protocol decode failures", async () => {
|
test("events.subscribe terminates on Effect protocol decode failures", async () => {
|
||||||
const httpClient = HttpClient.make((request) =>
|
const httpClient = HttpClient.make((request) =>
|
||||||
Effect.succeed(
|
Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(
|
||||||
@@ -55,27 +53,39 @@ test("event.subscribe terminates on Effect protocol decode failures", async () =
|
|||||||
)
|
)
|
||||||
const error = await Effect.gen(function* () {
|
const error = await Effect.gen(function* () {
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||||
return yield* client.event.subscribe().pipe(Stream.runCollect, Effect.flip)
|
return yield* client.events.subscribe().pipe(Stream.runCollect, Effect.flip)
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||||
|
|
||||||
expect(error._tag).toBe("ClientError")
|
expect(error._tag).toBe("ClientError")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("session methods retain decoded Effect inputs and outputs", async () => {
|
test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||||
const logQueries: Array<Record<string, string>> = []
|
const historyQueries: Array<Record<string, string>> = []
|
||||||
|
let historyPage = 0
|
||||||
const httpClient = HttpClient.make((request) => {
|
const httpClient = HttpClient.make((request) => {
|
||||||
const url = request.url
|
const url = request.url
|
||||||
if (url.includes("/log")) {
|
if (url.includes("/event")) {
|
||||||
logQueries.push(Object.fromEntries(request.urlParams.params))
|
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(
|
||||||
request,
|
request,
|
||||||
new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(caughtUp)}\n\n`, {
|
new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
|
||||||
headers: { "content-type": "text/event-stream" },
|
headers: { "content-type": "text/event-stream" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (url.includes("/history")) {
|
||||||
|
historyPage++
|
||||||
|
historyQueries.push(Object.fromEntries(request.urlParams.params))
|
||||||
|
return Effect.succeed(
|
||||||
|
HttpClientResponse.fromWeb(
|
||||||
|
request,
|
||||||
|
Response.json(
|
||||||
|
historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (url.includes("/prompt")) {
|
if (url.includes("/prompt")) {
|
||||||
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
|
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
|
||||||
}
|
}
|
||||||
@@ -87,10 +97,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
}
|
}
|
||||||
if (url.endsWith("/api/session/active")) {
|
if (url.endsWith("/api/session/active")) {
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(request, Response.json({ data: { ses_test: { type: "running" } } })),
|
||||||
request,
|
|
||||||
Response.json({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } }),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (request.method === "POST" && url.endsWith("/api/session")) {
|
if (request.method === "POST" && url.endsWith("/api/session")) {
|
||||||
@@ -100,46 +107,54 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
|
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
|
||||||
}
|
}
|
||||||
return Effect.succeed(
|
return Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(request, Response.json({ data: [session.data], cursor: { next: "next" } })),
|
||||||
request,
|
|
||||||
Response.json({ data: [session.data], watermarks: { ses_test: 3 }, cursor: { next: "next" } }),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const result = await Effect.gen(function* () {
|
const result = await Effect.gen(function* () {
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||||
const page = yield* client.session.list({ limit: 10 })
|
const page = yield* client.sessions.list({ limit: 10 })
|
||||||
const active = yield* client.session.active()
|
const active = yield* client.sessions.active()
|
||||||
const created = yield* client.session.create({
|
const created = yield* client.sessions.create({
|
||||||
location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }),
|
location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }),
|
||||||
})
|
})
|
||||||
yield* client.session.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") })
|
yield* client.sessions.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") })
|
||||||
yield* client.session.switchModel({
|
yield* client.sessions.switchModel({
|
||||||
sessionID: Session.ID.make("ses_test"),
|
sessionID: Session.ID.make("ses_test"),
|
||||||
model: Model.Ref.make({ id: "claude", providerID: "anthropic" }),
|
model: Model.Ref.make({ id: "claude", providerID: "anthropic" }),
|
||||||
})
|
})
|
||||||
const admitted = yield* client.session.prompt({
|
const admitted = yield* client.sessions.prompt({
|
||||||
sessionID: Session.ID.make("ses_test"),
|
sessionID: Session.ID.make("ses_test"),
|
||||||
prompt: Prompt.make({ text: "Hello" }),
|
prompt: Prompt.make({ text: "Hello" }),
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
yield* client.session.compact({ sessionID: Session.ID.make("ses_test") })
|
yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") })
|
||||||
yield* client.session.wait({ sessionID: Session.ID.make("ses_test") })
|
yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") })
|
||||||
const context = yield* client.session.context({ sessionID: Session.ID.make("ses_test") })
|
const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") })
|
||||||
const log = yield* client.session
|
const history = yield* client.sessions.history({
|
||||||
.log({ sessionID: Session.ID.make("ses_test"), after: Event.Seq.make(0) })
|
sessionID: Session.ID.make("ses_test"),
|
||||||
|
after: 0,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
const historyNext = history.hasMore
|
||||||
|
? yield* client.sessions.history({
|
||||||
|
sessionID: Session.ID.make("ses_test"),
|
||||||
|
after: history.data.at(-1)?.durable?.seq,
|
||||||
|
limit: 2,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
const events = yield* client.sessions
|
||||||
|
.events({ sessionID: Session.ID.make("ses_test"), after: 0 })
|
||||||
.pipe(Stream.runCollect)
|
.pipe(Stream.runCollect)
|
||||||
yield* client.session.interrupt({ sessionID: Session.ID.make("ses_test") })
|
yield* client.sessions.interrupt({ sessionID: Session.ID.make("ses_test") })
|
||||||
const message = yield* client.session.message({
|
const message = yield* client.sessions.message({
|
||||||
sessionID: Session.ID.make("ses_test"),
|
sessionID: Session.ID.make("ses_test"),
|
||||||
messageID: SessionMessage.ID.make("msg_model"),
|
messageID: SessionMessage.ID.make("msg_model"),
|
||||||
})
|
})
|
||||||
return { page, active, created, admitted, context, log, message }
|
return { page, active, created, admitted, context, history, historyNext, events, message }
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||||
|
|
||||||
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
|
||||||
expect(result.active).toEqual({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } })
|
expect(result.active).toEqual({ ses_test: { type: "running" } })
|
||||||
expect(result.page.watermarks).toEqual({ ses_test: 3 })
|
|
||||||
expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype)
|
expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype)
|
||||||
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
|
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
|
||||||
expect(result.created.id).toBe("ses_test")
|
expect(result.created.id).toBe("ses_test")
|
||||||
@@ -147,17 +162,16 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
|||||||
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
|
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
|
||||||
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
||||||
expect(result.context).toEqual([])
|
expect(result.context).toEqual([])
|
||||||
expect(logQueries[0]).toEqual({ after: "0" })
|
expect(DateTime.toEpochMillis(result.history.data[0].data.timestamp)).toBe(1_717_171_717_000)
|
||||||
const logged = Array.from(result.log)
|
expect(result.history).toEqual(expect.objectContaining({ hasMore: true }))
|
||||||
expect(logged.map((item) => item.type)).toEqual(["session.next.model.switched", "log.caught_up"])
|
expect(result.historyNext).toEqual({ data: [], hasMore: false })
|
||||||
expect(logged[0]?.type === "session.next.model.switched" && DateTime.toEpochMillis(logged[0].data.timestamp)).toBe(
|
expect(historyQueries[0]).toEqual({ limit: "1", after: "0" })
|
||||||
1_717_171_717_000,
|
expect(historyQueries[1]).toEqual({ limit: "2", after: "1" })
|
||||||
)
|
expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000)
|
||||||
expect(logged.at(-1)).toEqual(caughtUp)
|
|
||||||
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
|
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("session.log retains the typed SessionNotFoundError", async () => {
|
test("sessions.history retains the typed SessionNotFoundError", async () => {
|
||||||
const httpClient = HttpClient.make((request) =>
|
const httpClient = HttpClient.make((request) =>
|
||||||
Effect.succeed(
|
Effect.succeed(
|
||||||
HttpClientResponse.fromWeb(
|
HttpClientResponse.fromWeb(
|
||||||
@@ -171,7 +185,11 @@ test("session.log retains the typed SessionNotFoundError", async () => {
|
|||||||
)
|
)
|
||||||
const error = await Effect.gen(function* () {
|
const error = await Effect.gen(function* () {
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||||
return yield* client.session.log({ sessionID: Session.ID.make("ses_missing") }).pipe(Stream.runCollect, Effect.flip)
|
return yield* client.sessions
|
||||||
|
.history({
|
||||||
|
sessionID: Session.ID.make("ses_missing"),
|
||||||
|
})
|
||||||
|
.pipe(Effect.flip)
|
||||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||||
|
|
||||||
expect(error._tag).toBe("SessionNotFoundError")
|
expect(error._tag).toBe("SessionNotFoundError")
|
||||||
|
|||||||
@@ -7,30 +7,25 @@ test("exposes every standard HTTP API group", () => {
|
|||||||
expect(Object.keys(client)).toEqual([
|
expect(Object.keys(client)).toEqual([
|
||||||
"health",
|
"health",
|
||||||
"location",
|
"location",
|
||||||
"agent",
|
"agents",
|
||||||
"plugin",
|
"sessions",
|
||||||
"session",
|
"messages",
|
||||||
"message",
|
"models",
|
||||||
"model",
|
"providers",
|
||||||
"generate",
|
"integrations",
|
||||||
"provider",
|
"credentials",
|
||||||
"integration",
|
"permissions",
|
||||||
"server.mcp",
|
"files",
|
||||||
"credential",
|
"commands",
|
||||||
"project",
|
"skills",
|
||||||
"permission",
|
"events",
|
||||||
"file",
|
"ptys",
|
||||||
"command",
|
"questions",
|
||||||
"skill",
|
"references",
|
||||||
"event",
|
"projectCopies",
|
||||||
"pty",
|
|
||||||
"shell",
|
|
||||||
"question",
|
|
||||||
"reference",
|
|
||||||
"projectCopy",
|
|
||||||
])
|
])
|
||||||
expect(Object.keys(client.message)).toEqual(["list"])
|
expect(Object.keys(client.messages)).toEqual(["list"])
|
||||||
expect(Object.keys(client.integration)).toEqual([
|
expect(Object.keys(client.integrations)).toEqual([
|
||||||
"list",
|
"list",
|
||||||
"get",
|
"get",
|
||||||
"connectKey",
|
"connectKey",
|
||||||
@@ -39,95 +34,11 @@ test("exposes every standard HTTP API group", () => {
|
|||||||
"attemptComplete",
|
"attemptComplete",
|
||||||
"attemptCancel",
|
"attemptCancel",
|
||||||
])
|
])
|
||||||
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
|
expect(Object.keys(client.files)).toEqual(["list", "find"])
|
||||||
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"])
|
||||||
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "output", "remove"])
|
|
||||||
expect(Object.keys(client.project)).toEqual(["current", "directories"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("file.read returns binary content from the public HTTP contract", async () => {
|
test("sessions.get returns the wire projection", async () => {
|
||||||
let request: Request | undefined
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: "http://localhost:3000",
|
|
||||||
fetch: async (input) => {
|
|
||||||
request = input instanceof Request ? input : new Request(input)
|
|
||||||
return new Response(new Uint8Array([104, 105]))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const content = await client.file.read({
|
|
||||||
path: "src/a b#c.ts",
|
|
||||||
location: { directory: "/tmp/project" },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(Array.from(content)).toEqual([104, 105])
|
|
||||||
expect(request?.url).toBe(
|
|
||||||
"http://localhost:3000/api/fs/read/src/a%20b%23c.ts?location%5Bdirectory%5D=%2Ftmp%2Fproject",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("project methods use the public HTTP contract", async () => {
|
|
||||||
const requests: string[] = []
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: "http://localhost:3000",
|
|
||||||
fetch: async (input) => {
|
|
||||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
|
|
||||||
requests.push(url)
|
|
||||||
if (url.includes("/directories")) return Response.json([])
|
|
||||||
return Response.json({ id: "proj_test", directory: "/tmp/project" })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const current = await client.project.current({ location: { workspace: "wrk_test" } })
|
|
||||||
const directories = await client.project.directories({
|
|
||||||
projectID: current.id,
|
|
||||||
location: { directory: current.directory },
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(current).toEqual({ id: "proj_test", directory: "/tmp/project" })
|
|
||||||
expect(directories).toEqual([])
|
|
||||||
expect(requests).toEqual([
|
|
||||||
"http://localhost:3000/api/project/current?location%5Bworkspace%5D=wrk_test",
|
|
||||||
"http://localhost:3000/api/project/proj_test/directories?location%5Bdirectory%5D=%2Ftmp%2Fproject",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shell list and remove use the public HTTP contract", async () => {
|
|
||||||
const requests: Array<{ method: string; url: string }> = []
|
|
||||||
const shell = {
|
|
||||||
id: "sh_test",
|
|
||||||
status: "running",
|
|
||||||
command: "pwd",
|
|
||||||
cwd: "/tmp/project",
|
|
||||||
shell: "/bin/zsh",
|
|
||||||
file: "/tmp/opencode-shell",
|
|
||||||
metadata: { sessionID: "ses_test" },
|
|
||||||
time: { started: 1_717_171_717_000 },
|
|
||||||
}
|
|
||||||
const client = OpenCode.make({
|
|
||||||
baseUrl: "http://localhost:3000",
|
|
||||||
fetch: async (input, init) => {
|
|
||||||
const request = input instanceof Request ? input : new Request(input, init)
|
|
||||||
requests.push({ method: request.method, url: request.url })
|
|
||||||
if (request.method === "DELETE") return new Response(null, { status: 204 })
|
|
||||||
return Response.json({
|
|
||||||
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
|
|
||||||
data: [shell],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await client.shell.list({ location: { directory: "/tmp/project" } })
|
|
||||||
await client.shell.remove({ id: shell.id })
|
|
||||||
|
|
||||||
expect(result.data).toEqual([shell])
|
|
||||||
expect(requests).toEqual([
|
|
||||||
{ method: "GET", url: "http://localhost:3000/api/shell?location%5Bdirectory%5D=%2Ftmp%2Fproject" },
|
|
||||||
{ method: "DELETE", url: "http://localhost:3000/api/shell/sh_test" },
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("session.get returns the wire projection", async () => {
|
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
fetch: async (input) => {
|
fetch: async (input) => {
|
||||||
@@ -138,12 +49,12 @@ test("session.get returns the wire projection", async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await client.session.get({ sessionID: "ses_test" })
|
const result = await client.sessions.get({ sessionID: "ses_test" })
|
||||||
|
|
||||||
expect(result.time.created).toBe(1_717_171_717_000)
|
expect(result.time.created).toBe(1_717_171_717_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("event.subscribe exposes the Promise event stream wire projection", async () => {
|
test("events.subscribe exposes the Promise event stream wire projection", async () => {
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
fetch: async () =>
|
fetch: async () =>
|
||||||
@@ -154,19 +65,19 @@ test("event.subscribe exposes the Promise event stream wire projection", async (
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
const events = []
|
const events = []
|
||||||
for await (const event of client.event.subscribe()) events.push(event)
|
for await (const event of client.events.subscribe()) events.push(event)
|
||||||
|
|
||||||
expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
|
expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
|
||||||
expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
|
expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("event.subscribe terminates on malformed Promise SSE data", async () => {
|
test("events.subscribe terminates on malformed Promise SSE data", async () => {
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
fetch: async () => new Response("data: {not-json}\n\n", { headers: { "content-type": "text/event-stream" } }),
|
fetch: async () => new Response("data: {not-json}\n\n", { headers: { "content-type": "text/event-stream" } }),
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(client.event.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({
|
await expect(client.events.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({
|
||||||
name: "ClientError",
|
name: "ClientError",
|
||||||
reason: "MalformedResponse",
|
reason: "MalformedResponse",
|
||||||
})
|
})
|
||||||
@@ -174,6 +85,7 @@ test("event.subscribe terminates on malformed Promise SSE data", async () => {
|
|||||||
|
|
||||||
test("session methods use the public HTTP contract", async () => {
|
test("session methods use the public HTTP contract", async () => {
|
||||||
const requests: Array<{ url: string; init?: RequestInit }> = []
|
const requests: Array<{ url: string; init?: RequestInit }> = []
|
||||||
|
let historyPage = 0
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
fetch: async (input, init) => {
|
fetch: async (input, init) => {
|
||||||
@@ -184,49 +96,56 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
headers: { "content-type": "text/event-stream" },
|
headers: { "content-type": "text/event-stream" },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (url.includes("/log")) {
|
if (url.includes("/history")) {
|
||||||
return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(caughtUp)}\n\n`, {
|
historyPage++
|
||||||
headers: { "content-type": "text/event-stream" },
|
return Response.json(
|
||||||
})
|
historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (url.includes("/prompt")) return Response.json(admission)
|
if (url.includes("/prompt")) return Response.json(admission)
|
||||||
if (url.includes("/context")) return Response.json({ data: [] })
|
if (url.includes("/context")) return Response.json({ data: [] })
|
||||||
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
||||||
if (url.endsWith("/api/session/active"))
|
if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } })
|
||||||
return Response.json({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } })
|
|
||||||
if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
|
if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
|
||||||
if (init?.method === "POST") return new Response(null, { status: 204 })
|
if (init?.method === "POST") return new Response(null, { status: 204 })
|
||||||
return Response.json({ data: [session.data], cursor: { next: "next" } })
|
return Response.json({ data: [session.data], cursor: { next: "next" } })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const page = await client.session.list({ limit: 10, order: "desc" })
|
const page = await client.sessions.list({ limit: 10, order: "desc" })
|
||||||
const active = await client.session.active()
|
const active = await client.sessions.active()
|
||||||
const created = await client.session.create({ location: { directory: "/tmp/project" } })
|
const created = await client.sessions.create({ location: { directory: "/tmp/project" } })
|
||||||
await client.session.switchAgent({ sessionID: "ses_test", agent: "build" })
|
await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" })
|
||||||
await client.session.switchModel({
|
await client.sessions.switchModel({
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
model: { id: "claude", providerID: "anthropic" },
|
model: { id: "claude", providerID: "anthropic" },
|
||||||
})
|
})
|
||||||
const admitted = await client.session.prompt({
|
const admitted = await client.sessions.prompt({
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
prompt: { text: "Hello" },
|
prompt: { text: "Hello" },
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
await client.session.compact({ sessionID: "ses_test" })
|
await client.sessions.compact({ sessionID: "ses_test" })
|
||||||
await client.session.wait({ sessionID: "ses_test" })
|
await client.sessions.wait({ sessionID: "ses_test" })
|
||||||
const context = await client.session.context({ sessionID: "ses_test" })
|
const context = await client.sessions.context({ sessionID: "ses_test" })
|
||||||
const log = []
|
const history = await client.sessions.history({ sessionID: "ses_test", after: 0, limit: 1 })
|
||||||
for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item)
|
const historyAfter = history.data.at(-1)?.durable?.seq
|
||||||
await client.session.interrupt({ sessionID: "ses_test" })
|
const historyNext = history.hasMore
|
||||||
const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" })
|
? await client.sessions.history({ sessionID: "ses_test", after: historyAfter, limit: 2 })
|
||||||
|
: undefined
|
||||||
|
const events = []
|
||||||
|
for await (const event of client.sessions.events({ sessionID: "ses_test", after: 0 })) events.push(event)
|
||||||
|
await client.sessions.interrupt({ sessionID: "ses_test" })
|
||||||
|
const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" })
|
||||||
|
|
||||||
expect(page.cursor.next).toBe("next")
|
expect(page.cursor.next).toBe("next")
|
||||||
expect(active).toEqual({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } })
|
expect(active).toEqual({ ses_test: { type: "running" } })
|
||||||
expect(created.id).toBe("ses_test")
|
expect(created.id).toBe("ses_test")
|
||||||
expect(admitted.id).toBe("msg_test")
|
expect(admitted.id).toBe("msg_test")
|
||||||
expect(context).toEqual([])
|
expect(context).toEqual([])
|
||||||
expect(log).toEqual([modelSwitchedEvent, caughtUp])
|
expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true })
|
||||||
|
expect(historyNext).toEqual({ data: [], hasMore: false })
|
||||||
|
expect(events).toEqual([modelSwitchedEvent])
|
||||||
expect(message).toEqual(modelSwitchedMessage)
|
expect(message).toEqual(modelSwitchedMessage)
|
||||||
expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
|
expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
|
||||||
["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
|
["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
|
||||||
@@ -238,7 +157,9 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/context"],
|
["GET", "http://localhost:3000/api/session/ses_test/context"],
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/log?after=0"],
|
["GET", "http://localhost:3000/api/session/ses_test/history?limit=1&after=0"],
|
||||||
|
["GET", "http://localhost:3000/api/session/ses_test/history?limit=2&after=1"],
|
||||||
|
["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
|
["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
|
||||||
["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
|
["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
|
||||||
])
|
])
|
||||||
@@ -258,14 +179,14 @@ test("middleware errors remain declared client errors", async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.session.create({})
|
await client.sessions.create({})
|
||||||
throw new Error("Expected request to fail")
|
throw new Error("Expected request to fail")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(isUnauthorizedError(error)).toBe(true)
|
expect(isUnauthorizedError(error)).toBe(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("session.log decodes SessionNotFoundError", async () => {
|
test("sessions.history decodes SessionNotFoundError", async () => {
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
baseUrl: "http://localhost:3000",
|
baseUrl: "http://localhost:3000",
|
||||||
fetch: async () =>
|
fetch: async () =>
|
||||||
@@ -276,7 +197,7 @@ test("session.log decodes SessionNotFoundError", async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.session.log({ sessionID: "ses_missing" })[Symbol.asyncIterator]().next()
|
await client.sessions.history({ sessionID: "ses_missing" })
|
||||||
throw new Error("Expected request to fail")
|
throw new Error("Expected request to fail")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(isSessionNotFoundError(error)).toBe(true)
|
expect(isSessionNotFoundError(error)).toBe(true)
|
||||||
@@ -321,8 +242,6 @@ const modelSwitchedMessage = {
|
|||||||
model: { id: "claude", providerID: "anthropic" },
|
model: { id: "claude", providerID: "anthropic" },
|
||||||
}
|
}
|
||||||
|
|
||||||
const caughtUp = { type: "log.caught_up", aggregateID: "ses_test", seq: 1 }
|
|
||||||
|
|
||||||
const modelSwitchedEvent = {
|
const modelSwitchedEvent = {
|
||||||
id: "evt_model",
|
id: "evt_model",
|
||||||
type: "session.next.model.switched",
|
type: "session.next.model.switched",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.17.13",
|
"version": "1.17.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
|
"go.banner.text": "MiniMax M3: حد استخدام أكبر 3 مرات لفترة محدودة",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.2 وGLM-5.1 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
|
||||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||||
|
"go.banner.text": "MiniMax M3 tem limite de uso 3x maior por tempo limitado",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||||
|
"go.banner.text": "MiniMax M3 får tredoblet brugsgrænse i en begrænset periode",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||||
|
"go.banner.text": "MiniMax M3 erhält für begrenzte Zeit 3x Nutzungslimits",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
"Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "following exceptions",
|
"zen.privacy.exceptionsLink": "following exceptions",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||||
|
"go.banner.text": "MiniMax M3 gets 3× usage limits for a limited time",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
"Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Low cost coding models for everyone",
|
"go.hero.title": "Low cost coding models for everyone",
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||||
|
"go.banner.text": "MiniMax M3 tiene límites de uso 3x mayores por tiempo limitado",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
"Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||||
|
"go.banner.text": "MiniMax M3 bénéficie de limites d’utilisation 3x supérieures pour une durée limitée",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
"Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||||
|
"go.banner.text": "MiniMax M3 offre limiti di utilizzo 3x superiori per un periodo limitato",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
"Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "以下の例外",
|
"zen.privacy.exceptionsLink": "以下の例外",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||||
|
"go.banner.text": "MiniMax M3の利用上限が期間限定で3倍に",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Goは最初の月$5、その後$10/月で、GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
"Goは最初の月$5、その後$10/月で、GLM-5.2、GLM-5.1、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。",
|
||||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "다음 예외",
|
"zen.privacy.exceptionsLink": "다음 예외",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||||
|
"go.banner.text": "MiniMax M3 사용 한도가 한시적으로 3배 확대됩니다",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
"Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.",
|
||||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||||
|
"go.banner.text": "MiniMax M3 får 3x bruksgrense i en begrenset periode",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
"Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||||
|
"go.banner.text": "MiniMax M3 oferuje 3x wyższe limity użycia przez ograniczony czas",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
"Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||||
|
"go.banner.text": "MiniMax M3 получает 3x лимиты использования на ограниченное время",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
"Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
|
"go.banner.text": "MiniMax M3 เพิ่มโควตาการใช้งานเป็น 3 เท่าในช่วงเวลาจำกัด",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
"Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash",
|
||||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||||
|
"go.banner.text": "MiniMax M3 sınırlı bir süre için 3x kullanım limiti sunuyor",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
"Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.",
|
||||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export const dict = {
|
|||||||
"zen.privacy.exceptionsLink": "такими винятками",
|
"zen.privacy.exceptionsLink": "такими винятками",
|
||||||
|
|
||||||
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
|
||||||
|
"go.banner.text": "MiniMax M3 отримує 3x ліміти використання протягом обмеженого часу",
|
||||||
"go.meta.description":
|
"go.meta.description":
|
||||||
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
"Go починається від $5 за перший місяць, потім $10/місяць, з generous 5-годинними лімітами запитів для GLM-5.2, GLM-5.1, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.",
|
||||||
"go.hero.title": "Недорогі моделі кодування для всіх",
|
"go.hero.title": "Недорогі моделі кодування для всіх",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user