mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-16 01:19:19 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4898dbc1d | |||
| 350e6f5d56 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"@opencode-ai/core": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Preserve prompt cache prefixes when sessions move between locations with unchanged instructions.
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
"@opencode-ai/core": minor
|
|
||||||
"@opencode-ai/schema": minor
|
|
||||||
"@opencode-ai/protocol": minor
|
|
||||||
"@opencode-ai/client": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Replace Core shell permission parsing with portable, fail-closed Bash and PowerShell scanners.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
name: deploy-lab-catalog
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [v2]
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/deploy-lab-catalog.yml"
|
|
||||||
- "bun.lock"
|
|
||||||
- "package.json"
|
|
||||||
- "packages/drive/**"
|
|
||||||
- "packages/protocol/src/simulation.ts"
|
|
||||||
- "packages/simulation/**"
|
|
||||||
- "packages/lab/catalog/**"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy-lab-catalog-${{ github.ref_name }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'v2'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Install ffmpeg
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install --yes ffmpeg
|
|
||||||
|
|
||||||
- name: Validate
|
|
||||||
run: |
|
|
||||||
bun --cwd packages/protocol typecheck
|
|
||||||
bun --cwd packages/simulation typecheck
|
|
||||||
bun --cwd packages/drive run check
|
|
||||||
bun --cwd packages/drive run test
|
|
||||||
bun --cwd packages/lab/catalog run check
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
working-directory: packages/lab/catalog
|
|
||||||
run: bun run deploy
|
|
||||||
env:
|
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- ci
|
- ci
|
||||||
- dev
|
- dev
|
||||||
|
- v2
|
||||||
- beta
|
- beta
|
||||||
- fix/npm-native-binary-install
|
- fix/npm-native-binary-install
|
||||||
- snapshot-*
|
- snapshot-*
|
||||||
|
|||||||
@@ -72,20 +72,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
run: GITHUB_ACTIONS=false bun turbo test ${{ runner.os == 'Windows' && '--filter=!opencode-drive' || '' }}
|
run: GITHUB_ACTIONS=false bun turbo test
|
||||||
env:
|
env:
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||||
|
|
||||||
- name: Verify PowerShell 7 scanner conformance
|
|
||||||
if: always() && runner.os == 'Windows'
|
|
||||||
working-directory: packages/shell-scan
|
|
||||||
run: PWSH=pwsh bun run research:powershell
|
|
||||||
|
|
||||||
- name: Verify Windows PowerShell scanner conformance
|
|
||||||
if: always() && runner.os == 'Windows'
|
|
||||||
working-directory: packages/shell-scan
|
|
||||||
run: PWSH=powershell.exe bun run research:powershell
|
|
||||||
|
|
||||||
- name: Verify compiled service lifecycle
|
- name: Verify compiled service lifecycle
|
||||||
if: always()
|
if: always()
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# he Glossary
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
- Hebrew Academy approved IT terminology: https://terms.hebrew-academy.org.il/Millonim/ShowMillon?KodMillon=192
|
|
||||||
- Firefox Hebrew localization corpus: https://github.com/mozilla-l10n/firefox-l10n/tree/main/he
|
|
||||||
- KDE Hebrew localization team and corpus: https://l10n.kde.org/team-infos.php?teamcode=he
|
|
||||||
- Community-maintained VS Code Hebrew language pack: https://github.com/AMAARETS/vscode-language-pack-he
|
|
||||||
- Microsoft Hebrew developer documentation for Git terminology: https://learn.microsoft.com/he-il/power-platform/alm/tutorials/github-actions-deploy
|
|
||||||
- W3C guidance for bidirectional text: https://www.w3.org/International/articles/strings-and-bidi/
|
|
||||||
|
|
||||||
## Do Not Translate (Locale Additions)
|
|
||||||
|
|
||||||
- `OpenCode` (preserve casing in prose and UI copy)
|
|
||||||
- `API`, `MCP`, `LSP`, `OAuth`, `Git`, model names, and provider names
|
|
||||||
- Commands, flags, keyboard shortcuts, file paths, URLs, identifiers, hashes, and code literals
|
|
||||||
- Keep `commit` and `diff` when they name the exact Git artifact or operation
|
|
||||||
|
|
||||||
## Preferred Terms
|
|
||||||
|
|
||||||
| English / Context | Preferred | Notes |
|
|
||||||
| ----------------- | ------------- | ------------------------------------------------------------------------- |
|
|
||||||
| session | `הפעלה` | Use `שיחה` only when the source specifically means a chat or conversation |
|
|
||||||
| workspace | `סביבת עבודה` | |
|
|
||||||
| terminal | `מסוף` | Prefer the established Hebrew term over transliteration |
|
|
||||||
| command | `פקודה` | |
|
|
||||||
| provider | `ספק` | Use `ספק מודלים` where the bare noun is ambiguous |
|
|
||||||
| model | `מודל` | |
|
|
||||||
| API key | `מפתח API` | Keep the acronym in Latin letters |
|
|
||||||
| plugin | `תוסף` | |
|
|
||||||
| repository | `מאגר` | Use `מאגר Git` where context is ambiguous |
|
|
||||||
| branch | `ענף` | |
|
|
||||||
| context | `הקשר` | Use `חלון הקשר` for context window |
|
|
||||||
| tokens | `אסימונים` | |
|
|
||||||
|
|
||||||
## Guidance
|
|
||||||
|
|
||||||
- Prefer natural modern Israeli Hebrew over word-for-word translation or obscure coined terms.
|
|
||||||
- Use short action verbs for controls and translate complete phrases in context.
|
|
||||||
- Keep recognized developer acronyms and exact Git vocabulary in Latin script instead of phonetic transliteration.
|
|
||||||
- Treat embedded code, paths, commands, shortcuts, hashes, model IDs, and other Latin technical artifacts as LTR content inside the RTL interface.
|
|
||||||
- Keep recurring concepts consistent and do not collapse session, chat, run, and launch into one Hebrew term.
|
|
||||||
|
|
||||||
## Avoid
|
|
||||||
|
|
||||||
- Avoid transliterations such as `טרמינל`, `פלאגין`, and `קומנד` when `מסוף`, `תוסף`, and `פקודה` are clear.
|
|
||||||
- Avoid translating `commit` as `התחייבות`.
|
|
||||||
- Avoid inventing Hebrew expansions for `API`, `MCP`, or `LSP`.
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
---
|
|
||||||
name: opencode-drive
|
|
||||||
description: Use when an agent needs drive OpenCode via a script or interact with an isolated instance
|
|
||||||
---
|
|
||||||
|
|
||||||
# OpenCode Drive
|
|
||||||
|
|
||||||
Use `opencode-drive` to launch an isolated OpenCode instance and control it via commands or a script.
|
|
||||||
|
|
||||||
There are two modes. Always default to using a script unless specifically directed to be interactive (connect
|
|
||||||
to an existing running instance, or start a new one, and make a few changes to the UI and read it, and iterate
|
|
||||||
on changes).
|
|
||||||
|
|
||||||
Scripts allow you to run a full walkthrough in one run. When the script is done opencode-drive exits,
|
|
||||||
stops all processes, and cleans up all artifacts.
|
|
||||||
|
|
||||||
# Prepare The Environment
|
|
||||||
|
|
||||||
Use `init` when files must be added to the isolated home or project before OpenCode starts. It prints the artifact directory without launching OpenCode. A later `start` with the same name reuses it.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
artifacts=$(opencode-drive init --name demo)
|
|
||||||
cp -R ./fixtures/home/. "$artifacts/"
|
|
||||||
cp -R ./fixtures/project/. "$artifacts/files/"
|
|
||||||
opencode-drive start --name demo --dev ~/projects/opencode
|
|
||||||
```
|
|
||||||
|
|
||||||
The simulated project is under `$artifacts/files`. Running `start` without a prior `init` initializes the artifacts automatically.
|
|
||||||
|
|
||||||
# Scripted usage
|
|
||||||
|
|
||||||
You can write scripts that walk through entire flows, and gives you full access to controlling
|
|
||||||
the backend too. See examples of the script API at the bottom of this file.
|
|
||||||
|
|
||||||
After creating or editing a script, always typecheck it before running. Never skip this step:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive check ./reproduce-stale-exploring-empty.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Run it by passing `--script` to start:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive start --name auto-stop-reproduction --script ./reproduce-stale-exploring-empty.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
It will output information about the run, including paths to log files which you can read
|
|
||||||
to inspect what happened. If you need to dig into failures that aren't clear, read those log
|
|
||||||
files. If the script is unsuccessful, automatically fix the script and run it again.
|
|
||||||
|
|
||||||
Scripts use one typed definition object. `setup` runs before OpenCode starts,
|
|
||||||
and `fs.writeFile` always writes inside the simulated project.
|
|
||||||
|
|
||||||
You can read the full typed API here: https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/src/script/types.ts
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { defineScript } from "opencode-drive"
|
|
||||||
|
|
||||||
export default defineScript({
|
|
||||||
async setup({ fs, config }) {
|
|
||||||
config.autoupdate = false
|
|
||||||
await fs.writeFile("src/example.ts", "export const value = 1\n")
|
|
||||||
},
|
|
||||||
|
|
||||||
async run({ ui, llm }) {
|
|
||||||
await ui.submit("Open src/example.ts")
|
|
||||||
await llm.send(llm.text("The file exports `value`."))
|
|
||||||
await ui.waitFor("The file exports `value`.")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
`setup` receives the current OpenCode config object, which starts from the
|
|
||||||
default drive config unless the prepared instance already has one. When a script
|
|
||||||
needs custom config, mutate this `config` parameter instead of generating and
|
|
||||||
writing a new config object from scratch, so the script keeps the default
|
|
||||||
provider/model settings unless it intentionally changes them.
|
|
||||||
|
|
||||||
Note that the simulated model is a GPT model type, and opencode uses the `patch` tool for working with files Do not use a `edit` or `write` tool to edit files.
|
|
||||||
|
|
||||||
Use `launch: "manual"` when the script needs to launch the server and every TUI
|
|
||||||
itself (this is extremely rare, do not use this unless explicitly asked). In this
|
|
||||||
mode `ui` is typed as `null`; call `server.launch()` exactly
|
|
||||||
once before launching clients. Each `clients.launch(name)` result provides the
|
|
||||||
same UI methods as the automatic client. You can see an example of this API
|
|
||||||
here: https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/examples/multiple-clients.ts
|
|
||||||
|
|
||||||
Use the exported `wait(milliseconds)` utility for an unconditional delay.
|
|
||||||
|
|
||||||
`await llm.send(...)` waits for the next request and resolves after OpenCode
|
|
||||||
acknowledges its complete response. `llm.queue(...)` declares responses in
|
|
||||||
advance. Chunks may be built with `text`, `reasoning`, `toolCall`, `raw`,
|
|
||||||
`finish`, and `disconnect`. A normal response receives `finish("stop")`
|
|
||||||
automatically unless it yields or queues an explicit terminal event.
|
|
||||||
|
|
||||||
`llm.text(text, { delay, chunkSize })` defaults to a 2 ms delay and a
|
|
||||||
15-character target varied by plus or minus 5 per chunk.
|
|
||||||
|
|
||||||
`llm.reasoning` accepts the same options, and `llm.pause(milliseconds)` adds a
|
|
||||||
delay between any two outputs.
|
|
||||||
|
|
||||||
Use `llm.serve` for an ongoing typed response generator:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
llm.serve(async function* (request, index) {
|
|
||||||
yield llm.reasoning(`Handling request ${index + 1}`)
|
|
||||||
yield llm.text(`Received ${request.id}`)
|
|
||||||
yield llm.finish("stop")
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
The backend connection, response cleanup, cancellation, and recording
|
|
||||||
completion are automatic.
|
|
||||||
|
|
||||||
You can see some example scripts here:
|
|
||||||
|
|
||||||
- https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/examples/simple.ts
|
|
||||||
- https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/examples/serve.ts
|
|
||||||
|
|
||||||
## Prune
|
|
||||||
|
|
||||||
- `prune` removes artifact directories. These are always cleaned up after running a script
|
|
||||||
successfully, but leftover on failed runs. Always call this if a script fails.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive prune --name demo
|
|
||||||
|
|
||||||
// --force cleans up all artifcat directories
|
|
||||||
opencode-dirve prune --force
|
|
||||||
```
|
|
||||||
|
|
||||||
# Live interaction usage
|
|
||||||
|
|
||||||
- Always give headless instances a unique `--name`. Visible instances may omit it.
|
|
||||||
- A normal headless `start` detaches automatically and returns after the instance is ready.
|
|
||||||
- Do not add `&`; the long-running owner already runs in the background.
|
|
||||||
- Configure simulated model responses after startup when needed.
|
|
||||||
- Send ordered UI commands with `send`.
|
|
||||||
- Always stop the instance when finished.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive start --name demo
|
|
||||||
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.type '{"text":"Explain this project"}' \
|
|
||||||
--command.ui.enter
|
|
||||||
|
|
||||||
opencode-drive stop --name demo
|
|
||||||
```
|
|
||||||
|
|
||||||
## Send UI Commands
|
|
||||||
|
|
||||||
- Every `send` opens a connection to the named instance, runs its commands in order, and exits.
|
|
||||||
- Combine typing and Enter in one command when submitting a prompt.
|
|
||||||
- JSON-valued commands require one JSON argument.
|
|
||||||
- Multiple command flags execute from left to right.
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
|
|
||||||
- `--command.ui.type <json>` types into the focused editor. Arguments: `text` string.
|
|
||||||
- `--command.ui.press <json>` presses a key. Arguments: `key` string; optional `modifiers` object with boolean `ctrl`, `shift`, `meta`, `super`, or `hyper`.
|
|
||||||
- `--command.ui.enter` presses Enter. Arguments: none.
|
|
||||||
- `--command.ui.arrow <json>` presses an arrow key. Arguments: `direction` is `up`, `down`, `left`, or `right`.
|
|
||||||
- `--command.ui.focus <json>` focuses an element. Arguments: `target` is the numeric element `num` returned by `ui.state`.
|
|
||||||
- `--command.ui.click <json>` clicks an element. Arguments: numeric `target`, `x`, and `y`; use the element `num` returned by `ui.state` as `target`.
|
|
||||||
- `--command.ui.state` prints focus and interactive element metadata as JSON. Arguments: none.
|
|
||||||
- `--command.ui.matches <json>` prints whether literal, case-sensitive text appears on screen. Arguments: `text` string.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.type '{"text":"Find the relevant code and explain it"}' \
|
|
||||||
--command.ui.enter
|
|
||||||
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.press '{"key":"p","modifiers":{"ctrl":true}}'
|
|
||||||
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.arrow '{"direction":"down"}'
|
|
||||||
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.focus '{"target":12}'
|
|
||||||
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.click '{"target":12,"x":4,"y":1}'
|
|
||||||
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.matches '{"text":"OpenCode"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
To read the UI state and see information about interactable elements, use the `ui.state` command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive send --name demo --command.ui.state
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configure LLM Responses
|
|
||||||
|
|
||||||
- `responses` controls what the LLM responds with
|
|
||||||
- Only use this if you are wanting to reproduce an exact type of response
|
|
||||||
- Defaults are `text,reasoning,diff,tool` with `write,apply_patch`.
|
|
||||||
- Supported types are `text`, `reasoning`, `diff`, and `tool`.
|
|
||||||
- `--tools` limits generated tool calls to names offered by OpenCode.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive responses --name demo \
|
|
||||||
--types text,reasoning,diff,tool \
|
|
||||||
--tools write,apply_patch
|
|
||||||
|
|
||||||
opencode-drive responses --name demo \
|
|
||||||
--types tool \
|
|
||||||
--tools read,glob,grep
|
|
||||||
```
|
|
||||||
|
|
||||||
## Inspect The UI
|
|
||||||
|
|
||||||
- `ui.state` prints focus and interactive element metadata as JSON.
|
|
||||||
- `ui.matches` checks for literal, case-sensitive screen text.
|
|
||||||
- `screenshot` prints the generated image path.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive screenshot --name demo
|
|
||||||
```
|
|
||||||
|
|
||||||
## Lifecycle
|
|
||||||
|
|
||||||
- `stop` waits for recording export and owner cleanup before returning.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive stop --name demo
|
|
||||||
```
|
|
||||||
|
|
||||||
# Record The UI
|
|
||||||
|
|
||||||
- Start with `--record` to capture a headless instance from its first rendered frame.
|
|
||||||
- `stop` finishes the recording, exports an MP4, and prints its path.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive start --name demo --record
|
|
||||||
|
|
||||||
opencode-drive send --name demo \
|
|
||||||
--command.ui.type '{"text":"Show me the current architecture"}' \
|
|
||||||
--command.ui.enter
|
|
||||||
|
|
||||||
opencode-drive stop --name demo
|
|
||||||
```
|
|
||||||
|
|
||||||
# Artifacts dir
|
|
||||||
|
|
||||||
- `dir` prints the artifact directory for the instance.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
opencode-drive dir --name demo
|
|
||||||
```
|
|
||||||
@@ -171,4 +171,4 @@ const table = sqliteTable("session", {
|
|||||||
- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle.
|
- One step is one logical LLM call; its durable record covers only the model-visible span. Do not write "provider turn", and do not use bare "turn" for a single call: "turn" is reserved for the future assistant-turn unit containing all steps from prompt promotion until the session would go idle.
|
||||||
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
||||||
- Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry.
|
- Keep the Instructions algebra and built-ins in `src/instructions`; keep instruction producers with their observed domains, and keep Session History selection plus `InstructionState` and `InstructionEntry` persistence Session-owned. `InstructionDiscovery` observes ambient global and upward-project instructions. The runner composes built-ins, discovery, guidance, and entries explicitly in `loadInstructions`; there is no instruction registry.
|
||||||
- `session.instructions.updated` stores only changed source keys and content hashes. Blob values live once in `instruction_blob`; `instruction_state` is a rebuildable fold cache, never primary state. Render initial instructions and chronological updates from values during request assembly. Completed compaction moves the instruction epoch; Session movement retains it so destination instruction changes are chronological, while committed revert clears it. Unavailable sources retain the last value and block only the initial complete delta.
|
- `session.instructions.updated` stores only changed source keys and content hashes. Blob values live once in `instruction_blob`; `instruction_state` is a rebuildable fold cache, never primary state. Render initial instructions and chronological updates from values during request assembly. Completed compaction moves the instruction epoch; Session movement and committed revert clear it. Unavailable sources retain the last value and block only the initial complete delta.
|
||||||
|
|||||||
@@ -70,13 +70,19 @@
|
|||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@pierre/trees": "1.0.0-beta.4",
|
"@pierre/trees": "1.0.0-beta.4",
|
||||||
"@sentry/solid": "catalog:",
|
"@sentry/solid": "catalog:",
|
||||||
|
"@shikijs/transformers": "3.9.2",
|
||||||
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
|
"@solid-primitives/audio": "1.4.2",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
"@solid-primitives/event-listener": "2.4.5",
|
"@solid-primitives/event-listener": "2.4.5",
|
||||||
"@solid-primitives/i18n": "2.2.1",
|
"@solid-primitives/i18n": "2.2.1",
|
||||||
"@solid-primitives/media": "2.3.3",
|
"@solid-primitives/media": "2.3.3",
|
||||||
"@solid-primitives/resize-observer": "2.1.5",
|
"@solid-primitives/resize-observer": "2.1.5",
|
||||||
"@solid-primitives/scheduled": "1.5.3",
|
"@solid-primitives/scheduled": "1.5.3",
|
||||||
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solid-primitives/timer": "1.4.4",
|
||||||
|
"@solid-primitives/websocket": "1.3.1",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@tanstack/solid-query": "5.91.4",
|
"@tanstack/solid-query": "5.91.4",
|
||||||
@@ -139,7 +145,10 @@
|
|||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"semver": "catalog:",
|
"semver": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
|
"tree-sitter-bash": "0.25.0",
|
||||||
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"uqr": "0.1.3",
|
"uqr": "0.1.3",
|
||||||
|
"web-tree-sitter": "0.25.10",
|
||||||
"ws": "8.21.0",
|
"ws": "8.21.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -188,15 +197,12 @@
|
|||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"solid-js": "catalog:",
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"effect": "4.0.0-beta.101",
|
"effect": "4.0.0-beta.101",
|
||||||
"solid-js": ">=1.9.0",
|
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"effect",
|
"effect",
|
||||||
"solid-js",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
@@ -385,7 +391,11 @@
|
|||||||
"immer": "11.1.4",
|
"immer": "11.1.4",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
|
"tree-sitter-bash": "0.25.0",
|
||||||
|
"tree-sitter-powershell": "0.25.10",
|
||||||
|
"turndown": "7.2.0",
|
||||||
"venice-ai-sdk-provider": "2.1.1",
|
"venice-ai-sdk-provider": "2.1.1",
|
||||||
|
"web-tree-sitter": "0.25.10",
|
||||||
"which": "6.0.1",
|
"which": "6.0.1",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -393,7 +403,6 @@
|
|||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@effect/sql-sqlite-bun": "catalog:",
|
"@effect/sql-sqlite-bun": "catalog:",
|
||||||
"@opencode-ai/http-recorder": "workspace:*",
|
"@opencode-ai/http-recorder": "workspace:*",
|
||||||
"@opencode-ai/shell-scan": "workspace:*",
|
|
||||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||||
@@ -405,6 +414,7 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
|
"@types/turndown": "5.0.5",
|
||||||
"@types/which": "3.0.4",
|
"@types/which": "3.0.4",
|
||||||
"drizzle-kit": "catalog:",
|
"drizzle-kit": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -464,31 +474,6 @@
|
|||||||
"@parcel/watcher-win32-x64": "2.5.1",
|
"@parcel/watcher-win32-x64": "2.5.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/drive": {
|
|
||||||
"name": "opencode-drive",
|
|
||||||
"version": "1.4.3",
|
|
||||||
"bin": {
|
|
||||||
"opencode-drive": "bin/opencode-drive",
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@effect/platform-node": "catalog:",
|
|
||||||
"@napi-rs/canvas": "1.0.2",
|
|
||||||
"@opencode-ai/client": "workspace:*",
|
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"@wterm/core": "0.3.0",
|
|
||||||
"@wterm/ghostty": "0.3.0",
|
|
||||||
"effect": "catalog:",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@effect/vitest": "4.0.0-beta.101",
|
|
||||||
"@tsconfig/bun": "catalog:",
|
|
||||||
"oxlint": "1.60.0",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
"vitest": "4.1.10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.18.15",
|
"version": "1.18.15",
|
||||||
@@ -570,27 +555,6 @@
|
|||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/lab/catalog": {
|
|
||||||
"name": "@opencode-ai/lab-catalog",
|
|
||||||
"dependencies": {
|
|
||||||
"@fontsource/commit-mono": "5.2.5",
|
|
||||||
"@fontsource/noto-sans-math": "5.2.8",
|
|
||||||
"@fontsource/noto-sans-symbols": "5.2.8",
|
|
||||||
"@fontsource/noto-sans-symbols-2": "5.2.8",
|
|
||||||
"react": "^19.2.7",
|
|
||||||
"react-dom": "^19.2.7",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@napi-rs/canvas": "1.0.2",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@types/react": "^19.2.17",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"effect": "catalog:",
|
|
||||||
"opencode-drive": "workspace:*",
|
|
||||||
"typescript": "^7.0.2",
|
|
||||||
"wrangler": "4.110.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/merman": {
|
"packages/merman": {
|
||||||
"name": "@opencode-ai/merman",
|
"name": "@opencode-ai/merman",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -733,6 +697,8 @@
|
|||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@shikijs/stream": "catalog:",
|
"@shikijs/stream": "catalog:",
|
||||||
|
"@shikijs/transformers": "3.9.2",
|
||||||
|
"@solid-primitives/bounds": "0.1.3",
|
||||||
"@solid-primitives/event-listener": "2.4.5",
|
"@solid-primitives/event-listener": "2.4.5",
|
||||||
"@solid-primitives/media": "2.3.3",
|
"@solid-primitives/media": "2.3.3",
|
||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
@@ -760,15 +726,6 @@
|
|||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/shell-scan": {
|
|
||||||
"name": "@opencode-ai/shell-scan",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@tsconfig/bun": "catalog:",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/simulation": {
|
"packages/simulation": {
|
||||||
"name": "@opencode-ai/simulation",
|
"name": "@opencode-ai/simulation",
|
||||||
"version": "1.17.13",
|
"version": "1.17.13",
|
||||||
@@ -955,6 +912,8 @@
|
|||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@shikijs/stream": "catalog:",
|
"@shikijs/stream": "catalog:",
|
||||||
|
"@shikijs/transformers": "3.9.2",
|
||||||
|
"@solid-primitives/bounds": "0.1.3",
|
||||||
"@solid-primitives/event-listener": "2.4.5",
|
"@solid-primitives/event-listener": "2.4.5",
|
||||||
"@solid-primitives/media": "2.3.3",
|
"@solid-primitives/media": "2.3.3",
|
||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
@@ -967,6 +926,8 @@
|
|||||||
"marked-shiki": "catalog:",
|
"marked-shiki": "catalog:",
|
||||||
"morphdom": "2.7.8",
|
"morphdom": "2.7.8",
|
||||||
"motion": "12.34.5",
|
"motion": "12.34.5",
|
||||||
|
"motion-dom": "12.34.3",
|
||||||
|
"motion-utils": "12.29.2",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"remend": "catalog:",
|
"remend": "catalog:",
|
||||||
"shiki": "catalog:",
|
"shiki": "catalog:",
|
||||||
@@ -1029,6 +990,7 @@
|
|||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
"resolve.exports": "catalog:",
|
"resolve.exports": "catalog:",
|
||||||
|
"xdg-basedir": "5.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
@@ -1094,6 +1056,7 @@
|
|||||||
"unenv": "2.0.0-rc.24",
|
"unenv": "2.0.0-rc.24",
|
||||||
"vitest": "3.2.7",
|
"vitest": "3.2.7",
|
||||||
"wrangler": "4.28.0",
|
"wrangler": "4.28.0",
|
||||||
|
"xdg-basedir": "5.1.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/www": {
|
"packages/www": {
|
||||||
@@ -1113,9 +1076,12 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"electron",
|
|
||||||
"esbuild",
|
"esbuild",
|
||||||
|
"tree-sitter-powershell",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
|
"electron",
|
||||||
|
"web-tree-sitter",
|
||||||
|
"tree-sitter-bash",
|
||||||
],
|
],
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||||
@@ -1634,8 +1600,6 @@
|
|||||||
|
|
||||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="],
|
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="],
|
||||||
|
|
||||||
"@effect/vitest": ["@effect/vitest@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-F5Ur8pZYti0xkZFyb4hPZt8RrKQ1XBoC28ZkKxc7N1iPbhE9fWOvlWA1NC2XlN2wWG08yb5g5jR53LdOxjlhpQ=="],
|
|
||||||
|
|
||||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||||
|
|
||||||
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
|
||||||
@@ -1774,11 +1738,11 @@
|
|||||||
|
|
||||||
"@fontsource/inter": ["@fontsource/inter@5.3.0", "", {}, "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g=="],
|
"@fontsource/inter": ["@fontsource/inter@5.3.0", "", {}, "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g=="],
|
||||||
|
|
||||||
"@fontsource/noto-sans-math": ["@fontsource/noto-sans-math@5.2.8", "", {}, "sha512-LKP8MXf5if2NoQcpgkIWS3SKGk7b4IZCLKTTHEFmHuy1g/n5KKg21+wk4cTKEoJZtHaMo13jwd4Hw8TWxaz8gQ=="],
|
"@fontsource/noto-sans-math": ["@fontsource/noto-sans-math@5.2.5", "", {}, "sha512-1bxEvVlF51Vfgpju32mRZzI/CHvsfqjXjI2+sAuEyHYvXABUAIyj+93sCO3QZIoMG5drWyrzgoCqRQRaL6wQ8Q=="],
|
||||||
|
|
||||||
"@fontsource/noto-sans-symbols": ["@fontsource/noto-sans-symbols@5.2.8", "", {}, "sha512-x5U4btr3+aveZWNXK24tG3jvds7RRTg4diQ0D9nvZQxxtskbXVFEBP7Iok8TS86R9XHyg1TBS5GoR+ZLZSF5fw=="],
|
"@fontsource/noto-sans-symbols": ["@fontsource/noto-sans-symbols@5.2.5", "", {}, "sha512-mxoIRstsmZpZFzd/SRWiD+l6T7TGhpgCrGs7TEnnuGSQIfjVMrQT9Zej2enh9pkfmPNAFyeaGJkHkszJ1hH++w=="],
|
||||||
|
|
||||||
"@fontsource/noto-sans-symbols-2": ["@fontsource/noto-sans-symbols-2@5.2.8", "", {}, "sha512-OGKq1CaSIszlSBc4Ie2XPBS8yFfF1fMfzjjoRRfpijjgjk1Xg/WSo/3Bg0cJdoxxIdQ0a2XENzJHhfpWLNv5Lw=="],
|
"@fontsource/noto-sans-symbols-2": ["@fontsource/noto-sans-symbols-2@5.2.5", "", {}, "sha512-F4O9WLifwoZS1quNzY1ebjMNo2cQPe/UP68Dmud0ONi2lOxaR6xp6fFPO2gG17MI7DwAnfMyQFl64A2tAd28hg=="],
|
||||||
|
|
||||||
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
||||||
|
|
||||||
@@ -1954,6 +1918,8 @@
|
|||||||
|
|
||||||
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="],
|
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="],
|
||||||
|
|
||||||
|
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||||
|
|
||||||
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
||||||
@@ -2126,8 +2092,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
|
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog": ["@opencode-ai/lab-catalog@workspace:packages/lab/catalog"],
|
|
||||||
|
|
||||||
"@opencode-ai/merman": ["@opencode-ai/merman@workspace:packages/merman"],
|
"@opencode-ai/merman": ["@opencode-ai/merman@workspace:packages/merman"],
|
||||||
|
|
||||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||||
@@ -2146,8 +2110,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
|
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
|
||||||
|
|
||||||
"@opencode-ai/shell-scan": ["@opencode-ai/shell-scan@workspace:packages/shell-scan"],
|
|
||||||
|
|
||||||
"@opencode-ai/simulation": ["@opencode-ai/simulation@workspace:packages/simulation"],
|
"@opencode-ai/simulation": ["@opencode-ai/simulation@workspace:packages/simulation"],
|
||||||
|
|
||||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||||
@@ -2734,7 +2696,7 @@
|
|||||||
|
|
||||||
"@sentry/vite-plugin": ["@sentry/vite-plugin@4.6.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "4.6.0", "unplugin": "1.0.1" } }, "sha512-fMR2d+EHwbzBa0S1fp45SNUTProxmyFBp+DeBWWQOSP9IU6AH6ea2rqrpMAnp/skkcdW4z4LSRrOEpMZ5rWXLw=="],
|
"@sentry/vite-plugin": ["@sentry/vite-plugin@4.6.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "4.6.0", "unplugin": "1.0.1" } }, "sha512-fMR2d+EHwbzBa0S1fp45SNUTProxmyFBp+DeBWWQOSP9IU6AH6ea2rqrpMAnp/skkcdW4z4LSRrOEpMZ5rWXLw=="],
|
||||||
|
|
||||||
"@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
|
"@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="],
|
||||||
|
|
||||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="],
|
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="],
|
||||||
|
|
||||||
@@ -2748,11 +2710,11 @@
|
|||||||
|
|
||||||
"@shikijs/themes": ["@shikijs/themes@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w=="],
|
"@shikijs/themes": ["@shikijs/themes@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w=="],
|
||||||
|
|
||||||
"@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@shikijs/transformers": ["@shikijs/transformers@3.9.2", "", { "dependencies": { "@shikijs/core": "3.9.2", "@shikijs/types": "3.9.2" } }, "sha512-MW5hT4TyUp6bNAgTExRYLk1NNasVQMTCw1kgbxHcEC0O5cbepPWaB+1k+JzW9r3SP2/R8kiens8/3E6hGKfgsA=="],
|
||||||
|
|
||||||
"@shikijs/twoslash": ["@shikijs/twoslash@4.4.2", "", { "dependencies": { "@shikijs/core": "4.4.2", "@shikijs/types": "4.4.2", "twoslash": "^0.3.9" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-JjPKNbYCZn5+DVOxDZJ0ZruFPBM7m2JQBp4WbLh4tU2y3ed93wBCaQhKwMyfTFIsDzHks22szAkUUd2+jIPo/w=="],
|
"@shikijs/twoslash": ["@shikijs/twoslash@4.4.2", "", { "dependencies": { "@shikijs/core": "4.4.2", "@shikijs/types": "4.4.2", "twoslash": "^0.3.9" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-JjPKNbYCZn5+DVOxDZJ0ZruFPBM7m2JQBp4WbLh4tU2y3ed93wBCaQhKwMyfTFIsDzHks22szAkUUd2+jIPo/w=="],
|
||||||
|
|
||||||
"@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
"@shikijs/types": ["@shikijs/types@3.9.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw=="],
|
||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
@@ -2870,6 +2832,12 @@
|
|||||||
|
|
||||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
|
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
|
||||||
|
|
||||||
|
"@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
|
||||||
|
|
||||||
|
"@solid-primitives/bounds": ["@solid-primitives/bounds@0.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/resize-observer": "^2.1.3", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q=="],
|
||||||
|
|
||||||
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
|
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
|
||||||
|
|
||||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="],
|
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="],
|
||||||
@@ -2894,14 +2862,20 @@
|
|||||||
|
|
||||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g=="],
|
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g=="],
|
||||||
|
|
||||||
|
"@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="],
|
||||||
|
|
||||||
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-LgtVaVBtB7EbmS4+M0b8xY5Iq6pUWXBsIC4VgtrFKDGDdyCaDt88sHk0fUlx1Enxm/XZnZyLXJABRoa39RjJqA=="],
|
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-LgtVaVBtB7EbmS4+M0b8xY5Iq6pUWXBsIC4VgtrFKDGDdyCaDt88sHk0fUlx1Enxm/XZnZyLXJABRoa39RjJqA=="],
|
||||||
|
|
||||||
"@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12", "solid-start": "*" }, "optionalPeers": ["@tauri-apps/plugin-store", "solid-start"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="],
|
"@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12", "solid-start": "*" }, "optionalPeers": ["@tauri-apps/plugin-store", "solid-start"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="],
|
||||||
|
|
||||||
|
"@solid-primitives/timer": ["@solid-primitives/timer@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ayjyb3+v1hyU92vuLUN0tVHq2mmTCPGxSDLGJMsDydRqx9ZfJIc9xj6cxK4XvdY3pif3ps2mIv52pjgToybEpQ=="],
|
||||||
|
|
||||||
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ju0e+ZOD7hpOp7nptJimvDSZHWFvIvF9iBWMvuwt30smX7c5wmB8Kmc0AdDuv0wOTi06PQ1J5IrP1WJbH2yUBQ=="],
|
"@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.4", "", { "dependencies": { "@solid-primitives/utils": "^6.4.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ju0e+ZOD7hpOp7nptJimvDSZHWFvIvF9iBWMvuwt30smX7c5wmB8Kmc0AdDuv0wOTi06PQ1J5IrP1WJbH2yUBQ=="],
|
||||||
|
|
||||||
"@solid-primitives/utils": ["@solid-primitives/utils@6.4.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg=="],
|
"@solid-primitives/utils": ["@solid-primitives/utils@6.4.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg=="],
|
||||||
|
|
||||||
|
"@solid-primitives/websocket": ["@solid-primitives/websocket@1.3.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-F06tA2FKa5VsnS4E4WEc3jHpsJfXRlMTGOtolugTzCqV3JmJTyvk9UVg1oz6PgGHKGi1CQ91OP8iW34myyJgaQ=="],
|
||||||
|
|
||||||
"@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="],
|
"@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="],
|
||||||
|
|
||||||
"@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
|
"@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
|
||||||
@@ -3234,6 +3208,8 @@
|
|||||||
|
|
||||||
"@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="],
|
"@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="],
|
||||||
|
|
||||||
|
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||||
@@ -3260,46 +3236,6 @@
|
|||||||
|
|
||||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
|
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="],
|
||||||
|
|
||||||
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
|
|
||||||
|
|
||||||
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
|
||||||
|
|
||||||
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
"@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
||||||
|
|
||||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.8", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA=="],
|
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.8", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA=="],
|
||||||
@@ -3362,10 +3298,6 @@
|
|||||||
|
|
||||||
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
|
||||||
|
|
||||||
"@wterm/core": ["@wterm/core@0.3.0", "", {}, "sha512-aQ73QBP+eyA3kcn31mA7DmAi7qmEsQijZdmvgwxtSjCi2248EAOZgtkGDpjlspfrxyVxQbUl/IF+gCVcXt4nZQ=="],
|
|
||||||
|
|
||||||
"@wterm/ghostty": ["@wterm/ghostty@0.3.0", "", { "dependencies": { "@wterm/core": "0.3.0" } }, "sha512-TDjEmUSRD7IaFxq31P7+I3Inkp5mHCfBfO6LYj3/vbLbWOkOZOKPhjAcZ36AYFHe9Q0HLCFvLWmrxIO3aGCjDA=="],
|
|
||||||
|
|
||||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||||
|
|
||||||
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="],
|
"@yuuang/ffi-rs-android-arm64": ["@yuuang/ffi-rs-android-arm64@1.3.7", "", { "os": "android", "cpu": "arm64" }, "sha512-t6Wx3Xll6c07Nuk0k3xnZsxKFxlshm92i0U/BiTHc6kQbvu+fMJF+gKsj4yEj886jH51CM3EqZT9Xdhq9CdUVw=="],
|
||||||
@@ -4896,9 +4828,9 @@
|
|||||||
|
|
||||||
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
|
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
|
||||||
|
|
||||||
"motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
|
"motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="],
|
||||||
|
|
||||||
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
"motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
|
||||||
|
|
||||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||||
|
|
||||||
@@ -5020,8 +4952,6 @@
|
|||||||
|
|
||||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
"opencode-drive": ["opencode-drive@workspace:packages/drive"],
|
|
||||||
|
|
||||||
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
||||||
|
|
||||||
"opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
|
"opentui-spinner": ["opentui-spinner@0.0.7", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
|
||||||
@@ -5248,7 +5178,7 @@
|
|||||||
|
|
||||||
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
|
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
"react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
||||||
|
|
||||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||||
|
|
||||||
@@ -5416,7 +5346,7 @@
|
|||||||
|
|
||||||
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||||
|
|
||||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||||
|
|
||||||
@@ -5718,6 +5648,10 @@
|
|||||||
|
|
||||||
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
|
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
|
||||||
|
|
||||||
|
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
|
||||||
|
|
||||||
|
"tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
|
||||||
|
|
||||||
"treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="],
|
"treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="],
|
||||||
|
|
||||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||||
@@ -5744,6 +5678,8 @@
|
|||||||
|
|
||||||
"turbo": ["turbo@2.10.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.2", "@turbo/darwin-arm64": "2.10.2", "@turbo/linux-64": "2.10.2", "@turbo/linux-arm64": "2.10.2", "@turbo/windows-64": "2.10.2", "@turbo/windows-arm64": "2.10.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ=="],
|
"turbo": ["turbo@2.10.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.2", "@turbo/darwin-arm64": "2.10.2", "@turbo/linux-64": "2.10.2", "@turbo/linux-arm64": "2.10.2", "@turbo/windows-64": "2.10.2", "@turbo/windows-arm64": "2.10.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ=="],
|
||||||
|
|
||||||
|
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
"tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="],
|
"tw-to-css": ["tw-to-css@0.0.12", "", { "dependencies": { "postcss": "8.4.31", "postcss-css-variables": "0.18.0", "tailwindcss": "3.3.2" } }, "sha512-rQAsQvOtV1lBkyCw+iypMygNHrShYAItES5r8fMsrhhaj5qrV2LkZyXc8ccEH+u5bFjHjQ9iuxe90I7Kykf6pw=="],
|
||||||
@@ -5976,6 +5912,8 @@
|
|||||||
|
|
||||||
"xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="],
|
"xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="],
|
||||||
|
|
||||||
|
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
|
||||||
|
|
||||||
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
|
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
|
||||||
|
|
||||||
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
||||||
@@ -6344,16 +6282,12 @@
|
|||||||
|
|
||||||
"@jsx-email/cli/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
|
"@jsx-email/cli/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
|
||||||
|
|
||||||
"@jsx-email/cli/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@jsx-email/cli/tailwindcss": ["tailwindcss@3.3.3", "", { "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", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w=="],
|
"@jsx-email/cli/tailwindcss": ["tailwindcss@3.3.3", "", { "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", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w=="],
|
||||||
|
|
||||||
"@jsx-email/cli/vite": ["vite@4.5.14", "", { "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", "rollup": "^3.27.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g=="],
|
"@jsx-email/cli/vite": ["vite@4.5.14", "", { "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", "rollup": "^3.27.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g=="],
|
||||||
|
|
||||||
"@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
|
"@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
|
||||||
|
|
||||||
"@jsx-email/tailwind/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@kobalte/core/solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="],
|
"@kobalte/core/solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="],
|
||||||
|
|
||||||
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||||
@@ -6444,30 +6378,18 @@
|
|||||||
|
|
||||||
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
"@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/session-ui/@opencode-ai/sdk": ["@opencode-ai/sdk@../app/vendor/opencode-ai-sdk-1.18.8-dev.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-C2nfk4x0sPINwE5V6DPkFSuH3PkUmKPWHPzxpXC1j+3Ui5hslLCWJbkk8WcOG1Lyt3C0+yp4ea64v/kmtYCO4w=="],
|
"@opencode-ai/session-ui/@opencode-ai/sdk": ["@opencode-ai/sdk@../app/vendor/opencode-ai-sdk-1.18.8-dev.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-C2nfk4x0sPINwE5V6DPkFSuH3PkUmKPWHPzxpXC1j+3Ui5hslLCWJbkk8WcOG1Lyt3C0+yp4ea64v/kmtYCO4w=="],
|
||||||
|
|
||||||
"@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/simulation/@fontsource/noto-sans-math": ["@fontsource/noto-sans-math@5.2.5", "", {}, "sha512-1bxEvVlF51Vfgpju32mRZzI/CHvsfqjXjI2+sAuEyHYvXABUAIyj+93sCO3QZIoMG5drWyrzgoCqRQRaL6wQ8Q=="],
|
|
||||||
|
|
||||||
"@opencode-ai/simulation/@fontsource/noto-sans-symbols": ["@fontsource/noto-sans-symbols@5.2.5", "", {}, "sha512-mxoIRstsmZpZFzd/SRWiD+l6T7TGhpgCrGs7TEnnuGSQIfjVMrQT9Zej2enh9pkfmPNAFyeaGJkHkszJ1hH++w=="],
|
|
||||||
|
|
||||||
"@opencode-ai/simulation/@fontsource/noto-sans-symbols-2": ["@fontsource/noto-sans-symbols-2@5.2.5", "", {}, "sha512-F4O9WLifwoZS1quNzY1ebjMNo2cQPe/UP68Dmud0ONi2lOxaR6xp6fFPO2gG17MI7DwAnfMyQFl64A2tAd28hg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/storybook/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
|
"@opencode-ai/storybook/@types/react": ["@types/react@18.0.25", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g=="],
|
||||||
|
|
||||||
"@opencode-ai/storybook/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
|
"@opencode-ai/updates/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
|
||||||
|
|
||||||
|
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
|
|
||||||
"@opencode-ai/workerd-spike/vitest": ["vitest@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", "@vitest/mocker": "3.2.7", "@vitest/pretty-format": "^3.2.7", "@vitest/runner": "3.2.7", "@vitest/snapshot": "3.2.7", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.7", "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg=="],
|
"@opencode-ai/workerd-spike/vitest": ["vitest@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", "@vitest/mocker": "3.2.7", "@vitest/pretty-format": "^3.2.7", "@vitest/runner": "3.2.7", "@vitest/snapshot": "3.2.7", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.7", "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg=="],
|
||||||
|
|
||||||
"@opencode-ai/workerd-spike/wrangler": ["wrangler@4.28.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.6.0", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250803.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.19", "workerd": "1.20250803.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250803.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-y0yHIuScpok9oSErLqDbxkBChC2+/jZpvqMg2NxOto1JCyUtDUuKljOfcVMaI48d9GuhOCSoWSumYxLAHNxaLA=="],
|
"@opencode-ai/workerd-spike/wrangler": ["wrangler@4.28.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.6.0", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250803.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.19", "workerd": "1.20250803.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250803.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-y0yHIuScpok9oSErLqDbxkBChC2+/jZpvqMg2NxOto1JCyUtDUuKljOfcVMaI48d9GuhOCSoWSumYxLAHNxaLA=="],
|
||||||
@@ -6492,48 +6414,26 @@
|
|||||||
|
|
||||||
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||||
|
|
||||||
|
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
|
|
||||||
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||||
|
|
||||||
"@pierre/diffs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
"@pierre/diffs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
|
"@pierre/diffs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||||
|
|
||||||
|
"@pierre/theming/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||||
|
|
||||||
"@pierre/trees/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
"@pierre/trees/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
|
"@pierre/trees/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||||
|
|
||||||
"@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
|
"@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
|
||||||
|
|
||||||
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||||
|
|
||||||
"@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
|
"@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-collapsible/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-collection/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-focus-scope/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-popover/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-popper/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-portal/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-presence/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-primitive/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-toggle/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-toggle-group/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-tooltip/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-visually-hidden/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"@scalar/types/nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
"@scalar/types/nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="],
|
||||||
@@ -6552,9 +6452,17 @@
|
|||||||
|
|
||||||
"@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
"@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
||||||
|
|
||||||
"@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
"@shikijs/engine-oniguruma/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
||||||
|
|
||||||
|
"@shikijs/langs/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
||||||
|
|
||||||
|
"@shikijs/primitive/@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/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
|
||||||
|
|
||||||
|
"@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/twoslash/@shikijs/core": ["@shikijs/core@4.4.2", "", { "dependencies": { "@shikijs/primitive": "4.4.2", "@shikijs/types": "4.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA=="],
|
"@shikijs/twoslash/@shikijs/core": ["@shikijs/core@4.4.2", "", { "dependencies": { "@shikijs/primitive": "4.4.2", "@shikijs/types": "4.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA=="],
|
||||||
|
|
||||||
@@ -6592,6 +6500,8 @@
|
|||||||
|
|
||||||
"@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
"@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
|
"@storybook/addon-docs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||||
|
|
||||||
"@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
"@storybook/csf-plugin/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||||
@@ -6714,6 +6624,8 @@
|
|||||||
|
|
||||||
"blume/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
"blume/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
|
"blume/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||||
|
|
||||||
"blume/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
"blume/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
|
|
||||||
"blume/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"blume/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
@@ -6830,8 +6742,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"framer-motion/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
@@ -6938,8 +6848,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"react-dom/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
|
||||||
|
|
||||||
"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@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
"rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||||
@@ -6950,6 +6858,10 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"shiki/@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
|
||||||
|
|
||||||
|
"shiki/@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
|
||||||
|
|
||||||
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||||
@@ -6992,6 +6904,8 @@
|
|||||||
|
|
||||||
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||||
|
|
||||||
|
"tree-sitter-bash/node-addon-api": ["node-addon-api@8.9.1", "", {}, "sha512-4eUQWVPCUUUiBjLnHS3cXWeC6ryoPUc0U3rP7IuzapoGbzMqd/r6KKO0clr0b+snQhsrueFEhCZDdK+LK7hxKg=="],
|
||||||
|
|
||||||
"tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -7478,8 +7392,6 @@
|
|||||||
|
|
||||||
"@jsx-email/cli/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
|
"@jsx-email/cli/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
|
||||||
|
|
||||||
"@jsx-email/cli/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@jsx-email/cli/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
"@jsx-email/cli/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||||
|
|
||||||
"@jsx-email/cli/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"@jsx-email/cli/tailwindcss/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
@@ -7494,8 +7406,6 @@
|
|||||||
|
|
||||||
"@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
"@jsx-email/doiuse-email/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
|
|
||||||
"@jsx-email/tailwind/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
"@mapbox/node-pre-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
|
"@mapbox/node-pre-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
|
||||||
@@ -7594,18 +7504,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
|
|
||||||
|
|
||||||
"@opencode-ai/storybook/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
"@opencode-ai/updates/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
"@opencode-ai/updates/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
||||||
@@ -7616,6 +7514,10 @@
|
|||||||
|
|
||||||
"@opencode-ai/updates/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
|
"@opencode-ai/updates/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
|
||||||
|
|
||||||
|
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||||
|
|
||||||
|
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||||
|
|
||||||
"@opencode-ai/workerd-spike/vitest/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
"@opencode-ai/workerd-spike/vitest/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||||
|
|
||||||
"@opencode-ai/workerd-spike/vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
"@opencode-ai/workerd-spike/vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||||
@@ -7694,35 +7596,17 @@
|
|||||||
|
|
||||||
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||||
|
|
||||||
"@radix-ui/react-collapsible/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"@pierre/diffs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"@pierre/theming/react-dom/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
"@radix-ui/react-focus-scope/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"@pierre/theming/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-popover/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
"@pierre/trees/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-popper/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-portal/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-presence/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-primitive/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-toggle-group/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-toggle/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-tooltip/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-visually-hidden/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||||
|
|
||||||
@@ -7734,6 +7618,8 @@
|
|||||||
|
|
||||||
"@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"@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/twoslash/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.4.2", "", { "dependencies": { "@shikijs/types": "4.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA=="],
|
"@shikijs/twoslash/@shikijs/core/@shikijs/primitive": ["@shikijs/primitive@4.4.2", "", { "dependencies": { "@shikijs/types": "4.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA=="],
|
||||||
|
|
||||||
"@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/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||||
@@ -7784,6 +7670,8 @@
|
|||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
||||||
|
|
||||||
|
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"@storybook/csf-plugin/unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
"@storybook/csf-plugin/unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||||
|
|
||||||
"@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
"@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||||
@@ -7914,6 +7802,8 @@
|
|||||||
|
|
||||||
"blume/node-html-parser/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
"blume/node-html-parser/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
||||||
|
|
||||||
|
"blume/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -7960,8 +7850,6 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"framer-motion/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
|
||||||
|
|
||||||
"gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
"gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||||
|
|
||||||
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
@@ -7986,6 +7874,10 @@
|
|||||||
|
|
||||||
"mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
"mermaid/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
|
"motion/framer-motion/motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
|
||||||
|
|
||||||
|
"motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||||
|
|
||||||
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||||
@@ -8628,72 +8520,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
"@opencode-ai/updates/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
"@opencode-ai/updates/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||||
@@ -9388,54 +9214,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/desktop/@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
"@opencode-ai/desktop/@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
"@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
"@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||||
@@ -9716,8 +9494,6 @@
|
|||||||
|
|
||||||
"@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
"@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@opencode-ai/lab-catalog/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
|
||||||
|
|
||||||
"@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
"@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
|
"@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
|
||||||
|
|||||||
+4
-1
@@ -33,7 +33,6 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/console/*",
|
"packages/console/*",
|
||||||
"packages/lab/*",
|
|
||||||
"packages/stats/*",
|
"packages/stats/*",
|
||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
@@ -145,6 +144,10 @@
|
|||||||
"esbuild",
|
"esbuild",
|
||||||
"node-pty",
|
"node-pty",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
|
"tree-sitter",
|
||||||
|
"tree-sitter-bash",
|
||||||
|
"tree-sitter-powershell",
|
||||||
|
"web-tree-sitter",
|
||||||
"electron"
|
"electron"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
## Localization
|
## Localization
|
||||||
|
|
||||||
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors.
|
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors.
|
||||||
- Feature work adds English source strings only. Leave non-English keys absent so the runtime English fallback applies; translations land separately after language review.
|
|
||||||
- Render count-sensitive copy only through `language.plural(baseKey, count, params)`. Never select or pass `.zero`, `.one`, `.two`, `.few`, `.many`, or `.other` variants to `language.t(...)`.
|
|
||||||
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
||||||
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
||||||
- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar.
|
- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
|
|||||||
import { Event } from "@opencode-ai/schema/event"
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event"
|
import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event"
|
||||||
import { SessionV1 } from "@opencode-ai/schema/session-v1"
|
import { SessionV1 } from "@opencode-ai/schema/session-v1"
|
||||||
import type { SessionInfo, SessionMessageInfo, SessionStatus } from "@opencode-ai/client/promise"
|
import type { SessionInfo, SessionStatus } from "@opencode-ai/client/promise"
|
||||||
import type { AssistantMessage, Message, Part, ToolPart, ToolState, UserMessage } from "../../../src/types"
|
import type { AssistantMessage, Message, Part, ToolPart, ToolState, UserMessage } from "../../../src/types"
|
||||||
import { expect, type Page } from "@playwright/test"
|
import { expect, type Page } from "@playwright/test"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
@@ -90,8 +90,6 @@ export async function setupTimeline(
|
|||||||
page: Page,
|
page: Page,
|
||||||
input: {
|
input: {
|
||||||
messages?: TimelineMessage[]
|
messages?: TimelineMessage[]
|
||||||
currentMessages?: SessionMessageInfo[]
|
|
||||||
sessionStatus?: Record<string, SessionStatus>
|
|
||||||
settings?: Record<string, boolean>
|
settings?: Record<string, boolean>
|
||||||
sessions?: Session[]
|
sessions?: Session[]
|
||||||
cpuRate?: number
|
cpuRate?: number
|
||||||
@@ -104,22 +102,13 @@ export async function setupTimeline(
|
|||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const sessions = input.sessions ?? [session()]
|
const sessions = input.sessions ?? [session()]
|
||||||
const messages =
|
const messages = validateTimelineMessages([
|
||||||
input.currentMessages ??
|
|
||||||
validateTimelineMessages([
|
|
||||||
...(input.seedHistory ? historyMessages(18) : []),
|
...(input.seedHistory ? historyMessages(18) : []),
|
||||||
...(input.messages ?? [userMessage(), assistantMessage()]),
|
...(input.messages ?? [userMessage(), assistantMessage()]),
|
||||||
])
|
])
|
||||||
const active = messages.findLast((message) =>
|
const active = messages.findLast((message) => message.info.role === "assistant")
|
||||||
"info" in message ? message.info.role === "assistant" : message.type === "assistant",
|
|
||||||
)
|
|
||||||
const initialStatus = decodeStatus(
|
const initialStatus = decodeStatus(
|
||||||
active &&
|
active?.info.role === "assistant" && active.info.time.completed === undefined ? { type: "busy" } : { type: "idle" },
|
||||||
("info" in active
|
|
||||||
? active.info.role === "assistant" && active.info.time.completed === undefined
|
|
||||||
: active.type === "assistant" && active.time.completed === undefined)
|
|
||||||
? { type: "busy" }
|
|
||||||
: { type: "idle" },
|
|
||||||
decodeOptions,
|
decodeOptions,
|
||||||
)
|
)
|
||||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||||
@@ -133,7 +122,7 @@ export async function setupTimeline(
|
|||||||
project: project(),
|
project: project(),
|
||||||
provider: provider(),
|
provider: provider(),
|
||||||
sessions,
|
sessions,
|
||||||
sessionStatus: input.sessionStatus ?? { [sessionID]: initialStatus },
|
sessionStatus: { [sessionID]: initialStatus },
|
||||||
pageMessages: () => ({
|
pageMessages: () => ({
|
||||||
items: messages,
|
items: messages,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ test("applies message latency after a list response gate is released", async ()
|
|||||||
const gate = Promise.withResolvers<void>()
|
const gate = Promise.withResolvers<void>()
|
||||||
let handler: ((route: Route) => Promise<void>) | undefined
|
let handler: ((route: Route) => Promise<void>) | undefined
|
||||||
const page = {
|
const page = {
|
||||||
addInitScript: () => Promise.resolve(),
|
|
||||||
route: (_url: string, callback: (route: Route) => Promise<void>) => {
|
route: (_url: string, callback: (route: Route) => Promise<void>) => {
|
||||||
handler = callback
|
handler = callback
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { expect, test, type Page, type Route } from "@playwright/test"
|
import { expect, test, type Page, type Route } from "@playwright/test"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { currentSession } from "../utils/mock-server"
|
import { currentSession } from "../utils/mock-server"
|
||||||
import { installSseTransport } from "../utils/sse-transport"
|
|
||||||
|
|
||||||
const serverA = "http://127.0.0.1:4096"
|
const serverA = "http://127.0.0.1:4096"
|
||||||
const serverB = "http://127.0.0.1:4097"
|
const serverB = "http://127.0.0.1:4097"
|
||||||
@@ -79,8 +78,6 @@ function session(id: string, directory: string, title: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function mockServers(page: Page, requests: string[]) {
|
async function mockServers(page: Page, requests: string[]) {
|
||||||
await installSseTransport(page, { server: serverA })
|
|
||||||
await installSseTransport(page, { server: serverB })
|
|
||||||
await page.route("**/*", async (route) => {
|
await page.route("**/*", async (route) => {
|
||||||
const url = new URL(route.request().url())
|
const url = new URL(route.request().url())
|
||||||
if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
|
if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
|
||||||
@@ -88,6 +85,7 @@ async function mockServers(page: Page, requests: string[]) {
|
|||||||
const current = url.origin === serverA ? sessionA : sessionB
|
const current = url.origin === serverA ? sessionA : sessionB
|
||||||
const directory = url.searchParams.get("directory")
|
const directory = url.searchParams.get("directory")
|
||||||
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
|
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
|
||||||
|
if (url.pathname === "/api/event") return sse(route)
|
||||||
if (url.pathname === "/api/health") return json(route, { pid: 1 })
|
if (url.pathname === "/api/health") return json(route, { pid: 1 })
|
||||||
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
|
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
|
||||||
if (url.pathname === "/api/session/active") return json(route, { data: {} })
|
if (url.pathname === "/api/session/active") return json(route, { data: {} })
|
||||||
@@ -144,3 +142,7 @@ function json(route: Route, body: unknown, status = 200) {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sse(route: Route) {
|
||||||
|
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ const sessionB = session("ses_server_b", directoryB, "Server B session")
|
|||||||
|
|
||||||
test("session settings use the remote server context", async ({ page }) => {
|
test("session settings use the remote server context", async ({ page }) => {
|
||||||
const permissionRequests: string[] = []
|
const permissionRequests: string[] = []
|
||||||
await installSseTransport(page, { server: serverA })
|
|
||||||
await installSseTransport(page, { server: serverB })
|
|
||||||
await mockServers(page, permissionRequests)
|
await mockServers(page, permissionRequests)
|
||||||
await configureServers(page)
|
await configureServers(page)
|
||||||
|
|
||||||
@@ -48,7 +46,6 @@ test("session settings use the remote server context", async ({ page }) => {
|
|||||||
test("auto-accept responds for an unfocused server session", async ({ page }) => {
|
test("auto-accept responds for an unfocused server session", async ({ page }) => {
|
||||||
const permissionRequests: string[] = []
|
const permissionRequests: string[] = []
|
||||||
const permissionResponses: PermissionResponse[] = []
|
const permissionResponses: PermissionResponse[] = []
|
||||||
await installSseTransport(page, { server: serverB })
|
|
||||||
const transport = await installSseTransport<{ directory: string; payload: Record<string, unknown> }>(page, {
|
const transport = await installSseTransport<{ directory: string; payload: Record<string, unknown> }>(page, {
|
||||||
server: serverA,
|
server: serverA,
|
||||||
retry: 20,
|
retry: 20,
|
||||||
@@ -184,6 +181,7 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
|
|||||||
return json(route, true)
|
return json(route, true)
|
||||||
}
|
}
|
||||||
if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
|
if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
|
||||||
|
if (url.pathname === "/api/event") return sse(route)
|
||||||
if (url.pathname === "/api/provider")
|
if (url.pathname === "/api/provider")
|
||||||
return json(route, {
|
return json(route, {
|
||||||
location: { directory },
|
location: { directory },
|
||||||
@@ -327,3 +325,7 @@ function json(route: Route, body: unknown, status = 200) {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sse(route: Route) {
|
||||||
|
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,22 +95,22 @@ test("opens and searches project files inline", async ({ page }) => {
|
|||||||
const sidebarToggle = panel.getByRole("button", { name: "Toggle file tree" })
|
const sidebarToggle = panel.getByRole("button", { name: "Toggle file tree" })
|
||||||
const contextButton = page.getByRole("button", { name: "View context usage" })
|
const contextButton = page.getByRole("button", { name: "View context usage" })
|
||||||
await contextButton.click()
|
await contextButton.click()
|
||||||
await expect(panel.getByRole("tab", { name: "Context", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
||||||
await panel.getByRole("button", { name: "Open file" }).click()
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
await expect(panel.getByRole("tab", { name: "Open file", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
await expect(sidebarToggle).toBeDisabled()
|
await expect(sidebarToggle).toBeDisabled()
|
||||||
await expect(sidebar).toBeVisible()
|
await expect(sidebar).toBeVisible()
|
||||||
await contextButton.click()
|
await contextButton.click()
|
||||||
await expect(panel.getByRole("tab", { name: "Context", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
|
||||||
await expect(sidebar).toBeHidden()
|
await expect(sidebar).toBeHidden()
|
||||||
await panel.getByRole("button", { name: "Open file" }).click()
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
const filter = panel.getByRole("combobox", { name: "Filter files" })
|
const filter = panel.getByRole("combobox", { name: "Filter files" })
|
||||||
await expect(filter).toBeFocused()
|
await expect(filter).toBeFocused()
|
||||||
await expect(panel.getByRole("tab", { name: "Open file", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
await expect(panel.getByText("open-file-project", { exact: true })).toBeVisible()
|
await expect(panel.getByText("open-file-project", { exact: true })).toBeVisible()
|
||||||
|
|
||||||
await panel.getByRole("button", { name: "README.md" }).click()
|
await panel.getByRole("button", { name: "README.md" }).click()
|
||||||
await expect(panel.getByRole("tab", { name: "README.md", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "README.md" })).toHaveAttribute("data-selected", "")
|
||||||
await expect(sidebarToggle).toBeEnabled()
|
await expect(sidebarToggle).toBeEnabled()
|
||||||
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
|
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
|
||||||
await expect(sidebar).toHaveCount(0)
|
await expect(sidebar).toHaveCount(0)
|
||||||
@@ -125,21 +125,21 @@ test("opens and searches project files inline", async ({ page }) => {
|
|||||||
expect(resultID).toBeTruthy()
|
expect(resultID).toBeTruthy()
|
||||||
await expect(filter).toHaveAttribute("aria-activedescendant", resultID!)
|
await expect(filter).toHaveAttribute("aria-activedescendant", resultID!)
|
||||||
await filter.press("Enter")
|
await filter.press("Enter")
|
||||||
await expect(panel.getByRole("tab", { name: "nested.ts", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
||||||
await expect(sidebarToggle).toBeEnabled()
|
await expect(sidebarToggle).toBeEnabled()
|
||||||
await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible()
|
await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible()
|
||||||
expect(searches).toContainEqual({ query: "nested", dirs: "file", limit: 200 })
|
expect(searches).toContainEqual({ query: "nested", dirs: "file", limit: 200 })
|
||||||
|
|
||||||
await panel.getByRole("button", { name: "Open file" }).click()
|
await panel.getByRole("button", { name: "Open file" }).click()
|
||||||
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1)
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1)
|
||||||
await expect(panel.getByRole("tab", { name: "Open file", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
|
||||||
await expect(sidebarToggle).toBeDisabled()
|
await expect(sidebarToggle).toBeDisabled()
|
||||||
await panel.locator("#session-side-panel-review-tab").click()
|
await panel.locator("#session-side-panel-review-tab").click()
|
||||||
await expect(sidebarToggle).toBeEnabled()
|
await expect(sidebarToggle).toBeEnabled()
|
||||||
await panel.getByRole("tab", { name: "Open file" }).click()
|
await panel.getByRole("tab", { name: "Open file" }).click()
|
||||||
await page.keyboard.press("Control+w")
|
await page.keyboard.press("Control+w")
|
||||||
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveCount(0)
|
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveCount(0)
|
||||||
await expect(panel.getByRole("tab", { name: "nested.ts", selected: true })).toBeVisible()
|
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
|
||||||
})
|
})
|
||||||
|
|
||||||
function fileNode(path: string) {
|
function fileNode(path: string) {
|
||||||
|
|||||||
@@ -11,23 +11,18 @@ const title = "Request dock regression"
|
|||||||
|
|
||||||
test("shows a pending question dock", async ({ page }) => {
|
test("shows a pending question dock", async ({ page }) => {
|
||||||
await mockServer(page, {
|
await mockServer(page, {
|
||||||
forms: [
|
questions: [
|
||||||
{
|
{
|
||||||
id: "frm_question_request",
|
id: "question-request",
|
||||||
sessionID,
|
sessionID,
|
||||||
title: "Questions",
|
questions: [
|
||||||
metadata: { kind: "question" },
|
|
||||||
fields: [
|
|
||||||
{
|
{
|
||||||
key: "q0",
|
header: "Implementation",
|
||||||
type: "string",
|
question: "Which implementation should be used?",
|
||||||
title: "Implementation",
|
|
||||||
description: "Which implementation should be used?",
|
|
||||||
options: [
|
options: [
|
||||||
{ value: "minimal", label: "Minimal", description: "Use the smallest correct change" },
|
{ label: "Minimal", description: "Use the smallest correct change" },
|
||||||
{ value: "extended", label: "Extended", description: "Include additional behavior" },
|
{ label: "Extended", description: "Include additional behavior" },
|
||||||
],
|
],
|
||||||
custom: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -47,7 +42,7 @@ test("shows a pending question dock", async ({ page }) => {
|
|||||||
const rejectRequests: string[] = []
|
const rejectRequests: string[] = []
|
||||||
page.on("request", (request) => {
|
page.on("request", (request) => {
|
||||||
if (request.method() !== "POST") return
|
if (request.method() !== "POST") return
|
||||||
if (new URL(request.url()).pathname === `/api/session/${sessionID}/form/frm_question_request/cancel`)
|
if (new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reject`)
|
||||||
rejectRequests.push(request.url())
|
rejectRequests.push(request.url())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -72,10 +67,10 @@ test("shows a pending question dock", async ({ page }) => {
|
|||||||
const reply = page.waitForRequest(
|
const reply = page.waitForRequest(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.method() === "POST" &&
|
request.method() === "POST" &&
|
||||||
new URL(request.url()).pathname === `/api/session/${sessionID}/form/frm_question_request/reply`,
|
new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reply`,
|
||||||
)
|
)
|
||||||
await question.getByRole("button", { name: "Submit" }).click()
|
await question.getByRole("button", { name: "Submit" }).click()
|
||||||
expect((await reply).postDataJSON()).toEqual({ answer: { q0: "minimal" } })
|
expect((await reply).postDataJSON()).toEqual({ answers: [["Minimal"]] })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("shows a pending permission dock", async ({ page }) => {
|
test("shows a pending permission dock", async ({ page }) => {
|
||||||
@@ -87,7 +82,7 @@ test("shows a pending permission dock", async ({ page }) => {
|
|||||||
permission: "bash",
|
permission: "bash",
|
||||||
patterns: ["git status", "git diff"],
|
patterns: ["git status", "git diff"],
|
||||||
metadata: {},
|
metadata: {},
|
||||||
always: ["git *"],
|
always: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -114,7 +109,7 @@ test("restores the draft caret before typing after a request dock closes", async
|
|||||||
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
|
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
|
||||||
retry: 20,
|
retry: 20,
|
||||||
})
|
})
|
||||||
await mockServer(page, { forms: [] })
|
await mockServer(page, { questions: [] })
|
||||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||||
await transport.waitForConnection()
|
await transport.waitForConnection()
|
||||||
await expectSessionTitle(page, title)
|
await expectSessionTitle(page, title)
|
||||||
@@ -140,24 +135,18 @@ test("restores the draft caret before typing after a request dock closes", async
|
|||||||
await transport.send({
|
await transport.send({
|
||||||
directory,
|
directory,
|
||||||
payload: {
|
payload: {
|
||||||
type: "form.created",
|
type: "question.asked",
|
||||||
properties: {
|
properties: {
|
||||||
form: {
|
id: "question-caret",
|
||||||
id: "frm_question_caret",
|
|
||||||
sessionID,
|
sessionID,
|
||||||
title: "Questions",
|
questions: [
|
||||||
metadata: { kind: "question", tool: { messageID: "message-caret", id: "call-caret" } },
|
|
||||||
fields: [
|
|
||||||
{
|
{
|
||||||
key: "q0",
|
header: "Continue",
|
||||||
type: "string",
|
question: "Continue?",
|
||||||
title: "Continue",
|
options: [{ label: "Yes", description: "Continue the session" }],
|
||||||
description: "Continue?",
|
|
||||||
options: [{ value: "yes", label: "Yes", description: "Continue the session" }],
|
|
||||||
custom: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
tool: { messageID: "message-caret", callID: "call-caret" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -167,10 +156,7 @@ test("restores the draft caret before typing after a request dock closes", async
|
|||||||
|
|
||||||
await transport.send({
|
await transport.send({
|
||||||
directory,
|
directory,
|
||||||
payload: {
|
payload: { type: "question.rejected", properties: { sessionID, requestID: "question-caret" } },
|
||||||
type: "form.cancelled",
|
|
||||||
properties: { sessionID, id: "frm_question_caret" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
await expect(question).toHaveCount(0)
|
await expect(question).toHaveCount(0)
|
||||||
await expect(editor).toBeVisible()
|
await expect(editor).toBeVisible()
|
||||||
@@ -184,8 +170,6 @@ async function mockServer(
|
|||||||
requests: {
|
requests: {
|
||||||
permissions?: unknown[] | (() => unknown[])
|
permissions?: unknown[] | (() => unknown[])
|
||||||
questions?: unknown[] | (() => unknown[])
|
questions?: unknown[] | (() => unknown[])
|
||||||
forms?: unknown[] | (() => unknown[])
|
|
||||||
sessionStatus?: Record<string, unknown>
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
await mockOpenCodeServer(page, {
|
await mockOpenCodeServer(page, {
|
||||||
@@ -230,8 +214,6 @@ async function mockServer(
|
|||||||
pageMessages: () => ({ items: [] }),
|
pageMessages: () => ({ items: [] }),
|
||||||
permissions: requests.permissions,
|
permissions: requests.permissions,
|
||||||
questions: requests.questions,
|
questions: requests.questions,
|
||||||
forms: requests.forms,
|
|
||||||
sessionStatus: requests.sessionStatus,
|
|
||||||
})
|
})
|
||||||
await page.addInitScript(() => {
|
await page.addInitScript(() => {
|
||||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
|
||||||
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
|
||||||
import { session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture"
|
|
||||||
|
|
||||||
const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo
|
|
||||||
|
|
||||||
const assistant = (completed: boolean, tool = false, childID?: string) =>
|
|
||||||
({
|
|
||||||
id: "msg_assistant",
|
|
||||||
type: "assistant",
|
|
||||||
agent: "build",
|
|
||||||
model: { id: "model", providerID: "provider" },
|
|
||||||
content: tool
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: "tool",
|
|
||||||
id: "call_subagent",
|
|
||||||
name: "subagent",
|
|
||||||
state: { status: "running", input: {}, metadata: childID ? { sessionID: childID } : {} },
|
|
||||||
time: { created: 2 },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [{ type: "text", text: "Working" }],
|
|
||||||
time: { created: 2, ...(completed ? { completed: 3 } : {}) },
|
|
||||||
}) satisfies SessionMessageInfo
|
|
||||||
|
|
||||||
test("renders current protocol notices in CLI order", async ({ page }) => {
|
|
||||||
const ownerWarnings: string[] = []
|
|
||||||
page.on("console", (message) => {
|
|
||||||
if (message.text().includes("computations created outside a `createRoot` or `render`"))
|
|
||||||
ownerWarnings.push(message.text())
|
|
||||||
})
|
|
||||||
await setupTimeline(page, {
|
|
||||||
currentMessages: [
|
|
||||||
user,
|
|
||||||
{ id: "msg_agent", type: "agent-switched", agent: "explore", time: { created: 2 } },
|
|
||||||
assistant(true),
|
|
||||||
{
|
|
||||||
id: "msg_subagent",
|
|
||||||
type: "synthetic",
|
|
||||||
text: "done",
|
|
||||||
description: "Search code",
|
|
||||||
metadata: { source: "subagent", agent: "explore", state: "completed" },
|
|
||||||
time: { created: 4 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "msg_restart",
|
|
||||||
type: "synthetic",
|
|
||||||
text: "continue",
|
|
||||||
description: "Continuing after restart",
|
|
||||||
time: { created: 5 },
|
|
||||||
},
|
|
||||||
{ id: "msg_skill", type: "skill", skill: "review", name: "Review", text: "instructions", time: { created: 6 } },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const notices = page.locator('[data-slot="session-timeline-notice"]')
|
|
||||||
await expect(notices).toHaveCount(4)
|
|
||||||
await expect(notices.nth(0)).toContainText("Agent · explore")
|
|
||||||
await expect(notices.nth(1)).toContainText("explore finished · Search code")
|
|
||||||
await expect(notices.nth(2)).toContainText("Continuing after restart")
|
|
||||||
await expect(notices.nth(3)).toContainText("Skill · Review")
|
|
||||||
expect(ownerWarnings).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
|
|
||||||
await setupTimeline(page, { currentMessages: [user, assistant(false, true)] })
|
|
||||||
await expect(page.locator('[data-component="task-tool-card"]')).toBeVisible()
|
|
||||||
await expect(page.getByText("Called `subagent`", { exact: false })).toHaveCount(0)
|
|
||||||
await expect(page.locator('[data-component="background-tool-control"]')).toHaveCount(0)
|
|
||||||
await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background")
|
|
||||||
|
|
||||||
const request = page.waitForRequest(
|
|
||||||
(request) =>
|
|
||||||
request.method() === "POST" && new URL(request.url()).pathname === `/api/session/${sessionID}/background`,
|
|
||||||
)
|
|
||||||
await page.keyboard.press("Control+b")
|
|
||||||
await request
|
|
||||||
})
|
|
||||||
|
|
||||||
test("navigates from a running subagent card and hides background controls in the child", async ({ page }) => {
|
|
||||||
const childID = "ses_running_child"
|
|
||||||
await setupTimeline(page, {
|
|
||||||
currentMessages: [user, assistant(false, true, childID)],
|
|
||||||
sessions: [session(), session({ id: childID, parentID: sessionID, title: "Sleep for 5 minutes" })],
|
|
||||||
sessionStatus: { [sessionID]: { type: "busy" }, [childID]: { type: "busy" } },
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background")
|
|
||||||
await page.locator('[data-component="task-tool-card"]').click()
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/session/${childID}$`))
|
|
||||||
await expect(page.locator('[data-component="session-background-dock"]')).toHaveCount(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows a badge for active background work", async ({ page }) => {
|
|
||||||
const childID = "ses_background_child"
|
|
||||||
await setupTimeline(page, {
|
|
||||||
currentMessages: [user, assistant(true)],
|
|
||||||
sessions: [session(), session({ id: childID, parentID: sessionID })],
|
|
||||||
sessionStatus: { [childID]: { type: "busy" } },
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(page.locator('[data-component="session-background-dock"]')).toContainText("1 subagent in background")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("separates blocking and already-backgrounded work into two rows", async ({ page }) => {
|
|
||||||
const backgroundID = "ses_background_existing"
|
|
||||||
const blockingID = "ses_background_blocking"
|
|
||||||
await setupTimeline(page, {
|
|
||||||
currentMessages: [
|
|
||||||
user,
|
|
||||||
{
|
|
||||||
id: "msg_backgrounded",
|
|
||||||
type: "assistant",
|
|
||||||
agent: "build",
|
|
||||||
model: { id: "model", providerID: "provider" },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "tool",
|
|
||||||
id: "call_backgrounded",
|
|
||||||
name: "subagent",
|
|
||||||
state: {
|
|
||||||
status: "completed",
|
|
||||||
input: { description: "Background task" },
|
|
||||||
content: [{ type: "text", text: "working" }],
|
|
||||||
metadata: { sessionID: backgroundID, status: "running" },
|
|
||||||
},
|
|
||||||
time: { created: 2, completed: 3 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "tool",
|
|
||||||
id: "call_shell_backgrounded",
|
|
||||||
name: "shell",
|
|
||||||
state: {
|
|
||||||
status: "completed",
|
|
||||||
input: { command: "sleep 120" },
|
|
||||||
content: [{ type: "text", text: "working" }],
|
|
||||||
metadata: { shellID: "shell_backgrounded", status: "running" },
|
|
||||||
},
|
|
||||||
time: { created: 2, completed: 3 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
time: { created: 2, completed: 3 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "msg_blocking",
|
|
||||||
type: "assistant",
|
|
||||||
agent: "build",
|
|
||||||
model: { id: "model", providerID: "provider" },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "tool",
|
|
||||||
id: "call_blocking",
|
|
||||||
name: "subagent",
|
|
||||||
state: {
|
|
||||||
status: "running",
|
|
||||||
input: { description: "Foreground task" },
|
|
||||||
metadata: { sessionID: blockingID },
|
|
||||||
},
|
|
||||||
time: { created: 4 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
time: { created: 4 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sessions: [
|
|
||||||
session(),
|
|
||||||
session({ id: backgroundID, parentID: sessionID, title: "Background task" }),
|
|
||||||
session({ id: blockingID, parentID: sessionID, title: "Foreground task" }),
|
|
||||||
],
|
|
||||||
sessionStatus: {
|
|
||||||
[sessionID]: { type: "busy" },
|
|
||||||
[backgroundID]: { type: "busy" },
|
|
||||||
[blockingID]: { type: "busy" },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const dock = page.locator('[data-component="session-background-dock"]')
|
|
||||||
await expect(dock).toContainText("Move 1 subagent to background")
|
|
||||||
await expect(dock.getByText("Running 1 shell and 1 subagent in background", { exact: true })).toBeVisible()
|
|
||||||
await expect(
|
|
||||||
page.locator('[data-timeline-part-id="call_shell_backgrounded"] [data-component="text-shimmer"]'),
|
|
||||||
).toHaveAttribute("data-active", "true")
|
|
||||||
})
|
|
||||||
@@ -27,30 +27,6 @@ test("navigates to a subagent child session missing from the session list", asyn
|
|||||||
await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1)
|
await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps the parent visible while child lineage resolves", async ({ page }) => {
|
|
||||||
await setup(page)
|
|
||||||
const requested = Promise.withResolvers<void>()
|
|
||||||
const release = Promise.withResolvers<void>()
|
|
||||||
await page.route(
|
|
||||||
(url) => url.pathname === `/api/session/${childID}` && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"),
|
|
||||||
async (route) => {
|
|
||||||
requested.resolve()
|
|
||||||
await release.promise
|
|
||||||
await route.fallback()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await page.goto(sessionHref(parentID))
|
|
||||||
await expectSessionTitle(page, parentTitle)
|
|
||||||
|
|
||||||
await page.locator(`a[href="${sessionHref(childID)}"]`).click()
|
|
||||||
await requested.promise
|
|
||||||
await Promise.all([expect(page).toHaveURL(sessionHref(parentID)), expectSessionTitle(page, parentTitle)]).finally(
|
|
||||||
() => release.resolve(),
|
|
||||||
)
|
|
||||||
|
|
||||||
await expectSessionTitle(page, taskDescription)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows the not found fallback when the viewed session is deleted", async ({ page }) => {
|
test("shows the not found fallback when the viewed session is deleted", async ({ page }) => {
|
||||||
const events: EventPayload[] = []
|
const events: EventPayload[] = []
|
||||||
await setup(page, () => events.splice(0, 1))
|
await setup(page, () => events.splice(0, 1))
|
||||||
|
|||||||
@@ -32,110 +32,15 @@ export interface MockServerConfig {
|
|||||||
todos?: (sessionID: string) => unknown[]
|
todos?: (sessionID: string) => unknown[]
|
||||||
permissions?: unknown[] | (() => unknown[])
|
permissions?: unknown[] | (() => unknown[])
|
||||||
questions?: unknown[] | (() => unknown[])
|
questions?: unknown[] | (() => unknown[])
|
||||||
forms?: unknown[] | (() => unknown[])
|
|
||||||
fileList?: (path: string) => unknown | Promise<unknown>
|
fileList?: (path: string) => unknown | Promise<unknown>
|
||||||
fileContent?: (path: string) => unknown | Promise<unknown>
|
fileContent?: (path: string) => unknown | Promise<unknown>
|
||||||
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
|
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
|
||||||
sessionStatus?: Record<string, unknown> | (() => Record<string, unknown>)
|
sessionStatus?: Record<string, unknown> | (() => Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockStreamWindow = Window & {
|
|
||||||
__testSseTransport?: unknown
|
|
||||||
__mockServerStream?: { push: (payloads: unknown[]) => void }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||||
const cursors = new Map<string, string>()
|
const cursors = new Map<string, string>()
|
||||||
let nextCursor = 0
|
let nextCursor = 0
|
||||||
|
|
||||||
await page.addInitScript(
|
|
||||||
({ port, retry }) => {
|
|
||||||
const host = window as MockStreamWindow
|
|
||||||
if (host.__testSseTransport || host.__mockServerStream) return
|
|
||||||
const originalFetch = window.fetch.bind(window)
|
|
||||||
const encoder = new TextEncoder()
|
|
||||||
const state: {
|
|
||||||
controller?: ReadableStreamDefaultController<Uint8Array>
|
|
||||||
buffer: string[]
|
|
||||||
connections: number
|
|
||||||
} = { buffer: [], connections: 0 }
|
|
||||||
const frame = (payload: unknown) => `data: ${JSON.stringify(payload)}\n\n`
|
|
||||||
host.__mockServerStream = {
|
|
||||||
push(payloads: unknown[]) {
|
|
||||||
const frames = payloads.map(frame)
|
|
||||||
const controller = state.controller
|
|
||||||
if (!controller) {
|
|
||||||
state.buffer.push(...frames)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
frames.forEach((item) => controller.enqueue(encoder.encode(item)))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
||||||
const request = new Request(input, init)
|
|
||||||
const url = new URL(request.url)
|
|
||||||
if (url.port !== port || url.pathname !== "/api/event") return originalFetch(request)
|
|
||||||
state.connections += 1
|
|
||||||
const id = state.connections
|
|
||||||
let ended = false
|
|
||||||
let own: ReadableStreamDefaultController<Uint8Array> | undefined
|
|
||||||
const stream = new ReadableStream<Uint8Array>({
|
|
||||||
start(controller) {
|
|
||||||
own = controller
|
|
||||||
state.controller = controller
|
|
||||||
if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`))
|
|
||||||
controller.enqueue(
|
|
||||||
encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })),
|
|
||||||
)
|
|
||||||
state.buffer.splice(0).forEach((item) => controller.enqueue(encoder.encode(item)))
|
|
||||||
request.signal.addEventListener(
|
|
||||||
"abort",
|
|
||||||
() => {
|
|
||||||
if (ended) return
|
|
||||||
ended = true
|
|
||||||
if (state.controller === controller) state.controller = undefined
|
|
||||||
controller.error(request.signal.reason ?? new DOMException("The operation was aborted", "AbortError"))
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
if (ended) return
|
|
||||||
ended = true
|
|
||||||
if (state.controller === own) state.controller = undefined
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return Promise.resolve(
|
|
||||||
new Response(stream, {
|
|
||||||
status: 200,
|
|
||||||
headers: { "cache-control": "no-cache", "content-type": "text/event-stream" },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Object.defineProperty(window, "fetch", { configurable: true, writable: true, value: fetch })
|
|
||||||
},
|
|
||||||
{ port: process.env.PLAYWRIGHT_SERVER_PORT ?? "4096", retry: config.eventRetry },
|
|
||||||
)
|
|
||||||
|
|
||||||
if (config.events) {
|
|
||||||
const pump = { busy: false }
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
if (pump.busy) return
|
|
||||||
const batch = config.events?.() ?? []
|
|
||||||
if (batch.length === 0) return
|
|
||||||
pump.busy = true
|
|
||||||
void page
|
|
||||||
.evaluate(
|
|
||||||
(payloads) => (window as MockStreamWindow).__mockServerStream?.push(payloads),
|
|
||||||
batch.map(currentEvent),
|
|
||||||
)
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => {
|
|
||||||
pump.busy = false
|
|
||||||
})
|
|
||||||
}, 50)
|
|
||||||
page.on("close", () => clearInterval(timer))
|
|
||||||
}
|
|
||||||
const staticRoutes: Record<string, unknown> = {
|
const staticRoutes: Record<string, unknown> = {
|
||||||
"/path": {
|
"/path": {
|
||||||
state: config.directory,
|
state: config.directory,
|
||||||
@@ -267,34 +172,16 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
}
|
}
|
||||||
if (/^\/api\/credential\/[^/]+$/.test(path) && route.request().method() === "DELETE")
|
if (/^\/api\/credential\/[^/]+$/.test(path) && route.request().method() === "DELETE")
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
||||||
if (path === "/api/project") {
|
if (path === "/api/project") return json(route, [config.project])
|
||||||
const project = config.project as typeof config.project & { canonical?: string; worktree?: string }
|
|
||||||
return json(route, [
|
|
||||||
{
|
|
||||||
...project,
|
|
||||||
canonical: project.canonical ?? project.worktree ?? config.directory,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
if (path === "/api/project/current")
|
if (path === "/api/project/current")
|
||||||
return json(route, { id: (config.project as { id?: string }).id, directory: config.directory })
|
return json(route, { id: (config.project as { id?: string }).id, directory: config.directory })
|
||||||
const worktree = path.match(/^\/api\/experimental\/project\/([^/]+)\/worktree$/)?.[1]
|
|
||||||
if (worktree && route.request().method() === "GET")
|
|
||||||
return json(route, [
|
|
||||||
{ directory: config.directory },
|
|
||||||
...((config.project as { sandboxes?: string[] }).sandboxes ?? []).map((directory) => ({
|
|
||||||
directory,
|
|
||||||
strategy: "git",
|
|
||||||
})),
|
|
||||||
])
|
|
||||||
if (path === "/api/location") return json(route, location(config))
|
if (path === "/api/location") return json(route, location(config))
|
||||||
if (worktree && route.request().method() === "POST") {
|
const projectCopy = path.match(/^\/experimental\/project\/([^/]+)\/copy$/)?.[1]
|
||||||
|
if (projectCopy && route.request().method() === "POST") {
|
||||||
const input = route.request().postDataJSON() as { directory: string; name?: string }
|
const input = route.request().postDataJSON() as { directory: string; name?: string }
|
||||||
return json(route, { directory: `${input.directory}/${input.name ?? "copy"}` })
|
return json(route, { directory: `${input.directory}/${input.name ?? "copy"}` })
|
||||||
}
|
}
|
||||||
if (worktree && route.request().method() === "DELETE")
|
if (projectCopy && route.request().method() === "DELETE")
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
|
||||||
if (/^\/api\/experimental\/project\/[^/]+\/worktree\/refresh$/.test(path))
|
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
||||||
if (path === "/api/permission/request")
|
if (path === "/api/permission/request")
|
||||||
return json(route, {
|
return json(route, {
|
||||||
@@ -308,11 +195,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
location: location(config),
|
location: location(config),
|
||||||
data: typeof config.questions === "function" ? config.questions() : (config.questions ?? []),
|
data: typeof config.questions === "function" ? config.questions() : (config.questions ?? []),
|
||||||
})
|
})
|
||||||
if (path === "/api/form/request")
|
|
||||||
return json(route, {
|
|
||||||
location: location(config),
|
|
||||||
data: typeof config.forms === "function" ? config.forms() : (config.forms ?? []),
|
|
||||||
})
|
|
||||||
if (path === "/api/vcs")
|
if (path === "/api/vcs")
|
||||||
return json(route, { location: location(config), data: { branch: "main", defaultBranch: "main" } })
|
return json(route, { location: location(config), data: { branch: "main", defaultBranch: "main" } })
|
||||||
if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] })
|
if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] })
|
||||||
@@ -361,10 +243,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
const limit = Number(url.searchParams.get("limit") ?? 50)
|
const limit = Number(url.searchParams.get("limit") ?? 50)
|
||||||
const offset = Number(url.searchParams.get("cursor") ?? 0)
|
const offset = Number(url.searchParams.get("cursor") ?? 0)
|
||||||
const sessions = config.sessions
|
const sessions = config.sessions
|
||||||
.filter((session) => {
|
.filter((session) => !directory || session.directory === directory)
|
||||||
const location = session.location as { directory?: string } | undefined
|
|
||||||
return !directory || location?.directory === directory || session.directory === directory
|
|
||||||
})
|
|
||||||
.filter((session) => parentID !== "null" || session.parentID === undefined)
|
.filter((session) => parentID !== "null" || session.parentID === undefined)
|
||||||
.filter((session) => {
|
.filter((session) => {
|
||||||
const search = url.searchParams.get("search")?.toLowerCase()
|
const search = url.searchParams.get("search")?.toLowerCase()
|
||||||
@@ -401,18 +280,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||||||
if (/^\/api\/session\/[^/]+\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") {
|
if (/^\/api\/session\/[^/]+\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") {
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
||||||
}
|
}
|
||||||
const sessionForm = path.match(/^\/api\/session\/([^/]+)\/form$/)?.[1]
|
|
||||||
if (sessionForm && route.request().method() === "GET") {
|
|
||||||
const forms = typeof config.forms === "function" ? config.forms() : (config.forms ?? [])
|
|
||||||
return json(route, { data: forms.filter((form) => (form as { sessionID?: string }).sessionID === sessionForm) })
|
|
||||||
}
|
|
||||||
if (/^\/api\/session\/[^/]+\/form\/[^/]+\/(reply|cancel)$/.test(path) && route.request().method() === "POST") {
|
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
|
||||||
}
|
|
||||||
if (/^\/api\/session\/[^/]+\/background$/.test(path) && route.request().method() === "POST")
|
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
|
||||||
if (/^\/api\/session\/[^/]+\/inbox$/.test(path) && route.request().method() === "GET")
|
|
||||||
return json(route, { data: [] })
|
|
||||||
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
|
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
|
||||||
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
|
||||||
}
|
}
|
||||||
@@ -599,7 +466,6 @@ function currentPermission(value: unknown) {
|
|||||||
|
|
||||||
export function currentSession(session: { id: string } & Record<string, unknown>, fallbackDirectory?: string) {
|
export function currentSession(session: { id: string } & Record<string, unknown>, fallbackDirectory?: string) {
|
||||||
const time = session.time && typeof session.time === "object" ? session.time : {}
|
const time = session.time && typeof session.time === "object" ? session.time : {}
|
||||||
const location = session.location && typeof session.location === "object" ? session.location : {}
|
|
||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
parentID: session.parentID,
|
parentID: session.parentID,
|
||||||
@@ -617,19 +483,10 @@ export function currentSession(session: { id: string } & Record<string, unknown>
|
|||||||
},
|
},
|
||||||
title: session.title ?? session.id,
|
title: session.title ?? session.id,
|
||||||
location: {
|
location: {
|
||||||
directory:
|
directory: typeof session.directory === "string" ? session.directory : fallbackDirectory,
|
||||||
"directory" in location && typeof location.directory === "string"
|
...(typeof session.workspaceID === "string" ? { workspaceID: session.workspaceID } : {}),
|
||||||
? location.directory
|
|
||||||
: typeof session.directory === "string"
|
|
||||||
? session.directory
|
|
||||||
: fallbackDirectory,
|
|
||||||
...(typeof session.workspaceID === "string"
|
|
||||||
? { workspaceID: session.workspaceID }
|
|
||||||
: "workspaceID" in location && typeof location.workspaceID === "string"
|
|
||||||
? { workspaceID: location.workspaceID }
|
|
||||||
: {}),
|
|
||||||
},
|
},
|
||||||
subpath: session.subpath ?? session.path,
|
subpath: session.path,
|
||||||
revert: session.revert,
|
revert: session.revert,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,13 +63,19 @@
|
|||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@pierre/trees": "1.0.0-beta.4",
|
"@pierre/trees": "1.0.0-beta.4",
|
||||||
"@sentry/solid": "catalog:",
|
"@sentry/solid": "catalog:",
|
||||||
|
"@shikijs/transformers": "3.9.2",
|
||||||
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
|
"@solid-primitives/audio": "1.4.2",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
"@solid-primitives/event-listener": "2.4.5",
|
"@solid-primitives/event-listener": "2.4.5",
|
||||||
"@solid-primitives/i18n": "2.2.1",
|
"@solid-primitives/i18n": "2.2.1",
|
||||||
"@solid-primitives/media": "2.3.3",
|
"@solid-primitives/media": "2.3.3",
|
||||||
"@solid-primitives/resize-observer": "2.1.5",
|
"@solid-primitives/resize-observer": "2.1.5",
|
||||||
"@solid-primitives/scheduled": "1.5.3",
|
"@solid-primitives/scheduled": "1.5.3",
|
||||||
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solid-primitives/timer": "1.4.4",
|
||||||
|
"@solid-primitives/websocket": "1.3.1",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@tanstack/solid-query": "5.91.4",
|
"@tanstack/solid-query": "5.91.4",
|
||||||
|
|||||||
+246
-62
@@ -1,38 +1,48 @@
|
|||||||
import "@/index.css"
|
import "@/index.css"
|
||||||
import * as Sentry from "@sentry/solid"
|
import * as Sentry from "@sentry/solid"
|
||||||
import { I18nProvider } from "@opencode-ai/ui/context"
|
import { I18nProvider } from "@opencode-ai/ui/context"
|
||||||
import type { UiI18n } from "@opencode-ai/ui/context/i18n"
|
|
||||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||||
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
||||||
import { File } from "@opencode-ai/session-ui/file"
|
import { File } from "@opencode-ai/session-ui/file"
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
|
||||||
|
import { Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
|
For,
|
||||||
type JSX,
|
type JSX,
|
||||||
lazy,
|
lazy,
|
||||||
|
onCleanup,
|
||||||
type ParentProps,
|
type ParentProps,
|
||||||
Show,
|
Show,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
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 { ServerSyncProvider } from "@/context/server-sync"
|
||||||
import { GlobalProvider, useGlobal } from "@/context/global"
|
import { GlobalProvider, useGlobal } from "@/context/global"
|
||||||
import { HighlightsProvider } from "@/context/highlights"
|
import { HighlightsProvider } from "@/context/highlights"
|
||||||
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
|
||||||
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 { 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, ServersProvider, useServers } from "@/context/servers"
|
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
|
||||||
import { SettingsProvider } from "@/context/settings"
|
import { SettingsProvider } from "@/context/settings"
|
||||||
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
@@ -40,26 +50,26 @@ import { WslServersProvider } from "@/wsl/context"
|
|||||||
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
|
import { useCheckServerHealth } from "./utils/server-health"
|
||||||
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
|
|
||||||
import { TargetSessionRouteContent } from "@/pages/session"
|
import { TargetSessionRouteContent } from "@/pages/session"
|
||||||
import { Home } from "@/pages/home"
|
import { Home } from "@/pages/home"
|
||||||
import { ServerProvider } from "./context/server"
|
|
||||||
|
|
||||||
const NewSession = lazy(() => import("@/pages/new-session"))
|
const NewSession = lazy(() => import("@/pages/new-session"))
|
||||||
|
|
||||||
const DirectoryDraftRedirect = () => {
|
const DirectoryDraftRedirect = () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
||||||
const server = useServers()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (search.draftId || !tabs.ready()) return
|
if (search.draftId || !tabs.ready()) return
|
||||||
const directory = decode64(params.dir)
|
const directory = decode64(params.dir)
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
tabs.newDraft({ server: ServerConnection.key(server.list[0])!, directory }, search.prompt)
|
tabs.newDraft({ server: server.key, directory }, search.prompt)
|
||||||
})
|
})
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -76,13 +86,31 @@ function TargetServerRoute(props: ParentProps) {
|
|||||||
return (
|
return (
|
||||||
// Owns the server-identity remount. Session changes must not remount this subtree.
|
// Owns the server-identity remount. Session changes must not remount this subtree.
|
||||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||||
<Show when={conn()} keyed>
|
<ServerSDKProvider server={conn}>
|
||||||
{(conn) => <ServerProvider conn={conn}>{props.children}</ServerProvider>}
|
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
||||||
</Show>
|
</ServerSDKProvider>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TargetSessionRoute = () => (
|
||||||
|
<TargetServerRoute>
|
||||||
|
<TargetSessionRouteContent />
|
||||||
|
</TargetServerRoute>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
|
||||||
|
// server via ServerKey, then provide the server-scoped shell for that server.
|
||||||
|
function SelectedServerProviders(props: ParentProps) {
|
||||||
|
return (
|
||||||
|
<ServerKey>
|
||||||
|
<ServerSDKProvider>
|
||||||
|
<ServerSyncProvider>{props.children}</ServerSyncProvider>
|
||||||
|
</ServerSDKProvider>
|
||||||
|
</ServerKey>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function DraftRoute() {
|
function DraftRoute() {
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
@@ -102,24 +130,24 @@ function DraftRoute() {
|
|||||||
function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server))
|
const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server))
|
||||||
|
const directory = () => props.draft.directory
|
||||||
|
const serverKey = () => props.draft.server
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
|
<Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
|
||||||
<Show when={conn()} keyed>
|
<ServerSDKProvider server={conn}>
|
||||||
{(conn) => (
|
<ServerSyncProvider server={conn}>
|
||||||
<ServerProvider conn={conn}>
|
<ModelsProvider directory={directory}>
|
||||||
<ModelsProvider directory={props.draft.directory}>
|
<SDKProvider directory={directory}>
|
||||||
<SDKProvider directory={props.draft.directory}>
|
<DirectoryDataProvider directory={directory} server={serverKey}>
|
||||||
<DirectoryDataProvider directory={props.draft.directory} server={props.draft.server}>
|
|
||||||
<DraftProviders>
|
<DraftProviders>
|
||||||
<NewSession draftId={props.draft.draftID} />
|
<NewSession />
|
||||||
</DraftProviders>
|
</DraftProviders>
|
||||||
</DirectoryDataProvider>
|
</DirectoryDataProvider>
|
||||||
</SDKProvider>
|
</SDKProvider>
|
||||||
</ModelsProvider>
|
</ModelsProvider>
|
||||||
</ServerProvider>
|
</ServerSyncProvider>
|
||||||
)}
|
</ServerSDKProvider>
|
||||||
</Show>
|
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -128,13 +156,7 @@ function UiI18nBridge(props: ParentProps) {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
return (
|
return (
|
||||||
<I18nProvider
|
<I18nProvider
|
||||||
value={{
|
value={{ locale: language.intl, layoutLocale: language.layoutLocale, t: language.t, plural: language.plural }}
|
||||||
locale: language.intl,
|
|
||||||
layoutLocale: language.layoutLocale,
|
|
||||||
t: language.t as UiI18n["t"],
|
|
||||||
plural: language.plural,
|
|
||||||
pluralForm: language.pluralForm,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
@@ -158,8 +180,8 @@ function QueryProvider(props: ParentProps) {
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -179,6 +201,18 @@ function BodyDesignClass() {
|
|||||||
|
|
||||||
// Server-agnostic providers shared across every route. These live in the shared
|
// Server-agnostic providers shared across every route. These live in the shared
|
||||||
// shell (router root) so they stay mounted regardless of the active server/route.
|
// shell (router root) so they stay mounted regardless of the active server/route.
|
||||||
|
function SharedProviders(props: ParentProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BodyDesignClass />
|
||||||
|
<CommandProvider>
|
||||||
|
<DesktopCommands />
|
||||||
|
<HighlightsProvider>{props.children}</HighlightsProvider>
|
||||||
|
</CommandProvider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function DesktopCommands() {
|
function DesktopCommands() {
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -202,14 +236,30 @@ function DesktopCommands() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppLayout(props: ParentProps) {
|
type ServerScopedShellProps = ParentProps<{
|
||||||
|
directory?: () => string | undefined
|
||||||
|
serverScoped?: JSX.Element
|
||||||
|
}>
|
||||||
|
|
||||||
|
function ServerScopedProviders(props: ServerScopedShellProps) {
|
||||||
return (
|
return (
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<Layout>{props.children}</Layout>
|
{props.serverScoped}
|
||||||
|
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
|
||||||
|
return (
|
||||||
|
<SelectedServerProviders>
|
||||||
|
<ServerScopedProviders serverScoped={props.serverScoped}>
|
||||||
|
<Layout>{props.children}</Layout>
|
||||||
|
</ServerScopedProviders>
|
||||||
|
</SelectedServerProviders>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -259,6 +309,133 @@ export function AppBaseProviders(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; startup?: Promise<void> }>) {
|
||||||
|
const server = useServer()
|
||||||
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
|
const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking")
|
||||||
|
|
||||||
|
// performs repeated health check with a grace period for
|
||||||
|
// non-http connections, otherwise fails instantly
|
||||||
|
const [startupHealthCheck, healthCheckActions] = createResource(() =>
|
||||||
|
props.disableHealthCheck
|
||||||
|
? true
|
||||||
|
: Effect.gen(function* () {
|
||||||
|
if (!server.current) return true
|
||||||
|
const { http, type } = server.current
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const res = yield* Effect.promise(() => checkServerHealth(http))
|
||||||
|
if (res.healthy) return true
|
||||||
|
if (checkMode() === "background" || type === "http") return false
|
||||||
|
}
|
||||||
|
}).pipe(
|
||||||
|
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
|
||||||
|
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||||
|
Effect.runPromise,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const checking = createMemo(
|
||||||
|
() => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state),
|
||||||
|
)
|
||||||
|
const [startup] = createResource(async () => {
|
||||||
|
if (!props.startup) return true
|
||||||
|
await props.startup.catch((error) => {
|
||||||
|
console.error("[startup] startup gate failed", error)
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
const startupChecking = createMemo(
|
||||||
|
() => startupHealthCheck.latest === true && ["unresolved", "pending"].includes(startup.state),
|
||||||
|
)
|
||||||
|
const loading = createMemo(() => checking() || startupChecking())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={!checking()}>
|
||||||
|
<Show
|
||||||
|
when={startupHealthCheck.latest}
|
||||||
|
fallback={
|
||||||
|
<ConnectionError
|
||||||
|
onRetry={() => {
|
||||||
|
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||||
|
}}
|
||||||
|
onServerSelected={(key) => {
|
||||||
|
setCheckMode("blocking")
|
||||||
|
server.setActive(key)
|
||||||
|
void healthCheckActions.refetch()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
<Show when={loading()}>
|
||||||
|
<div class="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background-base">
|
||||||
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const server = useServer()
|
||||||
|
const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key)
|
||||||
|
const name = createMemo(() => server.name || server.key)
|
||||||
|
const serverToken = "\u0000server\u0000"
|
||||||
|
const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken))
|
||||||
|
|
||||||
|
const timer = setInterval(() => props.onRetry?.(), 1000)
|
||||||
|
onCleanup(() => clearInterval(timer))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base gap-6 p-6">
|
||||||
|
<div class="flex flex-col items-center max-w-md text-center">
|
||||||
|
<Splash class="w-12 h-15 mb-4" />
|
||||||
|
<p class="text-14-regular text-text-base">
|
||||||
|
{unreachable()[0]}
|
||||||
|
<span class="text-text-strong font-medium">{name()}</span>
|
||||||
|
{unreachable()[1]}
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-12-regular text-text-weak">{language.t("app.server.retrying")}</p>
|
||||||
|
</div>
|
||||||
|
<Show when={others().length > 0}>
|
||||||
|
<div class="flex flex-col gap-2 w-full max-w-sm">
|
||||||
|
<span class="text-12-regular text-text-base text-center">{language.t("app.server.otherServers")}</span>
|
||||||
|
<div class="flex flex-col gap-1 bg-surface-base rounded-lg p-2">
|
||||||
|
<For each={others()}>
|
||||||
|
{(conn) => {
|
||||||
|
const key = ServerConnection.key(conn)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-3 w-full px-3 py-2 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
|
||||||
|
onClick={() => props.onServerSelected?.(key)}
|
||||||
|
>
|
||||||
|
<span class="text-14-regular text-text-strong truncate">{serverName(conn)}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServerKey(props: ParentProps) {
|
||||||
|
const server = useServer()
|
||||||
|
return (
|
||||||
|
<Show when={server.key} keyed>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function AppInterface(props: {
|
export function AppInterface(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
defaultServer: ServerConnection.Key
|
defaultServer: ServerConnection.Key
|
||||||
@@ -267,60 +444,67 @@ export function AppInterface(props: {
|
|||||||
router?: Component<BaseRouterProps>
|
router?: Component<BaseRouterProps>
|
||||||
disableHealthCheck?: boolean
|
disableHealthCheck?: boolean
|
||||||
startup?: Promise<void>
|
startup?: Promise<void>
|
||||||
|
serverScoped?: JSX.Element
|
||||||
}) {
|
}) {
|
||||||
// The visual layout lives in the router root so it remains mounted across
|
// The visual layout lives in the router root so it remains mounted across
|
||||||
// route changes. Draft and session routes override only their server-bound data
|
// route changes. Draft and session routes override only their server-bound data
|
||||||
// providers beneath it.
|
// providers beneath it.
|
||||||
const Root = (rootProps: ParentProps) => (
|
const ServerShell = (shellProps: ParentProps) => (
|
||||||
<TabsProvider>
|
<QueryProvider>
|
||||||
<BodyDesignClass />
|
<SharedProviders>
|
||||||
<CommandProvider>
|
|
||||||
<DesktopCommands />
|
|
||||||
<HighlightsProvider>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
{rootProps.children}
|
{shellProps.children}
|
||||||
</HighlightsProvider>
|
</SharedProviders>
|
||||||
</CommandProvider>
|
</QueryProvider>
|
||||||
</TabsProvider>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServersProvider
|
<ServerProvider
|
||||||
defaultServer={props.defaultServer}
|
defaultServer={props.defaultServer}
|
||||||
canonicalLocalServer={props.canonicalLocalServer}
|
canonicalLocalServer={props.canonicalLocalServer}
|
||||||
servers={props.servers}
|
servers={props.servers}
|
||||||
>
|
>
|
||||||
<SettingsProvider>
|
|
||||||
<GlobalProvider>
|
<GlobalProvider>
|
||||||
<Dynamic component={props.router ?? Router} root={Root}>
|
<SettingsProvider>
|
||||||
{/* Proper Routes */}
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
||||||
<Route component={AppLayout}>
|
<Dynamic
|
||||||
<Route path="/" component={Home} />
|
component={props.router ?? Router}
|
||||||
<Route
|
root={(routerProps) => (
|
||||||
path="/server/:serverKey/session/:id"
|
<TabsProvider>
|
||||||
component={() => (
|
<PermissionProvider>
|
||||||
<TargetServerRoute>
|
<NotificationProvider>
|
||||||
<TargetSessionRouteContent />
|
<ServerShell>
|
||||||
</TargetServerRoute>
|
<AppLayout serverScoped={props.serverScoped}>{routerProps.children}</AppLayout>
|
||||||
|
</ServerShell>
|
||||||
|
</NotificationProvider>
|
||||||
|
</PermissionProvider>
|
||||||
|
</TabsProvider>
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
<Route path="/new-session" component={DraftRoute} />
|
<Routes />
|
||||||
</Route>
|
</Dynamic>
|
||||||
{/* Legacy Routes */}
|
</ConnectionGate>
|
||||||
<Route>
|
</SettingsProvider>
|
||||||
|
</GlobalProvider>
|
||||||
|
</ServerProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Routes() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Route path="/" component={Home} />
|
||||||
<Route path="/:dir" component={DirectoryDraftRedirect} />
|
<Route path="/:dir" component={DirectoryDraftRedirect} />
|
||||||
<Route path="/:dir/session" component={DirectoryDraftRedirect} />
|
<Route path="/:dir/session" component={DirectoryDraftRedirect} />
|
||||||
<Route path="/:dir/session/:id" component={LegacySessionRedirect} />
|
<Route path="/:dir/session/:id" component={LegacySessionRedirect} />
|
||||||
</Route>
|
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
|
||||||
</Dynamic>
|
<Route path="/new-session" component={DraftRoute} />
|
||||||
</GlobalProvider>
|
</>
|
||||||
</SettingsProvider>
|
|
||||||
</ServersProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function LegacySessionRedirect() {
|
function LegacySessionRedirect() {
|
||||||
const server = useServers()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const params = useParams<{ id: string }>()
|
const params = useParams<{ id: string }>()
|
||||||
|
|
||||||
@@ -331,7 +515,7 @@ function LegacySessionRedirect() {
|
|||||||
legacySessionServer(
|
legacySessionServer(
|
||||||
tabs.store.filter((item) => item.type === "session"),
|
tabs.store.filter((item) => item.type === "session"),
|
||||||
params.id,
|
params.id,
|
||||||
ServerConnection.key(server.list[0]!),
|
server.key,
|
||||||
),
|
),
|
||||||
params.id,
|
params.id,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ import { useFile } from "@/context/file"
|
|||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout, type LocalProject } from "@/context/layout"
|
import { useLayout, type LocalProject } from "@/context/layout"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { useServer } from "@/context/server"
|
|
||||||
|
|
||||||
export type CommandPaletteEntry = {
|
export type CommandPaletteEntry = {
|
||||||
id: string
|
id: string
|
||||||
@@ -84,7 +83,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()()
|
||||||
const serverCtx = global.ensureServerCtx(serverSDK.server)
|
const serverCtx = global.ensureServerCtx(serverSDK.server)
|
||||||
const appTabs = useTabs()
|
const appTabs = useTabs()
|
||||||
const { tabs: sessionTabs } = useSessionLayout()
|
const { tabs: sessionTabs } = useSessionLayout()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useIsRouting, useLocation } from "@solidjs/router"
|
import { useIsRouting, useLocation } from "@solidjs/router"
|
||||||
import { batch, createEffect, onCleanup, onMount, Show } from "solid-js"
|
import { batch, createEffect, onCleanup, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
@@ -571,7 +571,7 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={language.t(`debugBar.direction.${language.direction()}`)}
|
value={language.t(`debugBar.direction.${language.direction()}`)}
|
||||||
onClick={() => language.setDirection(language.direction() === "rtl" ? "ltr" : "rtl")}
|
onClick={() => language.setDirection(language.direction() === "rtl" ? "ltr" : "rtl")}
|
||||||
/>
|
/>
|
||||||
<Show when={platform.setForceFocus}>
|
{platform.setForceFocus && (
|
||||||
<ToggleCell
|
<ToggleCell
|
||||||
active={state.focus}
|
active={state.focus}
|
||||||
inline={props.inline}
|
inline={props.inline}
|
||||||
@@ -580,7 +580,7 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
|||||||
value={language.t(state.focus ? "debugBar.focus.on" : "debugBar.focus.off")}
|
value={language.t(state.focus ? "debugBar.focus.on" : "debugBar.focus.off")}
|
||||||
onClick={() => void toggleFocus()}
|
onClick={() => void toggleFocus()}
|
||||||
/>
|
/>
|
||||||
</Show>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { createEffect, createMemo, createResource, createSignal, For, Match, onC
|
|||||||
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
|
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||||
import { getRelativeTime } from "@/utils/time"
|
import { getRelativeTime } from "@/utils/time"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
|||||||
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
|
import { type Accessor, type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { ExternalLink } from "@/components/external-link"
|
import { ExternalLink } from "@/components/external-link"
|
||||||
@@ -20,7 +20,6 @@ import { useServerSync } from "@/context/server-sync"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
import { useIntegrations } from "@/hooks/use-integrations"
|
|
||||||
import { CustomProviderForm } from "./dialog-custom-provider"
|
import { CustomProviderForm } from "./dialog-custom-provider"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { createProviderConnectionController, type ProviderConnectMethod } from "./provider-connection-controller"
|
import { createProviderConnectionController, type ProviderConnectMethod } from "./provider-connection-controller"
|
||||||
@@ -41,7 +40,7 @@ export function useProviderConnectController(options: { onBack?: () => void } =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DialogConnectProvider: Component<{
|
export const DialogConnectProvider: Component<{
|
||||||
directory?: string
|
directory?: Accessor<string | undefined>
|
||||||
controller?: ReturnType<typeof useProviderConnectController>
|
controller?: ReturnType<typeof useProviderConnectController>
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const fallback = useProviderConnectController()
|
const fallback = useProviderConnectController()
|
||||||
@@ -137,11 +136,15 @@ export const DialogConnectProvider: Component<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProviderPicker(props: { directory?: string; onSelect: (provider: string) => void; onPrepare?: () => void }) {
|
function ProviderPicker(props: {
|
||||||
|
directory?: Accessor<string | undefined>
|
||||||
|
onSelect: (provider: string) => void
|
||||||
|
onPrepare?: () => void
|
||||||
|
}) {
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
if (settings.general.newLayoutDesigns())
|
if (settings.general.newLayoutDesigns())
|
||||||
return <ProviderPickerV2 directory={props.directory} onSelect={props.onSelect} onPrepare={props.onPrepare} />
|
return <ProviderPickerV2 directory={props.directory} onSelect={props.onSelect} onPrepare={props.onPrepare} />
|
||||||
const integrations = useIntegrations(() => props.directory)
|
const providers = useProviders(() => props.directory?.())
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const popularGroup = () => language.t("dialog.provider.group.popular")
|
const popularGroup = () => language.t("dialog.provider.group.popular")
|
||||||
const otherGroup = () => language.t("dialog.provider.group.other")
|
const otherGroup = () => language.t("dialog.provider.group.other")
|
||||||
@@ -163,7 +166,7 @@ function ProviderPicker(props: { directory?: string; onSelect: (provider: string
|
|||||||
key={(x) => x?.id}
|
key={(x) => x?.id}
|
||||||
items={() => {
|
items={() => {
|
||||||
language.locale()
|
language.locale()
|
||||||
return [{ id: CUSTOM_ID, name: customLabel() }, ...integrations.list()]
|
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
|
||||||
}}
|
}}
|
||||||
filterKeys={["id", "name"]}
|
filterKeys={["id", "name"]}
|
||||||
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
||||||
@@ -208,8 +211,12 @@ function ProviderPicker(props: { directory?: string; onSelect: (provider: string
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProviderPickerV2(props: { directory?: string; onSelect: (provider: string) => void; onPrepare?: () => void }) {
|
function ProviderPickerV2(props: {
|
||||||
const integrations = useIntegrations(() => props.directory)
|
directory?: Accessor<string | undefined>
|
||||||
|
onSelect: (provider: string) => void
|
||||||
|
onPrepare?: () => void
|
||||||
|
}) {
|
||||||
|
const providers = useProviders(() => props.directory?.())
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
filter: "",
|
filter: "",
|
||||||
@@ -221,7 +228,7 @@ function ProviderPickerV2(props: { directory?: string; onSelect: (provider: stri
|
|||||||
const all = createMemo(() => {
|
const all = createMemo(() => {
|
||||||
language.locale()
|
language.locale()
|
||||||
const query = store.filter.trim().toLowerCase()
|
const query = store.filter.trim().toLowerCase()
|
||||||
const values = [custom(), ...integrations.list()]
|
const values = [custom(), ...providers.all().values()]
|
||||||
if (!query) return values
|
if (!query) return values
|
||||||
return values.filter((provider) => `${provider.id} ${provider.name}`.toLowerCase().includes(query))
|
return values.filter((provider) => `${provider.id} ${provider.name}`.toLowerCase().includes(query))
|
||||||
})
|
})
|
||||||
@@ -357,7 +364,7 @@ function ProviderPickerV2(props: { directory?: string; onSelect: (provider: stri
|
|||||||
|
|
||||||
function ProviderConnection(props: {
|
function ProviderConnection(props: {
|
||||||
provider: string
|
provider: string
|
||||||
directory?: string
|
directory?: Accessor<string | undefined>
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
setBack: (handler: () => void) => void
|
setBack: (handler: () => void) => void
|
||||||
}) {
|
}) {
|
||||||
@@ -367,9 +374,12 @@ function ProviderConnection(props: {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const newLayout = settings.general.newLayoutDesigns
|
const newLayout = settings.general.newLayoutDesigns
|
||||||
const providers = useProviders(() => props.directory)
|
const providers = useProviders(() => props.directory?.())
|
||||||
const directory = () => props.directory ?? decode64(params.dir)
|
const directory = () => props.directory?.() ?? decode64(params.dir)
|
||||||
|
|
||||||
|
const provider = createMemo(
|
||||||
|
() => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!,
|
||||||
|
)
|
||||||
const controller = createProviderConnectionController({
|
const controller = createProviderConnectionController({
|
||||||
provider: () => props.provider,
|
provider: () => props.provider,
|
||||||
directory,
|
directory,
|
||||||
@@ -383,14 +393,6 @@ function ProviderConnection(props: {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const provider = createMemo(() => ({
|
|
||||||
id: props.provider,
|
|
||||||
name:
|
|
||||||
providers.all().get(props.provider)?.name ??
|
|
||||||
serverSync.data.provider.all.get(props.provider)?.name ??
|
|
||||||
controller.integration()?.name ??
|
|
||||||
props.provider,
|
|
||||||
}))
|
|
||||||
const methodLabel = (value?: { type?: string; label?: string }) => {
|
const methodLabel = (value?: { type?: string; label?: string }) => {
|
||||||
if (!value) return ""
|
if (!value) return ""
|
||||||
if (value.type === "key") return language.t("provider.connect.method.apiKey")
|
if (value.type === "key") return language.t("provider.connect.method.apiKey")
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
|
|||||||
const output = validateCustomProvider({
|
const output = validateCustomProvider({
|
||||||
form,
|
form,
|
||||||
t: language.t,
|
t: language.t,
|
||||||
disabledProviders: serverSync.data.config.disabled_providers ?? [],
|
disabledProviders: serverSync().data.config.disabled_providers ?? [],
|
||||||
existingProviderIDs: new Set(serverSync.data.provider.all.keys()),
|
existingProviderIDs: new Set(serverSync().data.provider.all.keys()),
|
||||||
})
|
})
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setForm("err", output.err)
|
setForm("err", output.err)
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
.project-settings-v2-dialog [data-slot="dialog-container"] {
|
|
||||||
background: var(--v2-background-bg-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-v2-dialog [data-slot="dialog-body"] {
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-v2 {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-v2-nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-v2-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden !important;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-v2-panel :is(input, textarea, [contenteditable="true"]) {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-v2-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-v2-scroll {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 40px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-page-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-page-header h2 {
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 640;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-page-header > span {
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 440;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extensions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-tabs {
|
|
||||||
flex: 1;
|
|
||||||
height: auto;
|
|
||||||
min-height: 0;
|
|
||||||
margin-top: 24px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"] {
|
|
||||||
width: auto;
|
|
||||||
padding-inline: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"]::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"]
|
|
||||||
> [data-slot="tabs-v2-list"]
|
|
||||||
[data-slot="tabs-v2-trigger-wrapper"] {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"]
|
|
||||||
> [data-slot="tabs-v2-list"]
|
|
||||||
[data-slot="tabs-v2-trigger"] {
|
|
||||||
padding-inline: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-tabs [data-slot="tabs-v2-content"] {
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 530;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-section-header > :last-child {
|
|
||||||
color: var(--v2-text-text-faint);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 440;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-link {
|
|
||||||
color: var(--v2-text-text-accent) !important;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-card {
|
|
||||||
padding-inline: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 0.5px solid var(--v2-border-border-base);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--v2-background-bg-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
min-height: 48px;
|
|
||||||
border-bottom: 0.5px solid var(--v2-border-border-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-row:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-row-main {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-row-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--v2-icon-icon-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-row-name {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 530;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-row-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-extension-row-status-dot {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--v2-state-fg-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-shared {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-shared-trigger {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
align-self: flex-start;
|
|
||||||
gap: 6px;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 530;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-shared-chevron {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
color: var(--v2-icon-icon-muted);
|
|
||||||
transition: transform 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-shared-chevron.open {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-settings-shared-count {
|
|
||||||
padding: 1px 4px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: var(--v2-background-bg-layer-02);
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
@@ -1,89 +1,30 @@
|
|||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
|
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
|
import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2"
|
||||||
import { Field } from "@opencode-ai/ui/v2/field-v2"
|
import { Field } from "@opencode-ai/ui/v2/field-v2"
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/v2/project-avatar-v2"
|
import { ProjectAvatar, PROJECT_AVATAR_VARIANTS } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
|
||||||
import { TextareaV2 } from "@opencode-ai/ui/v2/textarea-v2"
|
import { TextareaV2 } from "@opencode-ai/ui/v2/textarea-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { For, Show, createSignal, startTransition } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
|
import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
|
||||||
import { createEditProjectModel } from "./edit-project"
|
import { createEditProjectModel } from "./edit-project"
|
||||||
import { ProjectSettingsExtensions } from "./project-settings-extensions"
|
|
||||||
import { SettingsServerDataScope } from "./settings-server-picker"
|
|
||||||
import "./settings-v2/settings-v2.css"
|
|
||||||
import "./dialog-edit-project-v2.css"
|
|
||||||
|
|
||||||
export function DialogEditProjectV2(props: { project: LocalProject; server: ServerConnection.Any }) {
|
export function DialogEditProjectV2(props: { project: LocalProject; server: ServerConnection.Any }) {
|
||||||
return (
|
|
||||||
<SettingsServerDataScope server={props.server}>
|
|
||||||
<SDKProvider directory={props.project.worktree}>
|
|
||||||
<ProjectSettingsDialog project={props.project} server={props.server} />
|
|
||||||
</SDKProvider>
|
|
||||||
</SettingsServerDataScope>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectSettingsDialog(props: { project: LocalProject; server: ServerConnection.Any }) {
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const model = createEditProjectModel(props)
|
const model = createEditProjectModel(props)
|
||||||
const projectName = () => displayName(props.project)
|
|
||||||
const [tab, setTab] = createSignal("general")
|
|
||||||
|
|
||||||
const Footer = () => (
|
|
||||||
<DialogFooter>
|
|
||||||
<ButtonV2 type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</ButtonV2>
|
|
||||||
<ButtonV2 type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
|
|
||||||
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
|
|
||||||
</ButtonV2>
|
|
||||||
</DialogFooter>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog size="x-large" variant="settings" class="project-settings-v2-dialog">
|
<Dialog fit>
|
||||||
<TabsV2
|
<form onSubmit={model.submit} class="contents">
|
||||||
orientation="vertical"
|
<DialogHeader>
|
||||||
variant="settings"
|
<DialogTitle>{language.t("dialog.project.edit.title")}</DialogTitle>
|
||||||
value={tab()}
|
</DialogHeader>
|
||||||
onChange={(value) => void startTransition(() => setTab(value))}
|
<DividerV2 />
|
||||||
class="project-settings-v2"
|
<DialogBody class="flex max-h-[min(560px,calc(100vh-160px))] w-full flex-col gap-6 overflow-y-auto px-4 pt-4 pb-1">
|
||||||
>
|
|
||||||
<TabsV2.List>
|
|
||||||
<div class="project-settings-v2-nav">
|
|
||||||
<TabsV2.Trigger value="general">
|
|
||||||
<ProjectAvatar
|
|
||||||
fallback={projectName()}
|
|
||||||
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
|
||||||
class="!size-4 shrink-0"
|
|
||||||
/>
|
|
||||||
<span class="truncate">{projectName()}</span>
|
|
||||||
</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="scripts">
|
|
||||||
<Icon name="code" size="small" />
|
|
||||||
{language.t("project.settings.scripts")}
|
|
||||||
</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="extensions">
|
|
||||||
<Icon name="extensions" size="small" />
|
|
||||||
{language.t("settings.tab.extensions")}
|
|
||||||
</TabsV2.Trigger>
|
|
||||||
</div>
|
|
||||||
</TabsV2.List>
|
|
||||||
|
|
||||||
<TabsV2.Content value="general" class="project-settings-v2-panel">
|
|
||||||
<form onSubmit={model.submit} class="project-settings-v2-form">
|
|
||||||
<div class="project-settings-v2-scroll">
|
|
||||||
<div class="project-settings-page-header">
|
|
||||||
<h2>{language.t("dialog.project.edit.title")}</h2>
|
|
||||||
<span>{language.t("project.settings.general.description")}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
|
<Field.Label>{language.t("dialog.project.edit.name")}</Field.Label>
|
||||||
<TextInputV2
|
<TextInputV2
|
||||||
@@ -132,11 +73,13 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
"opacity-0": !model.store.iconHover,
|
"opacity-0": !model.store.iconHover,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconV2 name={model.store.iconOverride ? "close" : "share"} />
|
<Icon name={model.store.iconOverride ? "close" : "outline-share"} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
ref={(element) => model.setIconInput(element)}
|
ref={(element) => {
|
||||||
|
model.setIconInput(element)
|
||||||
|
}}
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@@ -185,39 +128,29 @@ function ProjectSettingsDialog(props: { project: LocalProject; server: ServerCon
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
</form>
|
|
||||||
</TabsV2.Content>
|
|
||||||
|
|
||||||
<TabsV2.Content value="scripts" class="project-settings-v2-panel">
|
|
||||||
<form onSubmit={model.submit} class="project-settings-v2-form">
|
|
||||||
<div class="project-settings-v2-scroll">
|
|
||||||
<div class="project-settings-page-header">
|
|
||||||
<h2>{language.t("project.settings.scripts")}</h2>
|
|
||||||
<span>{language.t("project.settings.scripts.description")}</span>
|
|
||||||
</div>
|
|
||||||
<Field>
|
<Field>
|
||||||
<Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
|
<Field.Label>{language.t("dialog.project.edit.worktree.startup")}</Field.Label>
|
||||||
<Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
|
<Field.Prefix>{language.t("dialog.project.edit.worktree.startup.description")}</Field.Prefix>
|
||||||
<TextareaV2
|
<TextareaV2
|
||||||
class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
|
class="!w-full [&_[data-slot=textarea-v2-textarea]]:font-mono"
|
||||||
rows={5}
|
rows={3}
|
||||||
value={model.store.startup}
|
value={model.store.startup}
|
||||||
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
onInput={(event) => model.setStore("startup", event.currentTarget.value)}
|
onInput={(event) => model.setStore("startup", event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</DialogBody>
|
||||||
<Footer />
|
<DialogFooter>
|
||||||
|
<ButtonV2 type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
|
||||||
|
{language.t("common.cancel")}
|
||||||
|
</ButtonV2>
|
||||||
|
<ButtonV2 type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
|
||||||
|
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
|
||||||
|
</ButtonV2>
|
||||||
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</TabsV2.Content>
|
|
||||||
|
|
||||||
<TabsV2.Content value="extensions" class="project-settings-v2-panel">
|
|
||||||
<ProjectSettingsExtensions />
|
|
||||||
</TabsV2.Content>
|
|
||||||
</TabsV2>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ import { extractPromptFromParts } from "@/utils/prompt"
|
|||||||
import type { TextPart as SDKTextPart } from "@/types"
|
import type { TextPart as SDKTextPart } from "@/types"
|
||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { sessionHref } from "@/utils/session-route"
|
|
||||||
import { ServerConnection } from "@/context/servers"
|
|
||||||
|
|
||||||
interface ForkableMessage {
|
interface ForkableMessage {
|
||||||
id: string
|
id: string
|
||||||
@@ -30,7 +27,6 @@ export const DialogFork: Component = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -77,7 +73,7 @@ export const DialogFork: Component = () => {
|
|||||||
.then((forked) => {
|
.then((forked) => {
|
||||||
dialog.close()
|
dialog.close()
|
||||||
prompt.set(restored, undefined, { dir, id: forked.id })
|
prompt.set(restored, undefined, { dir, id: forked.id })
|
||||||
navigate(sessionHref(ServerConnection.key(serverSDK.server), forked.id))
|
navigate(`/${dir}/session/${forked.id}`)
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const DialogManageModels: Component = () => {
|
|||||||
const directory = () => decode64(local.slug())
|
const directory = () => decode64(local.slug())
|
||||||
|
|
||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
void dialog.show(() => <DialogConnectProvider directory={directory()} />)
|
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
||||||
}
|
}
|
||||||
const providerRank = (id: string) => popularProviders.indexOf(id)
|
const providerRank = (id: string) => popularProviders.indexOf(id)
|
||||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||||
@@ -123,7 +123,7 @@ export const DialogManageModelsV2: Component = () => {
|
|||||||
const directory = () => decode64(local.slug())
|
const directory = () => decode64(local.slug())
|
||||||
|
|
||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
void dialog.show(() => <DialogConnectProvider directory={directory()} />)
|
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
||||||
}
|
}
|
||||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||||
const providerVisible = (providerID: string) =>
|
const providerVisible = (providerID: string) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal, Index, Show } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
@@ -83,28 +83,24 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
|||||||
{/* Bottom section - buttons and indicators (fixed position) */}
|
{/* Bottom section - buttons and indicators (fixed position) */}
|
||||||
<div class="flex flex-col gap-12">
|
<div class="flex flex-col gap-12">
|
||||||
<div class="flex flex-col items-start gap-3">
|
<div class="flex flex-col items-start gap-3">
|
||||||
<Show
|
{isLast() ? (
|
||||||
when={isLast()}
|
|
||||||
fallback={
|
|
||||||
<Button variant="secondary" size="large" onClick={handleNext}>
|
|
||||||
{language.t("dialog.releaseNotes.action.next")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button variant="primary" size="large" onClick={handleClose}>
|
<Button variant="primary" size="large" onClick={handleClose}>
|
||||||
{language.t("dialog.releaseNotes.action.getStarted")}
|
{language.t("dialog.releaseNotes.action.getStarted")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
) : (
|
||||||
|
<Button variant="secondary" size="large" onClick={handleNext}>
|
||||||
|
{language.t("dialog.releaseNotes.action.next")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button variant="ghost" size="small" onClick={handleDisable}>
|
<Button variant="ghost" size="small" onClick={handleDisable}>
|
||||||
{language.t("dialog.releaseNotes.action.hideFuture")}
|
{language.t("dialog.releaseNotes.action.hideFuture")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={paged()}>
|
{paged() && (
|
||||||
<div class="flex items-center gap-1.5 -my-2.5">
|
<div class="flex items-center gap-1.5 -my-2.5">
|
||||||
<Index each={props.highlights}>
|
{props.highlights.map((_, i) => (
|
||||||
{(_, i) => (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
|
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
|
||||||
@@ -122,32 +118,26 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
)}
|
))}
|
||||||
</Index>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Media content (edge to edge) */}
|
{/* Right side - Media content (edge to edge) */}
|
||||||
<Show when={feature()?.media}>
|
{feature()?.media && (
|
||||||
{(media) => (
|
|
||||||
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
|
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
|
||||||
<Show
|
{feature()!.media!.type === "image" ? (
|
||||||
when={media().type === "image"}
|
|
||||||
fallback={
|
|
||||||
<video src={media().src} autoplay loop muted playsinline class="w-full h-full object-cover" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src={media().src}
|
src={feature()!.media!.src}
|
||||||
alt={media().alt ?? feature()?.title ?? language.t("dialog.releaseNotes.media.alt")}
|
alt={feature()!.media!.alt ?? feature()?.title ?? language.t("dialog.releaseNotes.media.alt")}
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</Show>
|
) : (
|
||||||
|
<video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import type { Path } from "@/types"
|
import type { Path } from "@/types"
|
||||||
import {
|
import {
|
||||||
absoluteTreePath,
|
absoluteTreePath,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { ListRef } from "@opencode-ai/ui/list"
|
|||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { createMemo, createResource, createSignal } from "solid-js"
|
import { createMemo, createResource, createSignal } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
|
import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
|
||||||
import type { Path } from "@/types"
|
import type { Path } from "@/types"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { formatKeybind } from "@/context/command"
|
|||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { getRelativeTime } from "@/utils/time"
|
import { getRelativeTime } from "@/utils/time"
|
||||||
@@ -28,13 +29,14 @@ type DialogSelectFileMode = "all" | "files"
|
|||||||
|
|
||||||
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const settings = useSettings()
|
||||||
const filesOnly = () => props.mode === "files"
|
const filesOnly = () => props.mode === "files"
|
||||||
|
|
||||||
if (!filesOnly()) {
|
if (!filesOnly() && settings.general.newLayoutDesigns()) {
|
||||||
return <DialogCommandPaletteV2 onOpenFile={props.onOpenFile} />
|
return <DialogCommandPaletteV2 onOpenFile={props.onOpenFile} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filesOnly() && platform.platform === "desktop") {
|
if (filesOnly() && platform.platform === "desktop" && settings.general.newLayoutDesigns()) {
|
||||||
return <DialogSelectFileDesktopV2 onOpenFile={props.onOpenFile} />
|
return <DialogSelectFileDesktopV2 onOpenFile={props.onOpenFile} />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ function DialogSelectFileDesktopV2(props: { onOpenFile?: (path: string) => void
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogSelectFileV2
|
<DialogSelectFileV2
|
||||||
server={serverSDK.server}
|
server={serverSDK().server}
|
||||||
mode="file"
|
mode="file"
|
||||||
start={projectDirectory()}
|
start={projectDirectory()}
|
||||||
title={language.t("session.header.searchFiles")}
|
title={language.t("session.header.searchFiles")}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { useTheme } from "@opencode-ai/ui/theme"
|
import { useTheme } from "@opencode-ai/ui/theme"
|
||||||
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
|
import { createMemo, onCleanup, onMount, type Component, For, Show } from "solid-js"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { useIntegrations } from "@/hooks/use-integrations"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { decode64 } from "@/utils/base64"
|
import { decode64 } from "@/utils/base64"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ModelTooltip } from "./model-tooltip"
|
import { ModelTooltip } from "./model-tooltip"
|
||||||
@@ -22,7 +22,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const directory = () => decode64(local.slug())
|
const directory = () => decode64(local.slug())
|
||||||
const integrations = useIntegrations(directory)
|
const providers = useProviders(directory)
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const modelKey = (item: ReturnType<ModelState["list"]>[number]) => `${item.provider.id}:${item.id}`
|
const modelKey = (item: ReturnType<ModelState["list"]>[number]) => `${item.provider.id}:${item.id}`
|
||||||
const currentKey = createMemo(() => {
|
const currentKey = createMemo(() => {
|
||||||
@@ -37,7 +37,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
|||||||
void import("./dialog-connect-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
const controller = x.useProviderConnectController()
|
const controller = x.useProviderConnectController()
|
||||||
controller.select(provider)
|
controller.select(provider)
|
||||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory()} />)
|
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +127,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid w-full grid-cols-1 gap-y-1.5 gap-x-2 sm:grid-cols-2">
|
<div class="grid w-full grid-cols-1 gap-y-1.5 gap-x-2 sm:grid-cols-2">
|
||||||
<For
|
<For
|
||||||
each={integrations
|
each={[...providers.popular()]
|
||||||
.list()
|
|
||||||
.filter((provider) => featuredProviders.includes(provider.id))
|
.filter((provider) => featuredProviders.includes(provider.id))
|
||||||
.sort((a, b) => featuredProviders.indexOf(a.id) - featuredProviders.indexOf(b.id))}
|
.sort((a, b) => featuredProviders.indexOf(a.id) - featuredProviders.indexOf(b.id))}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
|||||||
void import("./dialog-connect-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
const controller = x.useProviderConnectController()
|
const controller = x.useProviderConnectController()
|
||||||
controller.select(provider)
|
controller.select(provider)
|
||||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory()} />)
|
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export function ModelSelectorPopover(props: {
|
|||||||
const handleConnectProvider = () => {
|
const handleConnectProvider = () => {
|
||||||
close("provider")
|
close("provider")
|
||||||
void import("./dialog-connect-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
void dialog.show(() => <x.DialogConnectProvider directory={directory()} />)
|
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -240,7 +240,7 @@ export function ModelSelectorPopoverV2(props: {
|
|||||||
trigger={props.trigger}
|
trigger={props.trigger}
|
||||||
models={controller.models}
|
models={controller.models}
|
||||||
groups={controller.groups}
|
groups={controller.groups}
|
||||||
current={controller.current()}
|
current={controller.current}
|
||||||
select={controller.select}
|
select={controller.select}
|
||||||
onManage={() => {
|
onManage={() => {
|
||||||
void import("./dialog-manage-models").then((module) => {
|
void import("./dialog-manage-models").then((module) => {
|
||||||
@@ -295,7 +295,7 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
trigger: ModelSelectorTrigger
|
trigger: ModelSelectorTrigger
|
||||||
models: (search: string) => ModelItem[]
|
models: (search: string) => ModelItem[]
|
||||||
groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[]
|
groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[]
|
||||||
current: string | undefined
|
current: () => string | undefined
|
||||||
select: (item: ModelItem) => void
|
select: (item: ModelItem) => void
|
||||||
onManage: () => void
|
onManage: () => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -310,7 +310,7 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
const groups = createMemo(() => props.groups(models()))
|
const groups = createMemo(() => props.groups(models()))
|
||||||
const keys = () => [...models().map(modelKey), manageKey]
|
const keys = () => [...models().map(modelKey), manageKey]
|
||||||
const initialActive = () => {
|
const initialActive = () => {
|
||||||
const selected = props.current
|
const selected = props.current()
|
||||||
const options = keys()
|
const options = keys()
|
||||||
if (selected && options.includes(selected)) return selected
|
if (selected && options.includes(selected)) return selected
|
||||||
return options[0] ?? ""
|
return options[0] ?? ""
|
||||||
@@ -453,7 +453,7 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
<MenuV2.GroupLabel class="gap-2 px-3">
|
<MenuV2.GroupLabel class="gap-2 px-3">
|
||||||
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
|
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
|
||||||
</MenuV2.GroupLabel>
|
</MenuV2.GroupLabel>
|
||||||
<MenuV2.RadioGroup value={props.current}>
|
<MenuV2.RadioGroup value={props.current()}>
|
||||||
<For each={group.items}>
|
<For each={group.items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<TooltipV2
|
<TooltipV2
|
||||||
@@ -473,7 +473,7 @@ function ModelSelectorPopoverV2View(props: {
|
|||||||
<MenuV2.RadioItem
|
<MenuV2.RadioItem
|
||||||
value={modelKey(item)}
|
value={modelKey(item)}
|
||||||
data-option-key={modelKey(item)}
|
data-option-key={modelKey(item)}
|
||||||
data-selected-model={props.current === modelKey(item) ? true : undefined}
|
data-selected-model={props.current() === modelKey(item) ? true : undefined}
|
||||||
class="scroll-my-6 w-full"
|
class="scroll-my-6 w-full"
|
||||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
@@ -529,7 +529,7 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat
|
|||||||
|
|
||||||
const provider = () => {
|
const provider = () => {
|
||||||
void import("./dialog-connect-provider").then((x) => {
|
void import("./dialog-connect-provider").then((x) => {
|
||||||
void dialog.show(() => <x.DialogConnectProvider directory={directory()} />)
|
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { ServerCollectionController } from "@/components/server/server-management-controller"
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { type ServerDomainController } from "@/components/server/server-management-controller"
|
||||||
|
|
||||||
type ServerConnectionFormController = {
|
type ServerConnectionFormController = {
|
||||||
state: {
|
state: {
|
||||||
@@ -113,11 +117,12 @@ function ServerForm(props: ServerFormProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServerConnectionList(props: {
|
export function ServerConnectionList(props: {
|
||||||
domain: ServerCollectionController
|
domain: ServerDomainController
|
||||||
onAdd: () => void
|
onAdd: () => void
|
||||||
onEdit: (server: ServerConnection.Http) => void
|
onEdit: (server: ServerConnection.Http) => void
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const settings = useSettings()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-1 min-h-0 flex-col gap-4">
|
<div class="flex flex-1 min-h-0 flex-col gap-4">
|
||||||
@@ -131,6 +136,9 @@ export function ServerConnectionList(props: {
|
|||||||
emptyMessage={language.t("dialog.server.empty")}
|
emptyMessage={language.t("dialog.server.empty")}
|
||||||
items={props.domain.collection.items}
|
items={props.domain.collection.items}
|
||||||
key={(x) => x.http.url}
|
key={(x) => x.http.url}
|
||||||
|
onSelect={(x) => {
|
||||||
|
if (x && !settings.general.newLayoutDesigns()) void props.domain.selection.select(x)
|
||||||
|
}}
|
||||||
divider={true}
|
divider={true}
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
@@ -155,6 +163,15 @@ export function ServerConnectionList(props: {
|
|||||||
showCredentials
|
showCredentials
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center justify-center gap-4 pl-4">
|
<div class="flex items-center justify-center gap-4 pl-4">
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
props.domain.collection.current() &&
|
||||||
|
ServerConnection.key(props.domain.collection.current()!) === key
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon name="check" class="h-6" />
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={i.type === "http"}>
|
<Show when={i.type === "http"}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import type { Platform } from "@/context/platform"
|
import type { Platform } from "@/context/platform"
|
||||||
|
|
||||||
export function directoryPickerKind(platform: Platform["platform"], server: ServerConnection.Any) {
|
export function directoryPickerKind(platform: Platform["platform"], server: ServerConnection.Any) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { lazy } from "solid-js"
|
import { lazy } from "solid-js"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createMemo } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { type LocalProject } from "@/context/layout"
|
import { type LocalProject } from "@/context/layout"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
|
|
||||||
export function createEditProjectModel(props: { project: LocalProject; server: ServerConnection.Any }) {
|
export function createEditProjectModel(props: { project: LocalProject; server: ServerConnection.Any }) {
|
||||||
const supported = !props.project.id || props.project.id === "global"
|
const supported = !props.project.id || props.project.id === "global"
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
|
||||||
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
|
||||||
import { type Component, For, Show, createMemo, createResource, createSignal } from "solid-js"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useMcpToggle } from "@/context/mcp"
|
|
||||||
import { useSDK } from "@/context/sdk"
|
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { useServerSync } from "@/context/server-sync"
|
|
||||||
import { useSync } from "@/context/sync"
|
|
||||||
import { ExternalLink } from "./external-link"
|
|
||||||
|
|
||||||
type SkillItem = {
|
|
||||||
name: string
|
|
||||||
location: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluginName = (item: string | [string, Record<string, unknown>]) => (typeof item === "string" ? item : item[0])
|
|
||||||
const skillKey = (item: SkillItem) => `${item.name}\n${item.location}`
|
|
||||||
|
|
||||||
const ExtensionCard: Component<{ children: unknown }> = (props) => (
|
|
||||||
<div class="project-settings-extension-card">{props.children as any}</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ExtensionRow: Component<{
|
|
||||||
icon: "mcp" | "cube" | "post-skill" | "code"
|
|
||||||
name: string
|
|
||||||
status?: string
|
|
||||||
children?: unknown
|
|
||||||
}> = (props) => (
|
|
||||||
<div class="project-settings-extension-row">
|
|
||||||
<div class="project-settings-extension-row-main">
|
|
||||||
<Icon name={props.icon} class="project-settings-extension-row-icon" />
|
|
||||||
<span class="project-settings-extension-row-name">{props.name}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={props.status}>
|
|
||||||
{(status) => (
|
|
||||||
<span class="project-settings-extension-row-status">
|
|
||||||
<span class="project-settings-extension-row-status-dot" />
|
|
||||||
{status()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
{props.children as any}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SharedSection: Component<{
|
|
||||||
count: number
|
|
||||||
children: unknown
|
|
||||||
}> = (props) => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const [open, setOpen] = createSignal(false)
|
|
||||||
return (
|
|
||||||
<Show when={props.count > 0}>
|
|
||||||
<div class="project-settings-shared">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="project-settings-shared-trigger"
|
|
||||||
aria-expanded={open()}
|
|
||||||
onClick={() => setOpen((value) => !value)}
|
|
||||||
>
|
|
||||||
<Icon name="chevron-right" classList={{ "project-settings-shared-chevron": true, open: open() }} />
|
|
||||||
<span>{language.t("project.settings.extensions.shared")}</span>
|
|
||||||
<span class="project-settings-shared-count">{props.count}</span>
|
|
||||||
</button>
|
|
||||||
<Show when={open()}>
|
|
||||||
<ExtensionCard>{props.children}</ExtensionCard>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProjectSettingsExtensions: Component = () => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const directorySDK = useSDK()
|
|
||||||
const serverSync = useServerSync()
|
|
||||||
const sync = useSync()
|
|
||||||
const toggleMcp = useMcpToggle()
|
|
||||||
|
|
||||||
const [serverMcp] = createResource(
|
|
||||||
serverSDK,
|
|
||||||
(sdk) =>
|
|
||||||
sdk.api.mcp
|
|
||||||
.list()
|
|
||||||
.then((result) => Object.fromEntries(result.data.map((server) => [server.name, server.status])))
|
|
||||||
.catch(() => ({})),
|
|
||||||
{ initialValue: {} },
|
|
||||||
)
|
|
||||||
const globalMcpNames = createMemo(() =>
|
|
||||||
[...new Set([...Object.keys(serverSync.data.config.mcp ?? {}), ...Object.keys(serverMcp.latest)])].sort(),
|
|
||||||
)
|
|
||||||
const projectMcpNames = createMemo(() => {
|
|
||||||
const shared = new Set(globalMcpNames())
|
|
||||||
const configured = Object.keys(sync().data.config.mcp ?? {}).filter((name) => !shared.has(name))
|
|
||||||
if (configured.length > 0) return configured.sort()
|
|
||||||
return Object.keys(sync().data.mcp ?? {})
|
|
||||||
.filter((name) => !shared.has(name))
|
|
||||||
.sort()
|
|
||||||
})
|
|
||||||
const mcpEnabled = (name: string) => sync().data.mcp?.[name]?.status === "connected"
|
|
||||||
|
|
||||||
const globalPlugins = createMemo(() => (serverSync.data.config.plugin ?? []).map(pluginName))
|
|
||||||
const projectPlugins = createMemo(() => {
|
|
||||||
const shared = new Set(globalPlugins())
|
|
||||||
return (sync().data.config.plugin ?? []).map(pluginName).filter((name) => !shared.has(name))
|
|
||||||
})
|
|
||||||
|
|
||||||
const [serverSkills] = createResource(
|
|
||||||
serverSDK,
|
|
||||||
(sdk): Promise<SkillItem[]> =>
|
|
||||||
sdk.api.skill.list().then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))),
|
|
||||||
{ initialValue: [] },
|
|
||||||
)
|
|
||||||
const [directorySkills] = createResource(
|
|
||||||
directorySDK,
|
|
||||||
(sdk): Promise<SkillItem[]> =>
|
|
||||||
sdk.api.skill
|
|
||||||
.list({ location: { directory: sdk.directory } })
|
|
||||||
.then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))),
|
|
||||||
{ initialValue: [] },
|
|
||||||
)
|
|
||||||
const projectSkills = createMemo(() => {
|
|
||||||
const shared = new Set(serverSkills.latest.map(skillKey))
|
|
||||||
return directorySkills.latest.filter((item) => !shared.has(skillKey(item)))
|
|
||||||
})
|
|
||||||
|
|
||||||
const mcpRows = (items: string[]) => (
|
|
||||||
<For each={items}>
|
|
||||||
{(name) => (
|
|
||||||
<ExtensionRow icon="mcp" name={name}>
|
|
||||||
<Switch
|
|
||||||
checked={mcpEnabled(name)}
|
|
||||||
disabled={toggleMcp.isPending && toggleMcp.variables === name}
|
|
||||||
hideLabel
|
|
||||||
onChange={() => {
|
|
||||||
if (toggleMcp.isPending) return
|
|
||||||
toggleMcp.mutate(name)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Switch>
|
|
||||||
</ExtensionRow>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
)
|
|
||||||
|
|
||||||
const pluginRows = (items: string[]) => <For each={items}>{(name) => <ExtensionRow icon="cube" name={name} />}</For>
|
|
||||||
|
|
||||||
const skillRows = (items: SkillItem[]) => (
|
|
||||||
<For each={items}>{(item) => <ExtensionRow icon="post-skill" name={item.name} />}</For>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="project-settings-extensions">
|
|
||||||
<div class="project-settings-page-header">
|
|
||||||
<h2>{language.t("settings.tab.extensions")}</h2>
|
|
||||||
<span>{language.t("project.settings.extensions.description")}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TabsV2 variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
|
|
||||||
<TabsV2.List>
|
|
||||||
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="lsps">{language.t("project.settings.extensions.tab.lsps")}</TabsV2.Trigger>
|
|
||||||
</TabsV2.List>
|
|
||||||
|
|
||||||
<TabsV2.Content value="mcps">
|
|
||||||
<div class="project-settings-extension-section">
|
|
||||||
<div class="project-settings-extension-section-header">
|
|
||||||
<span>{language.t("project.settings.extensions.added")}</span>
|
|
||||||
<span>{language.t("settings.extensions.manageConfig")}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={projectMcpNames().length > 0}>
|
|
||||||
<ExtensionCard>{mcpRows(projectMcpNames())}</ExtensionCard>
|
|
||||||
</Show>
|
|
||||||
<SharedSection count={globalMcpNames().length}>{mcpRows(globalMcpNames())}</SharedSection>
|
|
||||||
</div>
|
|
||||||
</TabsV2.Content>
|
|
||||||
|
|
||||||
<TabsV2.Content value="plugins">
|
|
||||||
<div class="project-settings-extension-section">
|
|
||||||
<div class="project-settings-extension-section-header">
|
|
||||||
<span>{language.t("project.settings.extensions.added")}</span>
|
|
||||||
<span>{language.t("settings.extensions.manageConfig")}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={projectPlugins().length > 0}>
|
|
||||||
<ExtensionCard>{pluginRows(projectPlugins())}</ExtensionCard>
|
|
||||||
</Show>
|
|
||||||
<SharedSection count={globalPlugins().length}>{pluginRows(globalPlugins())}</SharedSection>
|
|
||||||
</div>
|
|
||||||
</TabsV2.Content>
|
|
||||||
|
|
||||||
<TabsV2.Content value="skills">
|
|
||||||
<div class="project-settings-extension-section">
|
|
||||||
<div class="project-settings-extension-section-header">
|
|
||||||
<span>{language.t("project.settings.extensions.added")}</span>
|
|
||||||
<ExternalLink class="project-settings-extension-link" href="https://opencode.ai/docs/skills/">
|
|
||||||
{language.t("settings.extensions.addSkills")}
|
|
||||||
</ExternalLink>
|
|
||||||
</div>
|
|
||||||
<Show when={projectSkills().length > 0}>
|
|
||||||
<ExtensionCard>{skillRows(projectSkills())}</ExtensionCard>
|
|
||||||
</Show>
|
|
||||||
<SharedSection count={serverSkills.latest.length}>{skillRows(serverSkills.latest)}</SharedSection>
|
|
||||||
</div>
|
|
||||||
</TabsV2.Content>
|
|
||||||
|
|
||||||
<TabsV2.Content value="lsps">
|
|
||||||
<div class="project-settings-extension-section">
|
|
||||||
<div class="project-settings-extension-section-header">
|
|
||||||
<span>{language.t("project.settings.extensions.lsp.detected")}</span>
|
|
||||||
<span>{language.t("project.settings.extensions.lsp.description")}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={sync().data.lsp.length > 0}>
|
|
||||||
<ExtensionCard>
|
|
||||||
<For each={sync().data.lsp}>
|
|
||||||
{(item) => (
|
|
||||||
<ExtensionRow
|
|
||||||
icon="code"
|
|
||||||
name={item.name || item.id}
|
|
||||||
status={
|
|
||||||
item.status === "error" ? language.t("project.settings.extensions.setupRequired") : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ExtensionCard>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</TabsV2.Content>
|
|
||||||
</TabsV2>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -37,7 +37,6 @@ export type PromptInputV2ComposerProps = {
|
|||||||
class?: string
|
class?: string
|
||||||
controller: PromptInputV2ComposerController
|
controller: PromptInputV2ComposerController
|
||||||
borderUnderlay?: boolean
|
borderUnderlay?: boolean
|
||||||
accentSubmit?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptInputV2ControllerProps = Omit<PromptInputProps, "class" | "submission">
|
export type PromptInputV2ControllerProps = Omit<PromptInputProps, "class" | "submission">
|
||||||
@@ -54,7 +53,6 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
|
|||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<PromptInputV2
|
<PromptInputV2
|
||||||
controller={props.controller}
|
controller={props.controller}
|
||||||
accentSubmit={props.accentSubmit}
|
|
||||||
borderUnderlay={props.borderUnderlay}
|
borderUnderlay={props.borderUnderlay}
|
||||||
class={props.class}
|
class={props.class}
|
||||||
variantControlVisible={!props.controller.model.loading}
|
variantControlVisible={!props.controller.model.loading}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ 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, ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||||
|
import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -1517,7 +1518,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
role="textbox"
|
role="textbox"
|
||||||
aria-multiline="true"
|
aria-multiline="true"
|
||||||
aria-label={placeholder()}
|
aria-label={placeholder()}
|
||||||
dir={store.mode === "normal" ? "auto" : "ltr"}
|
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
autocapitalize={store.mode === "normal" ? "sentences" : "off"}
|
autocapitalize={store.mode === "normal" ? "sentences" : "off"}
|
||||||
autocorrect={store.mode === "normal" ? "on" : "off"}
|
autocorrect={store.mode === "normal" ? "on" : "off"}
|
||||||
@@ -1539,11 +1539,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
"[&_[data-type=agent]]:text-syntax-type": true,
|
"[&_[data-type=agent]]:text-syntax-type": true,
|
||||||
"font-mono!": store.mode === "shell",
|
"font-mono!": store.mode === "shell",
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{ "padding-bottom": space }}
|
||||||
"padding-bottom": space,
|
|
||||||
"unicode-bidi": store.mode === "normal" ? "plaintext" : undefined,
|
|
||||||
"text-align": "start",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
|
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { Prompt, PromptStore } from "@/context/prompt"
|
import type { Prompt, PromptStore } from "@/context/prompt"
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
import type { ModelSelection } from "@/context/local"
|
||||||
|
|
||||||
let createPromptSubmit: typeof import("./submit").createPromptSubmit
|
let createPromptSubmit: typeof import("./submit").createPromptSubmit
|
||||||
|
|
||||||
|
const createdClients: string[] = []
|
||||||
const createdSessions: string[] = []
|
const createdSessions: string[] = []
|
||||||
type SessionCreateInput = {
|
const sessionCreateInputs: Array<{
|
||||||
agent?: string
|
agent?: string
|
||||||
model?: { id: string; providerID: string; variant?: string }
|
model?: { id: string; providerID: string; variant?: string }
|
||||||
location?: { directory: string }
|
location?: { directory: string }
|
||||||
}
|
}> = []
|
||||||
|
const enabledAutoAccept: Array<{ server: string; sessionID: string; directory: string }> = []
|
||||||
const optimistic: Array<{
|
const optimistic: Array<{
|
||||||
directory?: string
|
directory?: string
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
@@ -20,9 +22,11 @@ const optimistic: Array<{
|
|||||||
variant?: string
|
variant?: string
|
||||||
}
|
}
|
||||||
}> = []
|
}> = []
|
||||||
|
const optimisticSeeded: boolean[] = []
|
||||||
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
|
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
|
||||||
|
const promoted: Array<{ directory: string; sessionID: string }> = []
|
||||||
const sentShell: Array<{ sessionID: string; id?: string; command: string }> = []
|
const sentShell: Array<{ sessionID: string; id?: string; command: string }> = []
|
||||||
const sentShellDirectories: string[] = []
|
const syncedDirectories: string[] = []
|
||||||
const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = []
|
const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = []
|
||||||
const sentPrompts: string[] = []
|
const sentPrompts: string[] = []
|
||||||
const promptInputs: unknown[] = []
|
const promptInputs: unknown[] = []
|
||||||
@@ -33,29 +37,15 @@ const switchedModels: Array<{
|
|||||||
model: { id: string; providerID: string; variant?: string }
|
model: { id: string; providerID: string; variant?: string }
|
||||||
}> = []
|
}> = []
|
||||||
const sessionRequestOrder: string[] = []
|
const sessionRequestOrder: string[] = []
|
||||||
const updatedDrafts: Array<{ draftID: string; worktree?: string }> = []
|
const commands: Array<{ name: string }> = []
|
||||||
const syncedServers: string[] = []
|
|
||||||
const optimisticServers: string[] = []
|
|
||||||
const promptCaptures: Array<{ scope?: unknown; target?: unknown }> = []
|
|
||||||
let serverSessionSyncs = 0
|
let serverSessionSyncs = 0
|
||||||
|
|
||||||
let params: { id?: string } = {}
|
let params: { id?: string } = {}
|
||||||
let search: { draftId?: string } = {}
|
let search: { draftId?: string } = {}
|
||||||
let selected = "/repo/worktree-a"
|
let selected = "/repo/worktree-a"
|
||||||
let variant: string | undefined
|
let variant: string | undefined
|
||||||
|
let permissionServer = "server-a"
|
||||||
let createSessionGate: Promise<void> | undefined
|
let createSessionGate: Promise<void> | undefined
|
||||||
let createWorktreeGate: Promise<void> | undefined
|
|
||||||
let worktreeFailure: Error | undefined
|
|
||||||
let locationFailure: Error | undefined
|
|
||||||
let worktreeCreates = 0
|
|
||||||
let activeSDK = "server-a"
|
|
||||||
let activeServerSync = "server-a"
|
|
||||||
let activeDirectorySync = "server-a"
|
|
||||||
let commands: Array<{ name: string }> = []
|
|
||||||
let worktreeDirectory = "/repo/new-0"
|
|
||||||
let worktreeID = 0
|
|
||||||
const draftServers: Record<string, string> = {}
|
|
||||||
const sessionDirectories: Record<string, string> = {}
|
|
||||||
|
|
||||||
let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||||
const [promptStore, setPromptStore] = createStore<PromptStore>({
|
const [promptStore, setPromptStore] = createStore<PromptStore>({
|
||||||
@@ -83,25 +73,21 @@ const prompt = {
|
|||||||
replaceComments: () => undefined,
|
replaceComments: () => undefined,
|
||||||
items: () => [],
|
items: () => [],
|
||||||
},
|
},
|
||||||
capture: (scope?: unknown, target?: unknown) => {
|
capture: () => prompt,
|
||||||
promptCaptures.push({ scope, target })
|
|
||||||
return prompt
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
||||||
|
|
||||||
const clientFor = (directory: string) => {
|
const clientFor = (directory: string) => {
|
||||||
|
createdClients.push(directory)
|
||||||
return {
|
return {
|
||||||
api: {
|
api: {
|
||||||
session: {
|
session: {
|
||||||
create: async (input: SessionCreateInput) => {
|
create: async (input: (typeof sessionCreateInputs)[number]) => {
|
||||||
await createSessionGate
|
await createSessionGate
|
||||||
const location = input.location?.directory ?? directory
|
const location = input.location?.directory ?? directory
|
||||||
createdSessions.push(location)
|
createdSessions.push(location)
|
||||||
const id = `session-${createdSessions.length}`
|
sessionCreateInputs.push(input)
|
||||||
sessionDirectories[id] = location
|
|
||||||
return {
|
return {
|
||||||
id,
|
id: `session-${createdSessions.length}`,
|
||||||
projectID: "project",
|
projectID: "project",
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
model: input.model,
|
model: input.model,
|
||||||
@@ -114,7 +100,7 @@ const clientFor = (directory: string) => {
|
|||||||
},
|
},
|
||||||
prompt: async (input: unknown) => {
|
prompt: async (input: unknown) => {
|
||||||
sessionRequestOrder.push("prompt")
|
sessionRequestOrder.push("prompt")
|
||||||
sentPrompts.push(sessionDirectories[(input as { sessionID: string }).sessionID] ?? directory)
|
sentPrompts.push(directory)
|
||||||
promptInputs.push(input)
|
promptInputs.push(input)
|
||||||
return { data: undefined }
|
return { data: undefined }
|
||||||
},
|
},
|
||||||
@@ -134,21 +120,6 @@ const clientFor = (directory: string) => {
|
|||||||
},
|
},
|
||||||
shell: async (input: { sessionID: string; id?: string; command: string }) => {
|
shell: async (input: { sessionID: string; id?: string; command: string }) => {
|
||||||
sentShell.push(input)
|
sentShell.push(input)
|
||||||
sentShellDirectories.push(sessionDirectories[input.sessionID] ?? directory)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
worktree: {
|
|
||||||
create: async (_input: unknown) => {
|
|
||||||
worktreeCreates++
|
|
||||||
await createWorktreeGate
|
|
||||||
if (worktreeFailure) throw worktreeFailure
|
|
||||||
return { directory: worktreeDirectory }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
location: {
|
|
||||||
get: async () => {
|
|
||||||
if (locationFailure) throw locationFailure
|
|
||||||
return { directory: worktreeDirectory }
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -156,6 +127,9 @@ const clientFor = (directory: string) => {
|
|||||||
command: async () => ({ data: undefined }),
|
command: async () => ({ data: undefined }),
|
||||||
abort: async () => ({ data: undefined }),
|
abort: async () => ({ data: undefined }),
|
||||||
},
|
},
|
||||||
|
worktree: {
|
||||||
|
create: async () => ({ data: { directory: `${directory}/new` } }),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +145,6 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
mock.module("@opencode-ai/ui/toast", () => ({
|
mock.module("@opencode-ai/ui/toast", () => ({
|
||||||
Toast: { Region: () => null },
|
Toast: { Region: () => null },
|
||||||
toaster: { create: () => undefined, show: () => undefined, dismiss: () => undefined },
|
|
||||||
showToast: () => 0,
|
showToast: () => 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -189,13 +162,20 @@ beforeAll(async () => {
|
|||||||
current: () => ({ name: "agent" }),
|
current: () => ({ name: "agent" }),
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
promote: () => undefined,
|
promote(directory: string, sessionID: string) {
|
||||||
|
promoted.push({ directory, sessionID })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@/context/permission", () => {
|
mock.module("@/context/permission", () => {
|
||||||
return { usePermission: () => ({ currentServerState: () => ({ enableAutoAccept: () => undefined }) }) }
|
const state = (server: string) => ({
|
||||||
|
enableAutoAccept(sessionID: string, directory: string) {
|
||||||
|
enabledAutoAccept.push({ server, sessionID, directory })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return { usePermission: () => ({ currentServerState: () => state(permissionServer) }) }
|
||||||
})
|
})
|
||||||
|
|
||||||
mock.module("@/context/server", () => ({
|
mock.module("@/context/server", () => ({
|
||||||
@@ -204,10 +184,7 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
mock.module("@/context/tabs", () => ({
|
mock.module("@/context/tabs", () => ({
|
||||||
useTabs: () => ({
|
useTabs: () => ({
|
||||||
draft: (draftID: string) => ({ server: draftServers[draftID] ?? "project-server" }),
|
draft: () => ({ server: "project-server" }),
|
||||||
updateDraft: (draftID: string, draft: { worktree?: string }) => {
|
|
||||||
updatedDrafts.push({ draftID, ...draft })
|
|
||||||
},
|
|
||||||
promoteDraft: (draftID: string, session: { server: string; sessionId: string }) => {
|
promoteDraft: (draftID: string, session: { server: string; sessionId: string }) => {
|
||||||
promotedDrafts.push({ draftID, ...session })
|
promotedDrafts.push({ draftID, ...session })
|
||||||
},
|
},
|
||||||
@@ -218,22 +195,29 @@ beforeAll(async () => {
|
|||||||
usePrompt: () => prompt,
|
usePrompt: () => prompt,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mock.module("@/context/layout", () => ({
|
||||||
|
useLayout: () => ({
|
||||||
|
handoff: {
|
||||||
|
setTabs: () => undefined,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
mock.module("@/context/sdk", () => ({
|
mock.module("@/context/sdk", () => ({
|
||||||
useSDK: () => {
|
useSDK: () => {
|
||||||
return () => ({
|
const sdk = {
|
||||||
scope: activeSDK === "server-a" ? ServerScope.local : "server-b",
|
scope: "local",
|
||||||
directory: activeSDK === "server-a" ? "/repo/main" : "/repo/other",
|
directory: "/repo/main",
|
||||||
api: rootClient.api,
|
api: rootClient.api,
|
||||||
url: "http://localhost:4096",
|
url: "http://localhost:4096",
|
||||||
})
|
}
|
||||||
|
return () => sdk
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@/context/sync", () => ({
|
mock.module("@/context/sync", () => ({
|
||||||
useSync: () => () => {
|
useSync: () => () => ({
|
||||||
const server = activeDirectorySync
|
data: { command: commands },
|
||||||
return {
|
|
||||||
data: { command: commands, project: "project" },
|
|
||||||
session: {
|
session: {
|
||||||
optimistic: {
|
optimistic: {
|
||||||
add: (value: {
|
add: (value: {
|
||||||
@@ -241,22 +225,22 @@ beforeAll(async () => {
|
|||||||
sessionID?: string
|
sessionID?: string
|
||||||
message: { agent: string; model: { providerID: string; modelID: string; variant?: string } }
|
message: { agent: string; model: { providerID: string; modelID: string; variant?: string } }
|
||||||
}) => {
|
}) => {
|
||||||
optimisticServers.push(server)
|
|
||||||
optimistic.push(value)
|
optimistic.push(value)
|
||||||
|
optimisticSeeded.push(
|
||||||
|
!!value.directory &&
|
||||||
|
!!value.sessionID &&
|
||||||
|
!!storedSessions[value.directory]?.find((item) => item.id === value.sessionID)?.title,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
remove: () => undefined,
|
remove: () => undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
set: () => undefined,
|
set: () => undefined,
|
||||||
project: { worktree: server === "server-a" ? "/repo/main" : "/repo/other" },
|
}),
|
||||||
}
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@/context/server-sync", () => ({
|
mock.module("@/context/server-sync", () => ({
|
||||||
useServerSync: () => {
|
useServerSync: () => () => ({
|
||||||
const server = activeServerSync
|
|
||||||
return {
|
|
||||||
session: {
|
session: {
|
||||||
remember: () => undefined,
|
remember: () => undefined,
|
||||||
set: () => undefined,
|
set: () => undefined,
|
||||||
@@ -265,7 +249,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
child: (directory: string) => {
|
child: (directory: string) => {
|
||||||
syncedServers.push(server)
|
syncedDirectories.push(directory)
|
||||||
storedSessions[directory] ??= []
|
storedSessions[directory] ??= []
|
||||||
return [
|
return [
|
||||||
{ session: storedSessions[directory] },
|
{ session: storedSessions[directory] },
|
||||||
@@ -282,8 +266,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}),
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module("@/context/platform", () => ({
|
mock.module("@/context/platform", () => ({
|
||||||
@@ -303,45 +286,153 @@ beforeAll(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
createdClients.length = 0
|
||||||
createdSessions.length = 0
|
createdSessions.length = 0
|
||||||
|
sessionCreateInputs.length = 0
|
||||||
|
enabledAutoAccept.length = 0
|
||||||
optimistic.length = 0
|
optimistic.length = 0
|
||||||
|
optimisticSeeded.length = 0
|
||||||
|
promoted.length = 0
|
||||||
promotedDrafts.length = 0
|
promotedDrafts.length = 0
|
||||||
updatedDrafts.length = 0
|
|
||||||
sentCommands.length = 0
|
|
||||||
sentPrompts.length = 0
|
sentPrompts.length = 0
|
||||||
promptInputs.length = 0
|
promptInputs.length = 0
|
||||||
|
sentCommands.length = 0
|
||||||
switchedAgents.length = 0
|
switchedAgents.length = 0
|
||||||
switchedModels.length = 0
|
switchedModels.length = 0
|
||||||
sessionRequestOrder.length = 0
|
sessionRequestOrder.length = 0
|
||||||
syncedServers.length = 0
|
commands.length = 0
|
||||||
optimisticServers.length = 0
|
promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
||||||
promptCaptures.length = 0
|
|
||||||
params = {}
|
params = {}
|
||||||
search = {}
|
search = {}
|
||||||
sentShell.length = 0
|
sentShell.length = 0
|
||||||
sentShellDirectories.length = 0
|
syncedDirectories.length = 0
|
||||||
selected = "/repo/worktree-a"
|
selected = "/repo/worktree-a"
|
||||||
variant = undefined
|
variant = undefined
|
||||||
activeSDK = "server-a"
|
permissionServer = "server-a"
|
||||||
activeServerSync = "server-a"
|
|
||||||
activeDirectorySync = "server-a"
|
|
||||||
commands = []
|
|
||||||
promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }]
|
|
||||||
worktreeDirectory = `/repo/new-${++worktreeID}`
|
|
||||||
createSessionGate = undefined
|
createSessionGate = undefined
|
||||||
serverSessionSyncs = 0
|
serverSessionSyncs = 0
|
||||||
createWorktreeGate = undefined
|
|
||||||
worktreeFailure = undefined
|
|
||||||
locationFailure = undefined
|
|
||||||
worktreeCreates = 0
|
|
||||||
for (const key of Object.keys(draftServers)) delete draftServers[key]
|
|
||||||
for (const key of Object.keys(sessionDirectories)) delete sessionDirectories[key]
|
|
||||||
for (const key of Object.keys(storedSessions)) delete storedSessions[key]
|
for (const key of Object.keys(storedSessions)) delete storedSessions[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("prompt submit worktree selection", () => {
|
||||||
|
test("reads the latest worktree accessor value per submit", async () => {
|
||||||
|
const submit = createPromptSubmit({
|
||||||
|
prompt,
|
||||||
|
info: () => undefined,
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => false,
|
||||||
|
mode: () => "shell",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
newSessionWorktree: () => selected,
|
||||||
|
onNewSessionWorktreeReset: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
const event = { preventDefault: () => undefined } as unknown as Event
|
const event = { preventDefault: () => undefined } as unknown as Event
|
||||||
const makeSubmit = (overrides: Partial<Parameters<typeof createPromptSubmit>[0]> = {}) =>
|
|
||||||
createPromptSubmit({
|
await submit.handleSubmit(event)
|
||||||
|
selected = "/repo/worktree-b"
|
||||||
|
await submit.handleSubmit(event)
|
||||||
|
|
||||||
|
expect(createdClients).toEqual([])
|
||||||
|
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
|
expect(sessionCreateInputs).toEqual([
|
||||||
|
{
|
||||||
|
agent: "agent",
|
||||||
|
model: { id: "model", providerID: "provider", variant: undefined },
|
||||||
|
location: { directory: "/repo/worktree-a" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "agent",
|
||||||
|
model: { id: "model", providerID: "provider", variant: undefined },
|
||||||
|
location: { directory: "/repo/worktree-b" },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(sentShell).toEqual([
|
||||||
|
expect.objectContaining({ sessionID: "session-1", id: expect.stringMatching(/^evt_/), command: "ls" }),
|
||||||
|
expect.objectContaining({ sessionID: "session-2", id: expect.stringMatching(/^evt_/), command: "ls" }),
|
||||||
|
])
|
||||||
|
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
||||||
|
expect(serverSessionSyncs).toBe(0)
|
||||||
|
expect(promoted).toEqual([
|
||||||
|
{ directory: "/repo/worktree-a", sessionID: "session-1" },
|
||||||
|
{ directory: "/repo/worktree-b", sessionID: "session-2" },
|
||||||
|
])
|
||||||
|
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("applies auto-accept to newly created sessions", async () => {
|
||||||
|
const submit = createPromptSubmit({
|
||||||
|
prompt,
|
||||||
|
info: () => undefined,
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => true,
|
||||||
|
mode: () => "shell",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
newSessionWorktree: () => selected,
|
||||||
|
onNewSessionWorktreeReset: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const event = { preventDefault: () => undefined } as unknown as Event
|
||||||
|
|
||||||
|
await submit.handleSubmit(event)
|
||||||
|
|
||||||
|
expect(enabledAutoAccept).toEqual([{ server: "server-a", sessionID: "session-1", directory: "/repo/worktree-a" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps auto-accept bound to the submission server", async () => {
|
||||||
|
let release = () => {}
|
||||||
|
createSessionGate = new Promise<void>((resolve) => {
|
||||||
|
release = resolve
|
||||||
|
})
|
||||||
|
const submit = createPromptSubmit({
|
||||||
|
prompt,
|
||||||
|
info: () => undefined,
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => true,
|
||||||
|
mode: () => "shell",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
newSessionWorktree: () => selected,
|
||||||
|
onNewSessionWorktreeReset: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event)
|
||||||
|
permissionServer = "server-b"
|
||||||
|
release()
|
||||||
|
await result
|
||||||
|
|
||||||
|
expect(enabledAutoAccept).toEqual([{ server: "server-a", sessionID: "session-1", directory: "/repo/worktree-a" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("promotes drafts using the selected project's server", async () => {
|
||||||
|
search = { draftId: "draft-1" }
|
||||||
|
const submit = createPromptSubmit({
|
||||||
prompt,
|
prompt,
|
||||||
info: () => undefined,
|
info: () => undefined,
|
||||||
imageAttachments: () => [],
|
imageAttachments: () => [],
|
||||||
@@ -359,85 +450,41 @@ const makeSubmit = (overrides: Partial<Parameters<typeof createPromptSubmit>[0]>
|
|||||||
newSessionWorktree: () => selected,
|
newSessionWorktree: () => selected,
|
||||||
onNewSessionWorktreeReset: () => undefined,
|
onNewSessionWorktreeReset: () => undefined,
|
||||||
onSubmit: () => undefined,
|
onSubmit: () => undefined,
|
||||||
...overrides,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("prompt submit worktree selection", () => {
|
await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event)
|
||||||
test("admits only one concurrent new-workspace submission", async () => {
|
|
||||||
selected = "create"
|
|
||||||
let release = () => {}
|
|
||||||
createWorktreeGate = new Promise<void>((resolve) => {
|
|
||||||
release = resolve
|
|
||||||
})
|
|
||||||
const submit = makeSubmit()
|
|
||||||
|
|
||||||
const first = submit.handleSubmit(event)
|
expect(promotedDrafts).toEqual([{ draftID: "draft-1", server: "project-server", sessionId: "session-1" }])
|
||||||
const duplicate = submit.handleSubmit(event)
|
|
||||||
expect(worktreeCreates).toBe(1)
|
|
||||||
|
|
||||||
release()
|
|
||||||
await Promise.all([first, duplicate])
|
|
||||||
expect(createdSessions).toEqual([worktreeDirectory])
|
|
||||||
await settle()
|
|
||||||
|
|
||||||
expect(worktreeCreates).toBe(1)
|
|
||||||
expect(createdSessions).toHaveLength(1)
|
|
||||||
expect(sentPrompts).toEqual([worktreeDirectory])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("stops when the created workspace cannot initialize", async () => {
|
|
||||||
selected = "create"
|
|
||||||
locationFailure = new Error("initialization failed")
|
|
||||||
|
|
||||||
await makeSubmit().handleSubmit(event)
|
|
||||||
|
|
||||||
expect(worktreeCreates).toBe(1)
|
|
||||||
expect(createdSessions).toEqual([])
|
|
||||||
expect(sentPrompts).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("keeps async submission effects bound to the initiating context", async () => {
|
|
||||||
search = { draftId: "draft-1" }
|
|
||||||
draftServers["draft-1"] = "project-server-a"
|
|
||||||
draftServers["draft-2"] = "project-server-b"
|
|
||||||
let release = () => {}
|
|
||||||
createSessionGate = new Promise<void>((resolve) => {
|
|
||||||
release = resolve
|
|
||||||
})
|
|
||||||
let submitted = 0
|
|
||||||
const submit = makeSubmit({
|
|
||||||
onSubmit: () => submitted++,
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = submit.handleSubmit(event)
|
|
||||||
activeSDK = "server-b"
|
|
||||||
activeServerSync = "server-b"
|
|
||||||
activeDirectorySync = "server-b"
|
|
||||||
search.draftId = "draft-2"
|
|
||||||
release()
|
|
||||||
await result
|
|
||||||
await settle()
|
|
||||||
|
|
||||||
expect(updatedDrafts).toEqual([{ draftID: "draft-1", worktree: undefined }])
|
|
||||||
expect(promotedDrafts).toEqual([{ draftID: "draft-1", server: "project-server-a", sessionId: "session-1" }])
|
|
||||||
expect(syncedServers.every((server) => server === "server-a")).toBe(true)
|
|
||||||
expect(optimisticServers).toEqual(["server-a"])
|
|
||||||
expect(promptCaptures.at(-1)?.target).toEqual({ server: "project-server-a", scope: ServerScope.local })
|
|
||||||
expect(submitted).toBe(0)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("switches the selected agent and model before prompting", async () => {
|
test("switches the selected agent and model before prompting", async () => {
|
||||||
params = { id: "session-1" }
|
params = { id: "session-1" }
|
||||||
variant = "high"
|
variant = "high"
|
||||||
|
|
||||||
const submit = makeSubmit({
|
const submit = createPromptSubmit({
|
||||||
|
prompt,
|
||||||
info: () => ({
|
info: () => ({
|
||||||
id: "session-1",
|
id: "session-1",
|
||||||
agent: "old-agent",
|
agent: "old-agent",
|
||||||
model: { id: "old-model", providerID: "old-provider" },
|
model: { id: "old-model", providerID: "old-provider" },
|
||||||
}),
|
}),
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => false,
|
||||||
|
mode: () => "normal",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const event = { preventDefault: () => undefined } as unknown as Event
|
||||||
|
|
||||||
await submit.handleSubmit(event)
|
await submit.handleSubmit(event)
|
||||||
await Bun.sleep(0)
|
await Bun.sleep(0)
|
||||||
|
|
||||||
@@ -472,12 +519,24 @@ describe("prompt submit worktree selection", () => {
|
|||||||
commands.push({ name: "review" })
|
commands.push({ name: "review" })
|
||||||
promptValue = [{ type: "text", content: "/review staged changes", start: 0, end: 22 }]
|
promptValue = [{ type: "text", content: "/review staged changes", start: 0, end: 22 }]
|
||||||
|
|
||||||
const submit = makeSubmit({
|
const submit = createPromptSubmit({
|
||||||
|
prompt,
|
||||||
info: () => ({ id: "session-1" }),
|
info: () => ({ id: "session-1" }),
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => false,
|
||||||
|
mode: () => "normal",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
await submit.handleSubmit(event)
|
await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event)
|
||||||
await settle()
|
|
||||||
|
|
||||||
expect(sentCommands).toEqual([
|
expect(sentCommands).toEqual([
|
||||||
{
|
{
|
||||||
@@ -493,19 +552,66 @@ describe("prompt submit worktree selection", () => {
|
|||||||
expect(serverSessionSyncs).toBe(0)
|
expect(serverSessionSyncs).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sends an initial shell after synchronous workspace creation", async () => {
|
test("uses an injected model selection", async () => {
|
||||||
selected = "create"
|
params = { id: "session-1" }
|
||||||
const submit = makeSubmit({
|
const model = {
|
||||||
mode: () => "shell",
|
current: () => ({ id: "draft-model", provider: { id: "draft-provider" } }),
|
||||||
|
variant: { current: () => "draft-variant" },
|
||||||
|
} as unknown as ModelSelection
|
||||||
|
const submit = createPromptSubmit({
|
||||||
|
prompt,
|
||||||
|
info: () => ({ id: "session-1" }),
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => false,
|
||||||
|
mode: () => "normal",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
model,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event)
|
||||||
|
|
||||||
|
expect(optimistic[0]).toMatchObject({
|
||||||
|
message: {
|
||||||
|
model: { providerID: "draft-provider", modelID: "draft-model", variant: "draft-variant" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("seeds new sessions before optimistic prompts are added", async () => {
|
||||||
|
const submit = createPromptSubmit({
|
||||||
|
prompt,
|
||||||
|
info: () => undefined,
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => false,
|
||||||
|
mode: () => "normal",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
newSessionWorktree: () => selected,
|
||||||
|
onNewSessionWorktreeReset: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const event = { preventDefault: () => undefined } as unknown as Event
|
||||||
|
|
||||||
await submit.handleSubmit(event)
|
await submit.handleSubmit(event)
|
||||||
await settle()
|
|
||||||
|
|
||||||
expect(sentShellDirectories).toEqual([worktreeDirectory])
|
expect(storedSessions["/repo/worktree-a"]).toHaveLength(1)
|
||||||
expect(sentShell[0]).toMatchObject({
|
expect(storedSessions["/repo/worktree-a"]?.[0]).toMatchObject({ id: "session-1", title: "New session 1" })
|
||||||
sessionID: "session-1",
|
expect(optimisticSeeded).toEqual([true])
|
||||||
command: "ls",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { batch, startTransition, type Accessor } from "solid-js"
|
|||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
import { useServerSync, type ServerSync } from "@/context/server-sync"
|
import { useServerSync, type ServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useLayout } from "@/context/layout"
|
||||||
import { useLocal, type ModelSelection } from "@/context/local"
|
import { useLocal, type ModelSelection } from "@/context/local"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { type ContextItem, type ImageAttachmentPart, type Prompt, type usePrompt } from "@/context/prompt"
|
import { type ContextItem, type ImageAttachmentPart, type Prompt, type usePrompt } from "@/context/prompt"
|
||||||
import { useSDK, type DirectorySDK } from "@/context/sdk"
|
import { useSDK, type DirectorySDK } from "@/context/sdk"
|
||||||
import { useSync, type DirectorySync } from "@/context/sync"
|
import { useSync, type DirectorySync } from "@/context/sync"
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
|
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||||
import { getDirectory } from "@opencode-ai/core/util/path"
|
import { getDirectory } from "@opencode-ai/core/util/path"
|
||||||
import { buildRequestParts } from "./build-request-parts"
|
import { buildRequestParts } from "./build-request-parts"
|
||||||
import { setCursorPosition } from "./editor-dom"
|
import { setCursorPosition } from "./editor-dom"
|
||||||
@@ -23,7 +25,12 @@ import { createPromptSubmissionState } from "./submission-state"
|
|||||||
import { Event } from "@opencode-ai/schema/event"
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
import { blobDataUrl } from "@/utils/draft-store"
|
import { blobDataUrl } from "@/utils/draft-store"
|
||||||
|
|
||||||
const submitting = new Set<string>()
|
type PendingPrompt = {
|
||||||
|
abort: AbortController
|
||||||
|
cleanup: VoidFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = new Map<string, PendingPrompt>()
|
||||||
|
|
||||||
export type FollowupDraft = {
|
export type FollowupDraft = {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
@@ -43,6 +50,7 @@ type FollowupSendInput = {
|
|||||||
draft: FollowupDraft
|
draft: FollowupDraft
|
||||||
messageID?: string
|
messageID?: string
|
||||||
optimisticBusy?: boolean
|
optimisticBusy?: boolean
|
||||||
|
before?: () => Promise<boolean> | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ? part.content : "")).join("")
|
const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ? part.content : "")).join("")
|
||||||
@@ -62,11 +70,22 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
|||||||
input.serverSync.session.set("session_status", input.draft.sessionID, { type: "idle" })
|
input.serverSync.session.set("session_status", input.draft.sessionID, { type: "idle" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wait = async () => {
|
||||||
|
const ok = await input.before?.()
|
||||||
|
if (ok === false) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const [head, ...tail] = text.split(" ")
|
const [head, ...tail] = text.split(" ")
|
||||||
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
|
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
|
||||||
if (cmd && input.sync.data.command.find((item) => item.name === cmd)) {
|
if (cmd && input.sync.data.command.find((item) => item.name === cmd)) {
|
||||||
setBusy()
|
setBusy()
|
||||||
try {
|
try {
|
||||||
|
if (!(await wait())) {
|
||||||
|
setIdle()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const messageID = Identifier.ascending("message")
|
const messageID = Identifier.ascending("message")
|
||||||
await input.api.command({
|
await input.api.command({
|
||||||
sessionID: input.draft.sessionID,
|
sessionID: input.draft.sessionID,
|
||||||
@@ -140,6 +159,14 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!(await wait())) {
|
||||||
|
batch(() => {
|
||||||
|
setIdle()
|
||||||
|
remove()
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const session = input.session()
|
const session = input.session()
|
||||||
if (session?.agent !== input.draft.agent) {
|
if (session?.agent !== input.draft.agent) {
|
||||||
await input.api.switchAgent({ sessionID: input.draft.sessionID, agent: input.draft.agent })
|
await input.api.switchAgent({ sessionID: input.draft.sessionID, agent: input.draft.agent })
|
||||||
@@ -231,10 +258,13 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const prompt = input.prompt
|
const prompt = input.prompt
|
||||||
|
const layout = useLayout()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
|
const pendingKey = (sessionID: string) => ScopedKey.from(sdk().scope, sessionID)
|
||||||
|
|
||||||
const errorMessage = (err: unknown) => {
|
const errorMessage = (err: unknown) => {
|
||||||
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message
|
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message
|
||||||
if (err && typeof err === "object" && "data" in err) {
|
if (err && typeof err === "object" && "data" in err) {
|
||||||
@@ -248,10 +278,19 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const abort = async () => {
|
const abort = async () => {
|
||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
if (!sessionID) return Promise.resolve()
|
if (!sessionID) return Promise.resolve()
|
||||||
serverSync.session.set("todo", sessionID, [])
|
|
||||||
|
serverSync().session.set("todo", sessionID, [])
|
||||||
|
|
||||||
input.onAbort?.()
|
input.onAbort?.()
|
||||||
|
|
||||||
|
const key = pendingKey(sessionID)
|
||||||
|
const queued = pending.get(key)
|
||||||
|
if (queued) {
|
||||||
|
queued.abort.abort()
|
||||||
|
queued.cleanup()
|
||||||
|
pending.delete(key)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
return sdk()
|
return sdk()
|
||||||
.api.session.interrupt({ sessionID })
|
.api.session.interrupt({ sessionID })
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@@ -280,9 +319,9 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seed = (target: ServerSync, dir: string, info: SessionInfo) => {
|
const seed = (dir: string, info: SessionInfo) => {
|
||||||
target.session.remember(info)
|
serverSync().session.remember(info)
|
||||||
const [, setStore] = target.child(dir)
|
const [, setStore] = serverSync().child(dir)
|
||||||
setStore("session", (list: SessionInfo[]) => {
|
setStore("session", (list: SessionInfo[]) => {
|
||||||
const result = Binary.search(list, info.id, (item) => item.id)
|
const result = Binary.search(list, info.id, (item) => item.id)
|
||||||
const next = [...list]
|
const next = [...list]
|
||||||
@@ -314,6 +353,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
if (input.working()) void abort()
|
if (input.working()) void abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelSelection = input.model ?? local.model
|
const modelSelection = input.model ?? local.model
|
||||||
const currentModel = modelSelection.current()
|
const currentModel = modelSelection.current()
|
||||||
const currentAgent = local.agent.current()
|
const currentAgent = local.agent.current()
|
||||||
@@ -326,55 +366,34 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissionSDK = sdk()
|
|
||||||
const submissionSync = sync()
|
|
||||||
const submissionServerSync = serverSync
|
|
||||||
const submissionScope = submissionSDK.scope
|
|
||||||
const projectDirectory = submissionSDK.directory
|
|
||||||
const sessionID = params.id
|
|
||||||
const isNewSession = !sessionID
|
|
||||||
const currentSession = input.info()
|
|
||||||
const draftID = search.draftId
|
|
||||||
const draftServer = draftID ? tabs.draft(draftID).server : undefined
|
|
||||||
const capturePrompt = prompt.capture
|
|
||||||
const localSession = local.session
|
|
||||||
const resetWorktree = input.onNewSessionWorktreeReset
|
|
||||||
const onSubmit = input.onSubmit
|
|
||||||
const permissionState = permission
|
|
||||||
const shouldAutoAccept = isNewSession && input.autoAccept()
|
|
||||||
const worktreeSelection = input.newSessionWorktree?.() || "main"
|
|
||||||
const submissionKey = ScopedKey.from(
|
|
||||||
submissionScope,
|
|
||||||
draftID ? `draft:${draftID}` : sessionID ? `session:${sessionID}` : `directory:${projectDirectory}`,
|
|
||||||
)
|
|
||||||
if (submitting.has(submissionKey)) return
|
|
||||||
submitting.add(submissionKey)
|
|
||||||
|
|
||||||
try {
|
|
||||||
input.addToHistory(currentPrompt, mode)
|
input.addToHistory(currentPrompt, mode)
|
||||||
input.resetHistoryNavigation()
|
input.resetHistoryNavigation()
|
||||||
|
|
||||||
|
const projectDirectory = sdk().directory
|
||||||
|
const permissionState = permission.currentServerState()
|
||||||
|
const isNewSession = !params.id
|
||||||
|
const shouldAutoAccept = isNewSession && input.autoAccept()
|
||||||
|
const worktreeSelection = input.newSessionWorktree?.() || "main"
|
||||||
|
|
||||||
let sessionDirectory = projectDirectory
|
let sessionDirectory = projectDirectory
|
||||||
if (isNewSession) {
|
if (isNewSession) {
|
||||||
if (worktreeSelection === "create") {
|
if (worktreeSelection === "create") {
|
||||||
const createdWorktree = await submissionSDK.api.worktree
|
const createdWorktree = await sdk()
|
||||||
.create({
|
.api.projectCopy.create({
|
||||||
projectID: submissionSync.data.project,
|
projectID: sync().data.project,
|
||||||
strategy: "git",
|
strategy: "git_worktree",
|
||||||
directory: getDirectory(submissionSync.project?.worktree ?? projectDirectory),
|
directory: getDirectory(projectDirectory),
|
||||||
})
|
location: { directory: projectDirectory },
|
||||||
.then(async (created) => {
|
|
||||||
await submissionSDK.api.location.get({ location: { directory: created.directory } })
|
|
||||||
return created
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||||
description: errorMessage(err),
|
description: errorMessage(err),
|
||||||
})
|
})
|
||||||
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!createdWorktree) return
|
if (!createdWorktree) return
|
||||||
|
WorktreeState.pending(sdk().scope, createdWorktree.directory)
|
||||||
sessionDirectory = createdWorktree.directory
|
sessionDirectory = createdWorktree.directory
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,14 +402,16 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sessionDirectory !== projectDirectory) {
|
if (sessionDirectory !== projectDirectory) {
|
||||||
submissionServerSync.child(sessionDirectory)
|
serverSync().child(sessionDirectory)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = currentSession
|
input.onNewSessionWorktreeReset?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = input.info()
|
||||||
if (!session && isNewSession) {
|
if (!session && isNewSession) {
|
||||||
const created = await submissionSDK.api.session
|
const created = await sdk()
|
||||||
.create({
|
.api.session.create({
|
||||||
agent: currentAgent.name,
|
agent: currentAgent.name,
|
||||||
model: { id: currentModel.id, providerID: currentModel.provider.id, variant },
|
model: { id: currentModel.id, providerID: currentModel.provider.id, variant },
|
||||||
location: { directory: sessionDirectory },
|
location: { directory: sessionDirectory },
|
||||||
@@ -403,26 +424,21 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
if (created) {
|
if (created) {
|
||||||
seed(submissionServerSync, sessionDirectory, created)
|
seed(sessionDirectory, created)
|
||||||
session = created
|
session = created
|
||||||
await startTransition(() => {
|
await startTransition(() => {
|
||||||
if (!session) return
|
if (!session) return
|
||||||
if (draftID) tabs.updateDraft(draftID, { worktree: undefined })
|
|
||||||
if (!draftID) resetWorktree?.()
|
|
||||||
if (shouldAutoAccept) permissionState.enableAutoAccept(session.id, sessionDirectory)
|
if (shouldAutoAccept) permissionState.enableAutoAccept(session.id, sessionDirectory)
|
||||||
localSession.promote(sessionDirectory, session.id, {
|
local.session.promote(sessionDirectory, session.id, {
|
||||||
agent: currentAgent.name,
|
agent: currentAgent.name,
|
||||||
model: { providerID: currentModel.provider.id, modelID: currentModel.id },
|
model: { providerID: currentModel.provider.id, modelID: currentModel.id },
|
||||||
variant: variant ?? null,
|
variant: variant ?? null,
|
||||||
})
|
})
|
||||||
if (draftID && draftServer) tabs.promoteDraft(draftID, { server: draftServer, sessionId: session.id })
|
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
||||||
|
const draftID = search.draftId
|
||||||
|
if (draftID) tabs.promoteDraft(draftID, { server: tabs.draft(draftID).server, sessionId: session.id })
|
||||||
else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||||
submission.retarget(
|
submission.retarget(prompt.capture({ dir: base64Encode(sessionDirectory), id: session.id }))
|
||||||
capturePrompt(
|
|
||||||
{ dir: base64Encode(sessionDirectory), id: session.id },
|
|
||||||
{ server: draftServer, scope: submissionScope },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,13 +495,13 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!draftID || search.draftId === draftID) onSubmit?.()
|
input.onSubmit?.()
|
||||||
|
|
||||||
if (mode === "shell") {
|
if (mode === "shell") {
|
||||||
clearInput()
|
clearInput()
|
||||||
const eventID = Event.ID.create()
|
const eventID = Event.ID.create()
|
||||||
void submissionSDK.api.session
|
sdk()
|
||||||
.shell({
|
.api.session.shell({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
id: eventID,
|
id: eventID,
|
||||||
command: text,
|
command: text,
|
||||||
@@ -503,13 +519,13 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
if (text.startsWith("/")) {
|
if (text.startsWith("/")) {
|
||||||
const [cmdName, ...args] = text.split(" ")
|
const [cmdName, ...args] = text.split(" ")
|
||||||
const commandName = cmdName.slice(1)
|
const commandName = cmdName.slice(1)
|
||||||
const customCommand = submissionSync.data.command.find((c) => c.name === commandName)
|
const customCommand = sync().data.command.find((c) => c.name === commandName)
|
||||||
if (customCommand) {
|
if (customCommand) {
|
||||||
clearInput()
|
clearInput()
|
||||||
const messageID = Identifier.ascending("message")
|
const messageID = Identifier.ascending("message")
|
||||||
submissionServerSync.session.set("session_status", session.id, { type: "busy" })
|
serverSync().session.set("session_status", session.id, { type: "busy" })
|
||||||
void submissionSDK.api.session
|
sdk()
|
||||||
.command({
|
.api.session.command({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
id: messageID,
|
id: messageID,
|
||||||
command: commandName,
|
command: commandName,
|
||||||
@@ -524,7 +540,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
submissionServerSync.session.set("session_status", session.id, { type: "idle" })
|
serverSync().session.set("session_status", session.id, { type: "idle" })
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("prompt.toast.commandSendFailed.title"),
|
title: language.t("prompt.toast.commandSendFailed.title"),
|
||||||
description: formatServerError(err, language.t, language.t("common.requestFailed")),
|
description: formatServerError(err, language.t, language.t("common.requestFailed")),
|
||||||
@@ -539,7 +555,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const messageID = Identifier.ascending("message")
|
const messageID = Identifier.ascending("message")
|
||||||
|
|
||||||
const removeOptimisticMessage = () => {
|
const removeOptimisticMessage = () => {
|
||||||
submissionSync.session.optimistic.remove({
|
sync().session.optimistic.remove({
|
||||||
directory: sessionDirectory,
|
directory: sessionDirectory,
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
messageID,
|
messageID,
|
||||||
@@ -549,17 +565,77 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
for (const item of commentItems) submission.target().context.remove(item.key)
|
for (const item of commentItems) submission.target().context.remove(item.key)
|
||||||
clearInput()
|
clearInput()
|
||||||
|
|
||||||
|
const waitForWorktree = async () => {
|
||||||
|
const worktree = WorktreeState.get(sdk().scope, sessionDirectory)
|
||||||
|
if (!worktree || worktree.status !== "pending") return true
|
||||||
|
|
||||||
|
if (sessionDirectory === projectDirectory) {
|
||||||
|
sync().set("session_status", session.id, { type: "busy" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const cleanup = () => {
|
||||||
|
if (sessionDirectory === projectDirectory) {
|
||||||
|
sync().set("session_status", session.id, { type: "idle" })
|
||||||
|
}
|
||||||
|
removeOptimisticMessage()
|
||||||
|
if (restoreInput()) restoreCommentItems(submission.target(), commentItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.set(pendingKey(session.id), { abort: controller, cleanup })
|
||||||
|
|
||||||
|
const abortWait = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
resolve({ status: "failed", message: "aborted" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
resolve({ status: "failed", message: "aborted" })
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeoutMs = 5 * 60 * 1000
|
||||||
|
const timer = { id: undefined as number | undefined }
|
||||||
|
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
|
||||||
|
timer.id = window.setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
status: "failed",
|
||||||
|
message: language.t("workspace.error.stillPreparing"),
|
||||||
|
})
|
||||||
|
}, timeoutMs)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await Promise.race([
|
||||||
|
WorktreeState.wait(sdk().scope, sessionDirectory),
|
||||||
|
abortWait,
|
||||||
|
timeout,
|
||||||
|
]).finally(() => {
|
||||||
|
if (timer.id === undefined) return
|
||||||
|
clearTimeout(timer.id)
|
||||||
|
})
|
||||||
|
pending.delete(pendingKey(session.id))
|
||||||
|
if (controller.signal.aborted) return false
|
||||||
|
if (result.status === "failed") throw new Error(result.message)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
void sendFollowupDraft({
|
void sendFollowupDraft({
|
||||||
api: submissionSDK.api.session,
|
api: sdk().api.session,
|
||||||
sync: submissionSync,
|
sync: sync(),
|
||||||
serverSync: submissionServerSync,
|
serverSync: serverSync(),
|
||||||
session: () => session,
|
session: () => input.info() ?? session,
|
||||||
draft,
|
draft,
|
||||||
messageID,
|
messageID,
|
||||||
optimisticBusy: sessionDirectory === projectDirectory,
|
optimisticBusy: sessionDirectory === projectDirectory,
|
||||||
|
before: waitForWorktree,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
pending.delete(pendingKey(session.id))
|
||||||
if (sessionDirectory === projectDirectory) {
|
if (sessionDirectory === projectDirectory) {
|
||||||
submissionSync.set("session_status", session.id, { type: "idle" })
|
sync().set("session_status", session.id, { type: "idle" })
|
||||||
}
|
}
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||||
@@ -568,9 +644,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
removeOptimisticMessage()
|
removeOptimisticMessage()
|
||||||
if (restoreInput()) restoreCommentItems(submission.target(), commentItems)
|
if (restoreInput()) restoreCommentItems(submission.target(), commentItems)
|
||||||
})
|
})
|
||||||
} finally {
|
|
||||||
submitting.delete(submissionKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,52 +1,35 @@
|
|||||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-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 { Icon } from "@opencode-ai/ui/v2/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { sameDirectory } from "@/utils/workspace"
|
|
||||||
|
|
||||||
export function PromptWorkspaceSelector(props: {
|
export function PromptWorkspaceSelector(props: {
|
||||||
value: string
|
value: string
|
||||||
projectRoot: string
|
projectRoot: string
|
||||||
workspaces: string[]
|
workspaces: string[]
|
||||||
branch?: string
|
branch?: string
|
||||||
onboarding?: boolean
|
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
onDone: () => void
|
onDone: () => void
|
||||||
onViewAll: () => void
|
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [search, setSearch] = createSignal("")
|
let pending: string | undefined
|
||||||
let searchInput: HTMLInputElement | undefined
|
const selected = () => (props.value === props.projectRoot ? "main" : props.value)
|
||||||
let focusSearch = false
|
|
||||||
let pending: { type: "select"; value: string } | { type: "viewAll" } | undefined
|
|
||||||
const selected = () => (sameDirectory(props.value, props.projectRoot) ? "main" : props.value)
|
|
||||||
const workspaces = createMemo(() => {
|
|
||||||
const query = search().trim().toLowerCase()
|
|
||||||
if (!query) return props.workspaces
|
|
||||||
return props.workspaces.filter((workspace) => getFilename(workspace).toLowerCase().includes(query))
|
|
||||||
})
|
|
||||||
const icon = () => {
|
const icon = () => {
|
||||||
if (selected() === "main") return "monitor"
|
if (selected() === "main") return "monitor"
|
||||||
if (selected() === "create") return "workspace-new"
|
if (selected() === "create") return "workspace-new"
|
||||||
return "workspace-isolated"
|
return "workspace"
|
||||||
}
|
}
|
||||||
const select = (value: string) => {
|
const select = (value: string) => {
|
||||||
pending = { type: "select", value }
|
pending = value
|
||||||
}
|
}
|
||||||
const onOpenChange = (open: boolean) => {
|
const onOpenChange = (open: boolean) => {
|
||||||
if (open) {
|
if (open) return
|
||||||
setSearch("")
|
const value = pending
|
||||||
return
|
|
||||||
}
|
|
||||||
const action = pending
|
|
||||||
pending = undefined
|
pending = undefined
|
||||||
if (action?.type === "select") props.onChange(action.value)
|
if (value) props.onChange(value)
|
||||||
if (action?.type === "viewAll") {
|
|
||||||
props.onViewAll()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
props.onDone()
|
props.onDone()
|
||||||
}
|
}
|
||||||
const label = () => {
|
const label = () => {
|
||||||
@@ -58,161 +41,44 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
||||||
<TooltipV2
|
|
||||||
placement="top"
|
|
||||||
openDelay={800}
|
|
||||||
value={
|
|
||||||
props.onboarding ? (
|
|
||||||
<div class="flex flex-col gap-1 text-start">
|
|
||||||
<div class="flex items-center gap-1.5 font-[530] text-v2-text-text-base">
|
|
||||||
<Icon name="workspace-isolated" size="small" class="shrink-0 text-v2-text-text-accent" />
|
|
||||||
<span>{language.t("workspace.onboarding.title")}</span>
|
|
||||||
</div>
|
|
||||||
<span class="font-[440] text-v2-text-text-muted">{language.t("workspace.onboarding.description")}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
language.t("session.new.workspace.trigger.tooltip")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
contentClass={props.onboarding ? "max-w-[280px]" : undefined}
|
|
||||||
class="min-w-0"
|
|
||||||
>
|
|
||||||
<MenuV2 placement="bottom" gutter={4} onOpenChange={onOpenChange}>
|
<MenuV2 placement="bottom" gutter={4} onOpenChange={onOpenChange}>
|
||||||
<MenuV2.Trigger
|
<MenuV2.Trigger class="flex h-7 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed data-[expanded]:text-v2-text-text-muted">
|
||||||
aria-description={language.t("session.new.workspace.trigger.tooltip")}
|
<IconV2 name={icon()} class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
class="flex h-6 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed data-[expanded]:text-v2-text-text-muted"
|
|
||||||
>
|
|
||||||
<Icon name={icon()} class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
<span class="min-w-0 truncate">{label()}</span>
|
<span class="min-w-0 truncate">{label()}</span>
|
||||||
<Show when={props.onboarding}>
|
|
||||||
<span
|
|
||||||
data-slot="workspace-onboarding-dot"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="size-1.5 shrink-0 rounded-full bg-v2-text-text-accent"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
</MenuV2.Trigger>
|
</MenuV2.Trigger>
|
||||||
<MenuV2.Portal>
|
<MenuV2.Portal>
|
||||||
<MenuV2.Content class="w-[200px]">
|
<MenuV2.Content class="w-[180px]">
|
||||||
<MenuV2.Group>
|
<MenuV2.Group>
|
||||||
<MenuV2.GroupLabel>{language.t("session.new.workspace.runIn")}</MenuV2.GroupLabel>
|
<MenuV2.GroupLabel>{language.t("session.new.workspace.runIn")}</MenuV2.GroupLabel>
|
||||||
<MenuV2.Item onSelect={() => select("main")}>
|
<MenuV2.Item onSelect={() => select("main")}>
|
||||||
<Icon name="monitor" />
|
<IconV2 name="monitor" />
|
||||||
<TooltipV2
|
<span class="min-w-0 flex-1 truncate">{language.t("session.new.workspace.local")}</span>
|
||||||
placement="right"
|
|
||||||
openDelay={800}
|
|
||||||
value={
|
|
||||||
<span class="flex flex-col gap-0.5">
|
|
||||||
<span>{language.t("session.new.workspace.local")}</span>
|
|
||||||
<span class="font-[440] text-v2-text-text-muted">
|
|
||||||
{language.t("session.new.workspace.local.tooltip")}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
class="min-w-0 flex-1"
|
|
||||||
>
|
|
||||||
<span class="min-w-0 truncate">{language.t("session.new.workspace.local")}</span>
|
|
||||||
</TooltipV2>
|
|
||||||
<Show when={selected() === "main"}>
|
<Show when={selected() === "main"}>
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
<Icon name="check" size="small" class="shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
</MenuV2.Item>
|
</MenuV2.Item>
|
||||||
<MenuV2.Item onSelect={() => select("create")}>
|
<MenuV2.Item onSelect={() => select("create")}>
|
||||||
<Icon name="workspace-new" />
|
<IconV2 name="workspace-new" />
|
||||||
<TooltipV2
|
<span class="min-w-0 flex-1 truncate">{language.t("workspace.new")}</span>
|
||||||
placement="right"
|
|
||||||
openDelay={800}
|
|
||||||
value={
|
|
||||||
<span class="flex flex-col gap-0.5">
|
|
||||||
<span>{language.t("workspace.new")}</span>
|
|
||||||
<span class="font-[440] text-v2-text-text-muted">
|
|
||||||
{language.t("session.new.workspace.new.tooltip")}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
class="min-w-0 flex-1"
|
|
||||||
>
|
|
||||||
<span class="min-w-0 truncate">{language.t("workspace.new")}</span>
|
|
||||||
</TooltipV2>
|
|
||||||
<Show when={selected() === "create"}>
|
<Show when={selected() === "create"}>
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
<Icon name="check" size="small" class="shrink-0" />
|
||||||
</Show>
|
</Show>
|
||||||
</MenuV2.Item>
|
</MenuV2.Item>
|
||||||
</MenuV2.Group>
|
</MenuV2.Group>
|
||||||
<Show
|
<Show when={props.workspaces.length > 0}>
|
||||||
when={props.workspaces.length > 0}
|
<MenuV2.Separator />
|
||||||
fallback={
|
<MenuV2.Sub gutter={0} overlap overflowPadding={8}>
|
||||||
<>
|
<MenuV2.SubTrigger>
|
||||||
<MenuV2.Separator class="h-[0.5px]" />
|
<IconV2 name="workspace" />
|
||||||
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
|
{language.t("session.new.workspace.existing")}
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
|
||||||
</MenuV2.Item>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuV2.Separator class="h-[0.5px]" />
|
|
||||||
<MenuV2.Sub
|
|
||||||
gutter={0}
|
|
||||||
overlap
|
|
||||||
overflowPadding={8}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
focusSearch = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!focusSearch || props.workspaces.length < 10) return
|
|
||||||
focusSearch = false
|
|
||||||
requestAnimationFrame(() => searchInput?.focus())
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuV2.SubTrigger
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (
|
|
||||||
event.key === "ArrowRight" ||
|
|
||||||
event.key === "ArrowLeft" ||
|
|
||||||
event.key === "Enter" ||
|
|
||||||
event.key === " "
|
|
||||||
)
|
|
||||||
focusSearch = true
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="workspace-isolated" />
|
|
||||||
<span class="min-w-0 flex-1 truncate">
|
|
||||||
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
|
||||||
</span>
|
|
||||||
</MenuV2.SubTrigger>
|
</MenuV2.SubTrigger>
|
||||||
<MenuV2.Portal>
|
<MenuV2.Portal>
|
||||||
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
|
<MenuV2.SubContent class="max-w-[200px]">
|
||||||
<Show when={props.workspaces.length >= 10}>
|
<For each={props.workspaces}>
|
||||||
<div class="flex h-7 items-center gap-2 rounded-sm ps-3 pe-2 text-v2-icon-icon-muted">
|
|
||||||
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
|
||||||
<input
|
|
||||||
ref={(element) => {
|
|
||||||
searchInput = element
|
|
||||||
}}
|
|
||||||
value={search()}
|
|
||||||
placeholder={language.t("session.new.workspace.search.placeholder")}
|
|
||||||
aria-label={language.t("session.new.workspace.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"
|
|
||||||
onInput={(event) => setSearch(event.currentTarget.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (
|
|
||||||
event.key === "Escape" ||
|
|
||||||
event.key === "ArrowDown" ||
|
|
||||||
event.key === "ArrowUp" ||
|
|
||||||
event.key === "Enter"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
event.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<For each={workspaces()}>
|
|
||||||
{(workspace) => (
|
{(workspace) => (
|
||||||
<MenuV2.Item onSelect={() => select(workspace)}>
|
<MenuV2.Item onSelect={() => select(workspace)}>
|
||||||
<Icon name="workspace-isolated" />
|
<IconV2 name="workspace-isolated" />
|
||||||
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
||||||
<Show when={selected() === workspace}>
|
<Show when={selected() === workspace}>
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
<Icon name="check" size="small" class="shrink-0" />
|
||||||
@@ -220,10 +86,6 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
</MenuV2.Item>
|
</MenuV2.Item>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<MenuV2.Separator class="h-[0.5px]" />
|
|
||||||
<MenuV2.Item onSelect={() => (pending = { type: "viewAll" })}>
|
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
|
||||||
</MenuV2.Item>
|
|
||||||
</MenuV2.SubContent>
|
</MenuV2.SubContent>
|
||||||
</MenuV2.Portal>
|
</MenuV2.Portal>
|
||||||
</MenuV2.Sub>
|
</MenuV2.Sub>
|
||||||
@@ -231,41 +93,35 @@ export function PromptWorkspaceSelector(props: {
|
|||||||
</MenuV2.Content>
|
</MenuV2.Content>
|
||||||
</MenuV2.Portal>
|
</MenuV2.Portal>
|
||||||
</MenuV2>
|
</MenuV2>
|
||||||
</TooltipV2>
|
<PromptGitStatus branch={props.branch} />
|
||||||
<PromptGitStatus branch={props.branch} from={selected() === "create"} class="ms-1" />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PromptGitStatus(props: { branch?: string; noGit?: boolean; from?: boolean; class?: string }) {
|
export function PromptGitStatus(props: { branch?: string; noGit?: boolean }) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const label = () => {
|
const label = () => {
|
||||||
if (props.noGit) return language.t("session.new.git.none")
|
if (props.noGit) return language.t("session.new.git.none")
|
||||||
if (!props.branch) return undefined
|
|
||||||
if (props.from) return language.t("session.new.workspace.fromBranch", { branch: props.branch })
|
|
||||||
return props.branch
|
return props.branch
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = () => {
|
|
||||||
if (props.noGit) return "monitor"
|
|
||||||
if (props.from) return "branch-out"
|
|
||||||
return "branch"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={label()}>
|
<Show when={label()}>
|
||||||
{(value) => (
|
{(value) => (
|
||||||
|
<>
|
||||||
|
<span class="hidden select-none opacity-50 sm:inline mx-1">/</span>
|
||||||
<TooltipV2
|
<TooltipV2
|
||||||
placement="top"
|
placement="top"
|
||||||
value={value()}
|
value={value()}
|
||||||
class={`min-w-0 max-w-[220px] ${props.class ?? ""}`}
|
class="min-w-0 max-w-[220px]"
|
||||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||||
>
|
>
|
||||||
<div class="flex h-6 min-w-0 max-w-[220px] items-center gap-1.5 rounded-full bg-v2-background-bg-layer-02 px-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint">
|
<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={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
<Icon name="branch" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
<span class="min-w-0 truncate">{value()}</span>
|
<span class="min-w-0 truncate">{value()}</span>
|
||||||
</div>
|
</div>
|
||||||
</TooltipV2>
|
</TooltipV2>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export function createProviderConnectionController(options: {
|
|||||||
const [integration] = createResource(
|
const [integration] = createResource(
|
||||||
() => ({ provider: options.provider(), directory: options.directory() }),
|
() => ({ provider: options.provider(), directory: options.directory() }),
|
||||||
(input) =>
|
(input) =>
|
||||||
serverSDK.api.integration
|
serverSDK()
|
||||||
.get({ integrationID: input.provider, location: location() })
|
.api.integration.get({ integrationID: input.provider, location: location() })
|
||||||
.then((result) => result.data),
|
.then((result) => result.data),
|
||||||
)
|
)
|
||||||
const methods = createMemo<ProviderConnectMethod[]>(() => {
|
const methods = createMemo<ProviderConnectMethod[]>(() => {
|
||||||
@@ -120,14 +120,14 @@ export function createProviderConnectionController(options: {
|
|||||||
cancelPolling()
|
cancelPolling()
|
||||||
const directory = options.directory()
|
const directory = options.directory()
|
||||||
await queryClient
|
await queryClient
|
||||||
.refetchQueries(serverSync.queryOptions.providers(directory ? pathKey(directory) : null))
|
.refetchQueries(serverSync().queryOptions.providers(directory ? pathKey(directory) : null))
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (polling.disposed) return
|
if (polling.disposed) return
|
||||||
options.onComplete()
|
options.onComplete()
|
||||||
}
|
}
|
||||||
const poll = async (authorization: Authorization, generation: number) => {
|
const poll = async (authorization: Authorization, generation: number) => {
|
||||||
const result = await serverSDK.api.integration.oauth
|
const result = await serverSDK()
|
||||||
.status({
|
.api.integration.oauth.status({
|
||||||
integrationID: options.provider(),
|
integrationID: options.provider(),
|
||||||
attemptID: authorization.attemptID,
|
attemptID: authorization.attemptID,
|
||||||
location: location(),
|
location: location(),
|
||||||
@@ -175,8 +175,8 @@ export function createProviderConnectionController(options: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
dispatch({ type: "auth.pending" })
|
dispatch({ type: "auth.pending" })
|
||||||
const result = await serverSDK.api.integration.oauth
|
const result = await serverSDK()
|
||||||
.connect({
|
.api.integration.oauth.connect({
|
||||||
integrationID: options.provider(),
|
integrationID: options.provider(),
|
||||||
methodID: selected.id,
|
methodID: selected.id,
|
||||||
...(answer ? { answer } : {}),
|
...(answer ? { answer } : {}),
|
||||||
@@ -197,7 +197,7 @@ export function createProviderConnectionController(options: {
|
|||||||
dispatch({ type: "method.reset" })
|
dispatch({ type: "method.reset" })
|
||||||
}
|
}
|
||||||
const connectKey = async (key: string) => {
|
const connectKey = async (key: string) => {
|
||||||
await serverSDK.api.integration.connect.key({
|
await serverSDK().api.integration.connect.key({
|
||||||
integrationID: options.provider(),
|
integrationID: options.provider(),
|
||||||
location: location(),
|
location: location(),
|
||||||
key,
|
key,
|
||||||
@@ -208,8 +208,8 @@ export function createProviderConnectionController(options: {
|
|||||||
const completeCode = async (code: string) => {
|
const completeCode = async (code: string) => {
|
||||||
const authorization = store.authorization
|
const authorization = store.authorization
|
||||||
if (!authorization) return language.t("provider.connect.oauth.code.invalid")
|
if (!authorization) return language.t("provider.connect.oauth.code.invalid")
|
||||||
const result = await serverSDK.api.integration.oauth
|
const result = await serverSDK()
|
||||||
.complete({
|
.api.integration.oauth.complete({
|
||||||
integrationID: options.provider(),
|
integrationID: options.provider(),
|
||||||
attemptID: authorization.attemptID,
|
attemptID: authorization.attemptID,
|
||||||
location: location(),
|
location: location(),
|
||||||
@@ -238,7 +238,6 @@ export function createProviderConnectionController(options: {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
loading: () => integration.loading,
|
loading: () => integration.loading,
|
||||||
integration: () => integration.latest,
|
|
||||||
methods,
|
methods,
|
||||||
currentMethod,
|
currentMethod,
|
||||||
methodIndex: () => store.methodIndex,
|
methodIndex: () => store.methodIndex,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createMemo, createResource } from "solid-js"
|
|||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { ServerConnection, useServers } from "@/context/servers"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
import { type ServerHealth } from "@/utils/server-health"
|
import { type ServerHealth } from "@/utils/server-health"
|
||||||
@@ -49,7 +49,7 @@ function useDefaultServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useServerActionsController() {
|
export function useServerActionsController() {
|
||||||
const server = useServers()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -72,16 +72,28 @@ export function useServerActionsController() {
|
|||||||
export type ServerActionsController = ReturnType<typeof useServerActionsController>
|
export type ServerActionsController = ReturnType<typeof useServerActionsController>
|
||||||
|
|
||||||
export function useServerCollectionController() {
|
export function useServerCollectionController() {
|
||||||
const server = useServers()
|
const server = useServer()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const actions = useServerActionsController()
|
const actions = useServerActionsController()
|
||||||
|
|
||||||
const items = createMemo(() => server.list)
|
const items = createMemo(() => {
|
||||||
|
const current = server.current
|
||||||
|
const list = server.list
|
||||||
|
if (!current) return list
|
||||||
|
if (!list.includes(current)) return [current, ...list]
|
||||||
|
return [current, ...list.filter((item) => item !== current)]
|
||||||
|
})
|
||||||
|
const current = createMemo<ServerConnection.Any | undefined>(() =>
|
||||||
|
settings.general.newLayoutDesigns()
|
||||||
|
? undefined
|
||||||
|
: (items().find((item) => ServerConnection.key(item) === server.key) ?? items()[0]),
|
||||||
|
)
|
||||||
const sorted = createMemo(() => {
|
const sorted = createMemo(() => {
|
||||||
const raw = items()
|
const raw = items()
|
||||||
const list = raw
|
const list = raw
|
||||||
if (!list.length) return list
|
if (!list.length) return list
|
||||||
|
const active = current()
|
||||||
const order = new Map(list.map((item, index) => [item, index] as const))
|
const order = new Map(list.map((item, index) => [item, index] as const))
|
||||||
const rank = (value?: ServerHealth) => {
|
const rank = (value?: ServerHealth) => {
|
||||||
if (value?.healthy === true) return 0
|
if (value?.healthy === true) return 0
|
||||||
@@ -89,6 +101,8 @@ export function useServerCollectionController() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return list.slice().sort((a, b) => {
|
return list.slice().sort((a, b) => {
|
||||||
|
if (a === active) return -1
|
||||||
|
if (b === active) return 1
|
||||||
const diff =
|
const diff =
|
||||||
rank(global.servers.health[ServerConnection.key(a)]) - rank(global.servers.health[ServerConnection.key(b)])
|
rank(global.servers.health[ServerConnection.key(a)]) - rank(global.servers.health[ServerConnection.key(b)])
|
||||||
if (diff !== 0) return diff
|
if (diff !== 0) return diff
|
||||||
@@ -99,6 +113,7 @@ export function useServerCollectionController() {
|
|||||||
return {
|
return {
|
||||||
collection: {
|
collection: {
|
||||||
items: sorted,
|
items: sorted,
|
||||||
|
current,
|
||||||
health: () => global.servers.health,
|
health: () => global.servers.health,
|
||||||
},
|
},
|
||||||
...actions,
|
...actions,
|
||||||
@@ -106,3 +121,21 @@ export function useServerCollectionController() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ServerCollectionController = ReturnType<typeof useServerCollectionController>
|
export type ServerCollectionController = ReturnType<typeof useServerCollectionController>
|
||||||
|
|
||||||
|
export function useServerDomainController(options: { onSelect?: () => void } = {}) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const server = useServer()
|
||||||
|
const global = useGlobal()
|
||||||
|
const collection = useServerCollectionController()
|
||||||
|
|
||||||
|
const select = async (connection: ServerConnection.Any) => {
|
||||||
|
if (global.servers.health[ServerConnection.key(connection)]?.healthy === false) return
|
||||||
|
options.onSelect?.()
|
||||||
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(ServerConnection.key(connection)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...collection, selection: { select } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerDomainController = ReturnType<typeof useServerDomainController>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { createServerHealthPreview, replaceServerConnection, type ServerFormValues } from "./server-management"
|
import { createServerHealthPreview, replaceServerConnection, type ServerFormValues } from "./server-management"
|
||||||
|
|
||||||
function deferred<T>() {
|
function deferred<T>() {
|
||||||
@@ -65,17 +65,20 @@ describe("replaceServerConnection", () => {
|
|||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
|
|
||||||
replaceServerConnection(ServerConnection.key(original), next, {
|
replaceServerConnection(ServerConnection.key(original), next, {
|
||||||
|
active: () => ServerConnection.key(original),
|
||||||
removeTabs: (key) => calls.push(`tabs:${key}`),
|
removeTabs: (key) => calls.push(`tabs:${key}`),
|
||||||
add: (server) => {
|
add: (server) => {
|
||||||
calls.push(`add:${ServerConnection.key(server)}`)
|
calls.push(`add:${ServerConnection.key(server)}`)
|
||||||
return server
|
return server
|
||||||
},
|
},
|
||||||
|
setActive: (key) => calls.push(`active:${key}`),
|
||||||
remove: (key) => calls.push(`remove:${key}`),
|
remove: (key) => calls.push(`remove:${key}`),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
"tabs:https://old.example.com",
|
"tabs:https://old.example.com",
|
||||||
"add:https://new.example.com",
|
"add:https://new.example.com",
|
||||||
|
"active:https://new.example.com",
|
||||||
"remove:https://old.example.com",
|
"remove:https://old.example.com",
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -84,8 +87,10 @@ describe("replaceServerConnection", () => {
|
|||||||
const removed: ServerConnection.Key[] = []
|
const removed: ServerConnection.Key[] = []
|
||||||
|
|
||||||
replaceServerConnection(ServerConnection.key(original), next, {
|
replaceServerConnection(ServerConnection.key(original), next, {
|
||||||
|
active: () => ServerConnection.key(original),
|
||||||
removeTabs: () => {},
|
removeTabs: () => {},
|
||||||
add: () => undefined,
|
add: () => undefined,
|
||||||
|
setActive: () => {},
|
||||||
remove: (key) => removed.push(key),
|
remove: (key) => removed.push(key),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { normalizeServerUrl, ServerConnection } from "@/context/servers"
|
import { normalizeServerUrl, ServerConnection } from "@/context/server"
|
||||||
import type { ServerHealth } from "@/utils/server-health"
|
import type { ServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
export type ServerFormValues = {
|
export type ServerFormValues = {
|
||||||
@@ -42,13 +42,18 @@ export function replaceServerConnection(
|
|||||||
originalKey: ServerConnection.Key,
|
originalKey: ServerConnection.Key,
|
||||||
next: ServerConnection.Http,
|
next: ServerConnection.Http,
|
||||||
operations: {
|
operations: {
|
||||||
|
active: () => ServerConnection.Key | undefined
|
||||||
removeTabs: (key: ServerConnection.Key) => void
|
removeTabs: (key: ServerConnection.Key) => void
|
||||||
add: (server: ServerConnection.Http) => ServerConnection.Any | undefined
|
add: (server: ServerConnection.Http) => ServerConnection.Any | undefined
|
||||||
|
setActive: (key: ServerConnection.Key) => void
|
||||||
remove: (key: ServerConnection.Key) => void
|
remove: (key: ServerConnection.Key) => void
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
const active = operations.active()
|
||||||
operations.removeTabs(originalKey)
|
operations.removeTabs(originalKey)
|
||||||
const added = operations.add(next)
|
const added = operations.add(next)
|
||||||
if (!added) return
|
if (!added) return
|
||||||
|
const nextActive = active === originalKey ? ServerConnection.key(added) : active
|
||||||
|
if (nextActive) operations.setActive(nextActive)
|
||||||
operations.remove(originalKey)
|
operations.remove(originalKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|||||||
import { type Component, Show } from "solid-js"
|
import { type Component, Show } from "solid-js"
|
||||||
import type { ServerActionsController } from "@/components/server/server-management-controller"
|
import type { ServerActionsController } from "@/components/server/server-management-controller"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
|
|
||||||
export const ServerRowMenu: Component<{
|
export const ServerRowMenu: Component<{
|
||||||
server: ServerConnection.Any
|
server: ServerConnection.Any
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Show,
|
Show,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { type ServerConnection, serverName } from "@/context/servers"
|
import { type ServerConnection, serverName } from "@/context/server"
|
||||||
import type { ServerHealth } from "@/utils/server-health"
|
import type { ServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
interface ServerRowProps extends ParentProps {
|
interface ServerRowProps extends ParentProps {
|
||||||
@@ -96,16 +96,13 @@ export function ServerRow(props: ServerRowProps) {
|
|||||||
{(conn) => (
|
{(conn) => (
|
||||||
<div class="flex flex-row gap-3">
|
<div class="flex flex-row gap-3">
|
||||||
<span>
|
<span>
|
||||||
<Show
|
{conn().http.username ? (
|
||||||
when={conn().http.username}
|
|
||||||
fallback={<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>}
|
|
||||||
>
|
|
||||||
<span class="text-text-weak">{conn().http.username}</span>
|
<span class="text-text-weak">{conn().http.username}</span>
|
||||||
</Show>
|
) : (
|
||||||
|
<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<Show when={conn().http.password}>
|
{conn().http.password && <span class="text-text-weak">••••••••</span>}
|
||||||
<span class="text-text-weak">••••••••</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ function openSessionContext(args: {
|
|||||||
}) {
|
}) {
|
||||||
args.view.reviewPanel.open(args.view.reviewPanel.opened() ? "other" : "context-button")
|
args.view.reviewPanel.open(args.view.reviewPanel.opened() ? "other" : "context-button")
|
||||||
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
||||||
void args.tabs.open("context").then(() => args.tabs.setActive("context"))
|
void args.tabs.open("context")
|
||||||
|
args.tabs.setActive("context")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionContextUsage(props: SessionContextUsageProps) {
|
export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { createSignal, For, Show, type ComponentProps, type JSX } from "solid-js"
|
|
||||||
import type { Project } from "@/types"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { useServerSync } from "@/context/server-sync"
|
|
||||||
import { useSettingsDialog } from "@/components/settings-dialog"
|
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
import { showToast } from "@/utils/toast"
|
|
||||||
import { containsDirectory, sameDirectory, workspaceDirectories } from "@/utils/workspace"
|
|
||||||
|
|
||||||
export function SessionWorkspaceMenu(props: {
|
|
||||||
eligible?: boolean
|
|
||||||
sessionID: string
|
|
||||||
project: Project
|
|
||||||
directory: string
|
|
||||||
placement?: ComponentProps<typeof MenuV2>["placement"]
|
|
||||||
gutter?: number
|
|
||||||
class?: string
|
|
||||||
contentClass?: string
|
|
||||||
children: JSX.Element
|
|
||||||
onOpenChange?: (open: boolean) => void
|
|
||||||
}) {
|
|
||||||
const language = useLanguage()
|
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const serverSync = useServerSync()
|
|
||||||
const openWorkspaces = useSettingsDialog("workspaces")
|
|
||||||
const [store, setStore] = createStore({ selected: undefined as string | undefined })
|
|
||||||
const [directories, setDirectories] = createSignal(workspaceDirectories(props.project))
|
|
||||||
const blocked = () => props.eligible === false || serverSync.session.data.session_working(props.sessionID)
|
|
||||||
const currentWorkspace = () => directories().find((workspace) => containsDirectory(workspace, props.directory))
|
|
||||||
const workspaces = () =>
|
|
||||||
directories().filter((workspace) => pathKey(workspace) !== pathKey(currentWorkspace() ?? props.directory))
|
|
||||||
const onOpenChange = (open: boolean) => {
|
|
||||||
props.onOpenChange?.(open)
|
|
||||||
if (!open) return
|
|
||||||
const sdk = serverSDK
|
|
||||||
void sdk.api.worktree
|
|
||||||
.refresh({ projectID: props.project.id })
|
|
||||||
.then(() => sdk.api.worktree.list({ projectID: props.project.id }))
|
|
||||||
.then((items) =>
|
|
||||||
setDirectories(
|
|
||||||
items.map((item) => item.directory).filter((directory) => !sameDirectory(props.project.worktree, directory)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.catch(() => undefined)
|
|
||||||
}
|
|
||||||
const move = async (selection: "create" | string) => {
|
|
||||||
if (store.selected || blocked()) return
|
|
||||||
const sdk = serverSDK
|
|
||||||
const sessionID = props.sessionID
|
|
||||||
setStore("selected", selection)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const destination = selection === "create" ? await createWorkspace(props.project, sdk) : selection
|
|
||||||
if (!destination) return
|
|
||||||
|
|
||||||
await sdk.api.session.move({ sessionID, directory: destination })
|
|
||||||
} catch (error) {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("workspace.move.failed"),
|
|
||||||
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setStore("selected", undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuV2
|
|
||||||
placement={props.placement ?? "bottom-end"}
|
|
||||||
gutter={props.gutter ?? 4}
|
|
||||||
modal={false}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
>
|
|
||||||
<MenuV2.Trigger class={props.class} disabled={blocked()}>
|
|
||||||
{props.children}
|
|
||||||
</MenuV2.Trigger>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content class={`w-[200px] ${props.contentClass ?? ""}`}>
|
|
||||||
<MenuV2.Group>
|
|
||||||
<MenuV2.GroupLabel>{language.t("workspace.move.menu.title")}</MenuV2.GroupLabel>
|
|
||||||
<Show when={pathKey(props.directory) !== pathKey(props.project.worktree)}>
|
|
||||||
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move(props.project.worktree)}>
|
|
||||||
<Icon name="monitor" />
|
|
||||||
{language.t("session.new.workspace.local")}
|
|
||||||
</MenuV2.Item>
|
|
||||||
</Show>
|
|
||||||
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move("create")}>
|
|
||||||
<Icon name="workspace-new" />
|
|
||||||
{language.t("workspace.new")}
|
|
||||||
</MenuV2.Item>
|
|
||||||
<Show when={workspaces().length > 0}>
|
|
||||||
<MenuV2.Sub gutter={0} overlap overflowPadding={8}>
|
|
||||||
<MenuV2.SubTrigger>
|
|
||||||
<Icon name="workspace-isolated" />
|
|
||||||
{language.t("session.new.workspace.existing").replace(/(…|\.{3})$/, "")}
|
|
||||||
</MenuV2.SubTrigger>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.SubContent class="max-h-[calc(100dvh-16px)] w-[200px] overflow-y-auto">
|
|
||||||
<For each={workspaces()}>
|
|
||||||
{(workspace) => (
|
|
||||||
<MenuV2.Item disabled={!!store.selected || blocked()} onSelect={() => void move(workspace)}>
|
|
||||||
<Icon name="workspace-isolated" />
|
|
||||||
<span class="min-w-0 flex-1 truncate">{getFilename(workspace)}</span>
|
|
||||||
</MenuV2.Item>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</MenuV2.SubContent>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2.Sub>
|
|
||||||
</Show>
|
|
||||||
</MenuV2.Group>
|
|
||||||
<MenuV2.Separator class="h-[0.5px] bg-v2-border-border-base" />
|
|
||||||
<MenuV2.Item onSelect={() => openWorkspaces()}>
|
|
||||||
<span class="min-w-0 flex-1 truncate">{language.t("common.viewAll")}</span>
|
|
||||||
</MenuV2.Item>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createWorkspace(project: Project, serverSDK: ReturnType<typeof useServerSDK>) {
|
|
||||||
const created = await serverSDK.api.worktree.create({
|
|
||||||
projectID: project.id,
|
|
||||||
strategy: "git",
|
|
||||||
directory: getDirectory(project.worktree),
|
|
||||||
})
|
|
||||||
await serverSDK.api.location.get({ location: { directory: created.directory } })
|
|
||||||
return created.directory
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,9 @@ import { createEffect, createMemo } 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 { useServer } from "@/context/server"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { useServer } from "@/context/server"
|
|
||||||
|
|
||||||
export const OPEN_APPS = [
|
export const OPEN_APPS = [
|
||||||
"vscode",
|
"vscode",
|
||||||
@@ -169,7 +169,7 @@ export function useOpenInApp(input: { directory: () => string }) {
|
|||||||
app: undefined as OpenApp | undefined,
|
app: undefined as OpenApp | undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal)
|
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||||
const current = createMemo(
|
const current = createMemo(
|
||||||
() =>
|
() =>
|
||||||
options().find((o) => o.id === prefs.app) ??
|
options().find((o) => o.id === prefs.app) ??
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
||||||
import { createMemo, Show } from "solid-js"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
|
import { showToast } from "@/utils/toast"
|
||||||
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { getFilename } from "@opencode-ai/core/util/path"
|
||||||
|
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useServer } from "@/context/server"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { useTerminal } from "@/context/terminal"
|
||||||
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { StatusPopoverV2 } from "../status-popover"
|
import { messageAgentColor } from "@/utils/agent"
|
||||||
|
import { decode64 } from "@/utils/base64"
|
||||||
|
import { fileManagerApp } from "@/utils/file-manager"
|
||||||
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
import { StatusPopover, StatusPopoverV2 } from "../status-popover"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
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 { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
@@ -14,15 +34,206 @@ import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
|||||||
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
|
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
|
||||||
import { useTitlebarRightMount } from "../titlebar"
|
import { useTitlebarRightMount } from "../titlebar"
|
||||||
|
|
||||||
|
const OPEN_APPS = [
|
||||||
|
"vscode",
|
||||||
|
"cursor",
|
||||||
|
"zed",
|
||||||
|
"textmate",
|
||||||
|
"antigravity",
|
||||||
|
"finder",
|
||||||
|
"terminal",
|
||||||
|
"iterm2",
|
||||||
|
"ghostty",
|
||||||
|
"warp",
|
||||||
|
"xcode",
|
||||||
|
"android-studio",
|
||||||
|
"powershell",
|
||||||
|
"sublime-text",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type OpenApp = (typeof OPEN_APPS)[number]
|
||||||
|
type OS = "macos" | "windows" | "linux" | "unknown"
|
||||||
|
|
||||||
|
const MAC_APPS = [
|
||||||
|
{
|
||||||
|
id: "vscode",
|
||||||
|
label: "session.header.open.app.vscode",
|
||||||
|
icon: "vscode",
|
||||||
|
openWith: "Visual Studio Code",
|
||||||
|
},
|
||||||
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" },
|
||||||
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" },
|
||||||
|
{ id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" },
|
||||||
|
{
|
||||||
|
id: "antigravity",
|
||||||
|
label: "session.header.open.app.antigravity",
|
||||||
|
icon: "antigravity",
|
||||||
|
openWith: "Antigravity",
|
||||||
|
},
|
||||||
|
{ id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" },
|
||||||
|
{ id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" },
|
||||||
|
{ id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" },
|
||||||
|
{ id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" },
|
||||||
|
{ id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" },
|
||||||
|
{
|
||||||
|
id: "android-studio",
|
||||||
|
label: "session.header.open.app.androidStudio",
|
||||||
|
icon: "android-studio",
|
||||||
|
openWith: "Android Studio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sublime-text",
|
||||||
|
label: "session.header.open.app.sublimeText",
|
||||||
|
icon: "sublime-text",
|
||||||
|
openWith: "Sublime Text",
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const WINDOWS_APPS = [
|
||||||
|
{ id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
|
||||||
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
|
||||||
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
||||||
|
{
|
||||||
|
id: "powershell",
|
||||||
|
label: "session.header.open.app.powershell",
|
||||||
|
icon: "powershell",
|
||||||
|
openWith: "powershell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sublime-text",
|
||||||
|
label: "session.header.open.app.sublimeText",
|
||||||
|
icon: "sublime-text",
|
||||||
|
openWith: "Sublime Text",
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const LINUX_APPS = [
|
||||||
|
{ id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
|
||||||
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
|
||||||
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
||||||
|
{
|
||||||
|
id: "sublime-text",
|
||||||
|
label: "session.header.open.app.sublimeText",
|
||||||
|
icon: "sublime-text",
|
||||||
|
openWith: "Sublime Text",
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const detectOS = (platform: ReturnType<typeof usePlatform>): OS => {
|
||||||
|
if (platform.platform === "desktop" && platform.os) return platform.os
|
||||||
|
if (typeof navigator !== "object") return "unknown"
|
||||||
|
const value = navigator.platform || navigator.userAgent
|
||||||
|
if (/Mac/i.test(value)) return "macos"
|
||||||
|
if (/Win/i.test(value)) return "windows"
|
||||||
|
if (/Linux/i.test(value)) return "linux"
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown) => {
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("common.requestFailed"),
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function SessionHeader() {
|
export function SessionHeader() {
|
||||||
|
const layout = useLayout()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
const server = useServer()
|
||||||
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const { view } = useSessionLayout()
|
const sync = useSync()
|
||||||
|
const terminal = useTerminal()
|
||||||
|
const { params, view } = useSessionLayout()
|
||||||
|
|
||||||
|
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||||
|
const project = createMemo(() => {
|
||||||
|
const directory = projectDirectory()
|
||||||
|
if (!directory) return
|
||||||
|
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
|
||||||
|
})
|
||||||
|
const name = createMemo(() => {
|
||||||
|
const current = project()
|
||||||
|
if (current) return current.name || getFilename(current.worktree)
|
||||||
|
return getFilename(projectDirectory())
|
||||||
|
})
|
||||||
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||||
|
const os = createMemo(() => detectOS(platform))
|
||||||
|
const isV2 = settings.general.newLayoutDesigns
|
||||||
|
const search = settings.visibility.search
|
||||||
const status = settings.visibility.status
|
const status = settings.visibility.status
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
|
|
||||||
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||||
|
finder: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apps = createMemo(() => {
|
||||||
|
if (os() === "macos") return MAC_APPS
|
||||||
|
if (os() === "windows") return WINDOWS_APPS
|
||||||
|
return LINUX_APPS
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileManager = createMemo(() => fileManagerApp(os()))
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (platform.platform !== "desktop") return
|
||||||
|
if (!platform.checkAppExists) return
|
||||||
|
|
||||||
|
const list = apps()
|
||||||
|
|
||||||
|
setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial<Record<OpenApp, boolean>>)
|
||||||
|
|
||||||
|
void Promise.all(
|
||||||
|
list.map((app) =>
|
||||||
|
Promise.resolve(platform.checkAppExists?.(app.openWith))
|
||||||
|
.then((value) => Boolean(value))
|
||||||
|
.catch(() => false)
|
||||||
|
.then((ok) => [app.id, ok] as const),
|
||||||
|
),
|
||||||
|
).then((entries) => {
|
||||||
|
setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = createMemo(() => {
|
||||||
|
return [
|
||||||
|
{ id: "finder", label: language.t(fileManager().label), icon: fileManager().icon },
|
||||||
|
...apps()
|
||||||
|
.filter((app) => exists[app.id])
|
||||||
|
.map((app) => ({ ...app, label: language.t(app.label) })),
|
||||||
|
] as const
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleTerminal = () => {
|
||||||
|
const next = !view().terminal.opened()
|
||||||
|
view().terminal.toggle()
|
||||||
|
if (!next) return
|
||||||
|
|
||||||
|
const id = terminal.active()
|
||||||
|
if (!id) return
|
||||||
|
focusTerminalById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||||
|
const [menu, setMenu] = createStore({ open: false })
|
||||||
|
const [openRequest, setOpenRequest] = createStore({
|
||||||
|
app: undefined as OpenApp | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||||
|
const current = createMemo(
|
||||||
|
() =>
|
||||||
|
options().find((o) => o.id === prefs.app) ??
|
||||||
|
options()[0] ??
|
||||||
|
({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const),
|
||||||
|
)
|
||||||
|
const opening = createMemo(() => openRequest.app !== undefined)
|
||||||
|
const tint = createMemo(() =>
|
||||||
|
messageAgentColor(params.id ? sync().data.message[params.id] : undefined, sync().data.agent),
|
||||||
|
)
|
||||||
const v2ActionsState = createMemo<SessionHeaderV2ActionsState>(() => ({
|
const v2ActionsState = createMemo<SessionHeaderV2ActionsState>(() => ({
|
||||||
statusVisible: status(),
|
statusVisible: status(),
|
||||||
statusLabel: language.t("status.popover.trigger"),
|
statusLabel: language.t("status.popover.trigger"),
|
||||||
@@ -33,16 +244,275 @@ export function SessionHeader() {
|
|||||||
onReviewToggle: () => view().reviewPanel.toggle(),
|
onReviewToggle: () => view().reviewPanel.toggle(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const selectApp = (app: OpenApp) => {
|
||||||
|
if (!options().some((item) => item.id === app)) return
|
||||||
|
setPrefs("app", app)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDir = (app: OpenApp) => {
|
||||||
|
if (opening() || !canOpen() || !platform.openPath) return
|
||||||
|
const directory = projectDirectory()
|
||||||
|
if (!directory) return
|
||||||
|
|
||||||
|
const item = options().find((o) => o.id === app)
|
||||||
|
const openWith = item && "openWith" in item ? item.openWith : undefined
|
||||||
|
setOpenRequest("app", app)
|
||||||
|
platform
|
||||||
|
.openPath(directory, openWith)
|
||||||
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
|
.finally(() => {
|
||||||
|
setOpenRequest("app", undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyPath = () => {
|
||||||
|
const directory = projectDirectory()
|
||||||
|
if (!directory) return
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(directory)
|
||||||
|
.then(() => {
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("session.share.copy.copied"),
|
||||||
|
description: directory,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
||||||
const rightMount = useTitlebarRightMount()
|
const rightMount = useTitlebarRightMount()
|
||||||
|
onMount(() => {
|
||||||
|
setCenterMount(document.getElementById("opencode-titlebar-center"))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={rightMount()} keyed>
|
<>
|
||||||
|
<Show when={search() && centerMount()} keyed>
|
||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount}>
|
<Portal mount={mount}>
|
||||||
<SessionHeaderV2Actions state={v2ActionsState()} />
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
class="hidden md:flex w-[240px] max-w-full min-w-0 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel shadow-none cursor-default"
|
||||||
|
onClick={() => command.trigger("file.open")}
|
||||||
|
aria-label={language.t("session.header.searchFiles")}
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 flex-1 items-center overflow-visible">
|
||||||
|
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
|
||||||
|
{language.t("session.header.search.placeholder", {
|
||||||
|
project: name(),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={hotkey()} keyed>
|
||||||
|
{(keybind) => (
|
||||||
|
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0 text-text-weaker">
|
||||||
|
{keybind}
|
||||||
|
</Keybind>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</Button>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={rightMount()} keyed>
|
||||||
|
{(mount) => (
|
||||||
|
<Portal mount={mount}>
|
||||||
|
<Show
|
||||||
|
when={isV2}
|
||||||
|
fallback={
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Show when={projectDirectory()}>
|
||||||
|
<div class="hidden xl:flex items-center">
|
||||||
|
<Show
|
||||||
|
when={canOpen()}
|
||||||
|
fallback={
|
||||||
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
|
||||||
|
onClick={copyPath}
|
||||||
|
aria-label={language.t("session.header.open.copyPath")}
|
||||||
|
>
|
||||||
|
<Icon name="copy" size="small" class="text-icon-base" />
|
||||||
|
<span class="text-12-regular text-text-strong">
|
||||||
|
{language.t("session.header.open.copyPath")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
|
||||||
|
classList={{
|
||||||
|
"bg-surface-raised-base-active": opening(),
|
||||||
|
}}
|
||||||
|
onClick={() => openDir(current().id)}
|
||||||
|
disabled={opening()}
|
||||||
|
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||||
|
>
|
||||||
|
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
||||||
|
<Show when={opening()} fallback={<AppIcon id={current().icon} />}>
|
||||||
|
<Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu
|
||||||
|
gutter={4}
|
||||||
|
placement="bottom-end"
|
||||||
|
open={menu.open}
|
||||||
|
onOpenChange={(open) => setMenu("open", open)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="chevron-down"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={opening()}
|
||||||
|
class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
|
||||||
|
classList={{
|
||||||
|
"bg-surface-raised-base-active": opening(),
|
||||||
|
}}
|
||||||
|
aria-label={language.t("session.header.open.menu")}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1">
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.GroupLabel class="!px-1 !py-1">
|
||||||
|
{language.t("session.header.openIn")}
|
||||||
|
</DropdownMenu.GroupLabel>
|
||||||
|
<DropdownMenu.RadioGroup
|
||||||
|
class="mt-1"
|
||||||
|
value={current().id}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||||
|
selectApp(value as OpenApp)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={options()}>
|
||||||
|
{(o) => (
|
||||||
|
<DropdownMenu.RadioItem
|
||||||
|
value={o.id}
|
||||||
|
disabled={opening()}
|
||||||
|
onSelect={() => {
|
||||||
|
setMenu("open", false)
|
||||||
|
openDir(o.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
||||||
|
<AppIcon id={o.icon} />
|
||||||
|
</div>
|
||||||
|
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||||
|
<DropdownMenu.ItemIndicator>
|
||||||
|
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||||
|
</DropdownMenu.ItemIndicator>
|
||||||
|
</DropdownMenu.RadioItem>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</DropdownMenu.RadioGroup>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setMenu("open", false)
|
||||||
|
copyPath()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||||
|
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||||
|
</div>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("session.header.open.copyPath")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Show when={status()}>
|
||||||
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
|
<StatusPopover />
|
||||||
|
</Tooltip>
|
||||||
|
</Show>
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("command.terminal.toggle")}
|
||||||
|
keybind={command.keybind("terminal.toggle")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
||||||
|
onClick={toggleTerminal}
|
||||||
|
aria-label={language.t("command.terminal.toggle")}
|
||||||
|
aria-expanded={view().terminal.opened()}
|
||||||
|
aria-controls="terminal-panel"
|
||||||
|
>
|
||||||
|
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
|
||||||
|
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("command.review.toggle")}
|
||||||
|
keybind={command.keybind("review.toggle")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/review-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
onClick={() => view().reviewPanel.toggle()}
|
||||||
|
aria-label={language.t("command.review.toggle")}
|
||||||
|
aria-expanded={view().reviewPanel.opened()}
|
||||||
|
aria-controls="review-panel"
|
||||||
|
>
|
||||||
|
<Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
|
||||||
|
<TooltipKeybind
|
||||||
|
title={language.t("command.fileTree.toggle")}
|
||||||
|
keybind={command.keybind("fileTree.toggle")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
onClick={() => layout.fileTree.toggle()}
|
||||||
|
aria-label={language.t("command.fileTree.toggle")}
|
||||||
|
aria-expanded={layout.fileTree.opened()}
|
||||||
|
aria-controls="file-tree-panel"
|
||||||
|
>
|
||||||
|
<div class="relative flex items-center justify-center size-4">
|
||||||
|
<Icon
|
||||||
|
size="small"
|
||||||
|
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
||||||
|
classList={{
|
||||||
|
"text-icon-strong": layout.fileTree.opened(),
|
||||||
|
"text-icon-weak": !layout.fileTree.opened(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SessionHeaderV2Actions state={v2ActionsState()} />
|
||||||
|
</Show>
|
||||||
|
</Portal>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +527,8 @@ type SessionHeaderV2ActionsState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Show when={props.state.statusVisible}>
|
<Show when={props.state.statusVisible}>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { FileVisual } from "./session-sortable-tab"
|
|||||||
|
|
||||||
export function SortableTabV2(props: {
|
export function SortableTabV2(props: {
|
||||||
tab: string
|
tab: string
|
||||||
index: number
|
index: () => number
|
||||||
temporary?: boolean
|
temporary?: boolean
|
||||||
onTabClose: (tab: string) => void
|
onTabClose: (tab: string) => void
|
||||||
onTabDoubleClick?: (tab: string) => void
|
onTabDoubleClick?: (tab: string) => void
|
||||||
@@ -26,7 +26,7 @@ export function SortableTabV2(props: {
|
|||||||
return props.tab
|
return props.tab
|
||||||
},
|
},
|
||||||
get index() {
|
get index() {
|
||||||
return props.index
|
return props.index()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const path = createMemo(() => file.pathFromTab(props.tab))
|
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { focusTerminalById } from "@/pages/session/helpers"
|
|||||||
|
|
||||||
export function SortableTerminalTabV2(props: {
|
export function SortableTerminalTabV2(props: {
|
||||||
terminal: LocalPTY
|
terminal: LocalPTY
|
||||||
index: number
|
index: () => number
|
||||||
newLayout: boolean
|
newLayout: boolean
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
@@ -25,7 +25,7 @@ export function SortableTerminalTabV2(props: {
|
|||||||
return props.terminal.id
|
return props.terminal.id
|
||||||
},
|
},
|
||||||
get index() {
|
get index() {
|
||||||
return props.index
|
return props.index()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { For, Show, createMemo, lazy, onCleanup } from "solid-js"
|
import { Component, For, Show, createMemo, lazy, onCleanup, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
@@ -9,6 +13,7 @@ import fuzzysort from "fuzzysort"
|
|||||||
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { SettingsList } from "./settings-list"
|
||||||
import { SettingsListV2 } from "./settings-v2/parts/list"
|
import { SettingsListV2 } from "./settings-v2/parts/list"
|
||||||
|
|
||||||
const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
|
const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
|
||||||
@@ -196,6 +201,69 @@ function filteredFor(
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useKeyCapture(input: {
|
||||||
|
active: () => string | null
|
||||||
|
stop: () => void
|
||||||
|
set: (id: string, keybind: string) => void
|
||||||
|
used: () => Map<string, { id: string; title: string }[]>
|
||||||
|
language: ReturnType<typeof useLanguage>
|
||||||
|
}) {
|
||||||
|
onMount(() => {
|
||||||
|
const handle = (event: KeyboardEvent) => {
|
||||||
|
const id = input.active()
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
input.stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear =
|
||||||
|
(event.key === "Backspace" || event.key === "Delete") &&
|
||||||
|
!event.ctrlKey &&
|
||||||
|
!event.metaKey &&
|
||||||
|
!event.altKey &&
|
||||||
|
!event.shiftKey
|
||||||
|
if (clear) {
|
||||||
|
input.set(id, "none")
|
||||||
|
input.stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = recordKeybind(event)
|
||||||
|
if (!next) return
|
||||||
|
|
||||||
|
const conflicts = new Map<string, string>()
|
||||||
|
for (const sig of signatures(next)) {
|
||||||
|
for (const item of input.used().get(sig) ?? []) {
|
||||||
|
if (item.id === id) continue
|
||||||
|
conflicts.set(item.id, item.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conflicts.size > 0) {
|
||||||
|
showToast({
|
||||||
|
title: input.language.t("settings.shortcuts.conflict.title"),
|
||||||
|
description: input.language.t("settings.shortcuts.conflict.description", {
|
||||||
|
keybind: formatKeybind(next, input.language.t),
|
||||||
|
titles: [...conflicts.values()].join(", "),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.set(id, next)
|
||||||
|
input.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
makeEventListener(document, "keydown", handle, { capture: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function createKeybindSettingsController(
|
export function createKeybindSettingsController(
|
||||||
input: {
|
input: {
|
||||||
command: Pick<CommandContext, "catalog" | "options" | "keybinds">
|
command: Pick<CommandContext, "catalog" | "options" | "keybinds">
|
||||||
@@ -342,7 +410,7 @@ export function createKeybindSettingsController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsKeybinds() {
|
function SettingsKeybindsV2() {
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const controller = createKeybindSettingsController({
|
const controller = createKeybindSettingsController({
|
||||||
@@ -356,9 +424,9 @@ export function SettingsKeybinds() {
|
|||||||
filtered={controller.catalog.filtered}
|
filtered={controller.catalog.filtered}
|
||||||
title={controller.catalog.title}
|
title={controller.catalog.title}
|
||||||
keybind={controller.catalog.keybind}
|
keybind={controller.catalog.keybind}
|
||||||
active={controller.capture.active()}
|
active={controller.capture.active}
|
||||||
onCapture={controller.capture.toggle}
|
onCapture={controller.capture.toggle}
|
||||||
hasOverrides={controller.settings.hasOverrides()}
|
hasOverrides={controller.settings.hasOverrides}
|
||||||
onReset={controller.settings.reset}
|
onReset={controller.settings.reset}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -369,9 +437,9 @@ function SettingsKeybindsV2View(props: {
|
|||||||
filtered: (query: string) => Map<KeybindGroup, string[]>
|
filtered: (query: string) => Map<KeybindGroup, string[]>
|
||||||
title: (id: string) => string
|
title: (id: string) => string
|
||||||
keybind: (id: string) => string
|
keybind: (id: string) => string
|
||||||
active: string | null
|
active: () => string | null
|
||||||
onCapture: (id: string) => void
|
onCapture: (id: string) => void
|
||||||
hasOverrides: boolean
|
hasOverrides: () => boolean
|
||||||
onReset: () => void
|
onReset: () => void
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -383,11 +451,8 @@ function SettingsKeybindsV2View(props: {
|
|||||||
<>
|
<>
|
||||||
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
||||||
<div class="settings-v2-tab-header-row">
|
<div class="settings-v2-tab-header-row">
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.shortcuts.description")}</span>
|
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides()}>
|
||||||
</div>
|
|
||||||
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides}>
|
|
||||||
{language.t("settings.shortcuts.reset.button")}
|
{language.t("settings.shortcuts.reset.button")}
|
||||||
</ButtonV2>
|
</ButtonV2>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,12 +498,12 @@ function SettingsKeybindsV2View(props: {
|
|||||||
data-keybind-id={id}
|
data-keybind-id={id}
|
||||||
classList={{
|
classList={{
|
||||||
"settings-v2-keybind-button": true,
|
"settings-v2-keybind-button": true,
|
||||||
"settings-v2-keybind-button--active": props.active === id,
|
"settings-v2-keybind-button--active": props.active() === id,
|
||||||
}}
|
}}
|
||||||
onClick={() => props.onCapture(id)}
|
onClick={() => props.onCapture(id)}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={props.active === id}
|
when={props.active() === id}
|
||||||
fallback={props.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
fallback={props.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||||
>
|
>
|
||||||
{language.t("settings.shortcuts.pressKeys")}
|
{language.t("settings.shortcuts.pressKeys")}
|
||||||
@@ -463,3 +528,254 @@ function SettingsKeybindsV2View(props: {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
||||||
|
if (props.v2) return <SettingsKeybindsV2 />
|
||||||
|
|
||||||
|
const command = useCommand()
|
||||||
|
const language = useLanguage()
|
||||||
|
const settings = useSettings()
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
active: null as string | null,
|
||||||
|
filter: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
if (!store.active) return
|
||||||
|
setStore("active", null)
|
||||||
|
command.keybinds(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = (id: string) => {
|
||||||
|
if (store.active === id) {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.active) stop()
|
||||||
|
|
||||||
|
setStore("active", id)
|
||||||
|
command.keybinds(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = createMemo(() => keybinds(settings.current.keybinds))
|
||||||
|
|
||||||
|
const hasOverrides = createMemo(() => Object.values(map()).some((x) => typeof x === "string"))
|
||||||
|
|
||||||
|
const resetAll = () => {
|
||||||
|
stop()
|
||||||
|
settings.keybinds.resetAll()
|
||||||
|
showToast({
|
||||||
|
title: language.t("settings.shortcuts.reset.toast.title"),
|
||||||
|
description: language.t("settings.shortcuts.reset.toast.description"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = createMemo(() => {
|
||||||
|
language.locale()
|
||||||
|
return listFor(command, map(), language.t("command.palette"))
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = (id: string) => list().get(id)?.title ?? ""
|
||||||
|
|
||||||
|
const grouped = createMemo(() => groupedFor(list()))
|
||||||
|
|
||||||
|
const filtered = createMemo(() => {
|
||||||
|
return filteredFor(store.filter, list(), grouped(), (id) => command.keybind(id) || "")
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasResults = createMemo(() => {
|
||||||
|
for (const group of GROUPS) {
|
||||||
|
const ids = filtered().get(group) ?? []
|
||||||
|
if (ids.length > 0) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const used = createMemo(() => {
|
||||||
|
const map = new Map<string, { id: string; title: string }[]>()
|
||||||
|
|
||||||
|
const add = (key: string, value: { id: string; title: string }) => {
|
||||||
|
const list = map.get(key)
|
||||||
|
if (!list) {
|
||||||
|
map.set(key, [value])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list.push(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
|
||||||
|
for (const sig of signatures(palette)) {
|
||||||
|
add(sig, { id: PALETTE_ID, title: title(PALETTE_ID) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueFor = (id: string) => {
|
||||||
|
const custom = settings.keybinds.get(id)
|
||||||
|
if (typeof custom === "string") return custom
|
||||||
|
|
||||||
|
const live = command.options.find((x) => x.id === id)
|
||||||
|
if (live?.keybind) return live.keybind
|
||||||
|
|
||||||
|
const meta = command.catalog.find((x) => x.id === id)
|
||||||
|
return meta?.keybind
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of list().keys()) {
|
||||||
|
if (id === PALETTE_ID) continue
|
||||||
|
for (const sig of signatures(valueFor(id))) {
|
||||||
|
add(sig, { id, title: title(id) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const setKeybind = (id: string, keybind: string) => settings.keybinds.set(id, keybind)
|
||||||
|
|
||||||
|
useKeyCapture({
|
||||||
|
active: () => store.active,
|
||||||
|
stop,
|
||||||
|
set: setKeybind,
|
||||||
|
used,
|
||||||
|
language,
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (store.active) command.keybinds(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyResults = (
|
||||||
|
<Show when={store.filter && !hasResults()}>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"flex flex-col items-center justify-center py-12 text-center": !props.v2,
|
||||||
|
"settings-v2-shortcuts-status": props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
classList={{
|
||||||
|
"text-14-regular text-text-weak": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("settings.shortcuts.search.empty")}
|
||||||
|
</span>
|
||||||
|
<Show when={store.filter}>
|
||||||
|
<span
|
||||||
|
classList={{
|
||||||
|
"text-14-regular text-text-strong mt-1": !props.v2,
|
||||||
|
"settings-v2-shortcuts-status-filter": props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
"{store.filter}"
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
|
||||||
|
const List = props.v2 ? SettingsListV2 : SettingsList
|
||||||
|
|
||||||
|
const groups = (
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"settings-v2-shortcuts flex flex-col gap-8": props.v2,
|
||||||
|
"flex flex-col gap-8 max-w-[720px]": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={GROUPS}>
|
||||||
|
{(group) => (
|
||||||
|
<Show when={(filtered().get(group) ?? []).length > 0}>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"settings-v2-section": props.v2,
|
||||||
|
"flex flex-col gap-1": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
classList={{
|
||||||
|
"settings-v2-section-title": props.v2,
|
||||||
|
"text-14-medium text-text-strong pb-2": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t(groupKey[group])}
|
||||||
|
</h3>
|
||||||
|
<List>
|
||||||
|
<For each={filtered().get(group) ?? []}>
|
||||||
|
{(id) => (
|
||||||
|
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||||
|
<span
|
||||||
|
classList={{
|
||||||
|
"text-14-regular text-text-strong": !props.v2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title(id)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-keybind-id={id}
|
||||||
|
classList={{
|
||||||
|
"settings-v2-keybind-button": props.v2,
|
||||||
|
"settings-v2-keybind-button--active": props.v2 && store.active === id,
|
||||||
|
"h-8 px-3 rounded-md text-12-regular": !props.v2,
|
||||||
|
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
|
||||||
|
!props.v2 && store.active !== id,
|
||||||
|
"border border-border-weak-base bg-surface-inset-base text-text-weak":
|
||||||
|
!props.v2 && store.active === id,
|
||||||
|
}}
|
||||||
|
onClick={() => start(id)}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={store.active === id}
|
||||||
|
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||||
|
>
|
||||||
|
{language.t("settings.shortcuts.pressKeys")}
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
{emptyResults}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
|
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
|
||||||
|
<Button size="small" variant="secondary" onClick={resetAll} disabled={!hasOverrides()}>
|
||||||
|
{language.t("settings.shortcuts.reset.button")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
|
||||||
|
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
|
||||||
|
<TextField
|
||||||
|
variant="ghost"
|
||||||
|
type="text"
|
||||||
|
value={store.filter}
|
||||||
|
onChange={(v) => setStore("filter", v)}
|
||||||
|
placeholder={language.t("settings.shortcuts.search.placeholder")}
|
||||||
|
spellcheck={false}
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<Show when={store.filter}>
|
||||||
|
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{groups}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { type ParentProps, Show } from "solid-js"
|
|
||||||
import { useGlobal } from "@/context/global"
|
|
||||||
import { ModelsProvider } from "@/context/models"
|
|
||||||
import { ServerProvider } from "@/context/server"
|
|
||||||
import { ServerConnection } from "@/context/servers"
|
|
||||||
|
|
||||||
export function SettingsServerScope(props: ParentProps<{ directory?: string }>) {
|
|
||||||
const global = useGlobal()
|
|
||||||
return (
|
|
||||||
<Show when={global.settings.server.selected()} keyed fallback={props.children}>
|
|
||||||
{(server) => (
|
|
||||||
<SettingsServerDataScope server={server} directory={props.directory}>
|
|
||||||
{props.children}
|
|
||||||
</SettingsServerDataScope>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsServerDataScope(props: ParentProps<{ server: ServerConnection.Any; directory?: string }>) {
|
|
||||||
return (
|
|
||||||
<ServerProvider conn={props.server}>
|
|
||||||
<ModelsProvider directory={props.directory}>{props.children}</ModelsProvider>
|
|
||||||
</ServerProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import { Component, createMemo } from "solid-js"
|
|
||||||
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { ExternalLink } from "../external-link"
|
|
||||||
import { SettingsListV2 } from "./parts/list"
|
|
||||||
import { SettingsRowV2 } from "./parts/row"
|
|
||||||
import { createAppearanceSettingsController, type AppearanceSettingsController } from "./general-controllers"
|
|
||||||
import "./settings-v2.css"
|
|
||||||
|
|
||||||
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
|
|
||||||
const fontSettings = {
|
|
||||||
ui: {
|
|
||||||
action: "settings-ui-font",
|
|
||||||
title: "settings.general.row.uiFont.title",
|
|
||||||
description: "settings.general.row.uiFont.description",
|
|
||||||
font: "ui",
|
|
||||||
input: "setUI",
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
action: "settings-code-font",
|
|
||||||
title: "settings.general.row.font.title",
|
|
||||||
description: "settings.general.row.font.description",
|
|
||||||
font: "code",
|
|
||||||
input: "setCode",
|
|
||||||
},
|
|
||||||
terminal: {
|
|
||||||
action: "settings-terminal-font",
|
|
||||||
title: "settings.general.row.terminalFont.title",
|
|
||||||
description: "settings.general.row.terminalFont.description",
|
|
||||||
font: "terminal",
|
|
||||||
input: "setTerminal",
|
|
||||||
},
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const FontSetting: Component<{
|
|
||||||
kind: "ui" | "code" | "terminal"
|
|
||||||
fonts: AppearanceSettingsController["fonts"]
|
|
||||||
}> = (props) => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const config = () => fontSettings[props.kind]
|
|
||||||
return (
|
|
||||||
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
|
||||||
<div class="w-full sm:w-[220px]">
|
|
||||||
<TextInputV2
|
|
||||||
data-action={config().action}
|
|
||||||
type="text"
|
|
||||||
appearance="base"
|
|
||||||
value={props.fonts[config().font]().value}
|
|
||||||
onInput={(event) => props.fonts[config().input](event.currentTarget.value)}
|
|
||||||
placeholder={props.fonts[config().font]().placeholder}
|
|
||||||
spellcheck={false}
|
|
||||||
autocorrect="off"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
aria-label={language.t(config().title)}
|
|
||||||
style={{ "font-family": props.fonts[config().font]().family }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRowV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsAppearanceV2: Component = () => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const appearance = createAppearanceSettingsController()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="settings-v2-tab-header">
|
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.general.section.appearance")}</h2>
|
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.appearance.description")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-v2-tab-body">
|
|
||||||
<div class="settings-v2-section">
|
|
||||||
<SettingsListV2>
|
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.general.row.colorScheme.title")}
|
|
||||||
description={language.t("settings.general.row.colorScheme.description")}
|
|
||||||
>
|
|
||||||
<SelectV2
|
|
||||||
appearance="inline"
|
|
||||||
data-action="settings-color-scheme"
|
|
||||||
options={schemeOptions}
|
|
||||||
current={schemeOptions.find((option) => option === appearance.scheme.current())}
|
|
||||||
placement="bottom-end"
|
|
||||||
gutter={6}
|
|
||||||
label={(option) => {
|
|
||||||
if (option === "system") return language.t("theme.scheme.system")
|
|
||||||
if (option === "light") return language.t("theme.scheme.light")
|
|
||||||
return language.t("theme.scheme.dark")
|
|
||||||
}}
|
|
||||||
onSelect={(option) => option && appearance.scheme.select(option)}
|
|
||||||
/>
|
|
||||||
</SettingsRowV2>
|
|
||||||
|
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.general.row.theme.title")}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
{language.t("settings.general.row.theme.description")}{" "}
|
|
||||||
<ExternalLink class="settings-v2-link" href="https://opencode.ai/docs/themes/">
|
|
||||||
{language.t("common.learnMore")}
|
|
||||||
</ExternalLink>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectV2
|
|
||||||
appearance="inline"
|
|
||||||
data-action="settings-theme"
|
|
||||||
options={appearance.theme.options()}
|
|
||||||
current={appearance.theme.current()}
|
|
||||||
placement="bottom-end"
|
|
||||||
gutter={6}
|
|
||||||
value={(option) => option.id}
|
|
||||||
label={(option) => option.name}
|
|
||||||
onSelect={appearance.theme.select}
|
|
||||||
/>
|
|
||||||
</SettingsRowV2>
|
|
||||||
|
|
||||||
<FontSetting kind="ui" fonts={appearance.fonts} />
|
|
||||||
<FontSetting kind="code" fonts={appearance.fonts} />
|
|
||||||
<FontSetting kind="terminal" fonts={appearance.fonts} />
|
|
||||||
</SettingsListV2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "@/components/server/server-management"
|
} from "@/components/server/server-management"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { normalizeServerUrl, ServerConnection, useServers } from "@/context/servers"
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
import { useCheckServerHealth } from "@/utils/server-health"
|
import { useCheckServerHealth } from "@/utils/server-health"
|
||||||
import "./settings-v2.css"
|
import "./settings-v2.css"
|
||||||
@@ -146,7 +146,7 @@ export const DialogServerV2: Component<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createFormController(options: { onSelect?: () => void } = {}) {
|
function createFormController(options: { onSelect?: () => void } = {}) {
|
||||||
const server = useServers()
|
const server = useServer()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -173,7 +173,8 @@ function createFormController(options: { onSelect?: () => void } = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const allServers = () => {
|
const allServers = () => {
|
||||||
return server.list
|
if (!server.current || server.list.includes(server.current)) return server.list
|
||||||
|
return [server.current, ...server.list]
|
||||||
}
|
}
|
||||||
const editing = createMemo(() =>
|
const editing = createMemo(() =>
|
||||||
allServers().find((item) => item.type === "http" && item.http.url === store.originalUrl),
|
allServers().find((item) => item.type === "http" && item.http.url === store.originalUrl),
|
||||||
@@ -181,8 +182,10 @@ function createFormController(options: { onSelect?: () => void } = {}) {
|
|||||||
const add = (connection: ServerConnection.Http) => server.add(connection)
|
const add = (connection: ServerConnection.Http) => server.add(connection)
|
||||||
const replace = (originalKey: ServerConnection.Key, next: ServerConnection.Http) =>
|
const replace = (originalKey: ServerConnection.Key, next: ServerConnection.Http) =>
|
||||||
replaceServerConnection(originalKey, next, {
|
replaceServerConnection(originalKey, next, {
|
||||||
|
active: () => server.key,
|
||||||
removeTabs: (key) => tabs.removeServer(key),
|
removeTabs: (key) => tabs.removeServer(key),
|
||||||
add,
|
add,
|
||||||
|
setActive: (key) => server.setActive(key),
|
||||||
remove: (key) => server.remove(key),
|
remove: (key) => server.remove(key),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
import { Component, createEffect, createMemo, createSignal, startTransition } from "solid-js"
|
import { Component, createMemo, createSignal, startTransition } from "solid-js"
|
||||||
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
|
import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
|
||||||
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { SettingsGeneral } from "./general"
|
import { SettingsGeneralV2 } from "./general"
|
||||||
import { SettingsAppearanceV2 } from "./appearance"
|
|
||||||
import { SettingsKeybinds } from "../settings-keybinds"
|
import { SettingsKeybinds } from "../settings-keybinds"
|
||||||
import { SettingsNotificationsV2 } from "./notifications"
|
|
||||||
import { SettingsProvidersV2 } from "./providers"
|
import { SettingsProvidersV2 } from "./providers"
|
||||||
import { SettingsModelsV2 } from "./models"
|
import { SettingsModelsV2 } from "./models"
|
||||||
|
import "./settings-v2.css"
|
||||||
import { SettingsServersV2 } from "./servers"
|
import { SettingsServersV2 } from "./servers"
|
||||||
import { SettingsWorkspacesV2 } from "./workspaces"
|
|
||||||
import { SettingsProjectsV2 } from "./projects"
|
|
||||||
import { SettingsExtensionsV2 } from "./extensions"
|
|
||||||
import { SettingsServerScope } from "../settings-server-picker"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
import { useGlobal, useServerCtx } from "@/context/global"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { ServerConnection, useServers } from "@/context/servers"
|
|
||||||
import "./settings-v2.css"
|
|
||||||
|
|
||||||
export const DialogSettings: Component<{
|
export const DialogSettings: Component<{
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
@@ -30,40 +23,17 @@ export const DialogSettings: Component<{
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const servers = useServers()
|
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const global = useGlobal()
|
const serverSync = useServerSync()
|
||||||
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
|
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
|
||||||
const server = createMemo(() => {
|
|
||||||
const route = layout.route()
|
|
||||||
switch (route.type) {
|
|
||||||
case "draft": {
|
|
||||||
const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID)
|
|
||||||
return servers.list.find((item) => ServerConnection.key(item) === draft?.server)
|
|
||||||
}
|
|
||||||
case "session":
|
|
||||||
return servers.list.find((item) => ServerConnection.key(item) === route.server)
|
|
||||||
case "home":
|
|
||||||
return servers.list.find((item) => ServerConnection.key(item) === layout.home.selection().server)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const serverCtx = useServerCtx(server)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const current = server()
|
|
||||||
if (current) global.settings.server.set(ServerConnection.key(current))
|
|
||||||
})
|
|
||||||
|
|
||||||
const directory = createMemo(() => {
|
const directory = createMemo(() => {
|
||||||
const selected = global.settings.server.selected()
|
|
||||||
const current = server()
|
|
||||||
if (!selected || !current || ServerConnection.key(selected) !== ServerConnection.key(current)) return
|
|
||||||
const route = layout.route()
|
const route = layout.route()
|
||||||
|
if (route.type === "dir-new-sesssion") return route.dir
|
||||||
if (route.type === "draft") {
|
if (route.type === "draft") {
|
||||||
const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID)
|
const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID)
|
||||||
return draft?.type === "draft" ? draft.directory : undefined
|
return draft?.type === "draft" ? draft.directory : undefined
|
||||||
}
|
}
|
||||||
if (route.type === "session") return serverCtx()?.sync.session.get(route.sessionId)?.location.directory
|
if (route.type === "session") return serverSync().session.get(route.sessionId)?.location.directory
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,45 +52,29 @@ export const DialogSettings: Component<{
|
|||||||
>
|
>
|
||||||
<TabsV2.List>
|
<TabsV2.List>
|
||||||
<div class="flex flex-col justify-between h-full w-full">
|
<div class="flex flex-col justify-between h-full w-full">
|
||||||
<div class="flex flex-col gap-4 w-full">
|
<div class="flex flex-col gap-3 w-full">
|
||||||
{/* Group 1: Preferences */}
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex flex-col gap-1 w-full">
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<TabsV2.SectionTitle>{language.t("settings.section.desktop")}</TabsV2.SectionTitle>
|
||||||
|
<div class="flex flex-col gap-1.5 w-full">
|
||||||
<TabsV2.Trigger value="general">
|
<TabsV2.Trigger value="general">
|
||||||
<Icon name="sliders" />
|
<Icon name="sliders" />
|
||||||
{language.t("settings.tab.preferences")}
|
{language.t("settings.tab.general")}
|
||||||
</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="appearance">
|
|
||||||
<Icon name="appearance" />
|
|
||||||
{language.t("settings.general.section.appearance")}
|
|
||||||
</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="notifications">
|
|
||||||
<Icon name="notifications" />
|
|
||||||
{language.t("settings.tab.notifications")}
|
|
||||||
</TabsV2.Trigger>
|
</TabsV2.Trigger>
|
||||||
<TabsV2.Trigger value="shortcuts">
|
<TabsV2.Trigger value="shortcuts">
|
||||||
<Icon name="keyboard" />
|
<Icon name="keyboard" />
|
||||||
{language.t("settings.tab.shortcuts")}
|
{language.t("settings.tab.shortcuts")}
|
||||||
</TabsV2.Trigger>
|
</TabsV2.Trigger>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Group 2: Environment & Workspaces */}
|
<div class="flex flex-col gap-1.5">
|
||||||
<div class="flex flex-col gap-1 w-full">
|
<TabsV2.SectionTitle>{language.t("settings.section.server")}</TabsV2.SectionTitle>
|
||||||
|
<div class="flex flex-col gap-1.5 w-full">
|
||||||
<TabsV2.Trigger value="servers">
|
<TabsV2.Trigger value="servers">
|
||||||
<Icon name="server" />
|
<Icon name="server" />
|
||||||
{language.t("status.popover.tab.servers")}
|
{language.t("status.popover.tab.servers")}
|
||||||
</TabsV2.Trigger>
|
</TabsV2.Trigger>
|
||||||
<TabsV2.Trigger value="projects">
|
|
||||||
<Icon name="folder" />
|
|
||||||
{language.t("settings.tab.projects")}
|
|
||||||
</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="workspaces">
|
|
||||||
<Icon name="workspace-isolated" />
|
|
||||||
{language.t("settings.tab.workspaces")}
|
|
||||||
</TabsV2.Trigger>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Group 3: Capabilities & Extensions */}
|
|
||||||
<div class="flex flex-col gap-1 w-full">
|
|
||||||
<TabsV2.Trigger value="providers">
|
<TabsV2.Trigger value="providers">
|
||||||
<Icon name="providers" />
|
<Icon name="providers" />
|
||||||
{language.t("settings.providers.title")}
|
{language.t("settings.providers.title")}
|
||||||
@@ -129,52 +83,31 @@ export const DialogSettings: Component<{
|
|||||||
<Icon name="models" />
|
<Icon name="models" />
|
||||||
{language.t("settings.models.title")}
|
{language.t("settings.models.title")}
|
||||||
</TabsV2.Trigger>
|
</TabsV2.Trigger>
|
||||||
<TabsV2.Trigger value="extensions">
|
|
||||||
<Icon name="extensions" />
|
|
||||||
{language.t("settings.tab.extensions")}
|
|
||||||
</TabsV2.Trigger>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="settings-v2-nav-footer">
|
<div class="settings-v2-nav-footer">
|
||||||
<span>{language.t("app.name.desktop")}</span>
|
<span>{language.t("app.name.desktop")}</span>
|
||||||
<span>v{platform.version}</span>
|
<span>v{platform.version}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsV2.List>
|
</TabsV2.List>
|
||||||
|
|
||||||
<TabsV2.Content value="general" class="settings-v2-panel">
|
<TabsV2.Content value="general" class="settings-v2-panel">
|
||||||
<SettingsGeneral server={server()} sessionID={props.sessionID} />
|
<SettingsGeneralV2 sessionID={props.sessionID} />
|
||||||
</TabsV2.Content>
|
|
||||||
<TabsV2.Content value="appearance" class="settings-v2-panel">
|
|
||||||
<SettingsAppearanceV2 />
|
|
||||||
</TabsV2.Content>
|
|
||||||
<TabsV2.Content value="notifications" class="settings-v2-panel">
|
|
||||||
<SettingsNotificationsV2 />
|
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
|
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
|
||||||
<SettingsKeybinds />
|
<SettingsKeybinds v2 />
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="servers" class="settings-v2-panel">
|
<TabsV2.Content value="servers" class="settings-v2-panel">
|
||||||
<SettingsServersV2 />
|
<SettingsServersV2 />
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="projects" class="settings-v2-panel">
|
|
||||||
<SettingsProjectsV2 />
|
|
||||||
</TabsV2.Content>
|
|
||||||
<SettingsServerScope directory={directory()}>
|
|
||||||
<TabsV2.Content value="workspaces" class="settings-v2-panel">
|
|
||||||
<SettingsWorkspacesV2 activeDirectory={directory()} />
|
|
||||||
</TabsV2.Content>
|
|
||||||
<TabsV2.Content value="providers" class="settings-v2-panel">
|
<TabsV2.Content value="providers" class="settings-v2-panel">
|
||||||
<SettingsProvidersV2 directory={directory()} onBack={showProviders} />
|
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="models" class="settings-v2-panel">
|
<TabsV2.Content value="models" class="settings-v2-panel">
|
||||||
<SettingsModelsV2 />
|
<SettingsModelsV2 />
|
||||||
</TabsV2.Content>
|
</TabsV2.Content>
|
||||||
<TabsV2.Content value="extensions" class="settings-v2-panel">
|
|
||||||
<SettingsExtensionsV2 />
|
|
||||||
</TabsV2.Content>
|
|
||||||
</SettingsServerScope>
|
|
||||||
</TabsV2>
|
</TabsV2>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import { Component, For, createEffect, createMemo } from "solid-js"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
|
||||||
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useServerSync } from "@/context/server-sync"
|
|
||||||
import { useData } from "@/context/server"
|
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { ExternalLink } from "../external-link"
|
|
||||||
import { InlineServerSelect } from "./parts/server-select"
|
|
||||||
import "./settings-v2.css"
|
|
||||||
|
|
||||||
interface McpRowItem {
|
|
||||||
name: string
|
|
||||||
enabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginRowItem {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsExtensionsV2: Component = () => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const serverSdk = useServerSDK()
|
|
||||||
const serverSync = useServerSync()
|
|
||||||
const data = useData()
|
|
||||||
const mcps = createMemo<McpRowItem[]>(() => {
|
|
||||||
const configMcp = serverSync.data.config.mcp ?? {}
|
|
||||||
return Object.entries(configMcp).map(([name, config]) => ({
|
|
||||||
name,
|
|
||||||
enabled: typeof config !== "object" || config === null || !("enabled" in config) || config.enabled !== false,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleMcpToggle = (item: McpRowItem, checked: boolean) => {
|
|
||||||
const before = serverSync.data.config.mcp ?? {}
|
|
||||||
const config = before[item.name]
|
|
||||||
if (typeof config !== "object" || config === null) return
|
|
||||||
const next = { ...before, [item.name]: { ...config, enabled: checked } }
|
|
||||||
serverSync.set("config", "mcp", next)
|
|
||||||
void serverSync.updateConfig({ mcp: next }).catch(() => serverSync.set("config", "mcp", before))
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugins = createMemo<PluginRowItem[]>(() => {
|
|
||||||
const raw = serverSync.data.config.plugin ?? []
|
|
||||||
return raw.map((item) => {
|
|
||||||
const name = typeof item === "string" ? item : item[0]
|
|
||||||
return { name }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (serverSdk.connection.status() !== "connected") return
|
|
||||||
void data.location.skill.sync().catch(() => undefined)
|
|
||||||
})
|
|
||||||
const skills = () => data.location.skill.list() ?? []
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="settings-v2-tab-header">
|
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.tab.extensions")}</h2>
|
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.extensions.description")}</span>
|
|
||||||
</div>
|
|
||||||
<InlineServerSelect />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-v2-tab-body">
|
|
||||||
<TabsV2 variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
|
|
||||||
<TabsV2.List>
|
|
||||||
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
|
|
||||||
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
|
|
||||||
</TabsV2.List>
|
|
||||||
|
|
||||||
<TabsV2.Content value="mcps">
|
|
||||||
<div class="settings-v2-section">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-13-medium text-v2-text-text-base">
|
|
||||||
{language.t("settings.extensions.availableAll")}
|
|
||||||
</span>
|
|
||||||
<span class="text-13-regular text-v2-text-faint">{language.t("settings.extensions.manageConfig")}</span>
|
|
||||||
</div>
|
|
||||||
<div class="bg-[var(--v2-background-bg-base)] border-[0.5px] border-[var(--v2-border-border-base)] rounded-[8px] pl-4 pr-3 overflow-hidden">
|
|
||||||
<For each={mcps()}>
|
|
||||||
{(item) => (
|
|
||||||
<div class="py-4 flex items-center justify-between border-b-[0.5px] border-[var(--v2-border-border-base)] last:border-b-0">
|
|
||||||
<div class="flex items-center gap-2.5 min-w-0">
|
|
||||||
<Icon name="mcp" class="text-v2-icon-icon-muted shrink-0" />
|
|
||||||
<span class="text-13-medium text-v2-text-text-base truncate">{item.name}</span>
|
|
||||||
</div>
|
|
||||||
<Switch checked={item.enabled} onChange={(checked) => handleMcpToggle(item, checked)} hideLabel>
|
|
||||||
{item.name}
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsV2.Content>
|
|
||||||
|
|
||||||
<TabsV2.Content value="plugins">
|
|
||||||
<div class="settings-v2-section">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-13-medium text-v2-text-text-base">
|
|
||||||
{language.t("settings.extensions.availableAll")}
|
|
||||||
</span>
|
|
||||||
<span class="text-13-regular text-v2-text-faint">{language.t("settings.extensions.manageConfig")}</span>
|
|
||||||
</div>
|
|
||||||
<div class="bg-[var(--v2-background-bg-base)] border-[0.5px] border-[var(--v2-border-border-base)] rounded-[8px] pl-4 pr-3 overflow-hidden">
|
|
||||||
<For each={plugins()}>
|
|
||||||
{(plugin) => (
|
|
||||||
<div class="py-4 flex items-center justify-between border-b-[0.5px] border-[var(--v2-border-border-base)] last:border-b-0">
|
|
||||||
<div class="flex items-center gap-2.5 min-w-0">
|
|
||||||
<Icon name="cube" class="text-v2-icon-icon-muted shrink-0" />
|
|
||||||
<span class="text-13-medium text-v2-text-text-base truncate font-mono">{plugin.name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsV2.Content>
|
|
||||||
|
|
||||||
<TabsV2.Content value="skills">
|
|
||||||
<div class="settings-v2-section">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-13-medium text-v2-text-text-base">
|
|
||||||
{language.t("settings.extensions.availableAll")}
|
|
||||||
</span>
|
|
||||||
<ExternalLink
|
|
||||||
class="text-13-regular text-v2-text-accent hover:underline"
|
|
||||||
href="https://opencode.ai/docs/skills/"
|
|
||||||
>
|
|
||||||
{language.t("settings.extensions.addSkills")}
|
|
||||||
</ExternalLink>
|
|
||||||
</div>
|
|
||||||
<div class="bg-[var(--v2-background-bg-base)] border-[0.5px] border-[var(--v2-border-border-base)] rounded-[8px] pl-4 pr-3 overflow-hidden">
|
|
||||||
<For each={skills()}>
|
|
||||||
{(skill) => (
|
|
||||||
<div class="py-4 flex items-center justify-between border-b-[0.5px] border-[var(--v2-border-border-base)] last:border-b-0">
|
|
||||||
<div class="flex items-center gap-2.5 min-w-0">
|
|
||||||
<Icon name="post-skill" class="text-v2-icon-icon-muted shrink-0" />
|
|
||||||
<span class="text-13-medium text-v2-text-text-base truncate">{skill.name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsV2.Content>
|
|
||||||
</TabsV2>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -17,24 +17,17 @@ import {
|
|||||||
} from "@/context/settings"
|
} from "@/context/settings"
|
||||||
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
|
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
|
||||||
import { createSoundPreviewController, type ShellOption } from "./general-controller-behavior"
|
import { createSoundPreviewController, type ShellOption } from "./general-controller-behavior"
|
||||||
import { ServerConnection } from "@/context/servers"
|
|
||||||
import { useGlobal, useServerCtx } from "@/context/global"
|
|
||||||
|
|
||||||
export { createShellOptions, createSoundPreviewController } from "./general-controller-behavior"
|
export { createShellOptions, createSoundPreviewController } from "./general-controller-behavior"
|
||||||
export type { ShellOption, ShellSelectOption } from "./general-controller-behavior"
|
export type { ShellOption, ShellSelectOption } from "./general-controller-behavior"
|
||||||
|
|
||||||
export function createPermissionScopeController(
|
export function createPermissionScopeController(sessionID: Accessor<string | undefined>) {
|
||||||
server: Accessor<ServerConnection.Any | undefined>,
|
const permission = usePermission()
|
||||||
sessionID: Accessor<string | undefined>,
|
const serverSync = useServerSync()
|
||||||
) {
|
|
||||||
const serverCtx = useServerCtx(server)
|
|
||||||
const permission = () => serverCtx()?.permission
|
|
||||||
|
|
||||||
const directory = createMemo(() => {
|
const directory = createMemo(() => {
|
||||||
const s = server()
|
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
if (!s || !id) return undefined
|
if (!id) return undefined
|
||||||
return serverCtx()?.sync.session.lineage.peek(id)?.session.location.directory
|
return serverSync().session.lineage.peek(id)?.session.location.directory
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -42,21 +35,21 @@ export function createPermissionScopeController(
|
|||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
const dir = directory()
|
const dir = directory()
|
||||||
if (!id || !dir) return false
|
if (!id || !dir) return false
|
||||||
return permission()?.isAutoAccepting(id, dir)
|
return permission.isAutoAccepting(id, dir)
|
||||||
}),
|
}),
|
||||||
enabled: createMemo(() => !!directory()),
|
enabled: createMemo(() => !!directory()),
|
||||||
set: (checked: boolean) => {
|
set: (checked: boolean) => {
|
||||||
const id = sessionID()
|
const id = sessionID()
|
||||||
const dir = directory()
|
const dir = directory()
|
||||||
if (!id || !dir) return
|
if (!id || !dir) return
|
||||||
if (checked) return permission()?.enableAutoAccept(id, dir)
|
if (checked) return permission.enableAutoAccept(id, dir)
|
||||||
permission()?.disableAutoAccept(id, dir)
|
permission.disableAutoAccept(id, dir)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createShellSettingsController(server: Accessor<ServerConnection.Any | undefined>) {
|
export function createShellSettingsController() {
|
||||||
const serverCtx = useServerCtx(server)
|
const serverSync = useServerSync()
|
||||||
const [shells] = createResource(
|
const [shells] = createResource(
|
||||||
async () => {
|
async () => {
|
||||||
// TODO: Dax is considering the V2 shell discovery and config update APIs.
|
// TODO: Dax is considering the V2 shell discovery and config update APIs.
|
||||||
@@ -65,7 +58,7 @@ export function createShellSettingsController(server: Accessor<ServerConnection.
|
|||||||
},
|
},
|
||||||
{ initialValue: [] as ShellOption[] },
|
{ initialValue: [] as ShellOption[] },
|
||||||
)
|
)
|
||||||
const current = createMemo(() => serverCtx()?.sync.data.config.shell ?? "")
|
const current = createMemo(() => serverSync().data.config.shell ?? "")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shells: () => shells.latest,
|
shells: () => shells.latest,
|
||||||
@@ -73,7 +66,7 @@ export function createShellSettingsController(server: Accessor<ServerConnection.
|
|||||||
select: (value: string) => {
|
select: (value: string) => {
|
||||||
if (value === current()) return
|
if (value === current()) return
|
||||||
// TODO: Dax is considering the V2 shell discovery and config update APIs.
|
// TODO: Dax is considering the V2 shell discovery and config update APIs.
|
||||||
// void serverSync.updateConfig({ shell: value })
|
// void serverSync().updateConfig({ shell: value })
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useUpdaterAction } from "../updater-action"
|
import { useUpdaterAction } from "../updater-action"
|
||||||
import { type WorkspaceDefaultDestination, useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { ExternalLink } from "../external-link"
|
import { ExternalLink } from "../external-link"
|
||||||
import { SettingsListV2 } from "./parts/list"
|
import { SettingsListV2 } from "./parts/list"
|
||||||
import { SettingsRowV2 } from "./parts/row"
|
import { SettingsRowV2 } from "./parts/row"
|
||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
type SoundSettingsController,
|
type SoundSettingsController,
|
||||||
} from "./general-controllers"
|
} from "./general-controllers"
|
||||||
import "./settings-v2.css"
|
import "./settings-v2.css"
|
||||||
import { ServerConnection } from "@/context/servers"
|
|
||||||
|
|
||||||
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
|
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
|
||||||
const fontSettings = {
|
const fontSettings = {
|
||||||
@@ -86,34 +85,6 @@ const PermissionScopeSetting: Component<{ controller: PermissionScopeController
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkspaceDestinationSetting: Component = () => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const settings = useSettings()
|
|
||||||
const options = createMemo((): { value: WorkspaceDefaultDestination; label: string }[] => [
|
|
||||||
{ value: "last-used", label: language.t("settings.workspaces.default.lastUsed") },
|
|
||||||
{ value: "local", label: language.t("settings.workspaces.default.local") },
|
|
||||||
{ value: "new", label: language.t("settings.workspaces.default.new") },
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.workspaces.default.title")}
|
|
||||||
description={language.t("settings.workspaces.default.description")}
|
|
||||||
>
|
|
||||||
<SelectV2
|
|
||||||
appearance="inline"
|
|
||||||
options={options()}
|
|
||||||
current={options().find((option) => option.value === settings.workspaces.defaultDestination())}
|
|
||||||
value={(option) => option.value}
|
|
||||||
label={(option) => option.label}
|
|
||||||
placement="bottom-end"
|
|
||||||
gutter={6}
|
|
||||||
onSelect={(option) => option && settings.workspaces.setDefaultDestination(option.value)}
|
|
||||||
/>
|
|
||||||
</SettingsRowV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShellSetting: Component<{ controller: ShellSettingsController }> = (props) => {
|
const ShellSetting: Component<{ controller: ShellSettingsController }> = (props) => {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const options = createMemo(() =>
|
const options = createMemo(() =>
|
||||||
@@ -298,20 +269,18 @@ const LanguageSetting = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsGeneral: Component<{
|
export const SettingsGeneralV2: Component<{
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
server?: ServerConnection.Any
|
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const mobile = createMediaQuery("(max-width: 767px)")
|
const mobile = createMediaQuery("(max-width: 767px)")
|
||||||
const updater = useUpdaterAction()
|
const updater = useUpdaterAction()
|
||||||
const permissionScope = createPermissionScopeController(
|
const permissionScope = createPermissionScopeController(() => props.sessionID)
|
||||||
() => props.server,
|
const shell = createShellSettingsController()
|
||||||
() => props.sessionID,
|
const appearance = createAppearanceSettingsController()
|
||||||
)
|
const sounds = createSoundSettingsController()
|
||||||
const shell = createShellSettingsController(() => props.server)
|
|
||||||
const desktop = createMemo(() => platform.platform === "desktop")
|
const desktop = createMemo(() => platform.platform === "desktop")
|
||||||
|
|
||||||
const [pinchZoom, { mutate: setPinchZoom }] = createResource(
|
const [pinchZoom, { mutate: setPinchZoom }] = createResource(
|
||||||
@@ -329,11 +298,9 @@ export const SettingsGeneral: Component<{
|
|||||||
|
|
||||||
const GeneralSection = () => (
|
const GeneralSection = () => (
|
||||||
<div class="settings-v2-section">
|
<div class="settings-v2-section">
|
||||||
<h3 class="settings-v2-section-title">{language.t("settings.general.section.general")}</h3>
|
|
||||||
<SettingsListV2>
|
<SettingsListV2>
|
||||||
<LanguageSetting />
|
<LanguageSetting />
|
||||||
|
|
||||||
<WorkspaceDestinationSetting />
|
|
||||||
<PermissionScopeSetting controller={permissionScope} />
|
<PermissionScopeSetting controller={permissionScope} />
|
||||||
|
|
||||||
<ShellSetting controller={shell} />
|
<ShellSetting controller={shell} />
|
||||||
@@ -396,6 +363,18 @@ export const SettingsGeneral: Component<{
|
|||||||
<h3 class="settings-v2-section-title">{language.t("settings.general.section.advanced")}</h3>
|
<h3 class="settings-v2-section-title">{language.t("settings.general.section.advanced")}</h3>
|
||||||
|
|
||||||
<SettingsListV2>
|
<SettingsListV2>
|
||||||
|
<SettingsRowV2
|
||||||
|
title={language.t("settings.general.row.showFileTree.title")}
|
||||||
|
description={language.t("settings.general.row.showFileTree.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-file-tree">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showFileTree()}
|
||||||
|
onChange={(checked) => settings.general.setShowFileTree(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRowV2>
|
||||||
|
|
||||||
<SettingsRowV2
|
<SettingsRowV2
|
||||||
title={language.t("settings.general.row.showSearch.title")}
|
title={language.t("settings.general.row.showSearch.title")}
|
||||||
description={language.t("settings.general.row.showSearch.description")}
|
description={language.t("settings.general.row.showSearch.description")}
|
||||||
@@ -531,18 +510,18 @@ export const SettingsGeneral: Component<{
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="settings-v2-tab-header">
|
<div class="settings-v2-tab-header">
|
||||||
<div class="settings-v2-tab-header-row">
|
<h2 class="settings-v2-tab-title">{language.t("settings.tab.general")}</h2>
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.tab.preferences")}</h2>
|
|
||||||
<span class="text-11-regular text-v2-text-text-muted">
|
|
||||||
{language.t("settings.preferences.description")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-v2-tab-body">
|
<div class="settings-v2-tab-body">
|
||||||
<GeneralSection />
|
<GeneralSection />
|
||||||
|
|
||||||
|
<AppearanceSection controller={appearance} />
|
||||||
|
|
||||||
|
<NotificationsSection />
|
||||||
|
|
||||||
|
<SoundsSection controller={sounds} />
|
||||||
|
|
||||||
<Show when={desktop()}>
|
<Show when={desktop()}>
|
||||||
<UpdatesSection />
|
<UpdatesSection />
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useModels } from "@/context/models"
|
|||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { InlineServerSelect } from "./parts/server-select"
|
|
||||||
import { SettingsListV2 } from "./parts/list"
|
import { SettingsListV2 } from "./parts/list"
|
||||||
import { SettingsRowV2 } from "./parts/row"
|
import { SettingsRowV2 } from "./parts/row"
|
||||||
import "./settings-v2.css"
|
import "./settings-v2.css"
|
||||||
@@ -25,7 +24,7 @@ export const SettingsModelsV2: Component = () => {
|
|||||||
const models = useModels()
|
const models = useModels()
|
||||||
const serverSdk = useServerSDK()
|
const serverSdk = useServerSDK()
|
||||||
const [store, setStore] = persisted(
|
const [store, setStore] = persisted(
|
||||||
Persist.serverGlobal(serverSdk.scope, "settings-v2.models.providers"),
|
Persist.serverGlobal(serverSdk().scope, "settings-v2.models.providers"),
|
||||||
createStore({ collapsed: {} as Record<string, boolean> }),
|
createStore({ collapsed: {} as Record<string, boolean> }),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,13 +53,7 @@ export const SettingsModelsV2: Component = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.models.title")}</h2>
|
<h2 class="settings-v2-tab-title">{language.t("settings.models.title")}</h2>
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.models.description")}</span>
|
|
||||||
</div>
|
|
||||||
<InlineServerSelect />
|
|
||||||
</div>
|
|
||||||
<div class="settings-v2-tab-search">
|
<div class="settings-v2-tab-search">
|
||||||
<TextInputV2
|
<TextInputV2
|
||||||
type="search"
|
type="search"
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
import { Component } from "solid-js"
|
|
||||||
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
|
||||||
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
import { SettingsListV2 } from "./parts/list"
|
|
||||||
import { SettingsRowV2 } from "./parts/row"
|
|
||||||
import { createSoundSettingsController, soundOptions, type SoundSettingsController } from "./general-controllers"
|
|
||||||
import "./settings-v2.css"
|
|
||||||
|
|
||||||
const soundSettings = {
|
|
||||||
agent: {
|
|
||||||
action: "settings-sounds-agent",
|
|
||||||
title: "settings.general.sounds.agent.title",
|
|
||||||
description: "settings.general.sounds.agent.description",
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
action: "settings-sounds-permissions",
|
|
||||||
title: "settings.general.sounds.permissions.title",
|
|
||||||
description: "settings.general.sounds.permissions.description",
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
action: "settings-sounds-errors",
|
|
||||||
title: "settings.general.sounds.errors.title",
|
|
||||||
description: "settings.general.sounds.errors.description",
|
|
||||||
},
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const SoundSetting: Component<{
|
|
||||||
kind: "agent" | "permissions" | "errors"
|
|
||||||
channel: SoundSettingsController["agent"]
|
|
||||||
}> = (props) => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const config = () => soundSettings[props.kind]
|
|
||||||
return (
|
|
||||||
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
|
|
||||||
<SelectV2
|
|
||||||
appearance="inline"
|
|
||||||
data-action={config().action}
|
|
||||||
options={soundOptions}
|
|
||||||
current={props.channel.current()}
|
|
||||||
value={(option) => option.id}
|
|
||||||
label={(option) => language.t(option.label)}
|
|
||||||
onHighlight={props.channel.highlight}
|
|
||||||
onSelect={props.channel.select}
|
|
||||||
placement="bottom-end"
|
|
||||||
gutter={6}
|
|
||||||
/>
|
|
||||||
</SettingsRowV2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsNotificationsV2: Component = () => {
|
|
||||||
const language = useLanguage()
|
|
||||||
const settings = useSettings()
|
|
||||||
const sounds = createSoundSettingsController()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="settings-v2-tab-header">
|
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.tab.notifications")}</h2>
|
|
||||||
<span class="text-11-regular text-v2-text-text-muted">
|
|
||||||
{language.t("settings.notifications.description")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-v2-tab-body">
|
|
||||||
<div class="settings-v2-section">
|
|
||||||
<h3 class="settings-v2-section-title">{language.t("settings.general.section.notifications")}</h3>
|
|
||||||
<SettingsListV2>
|
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.general.notifications.agent.title")}
|
|
||||||
description={language.t("settings.general.notifications.agent.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-notifications-agent">
|
|
||||||
<Switch
|
|
||||||
checked={settings.notifications.agent()}
|
|
||||||
onChange={(checked) => settings.notifications.setAgent(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRowV2>
|
|
||||||
|
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.general.notifications.permissions.title")}
|
|
||||||
description={language.t("settings.general.notifications.permissions.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-notifications-permissions">
|
|
||||||
<Switch
|
|
||||||
checked={settings.notifications.permissions()}
|
|
||||||
onChange={(checked) => settings.notifications.setPermissions(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRowV2>
|
|
||||||
|
|
||||||
<SettingsRowV2
|
|
||||||
title={language.t("settings.general.notifications.errors.title")}
|
|
||||||
description={language.t("settings.general.notifications.errors.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-notifications-errors">
|
|
||||||
<Switch
|
|
||||||
checked={settings.notifications.errors()}
|
|
||||||
onChange={(checked) => settings.notifications.setErrors(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRowV2>
|
|
||||||
</SettingsListV2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-v2-section">
|
|
||||||
<h3 class="settings-v2-section-title">{language.t("settings.general.section.sounds")}</h3>
|
|
||||||
<SettingsListV2>
|
|
||||||
<SoundSetting kind="agent" channel={sounds.agent} />
|
|
||||||
<SoundSetting kind="permissions" channel={sounds.permissions} />
|
|
||||||
<SoundSetting kind="errors" channel={sounds.errors} />
|
|
||||||
</SettingsListV2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { Show, createMemo, type Component } from "solid-js"
|
|
||||||
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
|
|
||||||
import { useGlobal } from "@/context/global"
|
|
||||||
import { ServerConnection, serverName } from "@/context/servers"
|
|
||||||
|
|
||||||
const allServers = { type: "all" } as const
|
|
||||||
type ServerOption = ServerConnection.Any | typeof allServers
|
|
||||||
|
|
||||||
export const InlineServerSelect: Component<{
|
|
||||||
all?: {
|
|
||||||
label: string
|
|
||||||
selected: () => boolean
|
|
||||||
onSelect: () => void
|
|
||||||
}
|
|
||||||
onServerSelect?: () => void
|
|
||||||
}> = (props) => {
|
|
||||||
const global = useGlobal()
|
|
||||||
const options = createMemo<ServerOption[]>(() => [...(props.all ? [allServers] : []), ...global.servers.list()])
|
|
||||||
const current = () => (props.all?.selected() ? allServers : global.settings.server.selected())
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show when={options().length > 1}>
|
|
||||||
<SelectV2
|
|
||||||
appearance="inline"
|
|
||||||
data-action="settings-server-select"
|
|
||||||
options={options()}
|
|
||||||
current={current()}
|
|
||||||
value={(server) => (server.type === "all" ? server.type : ServerConnection.key(server))}
|
|
||||||
label={(server) =>
|
|
||||||
server.type === "all" ? (props.all?.label ?? "") : serverName(server) || ServerConnection.key(server)
|
|
||||||
}
|
|
||||||
optionDisabled={(server) =>
|
|
||||||
server.type === "all" ? false : global.servers.health[ServerConnection.key(server)]?.healthy === false
|
|
||||||
}
|
|
||||||
placement="bottom-end"
|
|
||||||
gutter={6}
|
|
||||||
onSelect={(server) => {
|
|
||||||
if (!server) return
|
|
||||||
if (server.type === "all") {
|
|
||||||
props.all?.onSelect()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
global.settings.server.set(ServerConnection.key(server))
|
|
||||||
props.onServerSelect?.()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
||||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useGlobal } from "@/context/global"
|
|
||||||
import { getProjectAvatarVariant } from "@/context/layout"
|
|
||||||
import { ServerConnection, serverName } from "@/context/servers"
|
|
||||||
import { displayName } from "@/pages/layout/helpers"
|
|
||||||
import { InlineServerSelect } from "./parts/server-select"
|
|
||||||
import { DialogEditProjectV2 } from "../dialog-edit-project-v2"
|
|
||||||
import "./settings-v2.css"
|
|
||||||
|
|
||||||
export const SettingsProjectsV2: Component = () => {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const language = useLanguage()
|
|
||||||
const global = useGlobal()
|
|
||||||
const [allServers, setAllServers] = createSignal(true)
|
|
||||||
const selected = global.settings.server.selected
|
|
||||||
const projects = createMemo(() => {
|
|
||||||
const server = selected()
|
|
||||||
if (!server) return []
|
|
||||||
return global.ensureServerCtx(server).projects.list()
|
|
||||||
})
|
|
||||||
|
|
||||||
type ProjectItem = ReturnType<typeof projects>[number]
|
|
||||||
|
|
||||||
const groups = createMemo(() =>
|
|
||||||
global.servers
|
|
||||||
.list()
|
|
||||||
.map((server) => ({ server, projects: global.ensureServerCtx(server).projects.list() }))
|
|
||||||
.filter((group) => group.projects.length > 0),
|
|
||||||
)
|
|
||||||
|
|
||||||
const openProjectSettings = (project: ProjectItem, server = selected()) => {
|
|
||||||
if (!server) return
|
|
||||||
dialog.push(() => <DialogEditProjectV2 project={project} server={server} />)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectRow: Component<{ project: ProjectItem; server: ServerConnection.Any }> = (props) => {
|
|
||||||
const name = () => displayName(props.project)
|
|
||||||
const color = () => getProjectAvatarVariant(props.project.icon?.color)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="group flex items-center justify-between gap-5 px-4 py-2.5 rounded-lg bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)] cursor-pointer transition-all hover:bg-v2-background-bg-layer-01"
|
|
||||||
onClick={() => openProjectSettings(props.project, props.server)}
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2.5 min-w-0 flex-1">
|
|
||||||
<ProjectAvatar fallback={name()} variant={color()} class="shrink-0" />
|
|
||||||
<span class="text-13-medium text-v2-text-text-base truncate">{name()}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<IconButtonV2
|
|
||||||
type="button"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
icon={<IconV2 name="settings-gear" size="small" class="text-v2-icon-icon-muted" />}
|
|
||||||
onClick={(event: MouseEvent) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
openProjectSettings(props.project, props.server)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="settings-v2-tab-header">
|
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.projects.title")}</h2>
|
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.projects.description")}</span>
|
|
||||||
</div>
|
|
||||||
<InlineServerSelect
|
|
||||||
all={{
|
|
||||||
label: language.t("settings.projects.server.all"),
|
|
||||||
selected: allServers,
|
|
||||||
onSelect: () => setAllServers(true),
|
|
||||||
}}
|
|
||||||
onServerSelect={() => setAllServers(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-v2-tab-body">
|
|
||||||
<Show
|
|
||||||
when={allServers()}
|
|
||||||
fallback={
|
|
||||||
<div class="flex flex-col gap-2 w-full">
|
|
||||||
<Show
|
|
||||||
when={projects().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="py-12 text-center text-v2-text-text-muted text-13-regular">
|
|
||||||
{language.t("settings.projects.empty")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Show when={selected()} keyed>
|
|
||||||
{(server) => (
|
|
||||||
<For each={projects()}>{(project) => <ProjectRow project={project} server={server} />}</For>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-8 w-full">
|
|
||||||
<Show
|
|
||||||
when={groups().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="py-12 text-center text-v2-text-text-muted text-13-regular">
|
|
||||||
{language.t("settings.projects.empty")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={groups()}>
|
|
||||||
{(group) => (
|
|
||||||
<div class="settings-v2-section">
|
|
||||||
<h3 class="settings-v2-section-title">
|
|
||||||
{serverName(group.server) || ServerConnection.key(group.server)}
|
|
||||||
</h3>
|
|
||||||
<div class="flex flex-col gap-2 w-full">
|
|
||||||
<For each={group.projects}>
|
|
||||||
{(project) => <ProjectRow project={project} server={group.server} />}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,14 +4,12 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
import { createMemo, type Component, For, Show } from "solid-js"
|
import { createMemo, type Accessor, type Component, For, Show } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider"
|
import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider"
|
||||||
import { DialogCustomProvider } from "../dialog-custom-provider"
|
import { DialogCustomProvider } from "../dialog-custom-provider"
|
||||||
import { SettingsServerScope } from "../settings-server-picker"
|
|
||||||
import { InlineServerSelect } from "./parts/server-select"
|
|
||||||
import { SettingsListV2 } from "./parts/list"
|
import { SettingsListV2 } from "./parts/list"
|
||||||
import "./settings-v2.css"
|
import "./settings-v2.css"
|
||||||
|
|
||||||
@@ -32,35 +30,25 @@ const PROVIDER_NOTES = [
|
|||||||
const PROVIDER_ICON_SIZE = 16
|
const PROVIDER_ICON_SIZE = 16
|
||||||
|
|
||||||
export const SettingsProvidersV2: Component<{
|
export const SettingsProvidersV2: Component<{
|
||||||
directory: string | undefined
|
directory: Accessor<string | undefined>
|
||||||
onBack?: () => void
|
onBack?: () => void
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const serverSdk = useServerSDK()
|
const serverSdk = useServerSDK()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const providers = useProviders(() => props.directory)
|
const providers = useProviders(props.directory)
|
||||||
const providerConnect = useProviderConnectController({ onBack: props.onBack })
|
const providerConnect = useProviderConnectController({ onBack: props.onBack })
|
||||||
|
|
||||||
const connect = (provider?: string) => {
|
const connect = (provider?: string) => {
|
||||||
providerConnect.select(provider)
|
providerConnect.select(provider)
|
||||||
void dialog.show(() => (
|
void dialog.show(() => <DialogConnectProvider directory={props.directory} controller={providerConnect} />)
|
||||||
<SettingsServerScope directory={props.directory}>
|
|
||||||
<DialogConnectProvider directory={props.directory} controller={providerConnect} />
|
|
||||||
</SettingsServerScope>
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const connected = createMemo(() => {
|
const connected = createMemo(() => {
|
||||||
return providers.connected().filter(
|
return providers
|
||||||
(provider) =>
|
.connected()
|
||||||
provider.id !== "opencode" ||
|
.filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
|
||||||
Object.values(provider.models).some((model) => {
|
|
||||||
if (typeof model !== "object" || model === null || !("cost" in model)) return false
|
|
||||||
const cost = model.cost
|
|
||||||
return typeof cost === "object" && cost !== null && "input" in cost
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const popular = createMemo(() => {
|
const popular = createMemo(() => {
|
||||||
@@ -97,22 +85,45 @@ export const SettingsProvidersV2: Component<{
|
|||||||
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
|
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
|
||||||
|
|
||||||
const isConfigCustom = (providerID: string) => {
|
const isConfigCustom = (providerID: string) => {
|
||||||
const provider = serverSync.data.config.provider?.[providerID]
|
const provider = serverSync().data.config.provider?.[providerID]
|
||||||
if (!provider) return false
|
if (!provider) return false
|
||||||
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
if (provider.npm !== "@ai-sdk/openai-compatible") return false
|
||||||
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
if (!provider.models || Object.keys(provider.models).length === 0) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disableProvider = async (providerID: string, name: string) => {
|
||||||
|
return
|
||||||
|
const before = serverSync().data.config.disabled_providers ?? []
|
||||||
|
const next = before.includes(providerID) ? before : [...before, providerID]
|
||||||
|
serverSync().set("config", "disabled_providers", next)
|
||||||
|
|
||||||
|
await serverSync()
|
||||||
|
.updateConfig({ disabled_providers: next })
|
||||||
|
.then(() => {
|
||||||
|
showToast({
|
||||||
|
variant: "success",
|
||||||
|
icon: "circle-check",
|
||||||
|
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
|
||||||
|
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
serverSync().set("config", "disabled_providers", before)
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const disconnect = async (providerID: string, name: string) => {
|
const disconnect = async (providerID: string, name: string) => {
|
||||||
const location = props.directory ? { directory: props.directory } : undefined
|
const location = props.directory() ? { directory: props.directory() } : undefined
|
||||||
await serverSdk.api.integration
|
await serverSdk()
|
||||||
.get({ integrationID: providerID, location })
|
.api.integration.get({ integrationID: providerID, location })
|
||||||
.then(async (integration) => {
|
.then(async (integration) => {
|
||||||
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
|
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
|
||||||
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
|
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id, location })),
|
credentials.map((credential) => serverSdk().api.credential.remove({ credentialID: credential.id, location })),
|
||||||
)
|
)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
@@ -130,13 +141,7 @@ export const SettingsProvidersV2: Component<{
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="settings-v2-tab-header">
|
<div class="settings-v2-tab-header">
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.providers.title")}</h2>
|
<h2 class="settings-v2-tab-title">{language.t("settings.providers.title")}</h2>
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.providers.description")}</span>
|
|
||||||
</div>
|
|
||||||
<InlineServerSelect />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-v2-tab-body settings-v2-providers">
|
<div class="settings-v2-tab-body settings-v2-providers">
|
||||||
@@ -239,11 +244,7 @@ export const SettingsProvidersV2: Component<{
|
|||||||
variant="neutral"
|
variant="neutral"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dialog.show(() => (
|
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
|
||||||
<SettingsServerScope directory={props.directory}>
|
|
||||||
<DialogCustomProvider onBack={dialog.close} />
|
|
||||||
</SettingsServerScope>
|
|
||||||
))
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { createStore } from "solid-js/store"
|
|||||||
import { ServerRowMenu } from "@/components/server/server-row-menu"
|
import { ServerRowMenu } from "@/components/server/server-row-menu"
|
||||||
import { ServerHealthIndicator } from "@/components/server/server-row"
|
import { ServerHealthIndicator } from "@/components/server/server-row"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection, serverName } from "@/context/servers"
|
import { ServerConnection, serverName } from "@/context/server"
|
||||||
import { useServerCollectionController } from "../server/server-management-controller"
|
import { useServerCollectionController } from "../server/server-management-controller"
|
||||||
import { DialogServerV2 } from "./dialog-server-v2"
|
import { DialogServerV2 } from "./dialog-server-v2"
|
||||||
import { SettingsListV2 } from "./parts/list"
|
import { SettingsListV2 } from "./parts/list"
|
||||||
@@ -53,10 +53,7 @@ export const SettingsServersV2: Component = () => {
|
|||||||
classList={{ "settings-v2-tab-header--stacked": showSearch() }}
|
classList={{ "settings-v2-tab-header--stacked": showSearch() }}
|
||||||
>
|
>
|
||||||
<div class="settings-v2-tab-header-row">
|
<div class="settings-v2-tab-header-row">
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("status.popover.tab.servers")}</h2>
|
<h2 class="settings-v2-tab-title">{language.t("status.popover.tab.servers")}</h2>
|
||||||
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.servers.description")}</span>
|
|
||||||
</div>
|
|
||||||
<AddServerMenu onAddServer={openAdd} />
|
<AddServerMenu onAddServer={openAdd} />
|
||||||
</div>
|
</div>
|
||||||
<Show when={showSearch()}>
|
<Show when={showSearch()}>
|
||||||
|
|||||||
@@ -39,14 +39,6 @@
|
|||||||
background: linear-gradient(to bottom, var(--v2-background-bg-base) calc(100% - 24px), transparent);
|
background: linear-gradient(to bottom, var(--v2-background-bg-base) calc(100% - 24px), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-v2-tab-header-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-tab-title {
|
.settings-v2-tab-title {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 640;
|
font-weight: 640;
|
||||||
@@ -182,13 +174,13 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] > [data-slot="tabs-v2-list"] {
|
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-list"] {
|
||||||
background-color: var(--v2-background-bg-layer-01);
|
background-color: var(--v2-background-bg-layer-01);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 639px) {
|
@media (max-width: 639px) {
|
||||||
.settings-v2[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
|
.settings-v2[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
|
||||||
> [data-slot="tabs-v2-list"] {
|
[data-slot="tabs-v2-list"] {
|
||||||
width: 144px;
|
width: 144px;
|
||||||
min-width: 144px;
|
min-width: 144px;
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
@@ -692,223 +684,6 @@
|
|||||||
color: var(--v2-text-text-base);
|
color: var(--v2-text-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-v2-tab-header.settings-v2-workspaces-header {
|
|
||||||
padding-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-header .settings-v2-tab-title {
|
|
||||||
font-weight: 610;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-tab-body.settings-v2-workspaces {
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-toolbar {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-count {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 530;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-toolbar-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-delete-all {
|
|
||||||
color: var(--v2-state-fg-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-inventory [data-component="settings-v2-list"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-color: var(--v2-background-bg-base);
|
|
||||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-row {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-row:not(:last-child) {
|
|
||||||
padding-bottom: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 0.5px solid var(--v2-border-border-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-row-header {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-copy {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-main {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-row-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-main [data-component="tooltip-v2-trigger"] {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-path {
|
|
||||||
display: block;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 530;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.04px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0;
|
|
||||||
text-align: left;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-meta {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 440;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--v2-text-text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-active,
|
|
||||||
.settings-v2-workspaces-more {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 440;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--v2-text-text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-sessions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 0.5px solid var(--v2-border-border-base);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--v2-background-bg-base);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-session {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 440;
|
|
||||||
line-height: 16px;
|
|
||||||
color: var(--v2-text-text-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-session:not(:last-child) {
|
|
||||||
border-bottom: 0.5px solid var(--v2-border-border-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-session > span:first-child {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-session-time {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding-block: 48px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 440;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--v2-text-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 639px) {
|
|
||||||
.settings-v2-workspaces-header {
|
|
||||||
padding: 24px 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-tab-body.settings-v2-workspaces {
|
|
||||||
padding: 0 20px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-toolbar,
|
|
||||||
.settings-v2-workspaces-main {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-toolbar {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-toolbar-actions {
|
|
||||||
width: 100%;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-inventory [data-component="settings-v2-list"] {
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-path {
|
|
||||||
overflow: visible;
|
|
||||||
text-overflow: clip;
|
|
||||||
white-space: normal;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-workspaces-active {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-container"] {
|
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-container"] {
|
||||||
width: 480px;
|
width: 480px;
|
||||||
max-width: calc(100vw - 32px);
|
max-width: calc(100vw - 32px);
|
||||||
@@ -952,24 +727,3 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--v2-state-fg-danger);
|
color: var(--v2-state-fg-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"] {
|
|
||||||
width: 280px;
|
|
||||||
padding-inline: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"]::before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"]
|
|
||||||
> [data-slot="tabs-v2-list"]
|
|
||||||
[data-slot="tabs-v2-trigger-wrapper"] {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"]
|
|
||||||
> [data-slot="tabs-v2-list"]
|
|
||||||
[data-slot="tabs-v2-trigger"] {
|
|
||||||
padding-inline: 8px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,502 +0,0 @@
|
|||||||
import type { Component } from "solid-js"
|
|
||||||
import { For, Show, createMemo } from "solid-js"
|
|
||||||
import { createStore, produce } from "solid-js/store"
|
|
||||||
import type { SessionInfo } from "@opencode-ai/client/promise"
|
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
|
||||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
|
||||||
import { Dialog, DialogFooter, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2"
|
|
||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
|
||||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
|
||||||
import { useLanguage } from "@/context/language"
|
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
|
||||||
import { useServerSync } from "@/context/server-sync"
|
|
||||||
import { showToast } from "@/utils/toast"
|
|
||||||
import { getRelativeTime } from "@/utils/time"
|
|
||||||
import { pathKey } from "@/utils/path-key"
|
|
||||||
import { SettingsListV2 } from "./parts/list"
|
|
||||||
import { InlineServerSelect } from "./parts/server-select"
|
|
||||||
import { useTabs } from "@/context/tabs"
|
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { clearWorkspaceTerminals } from "@/context/terminal"
|
|
||||||
import { ServerConnection } from "@/context/servers"
|
|
||||||
import type { Project } from "@/types"
|
|
||||||
import {
|
|
||||||
containsDirectory,
|
|
||||||
filterWorkspaceInventory,
|
|
||||||
inspectWorkspaceDeletion,
|
|
||||||
managedWorkspaceDirectories,
|
|
||||||
mergeWorkspaceSessionInventory,
|
|
||||||
removeWorkspacesSequentially,
|
|
||||||
sessionsForWorkspace,
|
|
||||||
type WorkspaceDeleteInspection,
|
|
||||||
workspaceInventory,
|
|
||||||
} from "@/utils/workspace"
|
|
||||||
import { listAllSessions } from "@/utils/session"
|
|
||||||
import type { ServerScope } from "@/utils/server-scope"
|
|
||||||
import "./settings-v2.css"
|
|
||||||
|
|
||||||
type Workspace = {
|
|
||||||
directory: string
|
|
||||||
project: Project
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsWorkspacesV2: Component<{ activeDirectory?: string }> = (props) => {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const language = useLanguage()
|
|
||||||
const serverSDK = useServerSDK()
|
|
||||||
const serverSync = useServerSync()
|
|
||||||
const tabs = useTabs()
|
|
||||||
const platform = usePlatform()
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
project: "all",
|
|
||||||
transaction: undefined as "confirm" | "running" | undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const workspaces = createMemo(() => workspaceInventory(serverSync.data.project))
|
|
||||||
const projects = createMemo(() =>
|
|
||||||
serverSync.data.project.filter((project) => managedWorkspaceDirectories(project).length > 0),
|
|
||||||
)
|
|
||||||
const projectName = (project: Project) => project.name || getFilename(project.worktree)
|
|
||||||
const projectOptions = createMemo(() => [
|
|
||||||
{ id: "all", label: language.t("settings.workspaces.filter.all") },
|
|
||||||
...projects().map((project) => ({ id: project.id, label: projectName(project) })),
|
|
||||||
])
|
|
||||||
const selectedProject = createMemo(() =>
|
|
||||||
store.project === "all" || projects().some((project) => project.id === store.project) ? store.project : "all",
|
|
||||||
)
|
|
||||||
const filtered = createMemo(() => filterWorkspaceInventory(workspaces(), selectedProject()))
|
|
||||||
const captureDeleteContext = () => {
|
|
||||||
const sdk = serverSDK
|
|
||||||
return { sdk, sync: serverSync, server: ServerConnection.key(sdk.server), activeDirectory: props.activeDirectory }
|
|
||||||
}
|
|
||||||
const loadSessions = async (context = captureDeleteContext()) => {
|
|
||||||
const fetched = await listAllSessions(context.sdk.api.session, { order: "desc" })
|
|
||||||
return mergeWorkspaceSessionInventory(
|
|
||||||
fetched,
|
|
||||||
Object.values(context.sync.session.data.info).filter((session): session is SessionInfo => !!session),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const sessionQuery = useQuery(() => ({
|
|
||||||
queryKey: [serverSDK.scope, null, "settings-workspace-sessions"] as const,
|
|
||||||
queryFn: () => loadSessions(),
|
|
||||||
refetchOnMount: "always",
|
|
||||||
}))
|
|
||||||
const sessionsByWorkspace = createMemo(
|
|
||||||
() =>
|
|
||||||
new Map(
|
|
||||||
workspaces().map((workspace) => [
|
|
||||||
pathKey(workspace.directory),
|
|
||||||
sessionQuery.isSuccess ? sessionsForWorkspace(sessionQuery.data ?? [], workspace.directory) : [],
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const workspaceSessions = (workspace: Workspace) => sessionsByWorkspace().get(pathKey(workspace.directory)) ?? []
|
|
||||||
const sessionCount = (workspace: Workspace) => {
|
|
||||||
if (sessionQuery.isPending) return language.t("session.messages.loading")
|
|
||||||
if (sessionQuery.isError) return language.t("common.requestFailed")
|
|
||||||
const count = workspaceSessions(workspace).length
|
|
||||||
return language.plural("settings.workspaces.sessions", count, {
|
|
||||||
count,
|
|
||||||
project: projectName(workspace.project),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const lastActive = (workspace: Workspace) => {
|
|
||||||
const updated = workspaceSessions(workspace)[0]?.time.updated
|
|
||||||
if (!updated) return undefined
|
|
||||||
return getRelativeTime(new Date(updated).toISOString(), language.t)
|
|
||||||
}
|
|
||||||
const sessionTime = (session: SessionInfo) => {
|
|
||||||
if (!session.time.updated) return undefined
|
|
||||||
return getRelativeTime(new Date(session.time.updated).toISOString(), language.t)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inspect = async (workspace: Workspace, context = captureDeleteContext()) => {
|
|
||||||
const [working, branch, sessions] = await Promise.all([
|
|
||||||
context.sdk.api.vcs.status({ location: { directory: workspace.directory } }),
|
|
||||||
context.sdk.api.vcs.diff({ location: { directory: workspace.directory }, mode: "branch" }),
|
|
||||||
loadSessions(context),
|
|
||||||
])
|
|
||||||
const result = inspectWorkspaceDeletion({
|
|
||||||
workspace: workspace.directory,
|
|
||||||
activeDirectory: context.activeDirectory,
|
|
||||||
sessions,
|
|
||||||
status: working.data.length > 0 || branch.data.length > 0 ? "dirty" : "clean",
|
|
||||||
})
|
|
||||||
return { result, sessions }
|
|
||||||
}
|
|
||||||
const inspectionMessages = (result: WorkspaceDeleteInspection) => {
|
|
||||||
const messages = [
|
|
||||||
result.active ? language.t("settings.workspaces.delete.blocked.active") : undefined,
|
|
||||||
result.linked ? language.t("settings.workspaces.delete.blocked.linked") : undefined,
|
|
||||||
result.dirty ? language.t("workspace.status.dirty") : undefined,
|
|
||||||
].filter((message): message is string => message !== undefined)
|
|
||||||
return messages.length > 0 ? messages : [language.t("workspace.status.clean")]
|
|
||||||
}
|
|
||||||
const blocked = (result: WorkspaceDeleteInspection) => {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("workspace.delete.failed.title"),
|
|
||||||
description: inspectionMessages(result)[0],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const remove = async (workspace: Workspace, force = false, context = captureDeleteContext()) => {
|
|
||||||
const preflight = await inspect(workspace, context)
|
|
||||||
if (preflight.result.active || (!force && (preflight.result.linked || preflight.result.dirty))) {
|
|
||||||
blocked(preflight.result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const removed = await context.sdk.api.worktree
|
|
||||||
.remove({
|
|
||||||
projectID: workspace.project.id,
|
|
||||||
directory: workspace.directory,
|
|
||||||
force,
|
|
||||||
})
|
|
||||||
.then(() => true)
|
|
||||||
.catch((error) => {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("workspace.delete.failed.title"),
|
|
||||||
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if (!removed) return
|
|
||||||
tabs.store.forEach((tab) => {
|
|
||||||
if (tab.type !== "draft" || tab.server !== context.server) return
|
|
||||||
const directoryMatches = containsDirectory(workspace.directory, tab.directory)
|
|
||||||
const worktreeMatches = tab.worktree && containsDirectory(workspace.directory, tab.worktree)
|
|
||||||
if (!directoryMatches && !worktreeMatches) return
|
|
||||||
tabs.updateDraft(tab.draftID, {
|
|
||||||
directory: directoryMatches ? workspace.project.worktree : tab.directory,
|
|
||||||
worktree: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
clearWorkspaceTerminals(
|
|
||||||
workspace.directory,
|
|
||||||
preflight.sessions.map((session) => session.id),
|
|
||||||
platform,
|
|
||||||
context.sdk.scope,
|
|
||||||
)
|
|
||||||
context.sync.set(
|
|
||||||
"project",
|
|
||||||
produce((draft) => {
|
|
||||||
const project = draft.find((item) => item.id === workspace.project.id)
|
|
||||||
if (!project) return
|
|
||||||
project.sandboxes = (project.sandboxes ?? []).filter(
|
|
||||||
(directory) => pathKey(directory) !== pathKey(workspace.directory),
|
|
||||||
)
|
|
||||||
project.worktrees = project.worktrees.filter(
|
|
||||||
(worktree) => pathKey(worktree.directory) !== pathKey(workspace.directory),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let inspectionID = 0
|
|
||||||
const releaseConfirmation = () => {
|
|
||||||
if (store.transaction === "confirm") setStore("transaction", undefined)
|
|
||||||
}
|
|
||||||
const transact = async (task: () => Promise<void>) => {
|
|
||||||
if (store.transaction !== "confirm") return
|
|
||||||
setStore("transaction", "running")
|
|
||||||
try {
|
|
||||||
await task()
|
|
||||||
} catch (error) {
|
|
||||||
showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: language.t("workspace.delete.failed.title"),
|
|
||||||
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setStore("transaction", undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const confirmDelete = (workspace: Workspace) => {
|
|
||||||
if (store.transaction) return
|
|
||||||
const context = captureDeleteContext()
|
|
||||||
const current = ++inspectionID
|
|
||||||
setStore("transaction", "confirm")
|
|
||||||
void dialog.push(
|
|
||||||
() => (
|
|
||||||
<DialogDeleteWorkspace
|
|
||||||
workspace={workspace}
|
|
||||||
scope={context.sdk.scope}
|
|
||||||
inspectionID={current}
|
|
||||||
inspect={() => inspect(workspace, context)}
|
|
||||||
inspectionMessages={inspectionMessages}
|
|
||||||
onDelete={() => transact(() => remove(workspace, true, context))}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
releaseConfirmation,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const removeAll = async (inventory: Workspace[], context: ReturnType<typeof captureDeleteContext>) => {
|
|
||||||
await removeWorkspacesSequentially(inventory, (workspace) => remove(workspace, false, context))
|
|
||||||
}
|
|
||||||
const confirmDeleteAll = () => {
|
|
||||||
if (store.transaction) return
|
|
||||||
const context = captureDeleteContext()
|
|
||||||
const inventory = [...filtered()]
|
|
||||||
const project = projectOptions().find((option) => option.id === selectedProject())?.label ?? selectedProject()
|
|
||||||
setStore("transaction", "confirm")
|
|
||||||
void dialog.push(
|
|
||||||
() => (
|
|
||||||
<DialogDeleteAllWorkspaces
|
|
||||||
count={inventory.length}
|
|
||||||
project={project}
|
|
||||||
onDelete={() => transact(() => removeAll(inventory, context))}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
releaseConfirmation,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="settings-v2-tab-header settings-v2-workspaces-header">
|
|
||||||
<div class="settings-v2-tab-header-row">
|
|
||||||
<h2 class="settings-v2-tab-title">{language.t("settings.tab.workspaces")}</h2>
|
|
||||||
<InlineServerSelect />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-v2-tab-body settings-v2-workspaces">
|
|
||||||
<div class="settings-v2-workspaces-toolbar">
|
|
||||||
<span class="settings-v2-workspaces-count">
|
|
||||||
{language.plural("settings.workspaces.count", filtered().length)}
|
|
||||||
</span>
|
|
||||||
<div class="settings-v2-workspaces-toolbar-actions">
|
|
||||||
<Show when={projects().length > 1}>
|
|
||||||
<MenuV2 placement="bottom-end" gutter={6}>
|
|
||||||
<MenuV2.Trigger class="flex h-6 max-w-48 items-center gap-1 rounded-sm px-2 text-13-medium hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed">
|
|
||||||
<span class="min-w-0 truncate">
|
|
||||||
{projectOptions().find((option) => option.id === selectedProject())?.label}
|
|
||||||
</span>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
||||||
</MenuV2.Trigger>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content>
|
|
||||||
<For each={projectOptions()}>
|
|
||||||
{(option) => (
|
|
||||||
<MenuV2.Item onSelect={() => setStore("project", option.id)}>
|
|
||||||
<span class="min-w-0 flex-1 truncate">{option.label}</span>
|
|
||||||
<Show when={selectedProject() === option.id}>
|
|
||||||
<Icon name="check" size="small" class="shrink-0" />
|
|
||||||
</Show>
|
|
||||||
</MenuV2.Item>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
</Show>
|
|
||||||
<Show when={filtered().length > 0}>
|
|
||||||
<MenuV2 placement="bottom-end" gutter={4}>
|
|
||||||
<MenuV2.Trigger
|
|
||||||
as={IconButtonV2}
|
|
||||||
type="button"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
aria-label={language.t("common.moreOptions")}
|
|
||||||
disabled={!!store.transaction}
|
|
||||||
icon={<Icon name="outline-dots" size="small" />}
|
|
||||||
/>
|
|
||||||
<MenuV2.Portal>
|
|
||||||
<MenuV2.Content>
|
|
||||||
<MenuV2.Item onSelect={confirmDeleteAll}>
|
|
||||||
<span class="settings-v2-workspaces-delete-all">
|
|
||||||
{language.t("settings.workspaces.deleteAll")}
|
|
||||||
</span>
|
|
||||||
</MenuV2.Item>
|
|
||||||
</MenuV2.Content>
|
|
||||||
</MenuV2.Portal>
|
|
||||||
</MenuV2>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-v2-workspaces-inventory">
|
|
||||||
<Show
|
|
||||||
when={filtered().length > 0}
|
|
||||||
fallback={<div class="settings-v2-workspaces-empty">{language.t("settings.workspaces.empty")}</div>}
|
|
||||||
>
|
|
||||||
<SettingsListV2>
|
|
||||||
<For each={filtered()}>
|
|
||||||
{(workspace) => {
|
|
||||||
const linked = () => workspaceSessions(workspace)
|
|
||||||
return (
|
|
||||||
<div class="settings-v2-workspaces-row">
|
|
||||||
<div class="settings-v2-workspaces-row-header">
|
|
||||||
<div class="settings-v2-workspaces-copy">
|
|
||||||
<div class="settings-v2-workspaces-main">
|
|
||||||
<TooltipV2
|
|
||||||
value={workspace.directory}
|
|
||||||
placement="top-start"
|
|
||||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
tabIndex={0}
|
|
||||||
dir="ltr"
|
|
||||||
aria-label={workspace.directory}
|
|
||||||
class="settings-v2-workspaces-path"
|
|
||||||
>
|
|
||||||
{workspace.directory}
|
|
||||||
</span>
|
|
||||||
</TooltipV2>
|
|
||||||
</div>
|
|
||||||
<span class="settings-v2-workspaces-meta">{sessionCount(workspace)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-v2-workspaces-row-actions">
|
|
||||||
<Show when={lastActive(workspace)}>
|
|
||||||
{(value) => (
|
|
||||||
<TooltipV2
|
|
||||||
value={language.t("settings.workspaces.lastActiveSession")}
|
|
||||||
placement="top-end"
|
|
||||||
>
|
|
||||||
<span tabIndex={0} class="settings-v2-workspaces-active">
|
|
||||||
{value()}
|
|
||||||
</span>
|
|
||||||
</TooltipV2>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<IconButtonV2
|
|
||||||
type="button"
|
|
||||||
variant="ghost-muted"
|
|
||||||
size="small"
|
|
||||||
aria-label={language.t("workspace.delete.confirm", {
|
|
||||||
name: getFilename(workspace.directory),
|
|
||||||
})}
|
|
||||||
disabled={!!store.transaction}
|
|
||||||
icon={<Icon name="trash" size="small" />}
|
|
||||||
onClick={() => confirmDelete(workspace)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Show when={linked().length > 0}>
|
|
||||||
<div class="settings-v2-workspaces-sessions">
|
|
||||||
<For each={linked()}>
|
|
||||||
{(session) => (
|
|
||||||
<div class="settings-v2-workspaces-session">
|
|
||||||
<span>{session.title}</span>
|
|
||||||
<Show when={sessionTime(session)}>
|
|
||||||
{(time) => <span class="settings-v2-workspaces-session-time">{time()}</span>}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</SettingsListV2>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDeleteAllWorkspaces(props: { count: number; project: string; onDelete: () => Promise<void> }) {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const language = useLanguage()
|
|
||||||
const remove = () => {
|
|
||||||
const deleting = props.onDelete()
|
|
||||||
dialog.close()
|
|
||||||
void deleting
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog fit>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitleGroup
|
|
||||||
title={language.t("settings.workspaces.deleteAll")}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
{language.t("settings.workspaces.deleteAll.confirm", { count: props.count })}
|
|
||||||
<br />
|
|
||||||
{language.t("settings.workspaces.deleteAll.warning", { count: props.count, project: props.project })}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</ButtonV2>
|
|
||||||
<ButtonV2 type="button" variant="danger" onClick={remove}>
|
|
||||||
{language.t("settings.workspaces.deleteAll")}
|
|
||||||
</ButtonV2>
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDeleteWorkspace(props: {
|
|
||||||
workspace: Workspace
|
|
||||||
scope: ServerScope
|
|
||||||
inspectionID: number
|
|
||||||
inspect: () => Promise<{ result: WorkspaceDeleteInspection; sessions: SessionInfo[] }>
|
|
||||||
inspectionMessages: (result: WorkspaceDeleteInspection) => string[]
|
|
||||||
onDelete: () => Promise<void>
|
|
||||||
}) {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const language = useLanguage()
|
|
||||||
const status = useQuery(() => ({
|
|
||||||
queryKey: [props.scope, pathKey(props.workspace.directory), "workspace-delete-status", props.inspectionID] as const,
|
|
||||||
queryFn: props.inspect,
|
|
||||||
staleTime: 0,
|
|
||||||
}))
|
|
||||||
const descriptions = () => {
|
|
||||||
if (status.isPending) return [language.t("workspace.status.checking")]
|
|
||||||
if (status.isError) return [language.t("workspace.status.error")]
|
|
||||||
if (!status.data) return []
|
|
||||||
return props.inspectionMessages(status.data.result)
|
|
||||||
}
|
|
||||||
const remove = () => {
|
|
||||||
const deleting = props.onDelete()
|
|
||||||
dialog.close()
|
|
||||||
void deleting
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog fit>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitleGroup
|
|
||||||
title={language.t("workspace.delete.title")}
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
{language.t("workspace.delete.confirm", { name: getFilename(props.workspace.directory) })}
|
|
||||||
<br />
|
|
||||||
<code class="max-w-full rounded-[4px] bg-[color-mix(in_oklch,var(--v2-text-text-base)_8%,transparent)] px-1 py-0.5 font-mono text-xs font-medium leading-4 text-v2-text-text-base break-all">
|
|
||||||
{props.workspace.directory}
|
|
||||||
</code>
|
|
||||||
<br />
|
|
||||||
{language.t("settings.workspaces.delete.warning")}
|
|
||||||
<For each={descriptions()}>{(description) => <div>{description}</div>}</For>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<ButtonV2 type="button" variant="neutral" onClick={() => dialog.close()}>
|
|
||||||
{language.t("common.cancel")}
|
|
||||||
</ButtonV2>
|
|
||||||
<ButtonV2
|
|
||||||
type="button"
|
|
||||||
variant="danger"
|
|
||||||
disabled={status.isPending || status.isError || status.data?.result.active}
|
|
||||||
onClick={remove}
|
|
||||||
>
|
|
||||||
{language.t("workspace.delete.button")}
|
|
||||||
</ButtonV2>
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { createEffect, createMemo, createResource, For, type JSXElement, onCleanup, Show } from "solid-js"
|
import {
|
||||||
|
type Accessor,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createResource,
|
||||||
|
For,
|
||||||
|
type JSXElement,
|
||||||
|
onCleanup,
|
||||||
|
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 { ServerConnection, useServers } from "@/context/servers"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { type ServerHealth } from "@/utils/server-health"
|
import { type ServerHealth } from "@/utils/server-health"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useMcpToggle } from "@/context/mcp"
|
import { useMcpToggle } from "@/context/mcp"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useServer } from "@/context/server"
|
|
||||||
|
|
||||||
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
||||||
const parts = value.split(file)
|
const parts = value.split(file)
|
||||||
@@ -101,9 +109,12 @@ type ServerStatusItem = {
|
|||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusPopoverBody(props: { shown: boolean }) {
|
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const global = useGlobal()
|
||||||
|
const server = useServer()
|
||||||
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const fail = (err: unknown) => {
|
const fail = (err: unknown) => {
|
||||||
@@ -114,18 +125,29 @@ export function StatusPopoverBody(props: { shown: boolean }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.shown()) return
|
||||||
|
})
|
||||||
|
|
||||||
let dialogRun = 0
|
let dialogRun = 0
|
||||||
|
let dialogDead = false
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
dialogDead = true
|
||||||
dialogRun += 1
|
dialogRun += 1
|
||||||
})
|
})
|
||||||
|
const sortedServers = createMemo(() => {
|
||||||
|
const list = global.servers.list()
|
||||||
|
return listServersByHealth(list, server.key, global.servers.health)
|
||||||
|
})
|
||||||
const toggleMcp = useMcpToggle()
|
const toggleMcp = useMcpToggle()
|
||||||
|
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||||
const mcpNames = createMemo(() => Object.keys(sync().data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
const mcpNames = createMemo(() => Object.keys(sync().data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||||
const mcpStatus = (name: string) => sync().data.mcp?.[name]?.status
|
const mcpStatus = (name: string) => sync().data.mcp?.[name]?.status
|
||||||
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
||||||
const lspItems = createMemo(() => sync().data.lsp ?? [])
|
const lspItems = createMemo(() => sync().data.lsp ?? [])
|
||||||
const lspCount = createMemo(() => lspItems().length)
|
const lspCount = createMemo(() => lspItems().length)
|
||||||
const [pluginList] = createResource(
|
const [pluginList] = createResource(
|
||||||
() => (props.shown ? sdk().directory : undefined),
|
() => (props.shown() ? sdk().directory : undefined),
|
||||||
(directory) =>
|
(directory) =>
|
||||||
sdk()
|
sdk()
|
||||||
.api.plugin.list({ location: { directory } })
|
.api.plugin.list({ location: { directory } })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
|||||||
import { Popover } from "@opencode-ai/ui/popover"
|
import { Popover } from "@opencode-ai/ui/popover"
|
||||||
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection, useServers } from "@/context/servers"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
hasServiceNeedingAttention,
|
hasServiceNeedingAttention,
|
||||||
serverStatusDotClass,
|
serverStatusDotClass,
|
||||||
} from "./status-popover-indicator"
|
} from "./status-popover-indicator"
|
||||||
import { useServer } from "@/context/server"
|
|
||||||
|
|
||||||
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ export function StatusPopover() {
|
|||||||
<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />
|
<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Body shown={shown()} />
|
<Body shown={shown} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -83,12 +82,16 @@ export function StatusPopover() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StatusPopoverV2() {
|
export function StatusPopoverV2() {
|
||||||
|
return <DirectoryStatusPopover />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DirectoryStatusPopover() {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const server = useServer()
|
const server = useServerSDK()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const [shown, setShown] = createSignal(false)
|
const [shown, setShown] = createSignal(false)
|
||||||
const serverHealth = () => global.servers.health[server.key]?.healthy
|
const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy
|
||||||
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
||||||
const attention = createMemo(() =>
|
const attention = createMemo(() =>
|
||||||
hasServiceNeedingAttention({
|
hasServiceNeedingAttention({
|
||||||
@@ -111,7 +114,7 @@ export function StatusPopoverV2() {
|
|||||||
onOpenChange: setShown,
|
onOpenChange: setShown,
|
||||||
body: () => (
|
body: () => (
|
||||||
<StatusPopoverBody shown={shown()}>
|
<StatusPopoverBody shown={shown()}>
|
||||||
<Body shown={shown()} />
|
<Body shown={shown} />
|
||||||
</StatusPopoverBody>
|
</StatusPopoverBody>
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
// Intentional mount-time capture: the imperative xterm/WebSocket lifecycle needs stable values, and Terminal remounts when the SDK scope changes.
|
|
||||||
const directory = sdk().directory
|
const directory = sdk().directory
|
||||||
const url = sdk().url
|
const url = sdk().url
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { ServerConnection } from "@/context/servers"
|
import type { ServerConnection } from "@/context/server"
|
||||||
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "./titlebar-session-events"
|
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "./titlebar-session-events"
|
||||||
|
|
||||||
const remote = "remote" as ServerConnection.Key
|
const remote = "remote" as ServerConnection.Key
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ServerConnection } from "@/context/servers"
|
import type { ServerConnection } from "@/context/server"
|
||||||
|
|
||||||
export const SESSION_TABS_REMOVED_EVENT = "opencode:session-tabs-removed"
|
export const SESSION_TABS_REMOVED_EVENT = "opencode:session-tabs-removed"
|
||||||
|
|
||||||
export type SessionTabsRemovedDetail = {
|
export type SessionTabsRemovedDetail = {
|
||||||
server: ServerConnection.Key
|
server?: ServerConnection.Key
|
||||||
directory: string
|
directory: string
|
||||||
sessionIDs: string[]
|
sessionIDs: string[]
|
||||||
}
|
}
|
||||||
@@ -19,15 +19,16 @@ export function readSessionTabsRemovedDetail(event: Event): SessionTabsRemovedDe
|
|||||||
if (!detail || typeof detail !== "object") return undefined
|
if (!detail || typeof detail !== "object") return undefined
|
||||||
if (!("directory" in detail)) return undefined
|
if (!("directory" in detail)) return undefined
|
||||||
if (!("sessionIDs" in detail)) return undefined
|
if (!("sessionIDs" in detail)) return undefined
|
||||||
if (!("server" in detail) || typeof detail.server !== "string") return undefined
|
|
||||||
if (typeof detail.directory !== "string") return undefined
|
if (typeof detail.directory !== "string") return undefined
|
||||||
if (!Array.isArray(detail.sessionIDs)) return undefined
|
if (!Array.isArray(detail.sessionIDs)) return undefined
|
||||||
|
if ("server" in detail && detail.server !== undefined && typeof detail.server !== "string") return undefined
|
||||||
|
|
||||||
const sessionIDs = detail.sessionIDs.filter((id): id is string => typeof id === "string")
|
const sessionIDs = detail.sessionIDs.filter((id): id is string => typeof id === "string")
|
||||||
if (sessionIDs.length === 0) return undefined
|
if (sessionIDs.length === 0) return undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
server: ServerConnection.Key.make(detail.server),
|
server:
|
||||||
|
"server" in detail && typeof detail.server === "string" ? (detail.server as ServerConnection.Key) : undefined,
|
||||||
directory: detail.directory,
|
directory: detail.directory,
|
||||||
sessionIDs,
|
sessionIDs,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|||||||
import { createMutation } from "@tanstack/solid-query"
|
import { createMutation } from "@tanstack/solid-query"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
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, useServerCtx } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { ServerConnection, serverName, useServers } from "@/context/servers"
|
import { ServerConnection, serverName } from "@/context/server"
|
||||||
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||||
import type { SessionInfo } from "@opencode-ai/client/promise"
|
import type { SessionInfo } from "@opencode-ai/client/promise"
|
||||||
@@ -22,14 +22,14 @@ export function TabNavItem(props: {
|
|||||||
ref?: Ref<HTMLDivElement>
|
ref?: Ref<HTMLDivElement>
|
||||||
href: string
|
href: string
|
||||||
server: ServerConnection.Key
|
server: ServerConnection.Key
|
||||||
session: SessionInfo | undefined
|
session: () => SessionInfo | undefined
|
||||||
fallbackTitle?: string
|
fallbackTitle?: string
|
||||||
onRename: (title: string) => Promise<void>
|
onRename: (title: string) => Promise<void>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onNavigate: () => void
|
onNavigate: () => void
|
||||||
active?: boolean
|
active?: boolean
|
||||||
forceTruncate?: boolean
|
forceTruncate?: boolean
|
||||||
suppressNavigation?: boolean
|
suppressNavigation?: () => boolean
|
||||||
dragging?: boolean
|
dragging?: boolean
|
||||||
pressed?: boolean
|
pressed?: boolean
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
@@ -46,38 +46,41 @@ export function TabNavItem(props: {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
props.onClose()
|
props.onClose()
|
||||||
}
|
}
|
||||||
const servers = useServers()
|
const global = useGlobal()
|
||||||
const serverCtx = useServerCtx(() => servers.list.find((item) => ServerConnection.key(item) === props.server))
|
const serverCtx = createMemo(() => {
|
||||||
|
const conn = global.servers.list().find((item) => ServerConnection.key(item) === props.server)
|
||||||
|
if (conn) return global.ensureServerCtx(conn)
|
||||||
|
})
|
||||||
const project = createMemo(() => {
|
const project = createMemo(() => {
|
||||||
const session = props.session
|
const session = props.session()
|
||||||
if (!session) return
|
if (!session) return
|
||||||
return projectForSession(session, serverCtx()?.projects.list() ?? [])
|
return projectForSession(session, serverCtx()?.projects.list() ?? [])
|
||||||
})
|
})
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const session = props.session
|
const session = props.session()
|
||||||
return session ? sessionLabel(session) : props.fallbackTitle
|
return session ? sessionLabel(session) : props.fallbackTitle
|
||||||
})
|
})
|
||||||
|
|
||||||
const projectName = createMemo(() => {
|
const projectName = createMemo(() => {
|
||||||
const session = props.session
|
const session = props.session()
|
||||||
if (!session) return
|
if (!session) return
|
||||||
return displayName(project() ?? { worktree: session.location.directory })
|
return displayName(project() ?? { worktree: session.location.directory })
|
||||||
})
|
})
|
||||||
const previewPath = createMemo(() => {
|
const previewPath = createMemo(() => {
|
||||||
const session = props.session
|
const session = props.session()
|
||||||
if (!session) return
|
if (!session) return
|
||||||
const home = serverCtx()?.sync.data.path.home
|
const home = serverCtx()?.sync.data.path.home
|
||||||
return home ? session.location.directory.replace(home, "~") : session.location.directory
|
return home ? session.location.directory.replace(home, "~") : session.location.directory
|
||||||
})
|
})
|
||||||
// Only label the server when multiple servers are connected.
|
// Only label the server when multiple servers are connected.
|
||||||
const serverLabel = createMemo(() => {
|
const serverLabel = createMemo(() => {
|
||||||
if (servers.list.length <= 1) return
|
if (global.servers.list().length <= 1) return
|
||||||
const conn = servers.list.find((item) => ServerConnection.key(item) === props.server)
|
const conn = global.servers.list().find((item) => ServerConnection.key(item) === props.server)
|
||||||
return conn ? serverName(conn) : undefined
|
return conn ? serverName(conn) : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const [popoverOpen, setPopoverOpen] = createSignal(false)
|
const [popoverOpen, setPopoverOpen] = createSignal(false)
|
||||||
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session
|
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session()
|
||||||
|
|
||||||
const measureTitleOverflow = () => {
|
const measureTitleOverflow = () => {
|
||||||
if (!titleEl || editing()) {
|
if (!titleEl || editing()) {
|
||||||
@@ -118,7 +121,7 @@ export function TabNavItem(props: {
|
|||||||
const closeRename = async (save: boolean) => {
|
const closeRename = async (save: boolean) => {
|
||||||
if (rename.isPending || !editing()) return
|
if (rename.isPending || !editing()) return
|
||||||
|
|
||||||
const original = props.session?.title ?? ""
|
const original = props.session()?.title ?? ""
|
||||||
const next = (titleEl.textContent ?? "").trim()
|
const next = (titleEl.textContent ?? "").trim()
|
||||||
|
|
||||||
titleEl.scrollLeft = 0
|
titleEl.scrollLeft = 0
|
||||||
@@ -143,7 +146,7 @@ export function TabNavItem(props: {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
|
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
|
||||||
const session = props.session
|
const session = props.session()
|
||||||
if (!session) return
|
if (!session) return
|
||||||
titleEl.textContent = session.title ?? ""
|
titleEl.textContent = session.title ?? ""
|
||||||
setEditing(true)
|
setEditing(true)
|
||||||
@@ -210,7 +213,7 @@ export function TabNavItem(props: {
|
|||||||
// Navigate on mousedown to shave the press-release delay off tab switches.
|
// Navigate on mousedown to shave the press-release delay off tab switches.
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
if (editing()) return
|
if (editing()) return
|
||||||
if (props.suppressNavigation) return
|
if (props.suppressNavigation?.()) return
|
||||||
props.onNavigate()
|
props.onNavigate()
|
||||||
}}
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@@ -218,14 +221,14 @@ export function TabNavItem(props: {
|
|||||||
// Mouse navigation already happened on mousedown; detail 0 means keyboard activation.
|
// Mouse navigation already happened on mousedown; detail 0 means keyboard activation.
|
||||||
if (event.detail > 0) return
|
if (event.detail > 0) return
|
||||||
if (editing()) return
|
if (editing()) return
|
||||||
if (props.suppressNavigation) return
|
if (props.suppressNavigation?.()) return
|
||||||
props.onNavigate()
|
props.onNavigate()
|
||||||
}}
|
}}
|
||||||
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]"
|
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]"
|
||||||
>
|
>
|
||||||
<span data-slot="project-avatar-slot" class="flex size-4 shrink-0 items-center justify-center">
|
<span data-slot="project-avatar-slot" class="flex size-4 shrink-0 items-center justify-center">
|
||||||
<Show
|
<Show
|
||||||
when={props.session}
|
when={props.session()}
|
||||||
keyed
|
keyed
|
||||||
fallback={
|
fallback={
|
||||||
<span class="block size-4 rounded-[3px] border border-v2-border-border-muted" aria-hidden="true" />
|
<span class="block size-4 rounded-[3px] border border-v2-border-border-muted" aria-hidden="true" />
|
||||||
@@ -248,7 +251,6 @@ export function TabNavItem(props: {
|
|||||||
}}
|
}}
|
||||||
data-slot="tab-title"
|
data-slot="tab-title"
|
||||||
data-titlebar-tab-title
|
data-titlebar-tab-title
|
||||||
dir="auto"
|
|
||||||
class="min-w-0 flex-1 outline-none leading-4"
|
class="min-w-0 flex-1 outline-none leading-4"
|
||||||
classList={{
|
classList={{
|
||||||
"overflow-hidden text-clip whitespace-nowrap": !editing(),
|
"overflow-hidden text-clip whitespace-nowrap": !editing(),
|
||||||
@@ -265,7 +267,7 @@ export function TabNavItem(props: {
|
|||||||
}
|
}
|
||||||
if (event.key !== "Escape") return
|
if (event.key !== "Escape") return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
titleEl.textContent = props.session?.title ?? ""
|
titleEl.textContent = props.session()?.title ?? ""
|
||||||
void closeRename(false)
|
void closeRename(false)
|
||||||
}}
|
}}
|
||||||
onBlur={() => void closeRename(true)}
|
onBlur={() => void closeRename(true)}
|
||||||
@@ -306,7 +308,7 @@ export function TabNavItem(props: {
|
|||||||
}}
|
}}
|
||||||
data={{
|
data={{
|
||||||
projectName: projectName(),
|
projectName: projectName(),
|
||||||
title: props.session?.title,
|
title: props.session()?.title,
|
||||||
path: previewPath(),
|
path: previewPath(),
|
||||||
serverName: serverLabel(),
|
serverName: serverLabel(),
|
||||||
}}
|
}}
|
||||||
@@ -321,7 +323,7 @@ export function DraftTabItem(props: {
|
|||||||
active?: boolean
|
active?: boolean
|
||||||
onNavigate: () => void
|
onNavigate: () => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
suppressNavigation?: boolean
|
suppressNavigation?: () => boolean
|
||||||
dragging?: boolean
|
dragging?: boolean
|
||||||
pressed?: boolean
|
pressed?: boolean
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
@@ -364,14 +366,14 @@ export function DraftTabItem(props: {
|
|||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
// Navigate on mousedown to shave the press-release delay off tab switches.
|
// Navigate on mousedown to shave the press-release delay off tab switches.
|
||||||
if (event.button !== 0) return
|
if (event.button !== 0) return
|
||||||
if (props.suppressNavigation) return
|
if (props.suppressNavigation?.()) return
|
||||||
props.onNavigate()
|
props.onNavigate()
|
||||||
}}
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// Mouse navigation already happened on mousedown; detail 0 means keyboard activation.
|
// Mouse navigation already happened on mousedown; detail 0 means keyboard activation.
|
||||||
if (event.detail > 0) return
|
if (event.detail > 0) return
|
||||||
if (props.suppressNavigation) return
|
if (props.suppressNavigation?.()) return
|
||||||
props.onNavigate()
|
props.onNavigate()
|
||||||
}}
|
}}
|
||||||
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 [-webkit-user-drag:none]"
|
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 [-webkit-user-drag:none]"
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers"
|
|||||||
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
|
import { RestrictToElement } from "@dnd-kit/dom/modifiers"
|
||||||
import { arrayMove } from "@dnd-kit/helpers"
|
import { arrayMove } from "@dnd-kit/helpers"
|
||||||
import { tabHref, tabKey, type SessionTab, type Tab } from "@/context/tabs"
|
import { tabHref, tabKey, type SessionTab, type Tab } from "@/context/tabs"
|
||||||
import { ServerConnection } from "@/context/servers"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { DraftTabItem, TabNavItem } from "@/components/titlebar-tab-nav"
|
import { DraftTabItem, TabNavItem } from "@/components/titlebar-tab-nav"
|
||||||
import { useGlobal, useServerCtx, type ServerCtx } from "@/context/global"
|
import { useGlobal, type ServerCtx } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useTabs } from "@/context/tabs"
|
import { useTabs } from "@/context/tabs"
|
||||||
@@ -24,10 +24,10 @@ import type { SessionInfo } from "@opencode-ai/client/promise"
|
|||||||
function SessionTabSlot(props: {
|
function SessionTabSlot(props: {
|
||||||
tab: SessionTab
|
tab: SessionTab
|
||||||
id: string
|
id: string
|
||||||
index: number
|
index: () => number
|
||||||
active: boolean
|
active: () => boolean
|
||||||
forceTruncate: boolean
|
forceTruncate: boolean
|
||||||
session: SessionInfo | undefined
|
session: () => SessionInfo | undefined
|
||||||
fallbackTitle?: string
|
fallbackTitle?: string
|
||||||
onRename: (title: string) => Promise<void>
|
onRename: (title: string) => Promise<void>
|
||||||
onNavigate: (element: HTMLDivElement) => void
|
onNavigate: (element: HTMLDivElement) => void
|
||||||
@@ -38,7 +38,7 @@ function SessionTabSlot(props: {
|
|||||||
return props.id
|
return props.id
|
||||||
},
|
},
|
||||||
get index() {
|
get index() {
|
||||||
return props.index
|
return props.index()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
let ref!: HTMLDivElement
|
let ref!: HTMLDivElement
|
||||||
@@ -48,7 +48,7 @@ function SessionTabSlot(props: {
|
|||||||
ref={sortable.ref}
|
ref={sortable.ref}
|
||||||
data-titlebar-tab-slot
|
data-titlebar-tab-slot
|
||||||
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"
|
||||||
>
|
>
|
||||||
<TabNavItem
|
<TabNavItem
|
||||||
@@ -62,7 +62,7 @@ function SessionTabSlot(props: {
|
|||||||
onRename={props.onRename}
|
onRename={props.onRename}
|
||||||
onNavigate={() => props.onNavigate(ref)}
|
onNavigate={() => props.onNavigate(ref)}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
active={props.active}
|
active={props.active()}
|
||||||
forceTruncate={props.forceTruncate}
|
forceTruncate={props.forceTruncate}
|
||||||
dragging={sortable.isDragSource()}
|
dragging={sortable.isDragSource()}
|
||||||
/>
|
/>
|
||||||
@@ -73,34 +73,34 @@ function SessionTabSlot(props: {
|
|||||||
function SessionTabEntry(props: {
|
function SessionTabEntry(props: {
|
||||||
tab: SessionTab
|
tab: SessionTab
|
||||||
id: string
|
id: string
|
||||||
index: number
|
index: () => number
|
||||||
active: boolean
|
active: () => boolean
|
||||||
forceTruncate: boolean
|
forceTruncate: boolean
|
||||||
serverCtx: ServerCtx | undefined
|
serverCtx: () => ServerCtx | undefined
|
||||||
onVisibleChange: (visible: boolean) => void
|
onVisibleChange: (visible: boolean) => void
|
||||||
onNavigate: (element: HTMLDivElement) => void
|
onNavigate: (element: HTMLDivElement) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}) {
|
}) {
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sdk = createMemo(() => props.serverCtx?.sdk ?? null)
|
const sdk = createMemo(() => props.serverCtx()?.sdk ?? null)
|
||||||
const cachedSession = createMemo(() => props.serverCtx?.sync.session.peek(props.tab.sessionId))
|
const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId))
|
||||||
const persisted = createMemo(() => tabs.info[props.id])
|
const persisted = createMemo(() => tabs.info[props.id])
|
||||||
const [loadedSession] = createResource(
|
const [loadedSession] = createResource(
|
||||||
() => {
|
() => {
|
||||||
const ctx = props.serverCtx
|
const ctx = props.serverCtx()
|
||||||
return ctx ? { id: props.tab.sessionId, ctx } : null
|
return ctx ? { id: props.tab.sessionId, ctx } : null
|
||||||
},
|
},
|
||||||
({ 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())
|
const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session())
|
||||||
const visible = createMemo(() => !!session() || missingSession() || !!persisted()?.title)
|
const visible = createMemo(() => !!session() || missingSession() || !!persisted()?.title)
|
||||||
let prefetched = false
|
let prefetched = false
|
||||||
|
|
||||||
const rename = async (title: string) => {
|
const rename = async (title: string) => {
|
||||||
const value = session()
|
const value = session()
|
||||||
const ctx = props.serverCtx
|
const ctx = props.serverCtx()
|
||||||
if (!value || !ctx) return
|
if (!value || !ctx) return
|
||||||
|
|
||||||
ctx.sync.session.remember({ ...value, title })
|
ctx.sync.session.remember({ ...value, title })
|
||||||
@@ -108,7 +108,7 @@ function SessionTabEntry(props: {
|
|||||||
await ctx.sdk.api.session.rename({ sessionID: value.id, title })
|
await ctx.sdk.api.session.rename({ sessionID: value.id, title })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const current = session()
|
const current = session()
|
||||||
const currentCtx = props.serverCtx
|
const currentCtx = props.serverCtx()
|
||||||
if (current && currentCtx) currentCtx.sync.session.remember({ ...current, title: value.title })
|
if (current && currentCtx) currentCtx.sync.session.remember({ ...current, title: value.title })
|
||||||
showToast({
|
showToast({
|
||||||
title: language.t("common.requestFailed"),
|
title: language.t("common.requestFailed"),
|
||||||
@@ -120,7 +120,7 @@ function SessionTabEntry(props: {
|
|||||||
createEffect(() => props.onVisibleChange(visible()))
|
createEffect(() => props.onVisibleChange(visible()))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const ctx = props.serverCtx
|
const ctx = props.serverCtx()
|
||||||
const value = session()
|
const value = session()
|
||||||
if (!ctx || !value || prefetched) return
|
if (!ctx || !value || prefetched) return
|
||||||
prefetched = true
|
prefetched = true
|
||||||
@@ -157,7 +157,7 @@ function SessionTabEntry(props: {
|
|||||||
index={props.index}
|
index={props.index}
|
||||||
active={props.active}
|
active={props.active}
|
||||||
forceTruncate={props.forceTruncate}
|
forceTruncate={props.forceTruncate}
|
||||||
session={session()}
|
session={session}
|
||||||
fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
|
fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
|
||||||
onRename={rename}
|
onRename={rename}
|
||||||
onNavigate={props.onNavigate}
|
onNavigate={props.onNavigate}
|
||||||
@@ -170,8 +170,8 @@ function SessionTabEntry(props: {
|
|||||||
function DraftTabSlot(props: {
|
function DraftTabSlot(props: {
|
||||||
tab: Extract<Tab, { type: "draft" }>
|
tab: Extract<Tab, { type: "draft" }>
|
||||||
id: string
|
id: string
|
||||||
index: number
|
index: () => number
|
||||||
active: boolean
|
active: () => boolean
|
||||||
title: string
|
title: string
|
||||||
onNavigate: (element: HTMLDivElement) => void
|
onNavigate: (element: HTMLDivElement) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -181,7 +181,7 @@ function DraftTabSlot(props: {
|
|||||||
return props.id
|
return props.id
|
||||||
},
|
},
|
||||||
get index() {
|
get index() {
|
||||||
return props.index
|
return props.index()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
let ref!: HTMLDivElement
|
let ref!: HTMLDivElement
|
||||||
@@ -191,7 +191,7 @@ function DraftTabSlot(props: {
|
|||||||
ref={sortable.ref}
|
ref={sortable.ref}
|
||||||
data-titlebar-tab-slot
|
data-titlebar-tab-slot
|
||||||
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"
|
||||||
>
|
>
|
||||||
<DraftTabItem
|
<DraftTabItem
|
||||||
@@ -202,7 +202,7 @@ function DraftTabSlot(props: {
|
|||||||
title={props.title}
|
title={props.title}
|
||||||
onNavigate={() => props.onNavigate(ref)}
|
onNavigate={() => props.onNavigate(ref)}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
active={props.active}
|
active={props.active()}
|
||||||
dragging={sortable.isDragSource()}
|
dragging={sortable.isDragSource()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +211,7 @@ function DraftTabSlot(props: {
|
|||||||
|
|
||||||
export function TitlebarTabStrip(props: {
|
export function TitlebarTabStrip(props: {
|
||||||
tabs: Tab[]
|
tabs: Tab[]
|
||||||
currentTab: Tab | undefined
|
currentTab: () => Tab | undefined
|
||||||
forceTruncate: boolean
|
forceTruncate: boolean
|
||||||
onNavigate: (tab: Tab, el?: HTMLDivElement) => void
|
onNavigate: (tab: Tab, el?: HTMLDivElement) => void
|
||||||
onClose: (tab: Tab) => void
|
onClose: (tab: Tab) => void
|
||||||
@@ -248,7 +248,7 @@ export function TitlebarTabStrip(props: {
|
|||||||
])
|
])
|
||||||
|
|
||||||
function selectAdjacentTab(offset: -1 | 1) {
|
function selectAdjacentTab(offset: -1 | 1) {
|
||||||
const current = props.currentTab
|
const current = props.currentTab()
|
||||||
const key = adjacentTabKey(visibleTabIds(), current ? tabKey(current) : undefined, offset)
|
const key = adjacentTabKey(visibleTabIds(), current ? tabKey(current) : undefined, offset)
|
||||||
const next = props.tabs.find((tab) => tabKey(tab) === key)
|
const next = props.tabs.find((tab) => tabKey(tab) === key)
|
||||||
if (next) props.onNavigate(next)
|
if (next) props.onNavigate(next)
|
||||||
@@ -339,9 +339,10 @@ export function TitlebarTabStrip(props: {
|
|||||||
let ref!: HTMLDivElement
|
let ref!: HTMLDivElement
|
||||||
const visibleIndex = () => visibleTabs().findIndex((item) => tabKey(item) === id)
|
const visibleIndex = () => visibleTabs().findIndex((item) => tabKey(item) === id)
|
||||||
useTabShortcut(visibleIndex, () => props.onNavigate(tab, ref))
|
useTabShortcut(visibleIndex, () => props.onNavigate(tab, ref))
|
||||||
const serverCtx = useServerCtx(() => {
|
const serverCtx = createMemo(() => {
|
||||||
if (tab.type !== "session") return
|
if (tab.type !== "session") return
|
||||||
return global.servers.list().find((item) => ServerConnection.key(item) === tab.server)
|
const conn = global.servers.list().find((item) => ServerConnection.key(item) === tab.server)
|
||||||
|
if (conn) return global.ensureServerCtx(conn)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (tab.type === "session") {
|
if (tab.type === "session") {
|
||||||
@@ -349,10 +350,10 @@ export function TitlebarTabStrip(props: {
|
|||||||
<SessionTabEntry
|
<SessionTabEntry
|
||||||
tab={tab}
|
tab={tab}
|
||||||
id={id}
|
id={id}
|
||||||
index={visibleIndex()}
|
index={visibleIndex}
|
||||||
active={props.currentTab === tab}
|
active={() => props.currentTab() === tab}
|
||||||
forceTruncate={props.forceTruncate}
|
forceTruncate={props.forceTruncate}
|
||||||
serverCtx={serverCtx()}
|
serverCtx={serverCtx}
|
||||||
onVisibleChange={(visible) => setVisibility(id, visible)}
|
onVisibleChange={(visible) => setVisibility(id, visible)}
|
||||||
onNavigate={(element) => {
|
onNavigate={(element) => {
|
||||||
ref = element
|
ref = element
|
||||||
@@ -367,8 +368,8 @@ export function TitlebarTabStrip(props: {
|
|||||||
<DraftTabSlot
|
<DraftTabSlot
|
||||||
tab={tab}
|
tab={tab}
|
||||||
id={id}
|
id={id}
|
||||||
index={visibleIndex()}
|
index={visibleIndex}
|
||||||
active={props.currentTab === tab}
|
active={() => props.currentTab() === tab}
|
||||||
title={language.t("command.session.new")}
|
title={language.t("command.session.new")}
|
||||||
onNavigate={(element) => {
|
onNavigate={(element) => {
|
||||||
ref = element
|
ref = element
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ import { TitlebarTabStrip } from "@/components/titlebar-tab-strip"
|
|||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events"
|
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events"
|
||||||
import { useGlobal, useServerCtx } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { ServerConnection, useServers } from "@/context/servers"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
import { tabKey, useTabs } from "@/context/tabs"
|
import { tabKey, useTabs } from "@/context/tabs"
|
||||||
import type { PromptSession } from "@/context/prompt"
|
import type { PromptSession } from "@/context/prompt"
|
||||||
import "./titlebar.css"
|
import "./titlebar.css"
|
||||||
@@ -46,8 +46,8 @@ const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px
|
|||||||
const macTrafficLightsBaseWidth = 84
|
const macTrafficLightsBaseWidth = 84
|
||||||
|
|
||||||
export type TitlebarUpdate = {
|
export type TitlebarUpdate = {
|
||||||
version: string | undefined
|
version: () => string | undefined
|
||||||
installing: boolean
|
installing: () => boolean
|
||||||
install: () => void
|
install: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +61,15 @@ export function useTitlebarRightMount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visible: boolean; toggle: () => void } }) {
|
export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visible: boolean; toggle: () => void } }) {
|
||||||
|
const layout = useLayout()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const servers = useServers()
|
const server = useServer()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const params = useParams()
|
||||||
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
|
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
|
||||||
const mobile = createMediaQuery("(max-width: 767px)")
|
const mobile = createMediaQuery("(max-width: 767px)")
|
||||||
const bottom = createMemo(() => useV2Titlebar() && mobile() && settings.general.mobileTitlebarPosition() === "bottom")
|
const bottom = createMemo(() => useV2Titlebar() && mobile() && settings.general.mobileTitlebarPosition() === "bottom")
|
||||||
@@ -75,9 +77,11 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||||
|
const web = createMemo(() => platform.platform === "web")
|
||||||
const macTrafficLights = createMemo(() => mac() && !platform.windowFullscreen?.())
|
const macTrafficLights = createMemo(() => mac() && !platform.windowFullscreen?.())
|
||||||
const zoom = () => platform.webviewZoom?.() ?? 1
|
const zoom = () => platform.webviewZoom?.() ?? 1
|
||||||
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
||||||
|
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
|
||||||
const minHeight = () => {
|
const minHeight = () => {
|
||||||
const height = useV2Titlebar() ? v2TitlebarHeight : legacyTitlebarHeight
|
const height = useV2Titlebar() ? v2TitlebarHeight : legacyTitlebarHeight
|
||||||
if (mac()) return `${height / zoom()}px`
|
if (mac()) return `${height / zoom()}px`
|
||||||
@@ -93,6 +97,14 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
})
|
})
|
||||||
|
|
||||||
const path = () => `${location.pathname}${location.search}${location.hash}`
|
const path = () => `${location.pathname}${location.search}${location.hash}`
|
||||||
|
const creating = createMemo(() => {
|
||||||
|
const route = layout.route()
|
||||||
|
if (route.type === "draft" || route.type === "dir-new-sesssion") return true
|
||||||
|
if (!params.dir) return false
|
||||||
|
if (params.id) return false
|
||||||
|
const parts = location.pathname.replace(/\/+$/, "").split("/")
|
||||||
|
return parts.at(-1) === "session"
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const current = path()
|
const current = path()
|
||||||
@@ -104,9 +116,13 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canBack = createMemo(() => history.index > 0)
|
||||||
|
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||||
|
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||||
|
const nav = createMemo(() => (useV2Titlebar() ? settings.general.showNavigation() : true))
|
||||||
const updateState = createMemo<TitlebarUpdatePillState>(() => {
|
const updateState = createMemo<TitlebarUpdatePillState>(() => {
|
||||||
const installing = props.update?.installing ?? false
|
const installing = props.update?.installing() ?? false
|
||||||
const version = props.update?.version
|
const version = props.update?.version()
|
||||||
return {
|
return {
|
||||||
visible: version !== undefined || installing,
|
visible: version !== undefined || installing,
|
||||||
installing,
|
installing,
|
||||||
@@ -172,7 +188,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when>
|
<Match when={useV2Titlebar()}>
|
||||||
{(_) => {
|
{(_) => {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
@@ -184,7 +200,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
() => {
|
() => {
|
||||||
const route = layout.route()
|
const route = layout.route()
|
||||||
if (route.type !== "session") return undefined
|
if (route.type !== "session") return undefined
|
||||||
const conn = global.servers.list().find((item) => ServerConnection.key(item) === route.server)
|
const conn = global.servers
|
||||||
|
.list()
|
||||||
|
.find((item) => ServerConnection.key(item) === (route.server ?? server.key))
|
||||||
return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined
|
return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined
|
||||||
},
|
},
|
||||||
({ route, sdk }) => sdk.api.session.get({ sessionID: route.sessionId }).catch(() => {}),
|
({ route, sdk }) => sdk.api.session.get({ sessionID: route.sessionId }).catch(() => {}),
|
||||||
@@ -227,7 +245,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
const s = session()
|
const s = session()
|
||||||
if (!s) return
|
if (!s) return
|
||||||
const sessionId = s.parentID ?? s.id
|
const sessionId = s.parentID ?? s.id
|
||||||
const next = { server: route.server, sessionId }
|
const next = { server: route.server ?? server.key, sessionId }
|
||||||
tabsStoreActions.addSessionTab(next)
|
tabsStoreActions.addSessionTab(next)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -240,29 +258,26 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
|
|
||||||
const openNewTab = () => {
|
const openNewTab = () => {
|
||||||
const route = layout.route()
|
const route = layout.route()
|
||||||
switch (route.type) {
|
|
||||||
case "session": {
|
|
||||||
const activeSession = session()
|
const activeSession = session()
|
||||||
if (!activeSession) return
|
if (route.type === "session" && activeSession) {
|
||||||
|
|
||||||
const sessionTab = {
|
const sessionTab = {
|
||||||
type: "session" as const,
|
type: "session" as const,
|
||||||
server: route.server,
|
server: route.server ?? server.key,
|
||||||
sessionId: activeSession.id,
|
sessionId: activeSession.id,
|
||||||
}
|
}
|
||||||
const model = tabs.stateValue<PromptSession>(sessionTab, "prompt")?.model.current()
|
const model = tabs.stateValue<PromptSession>(sessionTab, "prompt")?.model.current()
|
||||||
tabs.newDraft({ server: sessionTab.server, directory: activeSession.location.directory }, "", model)
|
tabs.newDraft({ server: sessionTab.server, directory: activeSession.location.directory }, "", model)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "draft": {
|
|
||||||
const activeTab = currentTab()
|
|
||||||
if (activeTab?.type !== "draft") return
|
|
||||||
|
|
||||||
|
const activeTab = currentTab()
|
||||||
|
if (activeTab?.type === "draft") {
|
||||||
const model = tabs.stateValue<PromptSession>(activeTab, "prompt")?.model.current()
|
const model = tabs.stateValue<PromptSession>(activeTab, "prompt")?.model.current()
|
||||||
tabs.newDraft({ server: activeTab.server, directory: activeTab.directory }, "", model)
|
tabs.newDraft({ server: activeTab.server, directory: activeTab.directory }, "", model)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "home": {
|
|
||||||
|
if (route.type === "home") {
|
||||||
const selection = layout.home.selection()
|
const selection = layout.home.selection()
|
||||||
const conn = global.servers.list().find((item) => ServerConnection.key(item) === selection.server)
|
const conn = global.servers.list().find((item) => ServerConnection.key(item) === selection.server)
|
||||||
const project = conn
|
const project = conn
|
||||||
@@ -276,7 +291,20 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const current = layout.projects.list()[0]
|
||||||
|
if (current) {
|
||||||
|
tabs.newDraft({ server: server.key, directory: current.worktree }, "")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fallback = global.servers.list().flatMap((conn) => {
|
||||||
|
const project = global.ensureServerCtx(conn).projects.list()[0]
|
||||||
|
return project ? [{ server: ServerConnection.key(conn), project }] : []
|
||||||
|
})[0]
|
||||||
|
if (!fallback) return
|
||||||
|
|
||||||
|
tabs.newDraft({ server: fallback.server, directory: fallback.project.worktree }, "")
|
||||||
}
|
}
|
||||||
const toggleHome = () => tabs.toggleHome({ home: layout.route().type === "home", current: currentTab() })
|
const toggleHome = () => tabs.toggleHome({ home: layout.route().type === "home", current: currentTab() })
|
||||||
|
|
||||||
@@ -364,7 +392,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
|
|
||||||
<TitlebarTabStrip
|
<TitlebarTabStrip
|
||||||
tabs={tabsStore}
|
tabs={tabsStore}
|
||||||
currentTab={currentTab()}
|
currentTab={currentTab}
|
||||||
forceTruncate={tabsAreOverflowing()}
|
forceTruncate={tabsAreOverflowing()}
|
||||||
onOverflowChange={setTabsAreOverflowing}
|
onOverflowChange={setTabsAreOverflowing}
|
||||||
onNavigate={(tab, el) => {
|
onNavigate={(tab, el) => {
|
||||||
@@ -402,6 +430,158 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when>
|
||||||
|
<div
|
||||||
|
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
|
||||||
|
style={{ zoom: counterZoom() }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"flex items-center min-w-0": true,
|
||||||
|
"pl-2": !macTrafficLights(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={windows() || linux()}>
|
||||||
|
<WindowsAppMenu command={command} platform={platform} />
|
||||||
|
</Show>
|
||||||
|
<Show when={mac()}>
|
||||||
|
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
|
||||||
|
<IconButton
|
||||||
|
icon="menu"
|
||||||
|
variant="ghost"
|
||||||
|
class="titlebar-icon rounded-md"
|
||||||
|
onClick={layout.mobileSidebar.toggle}
|
||||||
|
aria-label={language.t("sidebar.menu.toggle")}
|
||||||
|
aria-expanded={layout.mobileSidebar.opened()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={!mac()}>
|
||||||
|
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
|
||||||
|
<IconButton
|
||||||
|
icon="menu"
|
||||||
|
variant="ghost"
|
||||||
|
class="titlebar-icon rounded-md"
|
||||||
|
onClick={layout.mobileSidebar.toggle}
|
||||||
|
aria-label={language.t("sidebar.menu.toggle")}
|
||||||
|
aria-expanded={layout.mobileSidebar.opened()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
|
<TooltipKeybind
|
||||||
|
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||||
|
placement="bottom"
|
||||||
|
title={language.t("command.sidebar.toggle")}
|
||||||
|
keybind={command.keybind("sidebar.toggle")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
onClick={layout.sidebar.toggle}
|
||||||
|
aria-label={language.t("command.sidebar.toggle")}
|
||||||
|
aria-expanded={layout.sidebar.opened()}
|
||||||
|
>
|
||||||
|
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
<div class="hidden xl:flex items-center shrink-0">
|
||||||
|
<Show when={params.dir}>
|
||||||
|
<div
|
||||||
|
class="flex items-center shrink-0 w-8 mr-1"
|
||||||
|
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="transition-opacity"
|
||||||
|
classList={{
|
||||||
|
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||||
|
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TooltipKeybind
|
||||||
|
placement="bottom"
|
||||||
|
title={language.t("command.session.new")}
|
||||||
|
keybind={command.keybind("session.new")}
|
||||||
|
openDelay={800}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
|
disabled={layout.sidebar.opened()}
|
||||||
|
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (!params.dir) return
|
||||||
|
navigate(`/${params.dir}/session`)
|
||||||
|
}}
|
||||||
|
aria-label={language.t("command.session.new")}
|
||||||
|
aria-current={creating() ? "page" : undefined}
|
||||||
|
>
|
||||||
|
<IconV2 name="edit" size="small" />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
class="flex items-center shrink-0"
|
||||||
|
classList={{
|
||||||
|
"ltr:-translate-x-[36px] rtl:translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
||||||
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={hasProjects() && nav()}>
|
||||||
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={800}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-left"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canBack()}
|
||||||
|
onClick={back}
|
||||||
|
aria-label={language.t("common.goBack")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={800}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
icon="chevron-right"
|
||||||
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
|
disabled={!canForward()}
|
||||||
|
onClick={forward}
|
||||||
|
aria-label={language.t("common.goForward")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChannelIndicator debugTools={props.debugTools} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||||
|
<div
|
||||||
|
id="opencode-titlebar-center"
|
||||||
|
class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"flex items-center min-w-0 justify-end": true,
|
||||||
|
"pr-2": !windows(),
|
||||||
|
}}
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
|
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
|
||||||
|
<Show when={windows()}>
|
||||||
|
<div class="shrink-0" style={{ width: windowsControlsWidth() }} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
@@ -477,10 +657,12 @@ function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={["local", "beta", "dev"].includes(channel)}>
|
<>
|
||||||
|
{["local", "beta", "dev"].includes(channel) && (
|
||||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||||
{channel.toUpperCase()}
|
{channel.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, type JSX } from "solid-js"
|
import { Show, type JSX } from "solid-js"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
@@ -50,20 +50,7 @@ export function WindowsAppMenu(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
|
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
|
||||||
<Show
|
{props.variant === "v2" ? (
|
||||||
when={props.variant === "v2"}
|
|
||||||
fallback={
|
|
||||||
<DropdownMenu.Trigger
|
|
||||||
as={IconButton}
|
|
||||||
icon="menu"
|
|
||||||
variant="ghost"
|
|
||||||
class="titlebar-icon rounded-md shrink-0"
|
|
||||||
aria-label={language.t("desktop.menu.ariaLabel")}
|
|
||||||
onPointerDown={rememberFocus}
|
|
||||||
onKeyDown={rememberFocus}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
data-component="desktop-icon-button"
|
data-component="desktop-icon-button"
|
||||||
class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
|
class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
|
||||||
@@ -78,31 +65,39 @@ export function WindowsAppMenu(props: {
|
|||||||
onKeyDown={rememberFocus}
|
onKeyDown={rememberFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
) : (
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="menu"
|
||||||
|
variant="ghost"
|
||||||
|
class="titlebar-icon rounded-md shrink-0"
|
||||||
|
aria-label={language.t("desktop.menu.ariaLabel")}
|
||||||
|
onPointerDown={rememberFocus}
|
||||||
|
onKeyDown={rememberFocus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content class="desktop-app-menu">
|
<DropdownMenu.Content class="desktop-app-menu">
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">OpenCode</DropdownMenu.GroupLabel>
|
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">OpenCode</DropdownMenu.GroupLabel>
|
||||||
<For each={DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows"))}>
|
{DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows")).map((menu) => (
|
||||||
{(menu) => (
|
|
||||||
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
|
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
|
||||||
<For each={menu.items?.filter((entry) => desktopMenuVisible(entry, "windows"))}>
|
{menu.items
|
||||||
{(entry) => {
|
?.filter((entry) => desktopMenuVisible(entry, "windows"))
|
||||||
// Static menu data: an early return keeps the union narrowing a Show fallback would lose.
|
.map((entry) =>
|
||||||
if (entry.type === "separator") return <DropdownMenu.Separator />
|
entry.type === "separator" ? (
|
||||||
return (
|
<DropdownMenu.Separator />
|
||||||
|
) : (
|
||||||
<DesktopMenuItem
|
<DesktopMenuItem
|
||||||
label={entry.labelKey ? language.t(entry.labelKey) : ""}
|
label={entry.labelKey ? language.t(entry.labelKey) : ""}
|
||||||
keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
|
keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
|
||||||
disabled={entry.command ? commandDisabled(entry.command) : false}
|
disabled={entry.command ? commandDisabled(entry.command) : false}
|
||||||
onSelect={() => runEntry(entry)}
|
onSelect={() => runEntry(entry)}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</DesktopMenuSubmenu>
|
|
||||||
)}
|
)}
|
||||||
</For>
|
</DesktopMenuSubmenu>
|
||||||
|
))}
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
|
|||||||
const decoded = decodeSessionKey(key)
|
const decoded = decodeSessionKey(key)
|
||||||
return createRoot((dispose) => ({
|
return createRoot((dispose) => ({
|
||||||
value: createCommentSession(
|
value: createCommentSession(
|
||||||
serverSDK.scope,
|
serverSDK().scope,
|
||||||
decoded.dir,
|
decoded.dir,
|
||||||
decoded.id === WORKSPACE_KEY ? undefined : decoded.id,
|
decoded.id === WORKSPACE_KEY ? undefined : decoded.id,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
const scope = createMemo(() => sdk().directory)
|
const scope = createMemo(() => sdk().directory)
|
||||||
const path = createPathHelpers(scope)
|
const path = createPathHelpers(scope)
|
||||||
const tabs = layout.tabs(() =>
|
const tabs = layout.tabs(() =>
|
||||||
SessionStateKey.from(serverSDK.scope, SessionRouteKey.fromRoute(base64Encode(sdk().directory), params.id)),
|
SessionStateKey.from(serverSDK().scope, SessionRouteKey.fromRoute(base64Encode(sdk().directory), params.id)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
@@ -123,7 +123,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const viewCache = createFileViewCache(serverSDK.scope)
|
const viewCache = createFileViewCache(serverSDK().scope)
|
||||||
const view = createMemo(() => viewCache.load(scope(), params.id))
|
const view = createMemo(() => viewCache.load(scope(), params.id))
|
||||||
|
|
||||||
const ensure = (file: string) => {
|
const ensure = (file: string) => {
|
||||||
@@ -211,8 +211,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) =>
|
const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) =>
|
||||||
serverSDK.api.file
|
serverSDK()
|
||||||
.find(
|
.api.file.find(
|
||||||
{
|
{
|
||||||
location: { directory: sdk().directory },
|
location: { directory: sdk().directory },
|
||||||
query,
|
query,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe("file watcher invalidation", () => {
|
|||||||
const refresh: string[] = []
|
const refresh: string[] = []
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "filesystem.changed",
|
type: "file.watcher.updated",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src/new.ts",
|
file: "src/new.ts",
|
||||||
event: "add",
|
event: "add",
|
||||||
@@ -32,7 +32,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "filesystem.changed",
|
type: "file.watcher.updated",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src/open.ts",
|
file: "src/open.ts",
|
||||||
event: "change",
|
event: "change",
|
||||||
@@ -63,7 +63,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "filesystem.changed",
|
type: "file.watcher.updated",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src",
|
file: "src",
|
||||||
event: "change",
|
event: "change",
|
||||||
@@ -81,7 +81,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "filesystem.changed",
|
type: "file.watcher.updated",
|
||||||
properties: {
|
properties: {
|
||||||
file: "src/file.ts",
|
file: "src/file.ts",
|
||||||
event: "change",
|
event: "change",
|
||||||
@@ -111,7 +111,7 @@ describe("file watcher invalidation", () => {
|
|||||||
|
|
||||||
invalidateFromWatcher(
|
invalidateFromWatcher(
|
||||||
{
|
{
|
||||||
type: "filesystem.changed",
|
type: "file.watcher.updated",
|
||||||
properties: {
|
properties: {
|
||||||
file: ".git/index.lock",
|
file: ".git/index.lock",
|
||||||
event: "change",
|
event: "change",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type WatcherOps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) {
|
export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) {
|
||||||
if (event.type !== "filesystem.changed") return
|
if (event.type !== "file.watcher.updated") return
|
||||||
const props =
|
const props =
|
||||||
typeof event.properties === "object" && event.properties ? (event.properties as Record<string, unknown>) : undefined
|
typeof event.properties === "object" && event.properties ? (event.properties as Record<string, unknown>) : undefined
|
||||||
const rawPath = typeof props?.file === "string" ? props.file : undefined
|
const rawPath = typeof props?.file === "string" ? props.file : undefined
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { QueryClient } from "@tanstack/solid-query"
|
import { QueryClient } from "@tanstack/solid-query"
|
||||||
import type { CatalogApi } from "@opencode-ai/client/promise"
|
import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise"
|
||||||
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { loadPathQuery, loadProjectsQuery, loadProvidersQuery } from "./bootstrap"
|
import {
|
||||||
|
loadAgentsQuery,
|
||||||
|
loadCommands,
|
||||||
|
loadPathQuery,
|
||||||
|
loadProjectsQuery,
|
||||||
|
loadProvidersQuery,
|
||||||
|
loadReferencesQuery,
|
||||||
|
} from "./bootstrap"
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
import { ServerScope } from "@/utils/server-scope"
|
||||||
import type { ServerApi } from "@/utils/server"
|
import type { ServerApi } from "@/utils/server"
|
||||||
|
|
||||||
type ProjectApi = ServerApi["project"]
|
type ProjectApi = ServerApi["project"]
|
||||||
type WorktreeApi = ServerApi["worktree"]
|
|
||||||
|
|
||||||
describe("query keys", () => {
|
describe("query keys", () => {
|
||||||
test("partitions identical directories by server scope", () => {
|
test("partitions identical directories by server scope", () => {
|
||||||
@@ -66,59 +72,64 @@ describe("query keys", () => {
|
|||||||
expect(result).toMatchObject({ directory: "/repo/subpath", worktree: "/repo" })
|
expect(result).toMatchObject({ directory: "/repo/subpath", worktree: "/repo" })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("loads agents from the current location-scoped endpoint", async () => {
|
||||||
|
const calls: unknown[] = []
|
||||||
|
const api = {
|
||||||
|
list: async (input: unknown) => {
|
||||||
|
calls.push(input)
|
||||||
|
return { location: {}, data: [] }
|
||||||
|
},
|
||||||
|
} as unknown as AgentApi
|
||||||
|
|
||||||
|
const result = await new QueryClient().fetchQuery(loadAgentsQuery(ServerScope.local, "/repo", api))
|
||||||
|
|
||||||
|
expect(calls).toEqual([{ location: { directory: "/repo" } }])
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loads commands from the current location-scoped endpoint", async () => {
|
||||||
|
const calls: unknown[] = []
|
||||||
|
const api = {
|
||||||
|
list: async (input: unknown) => {
|
||||||
|
calls.push(input)
|
||||||
|
return {
|
||||||
|
location: {},
|
||||||
|
data: [{ name: "review", template: "Review files" /* source: "command" as const */ }],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as unknown as CommandApi
|
||||||
|
|
||||||
|
const result = await loadCommands("/repo", api)
|
||||||
|
|
||||||
|
expect(calls).toEqual([{ location: { directory: "/repo" } }])
|
||||||
|
expect(result).toEqual([{ name: "review", template: "Review files" /* source: "command" */ }])
|
||||||
|
})
|
||||||
|
|
||||||
test("loads projects from the current endpoint", async () => {
|
test("loads projects from the current endpoint", async () => {
|
||||||
const calls: string[] = []
|
const api = {
|
||||||
const projects = {
|
|
||||||
list: async () => [
|
list: async () => [
|
||||||
{ id: "b", canonical: "/b", time: { created: 1, updated: 1 }, sandboxes: [] },
|
{ id: "b", worktree: "/b", time: { created: 1, updated: 1 }, sandboxes: [] },
|
||||||
{ id: "a", canonical: "/a", time: { created: 1, updated: 1 }, sandboxes: [] },
|
{ id: "a", worktree: "/a", time: { created: 1, updated: 1 }, sandboxes: [] },
|
||||||
],
|
],
|
||||||
} as unknown as ProjectApi
|
} as unknown as ProjectApi
|
||||||
const worktrees = {
|
|
||||||
list: async ({ projectID }: { projectID: string }) => {
|
|
||||||
calls.push(projectID)
|
|
||||||
return [
|
|
||||||
{ directory: `/${projectID}` },
|
|
||||||
{ directory: `/${projectID}/clone` },
|
|
||||||
{ directory: `/${projectID}/copy`, strategy: "git" },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
} as unknown as WorktreeApi
|
|
||||||
|
|
||||||
const result = await new QueryClient().fetchQuery(loadProjectsQuery(ServerScope.local, projects, worktrees))
|
const result = await new QueryClient().fetchQuery(loadProjectsQuery(ServerScope.local, api))
|
||||||
|
|
||||||
expect(result.map((project) => project.id)).toEqual(["a", "b"])
|
expect(result.map((project) => project.id)).toEqual(["a", "b"])
|
||||||
expect(result.map((project) => project.sandboxes)).toEqual([
|
|
||||||
["/a/clone", "/a/copy"],
|
|
||||||
["/b/clone", "/b/copy"],
|
|
||||||
])
|
|
||||||
expect(result.map((project) => project.worktrees)).toEqual([
|
|
||||||
[{ directory: "/a" }, { directory: "/a/clone" }, { directory: "/a/copy", strategy: "git" }],
|
|
||||||
[{ directory: "/b" }, { directory: "/b/clone" }, { directory: "/b/copy", strategy: "git" }],
|
|
||||||
])
|
|
||||||
expect(calls.toSorted()).toEqual(["a", "b"])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("keeps projects whose directory inventory cannot load", async () => {
|
test("loads references from the current location-scoped endpoint", async () => {
|
||||||
const projects = {
|
const calls: unknown[] = []
|
||||||
list: async () => [
|
const api = {
|
||||||
{ id: "a", canonical: "/a", time: { created: 1, updated: 1 }, sandboxes: [] },
|
list: async (input: unknown) => {
|
||||||
{ id: "b", canonical: "/b", time: { created: 1, updated: 1 }, sandboxes: [] },
|
calls.push(input)
|
||||||
],
|
return { location: {}, data: [{ name: "AGENTS.md", path: "/repo/AGENTS.md", source: "instructions" }] }
|
||||||
} as unknown as ProjectApi
|
|
||||||
const worktrees = {
|
|
||||||
list: async ({ projectID }: { projectID: string }) => {
|
|
||||||
if (projectID === "b") throw new Error("unavailable")
|
|
||||||
return [{ directory: "/a/copy", strategy: "git" as const }]
|
|
||||||
},
|
},
|
||||||
} as unknown as WorktreeApi
|
} as unknown as ReferenceApi
|
||||||
|
|
||||||
const result = await new QueryClient().fetchQuery(loadProjectsQuery(ServerScope.local, projects, worktrees))
|
const result = await new QueryClient().fetchQuery(loadReferencesQuery(ServerScope.local, "/repo", api))
|
||||||
|
|
||||||
expect(result.map((project) => ({ id: project.id, sandboxes: project.sandboxes }))).toEqual([
|
expect(calls).toEqual([{ location: { directory: "/repo" } }])
|
||||||
{ id: "a", sandboxes: ["/a/copy"] },
|
expect(result).toHaveLength(1)
|
||||||
{ id: "b", sandboxes: [] },
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
|
import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
|
||||||
import type {
|
import type {
|
||||||
|
AgentListInput,
|
||||||
|
AgentListOutput,
|
||||||
CatalogApi,
|
CatalogApi,
|
||||||
|
CommandInfo,
|
||||||
|
CommandListInput,
|
||||||
|
CommandListOutput,
|
||||||
LocationGetInput,
|
LocationGetInput,
|
||||||
LocationGetOutput,
|
LocationGetOutput,
|
||||||
PermissionRequest,
|
PermissionRequest,
|
||||||
ProjectCurrentInput,
|
ProjectCurrentInput,
|
||||||
ProjectCurrentOutput,
|
ProjectCurrentOutput,
|
||||||
ProjectListOutput,
|
ProjectListOutput,
|
||||||
|
ReferenceListInput,
|
||||||
|
ReferenceListOutput,
|
||||||
|
ReferenceInfo,
|
||||||
QuestionRequest,
|
QuestionRequest,
|
||||||
SessionApi,
|
SessionApi,
|
||||||
SessionInfo,
|
SessionInfo,
|
||||||
@@ -16,15 +24,15 @@ import { getFilename } from "@opencode-ai/core/util/path"
|
|||||||
import { retry } from "@opencode-ai/core/util/retry"
|
import { retry } from "@opencode-ai/core/util/retry"
|
||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type { State } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import type { ServerSession } from "../server-session"
|
import type { ServerSession } from "../server-session"
|
||||||
import { cmp, directoryKey, normalizeProjectInfo, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProjectInfo, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
||||||
|
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||||
import type { ServerApi } from "@/utils/server"
|
import type { ServerApi } from "@/utils/server"
|
||||||
import { sameDirectory } from "@/utils/workspace"
|
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -96,41 +104,22 @@ type ProjectApi = {
|
|||||||
readonly list: () => Promise<ProjectListOutput>
|
readonly list: () => Promise<ProjectListOutput>
|
||||||
readonly current: (input?: ProjectCurrentInput) => Promise<ProjectCurrentOutput>
|
readonly current: (input?: ProjectCurrentInput) => Promise<ProjectCurrentOutput>
|
||||||
}
|
}
|
||||||
type WorktreeApi = Pick<ServerApi["worktree"], "list">
|
|
||||||
type LocationApi = { readonly get: (input?: LocationGetInput) => Promise<LocationGetOutput> }
|
type LocationApi = { readonly get: (input?: LocationGetInput) => Promise<LocationGetOutput> }
|
||||||
|
|
||||||
|
type McpApi = ServerApi["mcp"]
|
||||||
type PermissionApi = ServerApi["permission"]
|
type PermissionApi = ServerApi["permission"]
|
||||||
type QuestionApi = ServerApi["question"]
|
type QuestionApi = ServerApi["question"]
|
||||||
|
type VcsApi = ServerApi["vcs"]
|
||||||
|
|
||||||
export const loadProjectsQuery = (scope: ServerScope, projects: ProjectApi, worktrees: WorktreeApi) =>
|
export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: [scope, "project"],
|
queryKey: [scope, "project"],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
projects.list().then(async (items) => {
|
api.list().then((projects) => {
|
||||||
return (
|
return projects
|
||||||
await Promise.all(
|
.filter((p) => !!p?.id)
|
||||||
items
|
.map(normalizeProjectInfo)
|
||||||
.filter((project) => !!project?.id)
|
|
||||||
.map(async (project) => {
|
|
||||||
const directories = await worktrees
|
|
||||||
.list({ projectID: project.id })
|
|
||||||
.catch(() => [
|
|
||||||
{ directory: project.canonical },
|
|
||||||
...(project.sandboxes ?? [])
|
|
||||||
.filter((directory) => !sameDirectory(project.canonical, directory))
|
|
||||||
.map((directory) => ({ directory })),
|
|
||||||
])
|
|
||||||
return normalizeProjectInfo({
|
|
||||||
...project,
|
|
||||||
sandboxes: directories
|
|
||||||
.map((item) => item.directory)
|
|
||||||
.filter((directory) => !sameDirectory(project.canonical, directory)),
|
|
||||||
worktrees: directories,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => cmp(a.id, b.id))
|
.sort((a, b) => cmp(a.id, b.id))
|
||||||
@@ -139,11 +128,7 @@ export const loadProjectsQuery = (scope: ServerScope, projects: ProjectApi, work
|
|||||||
})
|
})
|
||||||
|
|
||||||
export async function bootstrapGlobal(input: {
|
export async function bootstrapGlobal(input: {
|
||||||
serverAPI: CatalogApi & {
|
serverAPI: CatalogApi & { readonly location: LocationApi; readonly project: ProjectApi }
|
||||||
readonly location: LocationApi
|
|
||||||
readonly project: ProjectApi
|
|
||||||
readonly worktree: WorktreeApi
|
|
||||||
}
|
|
||||||
scope: ServerScope
|
scope: ServerScope
|
||||||
requestFailedTitle: string
|
requestFailedTitle: string
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
@@ -157,7 +142,7 @@ export async function bootstrapGlobal(input: {
|
|||||||
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)),
|
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)),
|
||||||
() =>
|
() =>
|
||||||
input.queryClient
|
input.queryClient
|
||||||
.fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project, input.serverAPI.worktree))
|
.fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project))
|
||||||
.then((data) => input.setGlobalStore("project", data)),
|
.then((data) => input.setGlobalStore("project", data)),
|
||||||
]
|
]
|
||||||
await runAll(slow)
|
await runAll(slow)
|
||||||
@@ -228,6 +213,27 @@ export const loadProvidersQuery = (scope: ServerScope, directory: string | null,
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type AgentListApi = {
|
||||||
|
readonly list: (input?: AgentListInput) => Promise<AgentListOutput>
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandListApi = {
|
||||||
|
readonly list: (input?: CommandListInput) => Promise<CommandListOutput>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferenceListApi = {
|
||||||
|
readonly list: (input?: ReferenceListInput) => Promise<ReferenceListOutput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAgentsQuery = (scope: ServerScope, directory: string, sdk: AgentListApi) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: [scope, directory, "agents"],
|
||||||
|
queryFn: () => retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const loadCommands = (directory: string, api: CommandListApi): Promise<CommandInfo[]> =>
|
||||||
|
retry(() => api.list({ location: { directory } }).then((result) => result.data))
|
||||||
|
|
||||||
export const loadPathQuery = (scope: ServerScope, directory: string | null, api: LocationApi) =>
|
export const loadPathQuery = (scope: ServerScope, directory: string | null, api: LocationApi) =>
|
||||||
queryOptions<Path>({
|
queryOptions<Path>({
|
||||||
queryKey: [scope, directory, "path"],
|
queryKey: [scope, directory, "path"],
|
||||||
@@ -241,18 +247,32 @@ export const loadPathQuery = (scope: ServerScope, directory: string | null, api:
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadReferencesQuery = (scope: ServerScope, directory: string, api: ReferenceListApi) =>
|
||||||
|
queryOptions<ReferenceInfo[]>({
|
||||||
|
queryKey: [scope, directory, "references"] as const,
|
||||||
|
queryFn: () => retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
|
||||||
|
placeholderData: [],
|
||||||
|
})
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
scope: ServerScope
|
scope: ServerScope
|
||||||
mcp: boolean
|
mcp: boolean
|
||||||
api: CatalogApi & {
|
api: CatalogApi & {
|
||||||
|
readonly agent: AgentListApi
|
||||||
|
readonly command: CommandListApi
|
||||||
|
readonly mcp: McpApi
|
||||||
readonly permission: PermissionApi
|
readonly permission: PermissionApi
|
||||||
readonly project: ProjectApi
|
readonly project: ProjectApi
|
||||||
readonly question: QuestionApi
|
readonly question: QuestionApi
|
||||||
|
readonly reference: ReferenceListApi
|
||||||
readonly session: SessionApi
|
readonly session: SessionApi
|
||||||
|
readonly vcs: VcsApi
|
||||||
|
readonly location: LocationApi
|
||||||
}
|
}
|
||||||
store: Store<State>
|
store: Store<State>
|
||||||
setStore: SetStoreFunction<State>
|
setStore: SetStoreFunction<State>
|
||||||
|
vcsCache: VcsCache
|
||||||
loadSessions: (directory: string) => Promise<void> | void
|
loadSessions: (directory: string) => Promise<void> | void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
global: {
|
global: {
|
||||||
@@ -266,7 +286,9 @@ export async function bootstrapDirectory(input: {
|
|||||||
}) {
|
}) {
|
||||||
const loading = input.store.status !== "complete"
|
const loading = input.store.status !== "complete"
|
||||||
const seededProject = projectID(input.directory, input.global.project)
|
const seededProject = projectID(input.directory, input.global.project)
|
||||||
|
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
||||||
if (seededProject) input.setStore("project", seededProject)
|
if (seededProject) input.setStore("project", seededProject)
|
||||||
|
if (seededPath) input.setStore("path", seededPath)
|
||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||||
}
|
}
|
||||||
@@ -275,13 +297,30 @@ export async function bootstrapDirectory(input: {
|
|||||||
const revKey = ScopedKey.from(input.scope, input.directory)
|
const revKey = ScopedKey.from(input.scope, input.directory)
|
||||||
const rev = (providerRev.get(revKey) ?? 0) + 1
|
const rev = (providerRev.get(revKey) ?? 0) + 1
|
||||||
providerRev.set(revKey, rev)
|
providerRev.set(revKey, rev)
|
||||||
|
;(async () => {
|
||||||
const slow = [
|
const slow = [
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
|
() =>
|
||||||
|
input.queryClient
|
||||||
|
.ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.api.agent))
|
||||||
|
.then((data) => input.setStore("agent", data)),
|
||||||
!seededProject &&
|
!seededProject &&
|
||||||
(() =>
|
(() =>
|
||||||
retry(() => input.api.project.current({ location: { directory: input.directory } })).then((project) =>
|
retry(() => input.api.project.current({ location: { directory: input.directory } })).then((project) =>
|
||||||
input.setStore("project", project.id),
|
input.setStore("project", project.id),
|
||||||
)),
|
)),
|
||||||
|
!seededPath &&
|
||||||
|
(() =>
|
||||||
|
input.queryClient
|
||||||
|
.ensureQueryData(loadPathQuery(input.scope, input.directory, input.api.location))
|
||||||
|
.then((data) => {
|
||||||
|
const next = projectID(data.directory ?? input.directory, input.global.project)
|
||||||
|
if (next) input.setStore("project", next)
|
||||||
|
})),
|
||||||
|
input.mcp &&
|
||||||
|
(() =>
|
||||||
|
loadCommands(input.directory, input.api.command).then((commands) => input.setStore("command", commands))),
|
||||||
|
() => input.queryClient.fetchQuery(loadReferencesQuery(input.scope, input.directory, input.api.reference)),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
retry(() =>
|
||||||
input.api.permission.request
|
input.api.permission.request
|
||||||
@@ -351,10 +390,11 @@ export async function bootstrapDirectory(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
|
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.api.mcp))),
|
||||||
|
input.mcp &&
|
||||||
|
(() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp))),
|
||||||
() =>
|
() =>
|
||||||
input.queryClient
|
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api)).catch((err) => {
|
||||||
.fetchQuery(loadProvidersQuery(input.scope, directoryKey(input.directory), input.api))
|
|
||||||
.catch((err) => {
|
|
||||||
const project = getFilename(input.directory)
|
const project = getFilename(input.directory)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
@@ -377,4 +417,5 @@ export async function bootstrapDirectory(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/con
|
|||||||
import type { State } from "./types"
|
import type { State } from "./types"
|
||||||
import type { QueryOptionsApi } from "../server-sync"
|
import type { QueryOptionsApi } from "../server-sync"
|
||||||
import { ServerScope } from "@/utils/server-scope"
|
import { ServerScope } from "@/utils/server-scope"
|
||||||
import type { Data } from "@opencode-ai/client/solid"
|
|
||||||
|
|
||||||
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
|
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
|
||||||
const querySingles: Array<() => { queryKey?: unknown[]; enabled?: boolean }> = []
|
const querySingles: Array<() => { queryKey?: unknown[]; enabled?: boolean }> = []
|
||||||
let providerQuerySuccess = true
|
|
||||||
const persist: typeof import("@/utils/persist").persisted = (_target, store) => [
|
const persist: typeof import("@/utils/persist").persisted = (_target, store) => [
|
||||||
store[0],
|
store[0],
|
||||||
store[1],
|
store[1],
|
||||||
@@ -19,20 +17,6 @@ const persist: typeof import("@/utils/persist").persisted = (_target, store) =>
|
|||||||
|
|
||||||
const child = () => createStore({} as State)
|
const child = () => createStore({} as State)
|
||||||
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||||
const path = { state: "", config: "", worktree: "", directory: "", home: "" }
|
|
||||||
const data = {
|
|
||||||
location: {
|
|
||||||
info: () => undefined,
|
|
||||||
agent: { list: () => undefined },
|
|
||||||
command: { list: () => undefined },
|
|
||||||
reference: { list: () => undefined },
|
|
||||||
mcp: {
|
|
||||||
server: { list: () => undefined },
|
|
||||||
resource: { list: () => undefined },
|
|
||||||
},
|
|
||||||
vcs: { info: () => undefined },
|
|
||||||
},
|
|
||||||
} as unknown as Data
|
|
||||||
|
|
||||||
const queryOptionsApi = {
|
const queryOptionsApi = {
|
||||||
globalConfig: () => ({ queryKey: ["globalConfig"], queryFn: async () => ({}) }),
|
globalConfig: () => ({ queryKey: ["globalConfig"], queryFn: async () => ({}) }),
|
||||||
@@ -74,17 +58,11 @@ beforeAll(async () => {
|
|||||||
get isLoading() {
|
get isLoading() {
|
||||||
return options().queryKey?.[1] === "path"
|
return options().queryKey?.[1] === "path"
|
||||||
},
|
},
|
||||||
get isSuccess() {
|
|
||||||
return options().queryKey?.[1] === "providers" && options().enabled === true && providerQuerySuccess
|
|
||||||
},
|
|
||||||
get isRefetchError() {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
get data() {
|
get data() {
|
||||||
if (options().queryKey?.[1] === "path") throw new Error("pending path data read")
|
if (options().queryKey?.[1] === "path") throw new Error("pending path data read")
|
||||||
if (options().queryKey?.[1] === "mcp") return options().enabled ? { demo: { status: "disabled" } } : undefined
|
if (options().queryKey?.[1] === "mcp") return options().enabled ? { demo: { status: "disabled" } } : undefined
|
||||||
if (options().queryKey?.[1] === "lsp") return []
|
if (options().queryKey?.[1] === "lsp") return []
|
||||||
if (options().queryKey?.[1] === "providers") return providerQuerySuccess ? provider : undefined
|
if (options().queryKey?.[1] === "providers") return provider
|
||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -105,7 +83,6 @@ describe("createChildStoreManager", () => {
|
|||||||
|
|
||||||
const manager = createChildStoreManager({
|
const manager = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
connected: () => true,
|
|
||||||
scope: ServerScope.local,
|
scope: ServerScope.local,
|
||||||
persist,
|
persist,
|
||||||
isBooting: () => false,
|
isBooting: () => false,
|
||||||
@@ -115,8 +92,7 @@ describe("createChildStoreManager", () => {
|
|||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: queryOptionsApi,
|
queryOptions: queryOptionsApi,
|
||||||
data,
|
global: { provider },
|
||||||
global: { path, provider },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
@@ -138,7 +114,6 @@ describe("createChildStoreManager", () => {
|
|||||||
const dispose = createOwner((owner) => {
|
const dispose = createOwner((owner) => {
|
||||||
manager = createChildStoreManager({
|
manager = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
connected: () => true,
|
|
||||||
scope: ServerScope.local,
|
scope: ServerScope.local,
|
||||||
persist,
|
persist,
|
||||||
isBooting: () => false,
|
isBooting: () => false,
|
||||||
@@ -150,8 +125,7 @@ describe("createChildStoreManager", () => {
|
|||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: queryOptionsApi,
|
queryOptions: queryOptionsApi,
|
||||||
data,
|
global: { provider },
|
||||||
global: { path, provider },
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -174,7 +148,6 @@ describe("createChildStoreManager", () => {
|
|||||||
const dispose = createOwner((owner) => {
|
const dispose = createOwner((owner) => {
|
||||||
manager = createChildStoreManager({
|
manager = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
connected: () => true,
|
|
||||||
scope: ServerScope.local,
|
scope: ServerScope.local,
|
||||||
persist,
|
persist,
|
||||||
isBooting: () => false,
|
isBooting: () => false,
|
||||||
@@ -184,8 +157,7 @@ describe("createChildStoreManager", () => {
|
|||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: queryOptionsApi,
|
queryOptions: queryOptionsApi,
|
||||||
data,
|
global: { provider },
|
||||||
global: { path, provider },
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -201,39 +173,7 @@ describe("createChildStoreManager", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("writes refreshed VCS data to the child store", () => {
|
test("enables MCP only when requested for the directory", () => {
|
||||||
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
|
||||||
const dispose = createOwner((owner) => {
|
|
||||||
manager = createChildStoreManager({
|
|
||||||
owner,
|
|
||||||
connected: () => true,
|
|
||||||
scope: ServerScope.local,
|
|
||||||
persist,
|
|
||||||
isBooting: () => false,
|
|
||||||
isLoadingSessions: () => false,
|
|
||||||
onBootstrap() {},
|
|
||||||
onMcp() {},
|
|
||||||
onDispose() {},
|
|
||||||
translate: (key) => key,
|
|
||||||
queryOptions: queryOptionsApi,
|
|
||||||
data,
|
|
||||||
global: { path, provider },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!manager) throw new Error("manager required")
|
|
||||||
const [store] = manager.child("/project", { bootstrap: false })
|
|
||||||
|
|
||||||
manager.vcs("/project", { branch: "feature", default_branch: "main" })
|
|
||||||
|
|
||||||
expect(store.vcs).toEqual({ branch: "feature", default_branch: "main" })
|
|
||||||
} finally {
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("syncs MCP only when requested for the directory", () => {
|
|
||||||
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
||||||
const offset = querySingles.length
|
const offset = querySingles.length
|
||||||
const mcpLoads: string[] = []
|
const mcpLoads: string[] = []
|
||||||
@@ -241,7 +181,6 @@ describe("createChildStoreManager", () => {
|
|||||||
const dispose = createOwner((owner) => {
|
const dispose = createOwner((owner) => {
|
||||||
manager = createChildStoreManager({
|
manager = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
connected: () => true,
|
|
||||||
scope: ServerScope.local,
|
scope: ServerScope.local,
|
||||||
persist,
|
persist,
|
||||||
isBooting: () => false,
|
isBooting: () => false,
|
||||||
@@ -253,21 +192,30 @@ describe("createChildStoreManager", () => {
|
|||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: queryOptionsApi,
|
queryOptions: queryOptionsApi,
|
||||||
data,
|
global: { provider },
|
||||||
global: { path, provider },
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!manager) throw new Error("manager required")
|
if (!manager) throw new Error("manager required")
|
||||||
const [, setStore] = manager.child("/project", { bootstrap: false })
|
const [store, setStore] = manager.child("/project", { bootstrap: false })
|
||||||
expect(querySingles.length - offset).toBe(2)
|
expect(querySingles.length - offset).toBe(6)
|
||||||
|
const query = querySingles[offset + 1]
|
||||||
|
const resourceQuery = querySingles[offset + 2]
|
||||||
|
if (!query) throw new Error("query required")
|
||||||
|
if (!resourceQuery) throw new Error("resource query required")
|
||||||
|
expect(query().enabled).toBe(false)
|
||||||
|
expect(resourceQuery().enabled).toBe(false)
|
||||||
|
|
||||||
setStore("status", "complete")
|
setStore("status", "complete")
|
||||||
manager.child("/project", { bootstrap: false, mcp: true })
|
manager.child("/project", { bootstrap: false, mcp: true })
|
||||||
|
expect(query().enabled).toBe(true)
|
||||||
|
expect(resourceQuery().enabled).toBe(true)
|
||||||
|
expect(store.mcp).toEqual({ demo: { status: "disabled" } })
|
||||||
expect(mcpLoads).toEqual(["/project"])
|
expect(mcpLoads).toEqual(["/project"])
|
||||||
|
|
||||||
manager.disableMcp("/project")
|
manager.disableMcp("/project")
|
||||||
|
expect(query().enabled).toBe(false)
|
||||||
expect(manager.mcp("/project")).toBe(false)
|
expect(manager.mcp("/project")).toBe(false)
|
||||||
} finally {
|
} finally {
|
||||||
dispose()
|
dispose()
|
||||||
@@ -282,7 +230,6 @@ describe("createChildStoreManager", () => {
|
|||||||
const dispose = createOwner((owner) => {
|
const dispose = createOwner((owner) => {
|
||||||
manager = createChildStoreManager({
|
manager = createChildStoreManager({
|
||||||
owner,
|
owner,
|
||||||
connected: () => true,
|
|
||||||
scope: ServerScope.local,
|
scope: ServerScope.local,
|
||||||
persist,
|
persist,
|
||||||
isBooting: () => false,
|
isBooting: () => false,
|
||||||
@@ -294,8 +241,7 @@ describe("createChildStoreManager", () => {
|
|||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: queryOptionsApi,
|
queryOptions: queryOptionsApi,
|
||||||
data,
|
global: { provider },
|
||||||
global: { path, provider },
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -304,9 +250,11 @@ describe("createChildStoreManager", () => {
|
|||||||
const [store] = manager.child("/project", { bootstrap: false })
|
const [store] = manager.child("/project", { bootstrap: false })
|
||||||
const queries = querySingles.slice(offset)
|
const queries = querySingles.slice(offset)
|
||||||
|
|
||||||
expect(queries).toHaveLength(2)
|
expect(queries).toHaveLength(6)
|
||||||
expect(queries[0]?.().enabled).toBe(false)
|
expect(queries[0]?.().enabled).toBe(false)
|
||||||
expect(queries[1]?.().enabled).toBe(false)
|
expect(queries[3]?.().enabled).toBe(false)
|
||||||
|
expect(queries[4]?.().enabled).toBe(false)
|
||||||
|
expect(queries[5]?.().enabled).toBe(false)
|
||||||
expect(store.path.directory).toBe("/project")
|
expect(store.path.directory).toBe("/project")
|
||||||
expect(store.provider_ready).toBe(false)
|
expect(store.provider_ready).toBe(false)
|
||||||
expect(store.lsp_ready).toBe(false)
|
expect(store.lsp_ready).toBe(false)
|
||||||
@@ -314,7 +262,9 @@ describe("createChildStoreManager", () => {
|
|||||||
|
|
||||||
manager.child("/project")
|
manager.child("/project")
|
||||||
expect(queries[0]?.().enabled).toBe(true)
|
expect(queries[0]?.().enabled).toBe(true)
|
||||||
expect(queries[1]?.().enabled).toBe(true)
|
expect(queries[3]?.().enabled).toBe(true)
|
||||||
|
expect(queries[4]?.().enabled).toBe(true)
|
||||||
|
expect(queries[5]?.().enabled).toBe(true)
|
||||||
expect(bootstraps).toEqual(["/project"])
|
expect(bootstraps).toEqual(["/project"])
|
||||||
|
|
||||||
manager.child("/project", { bootstrap: false })
|
manager.child("/project", { bootstrap: false })
|
||||||
@@ -323,73 +273,4 @@ describe("createChildStoreManager", () => {
|
|||||||
dispose()
|
dispose()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("does not mark a cancelled provider query as ready", () => {
|
|
||||||
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
|
||||||
providerQuerySuccess = false
|
|
||||||
|
|
||||||
const dispose = createOwner((owner) => {
|
|
||||||
manager = createChildStoreManager({
|
|
||||||
owner,
|
|
||||||
connected: () => true,
|
|
||||||
scope: ServerScope.local,
|
|
||||||
persist,
|
|
||||||
isBooting: () => false,
|
|
||||||
isLoadingSessions: () => false,
|
|
||||||
onBootstrap() {},
|
|
||||||
onMcp() {},
|
|
||||||
onDispose() {},
|
|
||||||
translate: (key) => key,
|
|
||||||
queryOptions: queryOptionsApi,
|
|
||||||
data,
|
|
||||||
global: { path, provider },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!manager) throw new Error("manager required")
|
|
||||||
const [store] = manager.child("/cancelled")
|
|
||||||
expect(store.provider_ready).toBe(false)
|
|
||||||
} finally {
|
|
||||||
providerQuerySuccess = true
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not enable location queries before the event handshake", () => {
|
|
||||||
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
|
||||||
let connected = false
|
|
||||||
const offset = querySingles.length
|
|
||||||
const dispose = createOwner((owner) => {
|
|
||||||
manager = createChildStoreManager({
|
|
||||||
owner,
|
|
||||||
connected: () => connected,
|
|
||||||
scope: ServerScope.local,
|
|
||||||
persist,
|
|
||||||
isBooting: () => false,
|
|
||||||
isLoadingSessions: () => false,
|
|
||||||
onBootstrap() {},
|
|
||||||
onMcp() {},
|
|
||||||
onDispose() {},
|
|
||||||
translate: (key) => key,
|
|
||||||
queryOptions: queryOptionsApi,
|
|
||||||
data,
|
|
||||||
global: { path, provider },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!manager) throw new Error("manager required")
|
|
||||||
manager.child("/handshake")
|
|
||||||
const queries = querySingles.slice(offset)
|
|
||||||
expect(queries[0]?.().enabled).toBe(false)
|
|
||||||
expect(queries[1]?.().enabled).toBe(false)
|
|
||||||
|
|
||||||
connected = true
|
|
||||||
expect(queries[0]?.().enabled).toBe(true)
|
|
||||||
expect(queries[1]?.().enabled).toBe(true)
|
|
||||||
} finally {
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createRoot, createSignal, getOwner, onCleanup, runWithOwner, type Accessor, type Owner } from "solid-js"
|
import { createRoot, createSignal, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import type { Path, VcsInfo } from "@/types"
|
import type { VcsInfo } from "@/types"
|
||||||
import {
|
import {
|
||||||
DIR_IDLE_TTL_MS,
|
DIR_IDLE_TTL_MS,
|
||||||
MAX_DIR_STORES,
|
MAX_DIR_STORES,
|
||||||
@@ -19,12 +19,9 @@ import { QueryOptionsApi } from "../server-sync"
|
|||||||
import { directoryKey, type DirectoryKey } from "./utils"
|
import { directoryKey, type DirectoryKey } from "./utils"
|
||||||
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||||
import type { ServerScope } from "@/utils/server-scope"
|
import type { ServerScope } from "@/utils/server-scope"
|
||||||
import type { Data } from "@opencode-ai/client/solid"
|
|
||||||
import { normalizeAgentList } from "./utils"
|
|
||||||
|
|
||||||
export function createChildStoreManager(input: {
|
export function createChildStoreManager(input: {
|
||||||
owner: Owner
|
owner: Owner
|
||||||
connected: Accessor<boolean>
|
|
||||||
scope: ServerScope
|
scope: ServerScope
|
||||||
persist: typeof persisted
|
persist: typeof persisted
|
||||||
isBooting: (directory: string) => boolean
|
isBooting: (directory: string) => boolean
|
||||||
@@ -34,9 +31,7 @@ export function createChildStoreManager(input: {
|
|||||||
onDispose: (directory: string) => void
|
onDispose: (directory: string) => void
|
||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
queryOptions: QueryOptionsApi
|
queryOptions: QueryOptionsApi
|
||||||
data: Data
|
|
||||||
global: {
|
global: {
|
||||||
path: Path
|
|
||||||
provider: NormalizedProviderListResponse
|
provider: NormalizedProviderListResponse
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
@@ -49,6 +44,7 @@ export function createChildStoreManager(input: {
|
|||||||
const ownerPins = new WeakMap<object, Set<string>>()
|
const ownerPins = new WeakMap<object, Set<string>>()
|
||||||
const disposers = new Map<string, () => void>()
|
const disposers = new Map<string, () => void>()
|
||||||
const mcpDirectories = new Set<string>()
|
const mcpDirectories = new Set<string>()
|
||||||
|
const mcpToggles = new Map<string, (enabled: boolean) => void>()
|
||||||
const activeDirectories = new Set<string>()
|
const activeDirectories = new Set<string>()
|
||||||
const activationToggles = new Map<string, (enabled: boolean) => void>()
|
const activationToggles = new Map<string, (enabled: boolean) => void>()
|
||||||
|
|
||||||
@@ -123,6 +119,7 @@ export function createChildStoreManager(input: {
|
|||||||
iconCache.delete(key)
|
iconCache.delete(key)
|
||||||
lifecycle.delete(key)
|
lifecycle.delete(key)
|
||||||
mcpDirectories.delete(key)
|
mcpDirectories.delete(key)
|
||||||
|
mcpToggles.delete(key)
|
||||||
activeDirectories.delete(key)
|
activeDirectories.delete(key)
|
||||||
activationToggles.delete(key)
|
activationToggles.delete(key)
|
||||||
const dispose = disposers.get(key)
|
const dispose = disposers.get(key)
|
||||||
@@ -188,49 +185,46 @@ export function createChildStoreManager(input: {
|
|||||||
createRoot((dispose) => {
|
createRoot((dispose) => {
|
||||||
const initialMeta = meta[0].value
|
const initialMeta = meta[0].value
|
||||||
const initialIcon = icon[0].value
|
const initialIcon = icon[0].value
|
||||||
|
const [mcpEnabled, setMcpEnabled] = createSignal(false)
|
||||||
const [instanceQueriesEnabled, setInstanceQueriesEnabled] = createSignal(false)
|
const [instanceQueriesEnabled, setInstanceQueriesEnabled] = createSignal(false)
|
||||||
const lspQuery = useQuery(() => ({
|
|
||||||
...input.queryOptions.lsp(key),
|
const pathQuery = useQuery(() => ({ ...input.queryOptions.path(key), enabled: instanceQueriesEnabled() }))
|
||||||
enabled: input.connected() && instanceQueriesEnabled(),
|
const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
|
||||||
}))
|
const mcpResourceQuery = useQuery(() => ({ ...input.queryOptions.mcpResources(key), enabled: mcpEnabled() }))
|
||||||
|
const lspQuery = useQuery(() => ({ ...input.queryOptions.lsp(key), enabled: instanceQueriesEnabled() }))
|
||||||
const providerQuery = useQuery(() => ({
|
const providerQuery = useQuery(() => ({
|
||||||
...input.queryOptions.providers(key),
|
...input.queryOptions.providers(key),
|
||||||
enabled: input.connected() && instanceQueriesEnabled(),
|
enabled: instanceQueriesEnabled(),
|
||||||
}))
|
}))
|
||||||
|
const referenceQuery = useQuery(() => ({
|
||||||
|
...input.queryOptions.references(key),
|
||||||
|
enabled: instanceQueriesEnabled(),
|
||||||
|
}))
|
||||||
|
|
||||||
const child = createStore<State>({
|
const child = createStore<State>({
|
||||||
project: "",
|
project: "",
|
||||||
projectMeta: initialMeta,
|
projectMeta: initialMeta,
|
||||||
icon: initialIcon,
|
icon: initialIcon,
|
||||||
get provider_ready() {
|
get provider_ready() {
|
||||||
return instanceQueriesEnabled() && (providerQuery.isSuccess || providerQuery.isRefetchError)
|
return instanceQueriesEnabled() && !providerQuery.isLoading
|
||||||
},
|
},
|
||||||
get provider() {
|
get provider() {
|
||||||
const EMPTY = { all: new Map(), connected: [], default: {} }
|
const EMPTY = { all: new Map(), connected: [], default: {} }
|
||||||
if (!providerQuery.isSuccess && !providerQuery.isRefetchError) return EMPTY
|
if (providerQuery.isLoading) return EMPTY
|
||||||
const provider = providerQuery.data
|
if (providerQuery.data?.all.size === 0 && input.global.provider.all.size > 0) return input.global.provider
|
||||||
if (provider.all.size === 0 && input.global.provider.all.size > 0) return input.global.provider
|
return providerQuery.data ?? EMPTY
|
||||||
return provider
|
|
||||||
},
|
},
|
||||||
config: {},
|
config: {},
|
||||||
get path() {
|
get path() {
|
||||||
const location = input.data.location.info({ directory })
|
const EMPTY = { state: "", config: "", worktree: "", directory, home: "" }
|
||||||
return {
|
if (pathQuery.isLoading) return EMPTY
|
||||||
state: "",
|
return pathQuery.data ?? EMPTY
|
||||||
config: "",
|
|
||||||
worktree: location?.project.directory ?? "",
|
|
||||||
directory: location?.directory ?? directory,
|
|
||||||
home: input.global.path.home,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
status: "loading" as const,
|
status: "loading" as const,
|
||||||
get agent() {
|
agent: [],
|
||||||
return normalizeAgentList(input.data.location.agent.list({ directory }) ?? [])
|
command: [],
|
||||||
},
|
|
||||||
get command() {
|
|
||||||
return input.data.location.command.list({ directory }) ?? []
|
|
||||||
},
|
|
||||||
get reference() {
|
get reference() {
|
||||||
return input.data.location.reference.list({ directory }) ?? []
|
return referenceQuery.isLoading ? [] : (referenceQuery.data ?? [])
|
||||||
},
|
},
|
||||||
session: [],
|
session: [],
|
||||||
sessionTotal: 0,
|
sessionTotal: 0,
|
||||||
@@ -244,20 +238,13 @@ export function createChildStoreManager(input: {
|
|||||||
permission: {},
|
permission: {},
|
||||||
question: {},
|
question: {},
|
||||||
get mcp_ready() {
|
get mcp_ready() {
|
||||||
return input.data.location.mcp.server.list({ directory }) !== undefined
|
return !mcpQuery.isLoading
|
||||||
},
|
},
|
||||||
get mcp() {
|
get mcp() {
|
||||||
return Object.fromEntries(
|
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
|
||||||
(input.data.location.mcp.server.list({ directory }) ?? []).map((server) => [server.name, server.status]),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
get mcp_resource() {
|
get mcp_resource() {
|
||||||
return Object.fromEntries(
|
return mcpResourceQuery.isLoading ? {} : (mcpResourceQuery.data ?? {})
|
||||||
(input.data.location.mcp.resource.list({ directory }) ?? []).map((resource) => [
|
|
||||||
`${resource.server}:${resource.uri}`,
|
|
||||||
resource,
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
get lsp_ready() {
|
get lsp_ready() {
|
||||||
return instanceQueriesEnabled() && !lspQuery.isLoading
|
return instanceQueriesEnabled() && !lspQuery.isLoading
|
||||||
@@ -265,11 +252,7 @@ export function createChildStoreManager(input: {
|
|||||||
get lsp() {
|
get lsp() {
|
||||||
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
|
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
|
||||||
},
|
},
|
||||||
get vcs() {
|
vcs: vcsStore.value,
|
||||||
const vcs = input.data.location.vcs.info({ directory })
|
|
||||||
if (!vcs) return vcsStore.value
|
|
||||||
return { branch: vcs.branch.current, default_branch: vcs.branch.default }
|
|
||||||
},
|
|
||||||
limit: 5,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
session_message: {},
|
session_message: {},
|
||||||
@@ -278,6 +261,7 @@ export function createChildStoreManager(input: {
|
|||||||
})
|
})
|
||||||
children[key] = child
|
children[key] = child
|
||||||
disposers.set(key, dispose)
|
disposers.set(key, dispose)
|
||||||
|
mcpToggles.set(key, setMcpEnabled)
|
||||||
activationToggles.set(key, setInstanceQueriesEnabled)
|
activationToggles.set(key, setInstanceQueriesEnabled)
|
||||||
|
|
||||||
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
||||||
@@ -341,6 +325,7 @@ export function createChildStoreManager(input: {
|
|||||||
function enableMcp(directory: string, key: DirectoryKey, childStore: [Store<State>, SetStoreFunction<State>]) {
|
function enableMcp(directory: string, key: DirectoryKey, childStore: [Store<State>, SetStoreFunction<State>]) {
|
||||||
if (mcpDirectories.has(key)) return
|
if (mcpDirectories.has(key)) return
|
||||||
mcpDirectories.add(key)
|
mcpDirectories.add(key)
|
||||||
|
mcpToggles.get(key)?.(true)
|
||||||
if (childStore[0].status !== "loading") input.onMcp(directory, childStore[1])
|
if (childStore[0].status !== "loading") input.onMcp(directory, childStore[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,6 +342,7 @@ export function createChildStoreManager(input: {
|
|||||||
function disableMcp(directory: string) {
|
function disableMcp(directory: string) {
|
||||||
const key = directoryKey(directory)
|
const key = directoryKey(directory)
|
||||||
if (!mcpDirectories.delete(key)) return
|
if (!mcpDirectories.delete(key)) return
|
||||||
|
mcpToggles.get(key)?.(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectMeta(directory: string, patch: ProjectMeta) {
|
function projectMeta(directory: string, patch: ProjectMeta) {
|
||||||
@@ -387,15 +373,6 @@ export function createChildStoreManager(input: {
|
|||||||
setStore("icon", value)
|
setStore("icon", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function vcs(directory: string, value: VcsInfo) {
|
|
||||||
const key = directoryKey(directory)
|
|
||||||
const child = ensureChild(directory)
|
|
||||||
const cached = vcsCache.get(key)
|
|
||||||
if (!cached) return
|
|
||||||
cached.setStore("value", value)
|
|
||||||
child[1]("vcs", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
children,
|
children,
|
||||||
ensureChild,
|
ensureChild,
|
||||||
@@ -403,7 +380,6 @@ export function createChildStoreManager(input: {
|
|||||||
peek,
|
peek,
|
||||||
projectMeta,
|
projectMeta,
|
||||||
projectIcon,
|
projectIcon,
|
||||||
vcs,
|
|
||||||
mark,
|
mark,
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe("applyGlobalEvent", () => {
|
|||||||
expect(refreshCount).toBe(1)
|
expect(refreshCount).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("leaves server.connected refresh to the connection sync", () => {
|
test("handles server.connected by triggering refresh", () => {
|
||||||
let refreshCount = 0
|
let refreshCount = 0
|
||||||
applyGlobalEvent({
|
applyGlobalEvent({
|
||||||
event: { type: "server.connected" },
|
event: { type: "server.connected" },
|
||||||
@@ -131,7 +131,7 @@ describe("applyGlobalEvent", () => {
|
|||||||
setGlobalProject() {},
|
setGlobalProject() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(refreshCount).toBe(0)
|
expect(refreshCount).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Binary } from "@opencode-ai/core/util/binary"
|
import { Binary } from "@opencode-ai/core/util/binary"
|
||||||
import { Worktree } from "@opencode-ai/schema/worktree"
|
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||||
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type { Message, Part, Project, Todo } from "@/types"
|
import type { Message, Part, Project, Todo } from "@/types"
|
||||||
import type {
|
import type {
|
||||||
@@ -38,10 +38,11 @@ export function applyGlobalEvent(input: {
|
|||||||
setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void
|
setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void
|
||||||
refresh: () => void
|
refresh: () => void
|
||||||
}) {
|
}) {
|
||||||
if (input.event.type === "global.disposed") {
|
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
|
||||||
input.refresh()
|
input.refresh()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.event.type !== "project.updated") return
|
if (input.event.type !== "project.updated") return
|
||||||
const properties = input.event.properties as Project
|
const properties = input.event.properties as Project
|
||||||
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
const result = Binary.search(input.project, properties.id, (s) => s.id)
|
||||||
@@ -187,10 +188,10 @@ export function applyDirectoryEvent(input: {
|
|||||||
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "worktree.resolved": {
|
case "project.directory.resolved": {
|
||||||
const properties = event.properties as { projectID: string; directory: string; previous: string }
|
const properties = event.properties as { projectID: string; directory: string; previous: string }
|
||||||
input.store.session.forEach((session, index) => {
|
input.store.session.forEach((session, index) => {
|
||||||
const adopted = Worktree.adopt(
|
const adopted = ProjectDirectories.adopt(
|
||||||
{ projectID: session.projectID, directory: session.location.directory },
|
{ projectID: session.projectID, directory: session.location.directory },
|
||||||
properties,
|
properties,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { Message, Part, Todo } from "@/types"
|
import type { Message, Part, Todo } from "@/types"
|
||||||
import type { FormInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
import type { PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||||
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
|
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ describe("app session cache", () => {
|
|||||||
part: Record<string, Part[] | undefined>
|
part: Record<string, Part[] | undefined>
|
||||||
permission: Record<string, PermissionRequest[] | undefined>
|
permission: Record<string, PermissionRequest[] | undefined>
|
||||||
question: Record<string, QuestionRequest[] | undefined>
|
question: Record<string, QuestionRequest[] | undefined>
|
||||||
form: Record<string, FormInfo[] | undefined>
|
|
||||||
part_text_accum_delta: Record<string, string | undefined>
|
part_text_accum_delta: Record<string, string | undefined>
|
||||||
} = {
|
} = {
|
||||||
session_status: { ses_1: { type: "busy" } as SessionStatus },
|
session_status: { ses_1: { type: "busy" } as SessionStatus },
|
||||||
@@ -45,7 +44,6 @@ describe("app session cache", () => {
|
|||||||
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
|
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
|
||||||
permission: { ses_1: [] as PermissionRequest[] },
|
permission: { ses_1: [] as PermissionRequest[] },
|
||||||
question: { ses_1: [] as QuestionRequest[] },
|
question: { ses_1: [] as QuestionRequest[] },
|
||||||
form: { ses_1: [] as FormInfo[] },
|
|
||||||
part_text_accum_delta: { prt_1: "streamed text" },
|
part_text_accum_delta: { prt_1: "streamed text" },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +57,6 @@ describe("app session cache", () => {
|
|||||||
expect(store.session_status.ses_1).toBeUndefined()
|
expect(store.session_status.ses_1).toBeUndefined()
|
||||||
expect(store.permission.ses_1).toBeUndefined()
|
expect(store.permission.ses_1).toBeUndefined()
|
||||||
expect(store.question.ses_1).toBeUndefined()
|
expect(store.question.ses_1).toBeUndefined()
|
||||||
expect(store.form.ses_1).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("dropSessionCaches clears message-backed parts", () => {
|
test("dropSessionCaches clears message-backed parts", () => {
|
||||||
@@ -73,7 +70,6 @@ describe("app session cache", () => {
|
|||||||
part: Record<string, Part[] | undefined>
|
part: Record<string, Part[] | undefined>
|
||||||
permission: Record<string, PermissionRequest[] | undefined>
|
permission: Record<string, PermissionRequest[] | undefined>
|
||||||
question: Record<string, QuestionRequest[] | undefined>
|
question: Record<string, QuestionRequest[] | undefined>
|
||||||
form: Record<string, FormInfo[] | undefined>
|
|
||||||
part_text_accum_delta: Record<string, string | undefined>
|
part_text_accum_delta: Record<string, string | undefined>
|
||||||
} = {
|
} = {
|
||||||
session_status: {},
|
session_status: {},
|
||||||
@@ -84,7 +80,6 @@ describe("app session cache", () => {
|
|||||||
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
|
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
|
||||||
permission: {},
|
permission: {},
|
||||||
question: {},
|
question: {},
|
||||||
form: {},
|
|
||||||
part_text_accum_delta: {},
|
part_text_accum_delta: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Message, Part, Todo } from "@/types"
|
import type { Message, Part, Todo } from "@/types"
|
||||||
import type { FormInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
import type { PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||||
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ type SessionCache = {
|
|||||||
part: Record<string, Part[] | undefined>
|
part: Record<string, Part[] | undefined>
|
||||||
permission: Record<string, PermissionRequest[] | undefined>
|
permission: Record<string, PermissionRequest[] | undefined>
|
||||||
question: Record<string, QuestionRequest[] | undefined>
|
question: Record<string, QuestionRequest[] | undefined>
|
||||||
form?: Record<string, FormInfo[] | undefined>
|
|
||||||
part_text_accum_delta: Record<string, string | undefined>
|
part_text_accum_delta: Record<string, string | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +38,6 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<stri
|
|||||||
delete store.session_status[sessionID]
|
delete store.session_status[sessionID]
|
||||||
delete store.permission[sessionID]
|
delete store.permission[sessionID]
|
||||||
delete store.question[sessionID]
|
delete store.question[sessionID]
|
||||||
if (store.form) delete store.form[sessionID]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,11 +124,9 @@ export function normalizeProviderList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeProjectInfo(project: Project | CurrentProject): Project {
|
export function normalizeProjectInfo(project: Project | CurrentProject): Project {
|
||||||
const worktree = "canonical" in project ? project.canonical : project.worktree
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
worktree,
|
worktree: "canonical" in project ? project.canonical : project.worktree,
|
||||||
worktrees: "worktrees" in project ? project.worktrees : [{ directory: worktree }],
|
|
||||||
vcs: project.vcs === "git" ? "git" : undefined,
|
vcs: project.vcs === "git" ? "git" : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { Accessor, createEffect, createMemo, createRoot } from "solid-js"
|
import { createEffect, createMemo, createRoot } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createServerProjects, RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServers } from "./servers"
|
import { createServerProjects, RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
import { useServerHealth } from "@/utils/server-health"
|
import { useServerHealth } from "@/utils/server-health"
|
||||||
import { createServerSdkContext } from "./server-sdk"
|
import { createServerSdkContext } from "./server-sdk"
|
||||||
import { createServerSyncContext } from "./server-sync"
|
import { createServerSyncContext } from "./server-sync"
|
||||||
import { getOwner } from "solid-js/web"
|
import { getOwner } from "solid-js/web"
|
||||||
import { createServerData } from "@opencode-ai/client/solid"
|
import { QueryClient } from "@tanstack/solid-query"
|
||||||
import type { ServerScope } from "@/utils/server-scope"
|
import type { ServerScope } from "@/utils/server-scope"
|
||||||
import { createServerPermissionState } from "./permission"
|
|
||||||
import { createServerNotificationState } from "./notification"
|
|
||||||
|
|
||||||
export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({
|
export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({
|
||||||
name: "Global",
|
name: "Global",
|
||||||
init: () => {
|
init: () => {
|
||||||
const server = useServers()
|
const server = useServer()
|
||||||
const serverHealth = useServerHealth(
|
const serverHealth = useServerHealth(
|
||||||
() => server.list,
|
() => server.list,
|
||||||
() => true,
|
() => true,
|
||||||
@@ -37,21 +35,23 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
|||||||
if (store.settings.serverKey !== key) setStore("settings", "serverKey", key)
|
if (store.settings.serverKey !== key) setStore("settings", "serverKey", key)
|
||||||
})
|
})
|
||||||
|
|
||||||
const serverCtxs = new Map<ServerConnection.Key, ReturnType<typeof createServerController>>()
|
const serverCtxs = new Map<
|
||||||
const serverCtxDisposers = new Map<ServerConnection.Key, () => void>()
|
ServerConnection.Key,
|
||||||
|
{ dispose: () => void; serverCtx: ReturnType<typeof createServerCtx> }
|
||||||
|
>()
|
||||||
|
|
||||||
const owner = getOwner()
|
const owner = getOwner()
|
||||||
|
|
||||||
const ensureServerCtx = (conn: ServerConnection.Any) => {
|
const ensureServerCtx = (conn: ServerConnection.Any) => {
|
||||||
const key = ServerConnection.key(conn)
|
const key = ServerConnection.key(conn)
|
||||||
const existing = serverCtxs.get(key)
|
const existing = serverCtxs.get(key)
|
||||||
if (existing) return existing
|
if (existing) return existing.serverCtx
|
||||||
const serverCtx = createRoot((dispose) => {
|
const root = createRoot((dispose) => {
|
||||||
serverCtxDisposers.set(key, dispose)
|
const serverCtx = createServerCtx(conn, server.scope(key), server.projects.forServer(key))
|
||||||
return createServerController(conn, server.scope(key), server.projects.forServer(key))
|
return { dispose, serverCtx }
|
||||||
}, owner as any)
|
}, owner as any)
|
||||||
serverCtxs.set(key, serverCtx)
|
serverCtxs.set(key, root)
|
||||||
return serverCtx
|
return root.serverCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
createMemo(() => {
|
createMemo(() => {
|
||||||
@@ -63,8 +63,8 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
for (const [key] of serverCtxs) {
|
for (const [key] of serverCtxs) {
|
||||||
if (!server.list.find((conn) => ServerConnection.key(conn) === key)) {
|
if (!server.list.find((conn) => ServerConnection.key(conn) === key)) {
|
||||||
serverCtxDisposers.get(key)?.()
|
const { dispose } = serverCtxs.get(key)!
|
||||||
serverCtxDisposers.delete(key)
|
dispose()
|
||||||
serverCtxs.delete(key)
|
serverCtxs.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,22 +93,22 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function createServerController(
|
function createServerCtx(
|
||||||
conn: ServerConnection.Any,
|
conn: ServerConnection.Any,
|
||||||
scope: ServerScope,
|
scope: ServerScope,
|
||||||
projects: ReturnType<typeof createServerProjects>,
|
projects: ReturnType<typeof createServerProjects>,
|
||||||
) {
|
) {
|
||||||
const connKey = ServerConnection.key(conn)
|
const queryClient = new QueryClient({
|
||||||
const sdk = createServerSdkContext(conn, scope)
|
defaultOptions: {
|
||||||
const data = createServerData({
|
queries: {
|
||||||
api: () => sdk.api,
|
refetchOnReconnect: false,
|
||||||
event: sdk.event,
|
refetchOnMount: false,
|
||||||
connection: sdk.connection,
|
refetchOnWindowFocus: false,
|
||||||
directory: "",
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const sync = createServerSyncContext(sdk, data)
|
const sdk = createServerSdkContext(conn, scope)
|
||||||
const permission = createServerPermissionState({ sdk, sync })
|
const sync = createServerSyncContext(sdk)
|
||||||
const notification = createServerNotificationState({ sdk, sync, key: connKey })
|
|
||||||
|
|
||||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||||
const [childStore] = sync.child(project.worktree, { bootstrap: false })
|
const [childStore] = sync.child(project.worktree, { bootstrap: false })
|
||||||
@@ -141,7 +141,7 @@ function createServerController(
|
|||||||
(conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url))
|
(conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
queryClient,
|
||||||
sdk,
|
sdk,
|
||||||
sync,
|
sync,
|
||||||
isLocal,
|
isLocal,
|
||||||
@@ -150,22 +150,10 @@ function createServerController(
|
|||||||
list: projectsList,
|
list: projectsList,
|
||||||
recentlyClosed: recentlyClosedList,
|
recentlyClosed: recentlyClosedList,
|
||||||
},
|
},
|
||||||
permission,
|
|
||||||
notification,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useServerCtx(server: Accessor<ServerConnection.Any>): Accessor<ServerCtx>
|
export type ServerCtx = ReturnType<typeof createServerCtx>
|
||||||
export function useServerCtx(server: Accessor<ServerConnection.Any | undefined>): Accessor<ServerCtx | undefined>
|
|
||||||
export function useServerCtx(server: Accessor<ServerConnection.Any | undefined>) {
|
|
||||||
const global = useGlobal()
|
|
||||||
return () => {
|
|
||||||
const s = server()
|
|
||||||
if (s) return global.ensureServerCtx(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ServerCtx = ReturnType<typeof createServerController>
|
|
||||||
|
|
||||||
function isLocalHost(url: string) {
|
function isLocalHost(url: string) {
|
||||||
const host = url.replace(/^https?:\/\//, "").split(":")[0]
|
const host = url.replace(/^https?:\/\//, "").split(":")[0]
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ import * as i18n from "@solid-primitives/i18n"
|
|||||||
import { createEffect, createMemo, createResource } from "solid-js"
|
import { createEffect, createMemo, createResource } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import {
|
import { pluralCategory, type UiI18nPluralKey } from "@opencode-ai/ui/context/i18n"
|
||||||
pluralCategory,
|
|
||||||
type UiI18nPluralLookupKey,
|
|
||||||
type UiI18nPluralKey,
|
|
||||||
type UiPluralCategory,
|
|
||||||
} from "@opencode-ai/ui/context/i18n"
|
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { dict as en } from "@/i18n/en"
|
import { dict as en } from "@/i18n/en"
|
||||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||||
@@ -25,7 +20,7 @@ import {
|
|||||||
export type Locale = DesktopNativeLocale
|
export type Locale = DesktopNativeLocale
|
||||||
export type Direction = "ltr" | "rtl"
|
export type Direction = "ltr" | "rtl"
|
||||||
|
|
||||||
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "he", "ur", "pa", "fa", "dv"])
|
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa", "fa", "dv"])
|
||||||
|
|
||||||
function localeDirection(locale: Locale): Direction {
|
function localeDirection(locale: Locale): Direction {
|
||||||
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
||||||
@@ -33,17 +28,11 @@ function localeDirection(locale: Locale): Direction {
|
|||||||
|
|
||||||
type RawDictionary = typeof en & typeof uiEn
|
type RawDictionary = typeof en & typeof uiEn
|
||||||
type Dictionary = i18n.Flatten<RawDictionary>
|
type Dictionary = i18n.Flatten<RawDictionary>
|
||||||
type AppI18nKey = Extract<keyof typeof en, string>
|
type PluralKey =
|
||||||
type AppI18nPluralKey = {
|
| UiI18nPluralKey
|
||||||
[Key in AppI18nKey]: Key extends `${infer Base}.other` ? (`${Base}.one` extends AppI18nKey ? Base : never) : never
|
| "session.question.pending"
|
||||||
}[AppI18nKey]
|
| "session.followupDock.summary"
|
||||||
type PluralKey = AppI18nPluralKey | UiI18nPluralKey
|
| "session.revertDock.summary"
|
||||||
type AppI18nPluralLookupKey = `${AppI18nPluralKey}.${UiPluralCategory}`
|
|
||||||
type TranslationKey<Key extends Extract<keyof Dictionary, string>> = Key extends
|
|
||||||
| AppI18nPluralLookupKey
|
|
||||||
| UiI18nPluralLookupKey
|
|
||||||
? never
|
|
||||||
: Key
|
|
||||||
type Source = { dict: Record<string, string> }
|
type Source = { dict: Record<string, string> }
|
||||||
|
|
||||||
function cookie(locale: Locale) {
|
function cookie(locale: Locale) {
|
||||||
@@ -73,7 +62,6 @@ const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
|
|||||||
ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")),
|
ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")),
|
||||||
uk: () => merge(import("@/i18n/uk"), import("@opencode-ai/ui/i18n/uk")),
|
uk: () => merge(import("@/i18n/uk"), import("@opencode-ai/ui/i18n/uk")),
|
||||||
ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")),
|
ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")),
|
||||||
he: () => merge(import("@/i18n/he"), import("@opencode-ai/ui/i18n/he")),
|
|
||||||
no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")),
|
no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")),
|
||||||
br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")),
|
br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")),
|
||||||
th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")),
|
th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")),
|
||||||
@@ -201,25 +189,18 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
initialValue: dicts.get(initial) ?? base,
|
initialValue: dicts.get(initial) ?? base,
|
||||||
})
|
})
|
||||||
|
|
||||||
const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as <
|
const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as (
|
||||||
Key extends Extract<keyof Dictionary, string>,
|
key: keyof Dictionary,
|
||||||
>(
|
|
||||||
key: TranslationKey<Key>,
|
|
||||||
params?: Record<string, string | number | boolean>,
|
params?: Record<string, string | number | boolean>,
|
||||||
) => string
|
) => string
|
||||||
|
|
||||||
const pluralForm = (
|
const plural = (key: PluralKey, count: number, params?: Record<string, string | number | boolean>) => {
|
||||||
key: PluralKey,
|
const category = pluralCategory(intl(), count)
|
||||||
category: UiPluralCategory,
|
|
||||||
params?: Record<string, string | number | boolean>,
|
|
||||||
) => {
|
|
||||||
const current = (dict.loading ? base : (dict() ?? base)) as Record<string, string>
|
const current = (dict.loading ? base : (dict() ?? base)) as Record<string, string>
|
||||||
const candidate = `${key}.${category}`
|
const candidate = `${key}.${category}`
|
||||||
const fallback = `${key}.other`
|
const fallback = `${key}.other`
|
||||||
return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, params)
|
return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count })
|
||||||
}
|
}
|
||||||
const plural = (key: PluralKey, count: number, params?: Record<string, string | number | boolean>) =>
|
|
||||||
pluralForm(key, pluralCategory(intl(), count), { ...params, count })
|
|
||||||
|
|
||||||
const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value]
|
const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value]
|
||||||
|
|
||||||
@@ -250,7 +231,6 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
|||||||
label,
|
label,
|
||||||
t,
|
t,
|
||||||
plural,
|
plural,
|
||||||
pluralForm,
|
|
||||||
setLocale(next: Locale) {
|
setLocale(next: Locale) {
|
||||||
setStore("locale", normalizeLocale(next))
|
setStore("locale", normalizeLocale(next))
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user