mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-18 06:44:23 +00:00
Compare commits
105 Commits
optimize-a
...
brendan/de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f3749b11 | ||
|
|
2a7b0df523 | ||
|
|
14f84369f8 | ||
|
|
60df8c0b3c | ||
|
|
8cde3554ac | ||
|
|
df4cc384de | ||
|
|
aad18173c7 | ||
|
|
6413e91947 | ||
|
|
9106d315e4 | ||
|
|
6097c04a50 | ||
|
|
667953db85 | ||
|
|
b2e86aaf24 | ||
|
|
032931c749 | ||
|
|
a90e8de050 | ||
|
|
56dfbbbc93 | ||
|
|
943bf316b6 | ||
|
|
1f0346725a | ||
|
|
eabf770053 | ||
|
|
4666daa581 | ||
|
|
caf1316116 | ||
|
|
1de66812bf | ||
|
|
173c16581d | ||
|
|
94d0c9940a | ||
|
|
76bcd22802 | ||
|
|
e06fcfdc43 | ||
|
|
246430cb8f | ||
|
|
f689fc7f75 | ||
|
|
9b2fd57e6e | ||
|
|
0d365fa613 | ||
|
|
601e631624 | ||
|
|
2fef02f487 | ||
|
|
4cc9104942 | ||
|
|
0eaa6b5fc8 | ||
|
|
e563cff034 | ||
|
|
3b2550106b | ||
|
|
3ec6bff038 | ||
|
|
aab2a6df3b | ||
|
|
84171018f2 | ||
|
|
280d7e6f91 | ||
|
|
5952891b1e | ||
|
|
d7c8a3f50d | ||
|
|
ce353819e8 | ||
|
|
2dae94e5a3 | ||
|
|
c6adc19e41 | ||
|
|
ce56166510 | ||
|
|
5911e4c06a | ||
|
|
42fb840f22 | ||
|
|
4dcfdf6572 | ||
|
|
25f3d6d5a9 | ||
|
|
e19a9e9614 | ||
|
|
bb4d978684 | ||
|
|
fcc903489b | ||
|
|
949e69a9bf | ||
|
|
afec40e8da | ||
|
|
8c30f551e2 | ||
|
|
cb721497c1 | ||
|
|
4ec6293054 | ||
|
|
b7a323355c | ||
|
|
d4f053042c | ||
|
|
5f552534c7 | ||
|
|
ad5b790bb3 | ||
|
|
ed87341c4f | ||
|
|
794ecab028 | ||
|
|
eeb235724b | ||
|
|
61084e7f6f | ||
|
|
200aef2eb3 | ||
|
|
f6e375a555 | ||
|
|
db908deee5 | ||
|
|
7b72cc3a48 | ||
|
|
b8cbfd48ec | ||
|
|
498cbb2c26 | ||
|
|
d6fbd255b6 | ||
|
|
2de1c82bf7 | ||
|
|
34ebb3d051 | ||
|
|
9c3e3c1ab5 | ||
|
|
3ea499f04e | ||
|
|
ab13c1d1c4 | ||
|
|
53b610c331 | ||
|
|
e3519356f2 | ||
|
|
2619acc0ff | ||
|
|
1bc45dc266 | ||
|
|
2e8feb1c78 | ||
|
|
00e60899cc | ||
|
|
30a918e9d4 | ||
|
|
ac16068140 | ||
|
|
19a41ab297 | ||
|
|
cd174d8cba | ||
|
|
246e901e42 | ||
|
|
0ccef1b31f | ||
|
|
7706f5b6a8 | ||
|
|
63e38555c9 | ||
|
|
f40685ab13 | ||
|
|
a48a5a3462 | ||
|
|
5e1639de2b | ||
|
|
2b05833c32 | ||
|
|
acdcf7fa88 | ||
|
|
bf0754caeb | ||
|
|
4d50a32979 | ||
|
|
57edb0ddc5 | ||
|
|
a614b78c6d | ||
|
|
b9f5a34247 | ||
|
|
81b47a44e2 | ||
|
|
0c1c07467e | ||
|
|
105688bf90 | ||
|
|
1e7b4768b1 |
1
.github/VOUCHED.td
vendored
1
.github/VOUCHED.td
vendored
@@ -8,7 +8,6 @@
|
||||
# - Denounce with minus prefix: -username or -platform:username.
|
||||
# - Optional details after a space following the handle.
|
||||
adamdotdevin
|
||||
ariane-emory
|
||||
-florianleibert
|
||||
fwang
|
||||
iamdavidhill
|
||||
|
||||
1
.github/actions/setup-bun/action.yml
vendored
1
.github/actions/setup-bun/action.yml
vendored
@@ -4,7 +4,6 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Mount Bun Cache
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: useblacksmith/stickydisk@v1
|
||||
with:
|
||||
key: ${{ github.repository }}-bun-cache-${{ runner.os }}
|
||||
|
||||
4
.github/workflows/pr-management.yml
vendored
4
.github/workflows/pr-management.yml
vendored
@@ -60,11 +60,9 @@ jobs:
|
||||
run: |
|
||||
COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates")
|
||||
|
||||
if [ "$COMMENT" != "No duplicate PRs found" ]; then
|
||||
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
|
||||
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
|
||||
|
||||
$COMMENT"
|
||||
fi
|
||||
|
||||
add-contributor-label:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
24
.github/workflows/publish.yml
vendored
24
.github/workflows/publish.yml
vendored
@@ -137,7 +137,7 @@ jobs:
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/apt-cache
|
||||
path: /var/cache/apt/archives
|
||||
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.settings.target }}-apt-
|
||||
@@ -145,10 +145,8 @@ jobs:
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
run: |
|
||||
mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo chmod -R a+rw ~/apt-cache
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -171,23 +169,13 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
|
||||
- name: Resolve tauri portable SHA
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV"
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- name: Install tauri-cli from portable appimage branch
|
||||
uses: taiki-e/cache-cargo-install-action@v3
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
with:
|
||||
tool: tauri-cli
|
||||
git: https://github.com/tauri-apps/tauri
|
||||
# branch: feat/truly-portable-appimage
|
||||
rev: ${{ env.TAURI_PORTABLE_SHA }}
|
||||
|
||||
- name: Show tauri-cli version
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
run: cargo tauri --version
|
||||
run: |
|
||||
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
|
||||
echo "Installed tauri-cli version:"
|
||||
cargo tauri --version
|
||||
|
||||
- name: Build and upload artifacts
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
|
||||
54
.github/workflows/sign-cli.yml
vendored
54
.github/workflows/sign-cli.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: sign-cli
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- brendan/desktop-signpath
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
sign-cli:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-tags: true
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
./packages/opencode/script/build.ts
|
||||
|
||||
- name: Upload unsigned Windows CLI
|
||||
id: upload_unsigned_windows_cli
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unsigned-opencode-windows-cli
|
||||
path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Submit SignPath signing request
|
||||
id: submit_signpath_signing_request
|
||||
uses: signpath/github-action-submit-signing-request@v1
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_KEY }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
|
||||
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
|
||||
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
|
||||
github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: signed-opencode-cli
|
||||
|
||||
- name: Upload signed Windows CLI
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signed-opencode-windows-cli
|
||||
path: signed-opencode-cli/*.exe
|
||||
if-no-files-found: error
|
||||
@@ -359,7 +359,6 @@ opencode serve --hostname 0.0.0.0 --port 4096
|
||||
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||
opencode session [command]
|
||||
opencode session list
|
||||
opencode session delete <sessionID>
|
||||
opencode stats
|
||||
opencode uninstall
|
||||
opencode upgrade
|
||||
@@ -599,7 +598,6 @@ OPENCODE_EXPERIMENTAL_MARKDOWN
|
||||
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
|
||||
OPENCODE_EXPERIMENTAL_OXFMT
|
||||
OPENCODE_EXPERIMENTAL_PLAN_MODE
|
||||
OPENCODE_ENABLE_QUESTION_TOOL
|
||||
OPENCODE_FAKE_VCS
|
||||
OPENCODE_GIT_BASH_PATH
|
||||
OPENCODE_MODEL
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/minimax-m2.5
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#44BA81"
|
||||
tools:
|
||||
"*": false
|
||||
@@ -12,8 +12,6 @@ You are a triage agent responsible for triaging github issues.
|
||||
|
||||
Use your github-triage tool to triage issues.
|
||||
|
||||
This file is the source of truth for ownership/routing rules.
|
||||
|
||||
## Labels
|
||||
|
||||
### windows
|
||||
@@ -45,34 +43,12 @@ Desktop app issues:
|
||||
|
||||
**Only** add if the issue explicitly mentions nix.
|
||||
|
||||
If the issue does not mention nix, do not add nix.
|
||||
|
||||
If the issue mentions nix, assign to `rekram1-node`.
|
||||
|
||||
#### zen
|
||||
|
||||
**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black".
|
||||
|
||||
If the issue doesn't have "zen" or "opencode black" in it then don't add zen label
|
||||
|
||||
#### core
|
||||
|
||||
Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`.
|
||||
|
||||
Examples:
|
||||
|
||||
- LSP server behavior
|
||||
- Harness behavior (agent + tools)
|
||||
- Feature requests for server behavior
|
||||
- Agent context construction
|
||||
- API endpoints
|
||||
- Provider integration issues
|
||||
- New, broken, or poor-quality models
|
||||
|
||||
#### acp
|
||||
|
||||
If the issue mentions acp support, assign acp label.
|
||||
|
||||
#### docs
|
||||
|
||||
Add if the issue requests better documentation or docs updates.
|
||||
@@ -90,51 +66,13 @@ TUI issues potentially caused by our underlying TUI library:
|
||||
|
||||
When assigning to people here are the following rules:
|
||||
|
||||
Desktop / Web:
|
||||
Use for desktop-labeled issues only.
|
||||
adamdotdev:
|
||||
ONLY assign adam if the issue will have the "desktop" label.
|
||||
|
||||
- adamdotdevin
|
||||
- iamdavidhill
|
||||
- Brendonovich
|
||||
- nexxeln
|
||||
fwang:
|
||||
ONLY assign fwang if the issue will have the "zen" label.
|
||||
|
||||
Zen:
|
||||
ONLY assign if the issue will have the "zen" label.
|
||||
jayair:
|
||||
ONLY assign jayair if the issue will have the "docs" label.
|
||||
|
||||
- fwang
|
||||
- MrMushrooooom
|
||||
|
||||
TUI (`packages/opencode/src/cli/cmd/tui/...`):
|
||||
|
||||
- thdxr for TUI UX/UI product decisions and interaction flow
|
||||
- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks
|
||||
- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues
|
||||
|
||||
Core (`packages/opencode/...`, excluding TUI subtree):
|
||||
|
||||
- thdxr for sqlite/snapshot/memory bugs and larger architectural core features
|
||||
- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable)
|
||||
- rekram1-node for harness issues, provider issues, and other bug-squashing
|
||||
|
||||
For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable.
|
||||
|
||||
Docs:
|
||||
|
||||
- R44VC0RP
|
||||
|
||||
Windows:
|
||||
|
||||
- Hona (assign any issue that mentions Windows or is likely Windows-specific)
|
||||
|
||||
Determinism rules:
|
||||
|
||||
- If title + body does not contain "zen", do not add the "zen" label
|
||||
- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix"
|
||||
- If title + body mentions nix/nixos, assign to `rekram1-node`
|
||||
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
|
||||
|
||||
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
||||
|
||||
ACP:
|
||||
|
||||
- rekram1-node (assign any acp issues to rekram1-node)
|
||||
In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node.
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// "enterprise": {
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {},
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
// import { Octokit } from "@octokit/rest"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import DESCRIPTION from "./github-triage.txt"
|
||||
|
||||
const TEAM = {
|
||||
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
|
||||
zen: ["fwang", "MrMushrooooom"],
|
||||
tui: ["thdxr", "kommander", "rekram1-node"],
|
||||
core: ["thdxr", "rekram1-node", "jlongster"],
|
||||
docs: ["R44VC0RP"],
|
||||
windows: ["Hona"],
|
||||
} as const
|
||||
|
||||
const ASSIGNEES = [...new Set(Object.values(TEAM).flat())]
|
||||
|
||||
function pick<T>(items: readonly T[]) {
|
||||
return items[Math.floor(Math.random() * items.length)]!
|
||||
}
|
||||
|
||||
function getIssueNumber(): number {
|
||||
const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10)
|
||||
if (!issue) throw new Error("ISSUE_NUMBER env var not set")
|
||||
@@ -43,69 +29,60 @@ export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
assignee: tool.schema
|
||||
.enum(ASSIGNEES as [string, ...string[]])
|
||||
.enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"])
|
||||
.describe("The username of the assignee")
|
||||
.default("rekram1-node"),
|
||||
labels: tool.schema
|
||||
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
|
||||
.array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"]))
|
||||
.describe("The labels(s) to add to the issue")
|
||||
.default([]),
|
||||
},
|
||||
async execute(args) {
|
||||
const issue = getIssueNumber()
|
||||
// const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
|
||||
const owner = "anomalyco"
|
||||
const repo = "opencode"
|
||||
|
||||
const results: string[] = []
|
||||
let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))]
|
||||
const web = labels.includes("web")
|
||||
const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase()
|
||||
const zen = /\bzen\b/.test(text) || text.includes("opencode black")
|
||||
const nix = /\bnix(os)?\b/.test(text)
|
||||
|
||||
if (labels.includes("nix") && !nix) {
|
||||
labels = labels.filter((x) => x !== "nix")
|
||||
results.push("Dropped label: nix (issue does not mention nix)")
|
||||
if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) {
|
||||
throw new Error("Only desktop issues should be assigned to adamdotdevin")
|
||||
}
|
||||
|
||||
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
||||
|
||||
if (labels.includes("zen") && !zen) {
|
||||
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
||||
if (args.assignee === "fwang" && !args.labels.includes("zen")) {
|
||||
throw new Error("Only zen issues should be assigned to fwang")
|
||||
}
|
||||
|
||||
if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) {
|
||||
throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln")
|
||||
}
|
||||
|
||||
if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) {
|
||||
throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom")
|
||||
}
|
||||
|
||||
if (assignee === "Hona" && !labels.includes("windows")) {
|
||||
throw new Error("Only windows issues should be assigned to Hona")
|
||||
}
|
||||
|
||||
if (assignee === "R44VC0RP" && !labels.includes("docs")) {
|
||||
throw new Error("Only docs issues should be assigned to R44VC0RP")
|
||||
}
|
||||
|
||||
if (assignee === "kommander" && !labels.includes("opentui")) {
|
||||
if (args.assignee === "kommander" && !args.labels.includes("opentui")) {
|
||||
throw new Error("Only opentui issues should be assigned to kommander")
|
||||
}
|
||||
|
||||
// await octokit.rest.issues.addAssignees({
|
||||
// owner,
|
||||
// repo,
|
||||
// issue_number: issue,
|
||||
// assignees: [args.assignee],
|
||||
// })
|
||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ assignees: [assignee] }),
|
||||
body: JSON.stringify({ assignees: [args.assignee] }),
|
||||
})
|
||||
results.push(`Assigned @${assignee} to issue #${issue}`)
|
||||
results.push(`Assigned @${args.assignee} to issue #${issue}`)
|
||||
|
||||
const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label))
|
||||
|
||||
if (labels.length > 0) {
|
||||
// await octokit.rest.issues.addLabels({
|
||||
// owner,
|
||||
// repo,
|
||||
// issue_number: issue,
|
||||
// labels,
|
||||
// })
|
||||
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ labels }),
|
||||
})
|
||||
results.push(`Added labels: ${labels.join(", ")}`)
|
||||
results.push(`Added labels: ${args.labels.join(", ")}`)
|
||||
}
|
||||
|
||||
return results.join("\n")
|
||||
|
||||
@@ -1,6 +1,88 @@
|
||||
Use this tool to assign and/or label a GitHub issue.
|
||||
|
||||
Choose labels and assignee using the current triage policy and ownership rules.
|
||||
Pick the most fitting labels for the issue and assign one owner.
|
||||
You can assign the following users:
|
||||
- thdxr
|
||||
- adamdotdevin
|
||||
- fwang
|
||||
- jayair
|
||||
- kommander
|
||||
- rekram1-node
|
||||
|
||||
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
||||
|
||||
You can use the following labels:
|
||||
- nix
|
||||
- opentui
|
||||
- perf
|
||||
- web
|
||||
- zen
|
||||
- docs
|
||||
|
||||
Always try to assign an issue, if in doubt, assign rekram1-node to it.
|
||||
|
||||
## Breakdown of responsibilities:
|
||||
|
||||
### thdxr
|
||||
|
||||
Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him.
|
||||
|
||||
This relates to OpenCode server primarily but has overlap with just about anything
|
||||
|
||||
### adamdotdevin
|
||||
|
||||
Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him.
|
||||
|
||||
|
||||
### fwang
|
||||
|
||||
Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue.
|
||||
|
||||
### jayair
|
||||
|
||||
Jay is responsible for documentation. If there is an issue relating to documentation assign him.
|
||||
|
||||
### kommander
|
||||
|
||||
Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about:
|
||||
- random characters on screen
|
||||
- keybinds not working on different terminals
|
||||
- general terminal stuff
|
||||
Then assign the issue to Him.
|
||||
|
||||
### rekram1-node
|
||||
|
||||
ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label.
|
||||
|
||||
Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things.
|
||||
If no one else makes sense to assign, assign rekram1-node to it.
|
||||
|
||||
Always assign to aiden if the issue mentions "acp", "zed", or model performance issues
|
||||
|
||||
## Breakdown of Labels:
|
||||
|
||||
### nix
|
||||
|
||||
Any issue that mentions nix, or nixos should have a nix label
|
||||
|
||||
### opentui
|
||||
|
||||
Anything relating to the TUI itself should have an opentui label
|
||||
|
||||
### perf
|
||||
|
||||
Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label
|
||||
|
||||
### desktop
|
||||
|
||||
Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related
|
||||
|
||||
### zen
|
||||
|
||||
Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label
|
||||
|
||||
### docs
|
||||
|
||||
Anything related to the documentation should have a docs label
|
||||
|
||||
### windows
|
||||
|
||||
Use for any issue that involves the windows OS
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
github-policies:
|
||||
runners:
|
||||
allowed_groups:
|
||||
- "GitHub Actions"
|
||||
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS و Linux (موصى به، دائما محدث)
|
||||
brew install opencode # macOS و Linux (صيغة brew الرسمية، تحديث اقل)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # اي نظام
|
||||
nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث فرع dev
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS e Linux (recomendado, sempre atualizado)
|
||||
brew install opencode # macOS e Linux (fórmula oficial do brew, atualiza menos)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # qualquer sistema
|
||||
nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch dev mais recente
|
||||
```
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -52,8 +51,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS i Linux (preporučeno, uvijek ažurno)
|
||||
brew install opencode # macOS i Linux (zvanična brew formula, rjeđe se ažurira)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # Bilo koji OS
|
||||
nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji dev branch
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS og Linux (anbefalet, altid up to date)
|
||||
brew install opencode # macOS og Linux (officiel brew formula, opdateres sjældnere)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # alle OS
|
||||
nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS und Linux (empfohlen, immer aktuell)
|
||||
brew install opencode # macOS und Linux (offizielle Brew-Formula, seltener aktualisiert)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # jedes Betriebssystem
|
||||
nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neuesten dev-Branch
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS y Linux (recomendado, siempre al día)
|
||||
brew install opencode # macOS y Linux (fórmula oficial de brew, se actualiza menos)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # cualquier sistema
|
||||
nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama dev más reciente
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS et Linux (recommandé, toujours à jour)
|
||||
brew install opencode # macOS et Linux (formule officielle brew, mise à jour moins fréquente)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # n'importe quel OS
|
||||
nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branche dev la plus récente
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato)
|
||||
brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # Qualsiasi OS
|
||||
nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ultima branch di sviluppo
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS と Linux(推奨。常に最新)
|
||||
brew install opencode # macOS と Linux(公式 brew formula。更新頻度は低め)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # どのOSでも
|
||||
nix run nixpkgs#opencode # または github:anomalyco/opencode で最新 dev ブランチ
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS 및 Linux (권장, 항상 최신)
|
||||
brew install opencode # macOS 및 Linux (공식 brew formula, 업데이트 빈도 낮음)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # 어떤 OS든
|
||||
nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신 dev 브랜치
|
||||
```
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -52,8 +51,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
|
||||
brew install opencode # macOS and Linux (official brew formula, updated less)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # Any OS
|
||||
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS og Linux (anbefalt, alltid oppdatert)
|
||||
brew install opencode # macOS og Linux (offisiell brew-formel, oppdateres sjeldnere)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # alle OS
|
||||
nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS i Linux (polecane, zawsze aktualne)
|
||||
brew install opencode # macOS i Linux (oficjalna formuła brew, rzadziej aktualizowana)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # dowolny system
|
||||
nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowszej gałęzi dev
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS и Linux (рекомендуем, всегда актуально)
|
||||
brew install opencode # macOS и Linux (официальная формула brew, обновляется реже)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # любая ОС
|
||||
nix run nixpkgs#opencode # или github:anomalyco/opencode для самой свежей ветки dev
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ)
|
||||
brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # ระบบปฏิบัติการใดก็ได้
|
||||
nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS ve Linux (önerilir, her zaman güncel)
|
||||
brew install opencode # macOS ve Linux (resmi brew formülü, daha az güncellenir)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # Tüm işletim sistemleri
|
||||
nix run nixpkgs#opencode # veya en güncel geliştirme dalı için github:anomalyco/opencode
|
||||
```
|
||||
|
||||
139
README.uk.md
139
README.uk.md
@@ -1,139 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai">
|
||||
<picture>
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">AI-агент для програмування з відкритим кодом.</p>
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">English</a> |
|
||||
<a href="README.zh.md">简体中文</a> |
|
||||
<a href="README.zht.md">繁體中文</a> |
|
||||
<a href="README.ko.md">한국어</a> |
|
||||
<a href="README.de.md">Deutsch</a> |
|
||||
<a href="README.es.md">Español</a> |
|
||||
<a href="README.fr.md">Français</a> |
|
||||
<a href="README.it.md">Italiano</a> |
|
||||
<a href="README.da.md">Dansk</a> |
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.bs.md">Bosanski</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
---
|
||||
|
||||
### Встановлення
|
||||
|
||||
```bash
|
||||
# YOLO
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Менеджери пакетів
|
||||
npm i -g opencode-ai@latest # або bun/pnpm/yarn
|
||||
scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS і Linux (рекомендовано, завжди актуально)
|
||||
brew install opencode # macOS і Linux (офіційна формула Homebrew, оновлюється рідше)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
mise use -g opencode # Будь-яка ОС
|
||||
nix run nixpkgs#opencode # або github:anomalyco/opencode для найновішої dev-гілки
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Перед встановленням видаліть версії старші за 0.1.x.
|
||||
|
||||
### Десктопний застосунок (BETA)
|
||||
|
||||
OpenCode також доступний як десктопний застосунок. Завантажуйте напряму зі [сторінки релізів](https://github.com/anomalyco/opencode/releases) або [opencode.ai/download](https://opencode.ai/download).
|
||||
|
||||
| Платформа | Завантаження |
|
||||
| --------------------- | ------------------------------------- |
|
||||
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||
| Linux | `.deb`, `.rpm` або AppImage |
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
brew install --cask opencode-desktop
|
||||
# Windows (Scoop)
|
||||
scoop bucket add extras; scoop install extras/opencode-desktop
|
||||
```
|
||||
|
||||
#### Каталог встановлення
|
||||
|
||||
Скрипт встановлення дотримується такого порядку пріоритету для шляху встановлення:
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` - Користувацький каталог встановлення
|
||||
2. `$XDG_BIN_DIR` - Шлях, сумісний зі специфікацією XDG Base Directory
|
||||
3. `$HOME/bin` - Стандартний каталог користувацьких бінарників (якщо існує або його можна створити)
|
||||
4. `$HOME/.opencode/bin` - Резервний варіант за замовчуванням
|
||||
|
||||
```bash
|
||||
# Приклади
|
||||
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
### Агенти
|
||||
|
||||
OpenCode містить два вбудовані агенти, між якими можна перемикатися клавішею `Tab`.
|
||||
|
||||
- **build** - Агент за замовчуванням із повним доступом для завдань розробки
|
||||
- **plan** - Агент лише для читання для аналізу та дослідження коду
|
||||
- За замовчуванням забороняє редагування файлів
|
||||
- Запитує дозвіл перед запуском bash-команд
|
||||
- Ідеально підходить для дослідження незнайомих кодових баз або планування змін
|
||||
|
||||
Також доступний допоміжний агент **general** для складного пошуку та багатокрокових завдань.
|
||||
Він використовується всередині системи й може бути викликаний у повідомленнях через `@general`.
|
||||
|
||||
Дізнайтеся більше про [agents](https://opencode.ai/docs/agents).
|
||||
|
||||
### Документація
|
||||
|
||||
Щоб дізнатися більше про налаштування OpenCode, [**перейдіть до нашої документації**](https://opencode.ai/docs).
|
||||
|
||||
### Внесок
|
||||
|
||||
Якщо ви хочете зробити внесок в OpenCode, будь ласка, прочитайте нашу [документацію для контриб'юторів](./CONTRIBUTING.md) перед надсиланням pull request.
|
||||
|
||||
### Проєкти на базі OpenCode
|
||||
|
||||
Якщо ви працюєте над проєктом, пов'язаним з OpenCode, і використовуєте "opencode" у назві, наприклад "opencode-dashboard" або "opencode-mobile", додайте примітку до свого README.
|
||||
Уточніть, що цей проєкт не створений командою OpenCode і жодним чином не афілійований із нами.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### Чим це відрізняється від Claude Code?
|
||||
|
||||
За можливостями це дуже схоже на Claude Code. Ось ключові відмінності:
|
||||
|
||||
- 100% open source
|
||||
- Немає прив'язки до конкретного провайдера. Ми рекомендуємо моделі, які надаємо через [OpenCode Zen](https://opencode.ai/zen), але OpenCode також працює з Claude, OpenAI, Google і навіть локальними моделями. З розвитком моделей різниця між ними зменшуватиметься, а ціни падатимуть, тому незалежність від провайдера має значення.
|
||||
- Підтримка LSP з коробки
|
||||
- Фокус на TUI. OpenCode створено користувачами neovim та авторами [terminal.shop](https://terminal.shop); ми й надалі розширюватимемо межі можливого в терміналі.
|
||||
- Клієнт-серверна архітектура. Наприклад, це дає змогу запускати OpenCode на вашому комп'ютері й керувати ним віддалено з мобільного застосунку, тобто TUI-фронтенд - лише один із можливих клієнтів.
|
||||
|
||||
---
|
||||
|
||||
**Приєднуйтеся до нашої спільноти** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新)
|
||||
brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # 任意系统
|
||||
nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最新 dev 分支
|
||||
```
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
<a href="README.th.md">ไทย</a> |
|
||||
<a href="README.tr.md">Türkçe</a> |
|
||||
<a href="README.uk.md">Українська</a>
|
||||
<a href="README.tr.md">Türkçe</a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -51,8 +50,7 @@ scoop install opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新)
|
||||
brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低)
|
||||
sudo pacman -S opencode # Arch Linux (Stable)
|
||||
paru -S opencode-bin # Arch Linux (Latest from AUR)
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # 任何作業系統
|
||||
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支
|
||||
```
|
||||
|
||||
219
bun.lock
219
bun.lock
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -107,7 +107,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -134,7 +134,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -182,7 +182,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -215,7 +215,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -244,7 +244,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -268,15 +268,15 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.14.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.79",
|
||||
"@ai-sdk/anthropic": "2.0.62",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.74",
|
||||
"@ai-sdk/anthropic": "2.0.58",
|
||||
"@ai-sdk/azure": "2.0.91",
|
||||
"@ai-sdk/cerebras": "1.0.36",
|
||||
"@ai-sdk/cohere": "2.0.22",
|
||||
"@ai-sdk/deepinfra": "1.0.36",
|
||||
"@ai-sdk/deepinfra": "1.0.33",
|
||||
"@ai-sdk/gateway": "2.0.30",
|
||||
"@ai-sdk/google": "2.0.52",
|
||||
"@ai-sdk/google-vertex": "3.0.103",
|
||||
"@ai-sdk/google-vertex": "3.0.98",
|
||||
"@ai-sdk/groq": "2.0.34",
|
||||
"@ai-sdk/mistral": "2.0.27",
|
||||
"@ai-sdk/openai": "2.0.89",
|
||||
@@ -288,8 +288,8 @@
|
||||
"@ai-sdk/vercel": "1.0.33",
|
||||
"@ai-sdk/xai": "2.0.51",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@gitlab/gitlab-ai-provider": "3.5.1",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@gitlab/gitlab-ai-provider": "3.5.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.2",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
@@ -369,7 +369,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -389,7 +389,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -400,7 +400,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -413,7 +413,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -455,7 +455,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -466,7 +466,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -504,7 +504,6 @@
|
||||
"tree-sitter-bash",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
},
|
||||
"overrides": {
|
||||
@@ -517,7 +516,7 @@
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.1.0-beta.13",
|
||||
"@pierre/diffs": "1.0.2",
|
||||
"@playwright/test": "1.51.0",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
@@ -571,7 +570,7 @@
|
||||
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.79", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GfAQUb1GEmdTjLu5Ud1d5sieNHDpwoQdb4S14KmJlA5RsGREUZ1tfSKngFaiClxFtL0xPSZjePhTMV6Z65A7/g=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q83HE3FBb/HPIvjXsehrHOgCuGHPorSMFt6BYnzIYZy8gNnSqV1OWX4oXVsCAuYPPMtYW/KMK35hmoIFV8QKoQ=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
|
||||
|
||||
@@ -581,21 +580,21 @@
|
||||
|
||||
"@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="],
|
||||
|
||||
"@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yQ5izccuO7+WDtitbJsqH7qX7BqVVonUbPZBxQypF3zqBXbCI3/3CH+0XbsWRVRWFN8/rmCAbgHg8DXjaqVQsw=="],
|
||||
"@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lqmINr+1Jy2yGXxnQB6IrC2xMtUY5uK96pyKfqTj1kLlXGatKnJfXF7WTkOGgQrFqIYqpjDz+sPVR3n0KUEUtA=="],
|
||||
|
||||
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LndvRktEgY2IFu4peDJMEXcjhHEEFtM0upLx/J64kCpFHCifalXpK4PPSX3PVndnn0bJzvamO5+fc0z2ooqBZw=="],
|
||||
"@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hn2y8Q+2iZgGNVJyzPsH8EECECryFMVmxBJrBvBWoi8xcJPRyt0fZP5dOSLyGg3q0oxmPS9M0Eq0NNlKot/bYQ=="],
|
||||
|
||||
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-H+5UGOGGZB5tYpX+3fcWxoPPDzRTEH1w6z+yD7053PmKZfHcxSJWv9HwLEyEkAv3ef1E7MIyG5EB+HmkclQ+KQ=="],
|
||||
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NiKjvqXI/96e/7SjZGgQH141PBqggsF7fNbjGTv4RgVWayMXp9mj0Ou2NjAUGwwxJwj/qseY0gXiDCYaHWFBkw=="],
|
||||
|
||||
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B9uz4+KEB5RkphL9d9XL03iA24g3f0VAeklNlq7StY7L8Mo2sBx3Bg8Udzv7G3xJmT41GuzR5pR0FkKUTju0Rg=="],
|
||||
"@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4d5EKu0OW7Gf5WFpGo4ixn0iWEwA+GpteqUjEznWGmi7qdLE5zdkbRik5B1HrDDiw5P90yO51xBex/Fp50JcVA=="],
|
||||
|
||||
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JvRdp8bMokbmp/mFz0qHPAhvAZT+vR+c9o4lTkENkDcbRBcNYUN05sSWCuwiVDdz9T+8GW7goAec6fXJBzjIFw=="],
|
||||
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WWOz5Kj+5fVe94h7WeReqjUOVtAquDE2kM575FUc8CsVxH2tRfA5cLa8nu3bknSezsKt3i67YM6mvCRxiXCkWA=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.103", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.63", "@ai-sdk/google": "2.0.53", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MPZRSVOJFxYGHE4s6XjSWaiUPru7u2i/LUUA1Ih2nzNYZaei8c46Z56imOCD/KQjQX3afRA2iZh6P5McsmwhqA=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.98", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uuv0RHkdJ5vTzeH1+iuBlv7GAjRcOPd2jiqtGLz6IKOUDH+PRQoE3ExrvOysVnKuhhTBMqvawkktDhMDQE6sVQ=="],
|
||||
|
||||
"@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="],
|
||||
|
||||
@@ -765,13 +764,13 @@
|
||||
|
||||
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
|
||||
|
||||
"@azure/msal-browser": ["@azure/msal-browser@4.28.2", "", { "dependencies": { "@azure/msal-common": "15.14.2" } }, "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ=="],
|
||||
"@azure/msal-browser": ["@azure/msal-browser@4.28.1", "", { "dependencies": { "@azure/msal-common": "15.14.1" } }, "sha512-al2u2fTchbClq3L4C1NlqLm+vwKfhYCPtZN2LR/9xJVaQ4Mnrwf5vANvuyPSJHcGvw50UBmhuVmYUAhTEetTpA=="],
|
||||
|
||||
"@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="],
|
||||
"@azure/msal-common": ["@azure/msal-common@15.14.1", "", {}, "sha512-IkzF7Pywt6QKTS0kwdCv/XV8x8JXknZDvSjj/IccooxnP373T5jaadO3FnOrbWo3S0UqkfIDyZNTaQ/oAgRdXw=="],
|
||||
|
||||
"@azure/msal-node": ["@azure/msal-node@3.8.7", "", { "dependencies": { "@azure/msal-common": "15.14.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg=="],
|
||||
"@azure/msal-node": ["@azure/msal-node@3.8.6", "", { "dependencies": { "@azure/msal-common": "15.14.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w=="],
|
||||
|
||||
"@azure/storage-blob": ["@azure/storage-blob@12.31.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.3.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg=="],
|
||||
"@azure/storage-blob": ["@azure/storage-blob@12.30.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.3", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/core-xml": "^1.4.5", "@azure/logger": "^1.1.4", "@azure/storage-common": "^12.2.0", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-peDCR8blSqhsAKDbpSP/o55S4sheNwSrblvCaHUZ5xUI73XA7ieUGGwrONgD/Fng0EoDe1VOa3fAQ7+WGB3Ocg=="],
|
||||
|
||||
"@azure/storage-common": ["@azure/storage-common@12.3.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-rest-pipeline": "^1.19.1", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.3.0", "tslib": "^2.8.1" } }, "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ=="],
|
||||
|
||||
@@ -989,9 +988,9 @@
|
||||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.5.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-I8+EGdUeKmGJSjAdFobHtqpxM9Fm00w0j7NJbtln/D/XQ1SKEGoZIuqJko4v0pV2mkhGUIs7qezljH/2kbXovA=="],
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.5.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-OoAwCz4fOci3h/2l+PRHMclclh3IaFq8w1es2wvBJ8ca7vtglKsBYT7dvmYpsXlu7pg9mopbjcexvmVCQEUTAQ=="],
|
||||
|
||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.2", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-pvGrC+aDVLY8bRCC/fZaG/Qihvt2r4by5xbTo5JTSz9O7yIcR6xG2d9Wkuu4bcXFz674z2C+i5bUk+J/RSdBpg=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
|
||||
@@ -1427,7 +1426,7 @@
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
|
||||
"@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
@@ -1597,7 +1596,7 @@
|
||||
|
||||
"@slack/socket-mode": ["@slack/socket-mode@1.3.6", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/web-api": "^6.12.1", "@types/node": ">=12.0.0", "@types/ws": "^7.4.7", "eventemitter3": "^5", "finity": "^0.5.4", "ws": "^7.5.3" } }, "sha512-G+im7OP7jVqHhiNSdHgv2VVrnN5U7KY845/5EZimZkrD4ZmtV0P3BiWkgeJhPtdLuM7C7i6+M6h6Bh+S4OOalA=="],
|
||||
|
||||
"@slack/types": ["@slack/types@2.20.0", "", {}, "sha512-PVF6P6nxzDMrzPC8fSCsnwaI+kF8YfEpxf3MqXmdyjyWTYsZQURpkK7WWUWvP5QpH55pB7zyYL9Qem/xSgc5VA=="],
|
||||
"@slack/types": ["@slack/types@2.19.0", "", {}, "sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA=="],
|
||||
|
||||
"@slack/web-api": ["@slack/web-api@6.13.0", "", { "dependencies": { "@slack/logger": "^3.0.0", "@slack/types": "^2.11.0", "@types/is-stream": "^1.1.0", "@types/node": ">=12.0.0", "axios": "^1.7.4", "eventemitter3": "^3.1.0", "form-data": "^2.5.0", "is-electron": "2.2.2", "is-stream": "^1.1.0", "p-queue": "^6.6.1", "p-retry": "^4.0.0" } }, "sha512-dv65crIgdh9ZYHrevLU6XFHTQwTyDmNqEqzuIrV+Vqe/vgiG6w37oex5ePDU1RGm2IJ90H8iOvHFvzdEO/vB+g=="],
|
||||
|
||||
@@ -1609,7 +1608,7 @@
|
||||
|
||||
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.23.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg=="],
|
||||
"@smithy/core": ["@smithy/core@3.22.1", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="],
|
||||
|
||||
@@ -1639,9 +1638,9 @@
|
||||
|
||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="],
|
||||
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.14", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag=="],
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.13", "", { "dependencies": { "@smithy/core": "^3.22.1", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w=="],
|
||||
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.31", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg=="],
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.30", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg=="],
|
||||
|
||||
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="],
|
||||
|
||||
@@ -1649,7 +1648,7 @@
|
||||
|
||||
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.10", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA=="],
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.9", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w=="],
|
||||
|
||||
"@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="],
|
||||
|
||||
@@ -1665,7 +1664,7 @@
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="],
|
||||
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.3", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg=="],
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.11.2", "", { "dependencies": { "@smithy/core": "^3.22.1", "@smithy/middleware-endpoint": "^4.4.13", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" } }, "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="],
|
||||
|
||||
@@ -1681,9 +1680,9 @@
|
||||
|
||||
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
|
||||
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.30", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng=="],
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.29", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q=="],
|
||||
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.33", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA=="],
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.32", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q=="],
|
||||
|
||||
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="],
|
||||
|
||||
@@ -1693,7 +1692,7 @@
|
||||
|
||||
"@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="],
|
||||
|
||||
"@smithy/util-stream": ["@smithy/util-stream@4.5.12", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg=="],
|
||||
"@smithy/util-stream": ["@smithy/util-stream@4.5.11", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.9", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA=="],
|
||||
|
||||
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
|
||||
|
||||
@@ -2117,7 +2116,7 @@
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"avvio": ["avvio@9.2.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ=="],
|
||||
"avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="],
|
||||
|
||||
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
||||
|
||||
@@ -2131,7 +2130,7 @@
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"b4a": ["b4a@1.7.4", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug=="],
|
||||
"b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="],
|
||||
|
||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
||||
|
||||
@@ -2143,7 +2142,7 @@
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="],
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
|
||||
|
||||
@@ -2185,7 +2184,7 @@
|
||||
|
||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
@@ -2437,7 +2436,7 @@
|
||||
|
||||
"dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
"dotenv": ["dotenv@17.2.4", "", {}, "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="],
|
||||
|
||||
@@ -2707,7 +2706,7 @@
|
||||
|
||||
"h3": ["h3@2.0.1-rc.4", "", { "dependencies": { "rou3": "^0.7.8", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw=="],
|
||||
|
||||
"happy-dom": ["happy-dom@20.6.1", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^6.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ=="],
|
||||
"happy-dom": ["happy-dom@20.5.4", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^6.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-YkU/T7/CtigSrxsDwwkjaTXgicJzNu3QeTFoVbVhEteHOtDV1hng7zB9CwPTCbnGgmACA38SQm3Y7uPQsuZj6w=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
@@ -3441,7 +3440,7 @@
|
||||
|
||||
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
|
||||
|
||||
"planck": ["planck@1.4.3", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-B+lHKhRSeg7vZOfEyEzyQVu7nx8JHcX3QgnAcHXrPW0j04XYKX5eXSiUrxH2Z5QR8OoqvjD6zKIaPMdMYAd0uA=="],
|
||||
"planck": ["planck@1.4.2", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew=="],
|
||||
|
||||
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
|
||||
|
||||
@@ -3497,7 +3496,7 @@
|
||||
|
||||
"punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="],
|
||||
|
||||
"qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
@@ -3741,7 +3740,7 @@
|
||||
|
||||
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
|
||||
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
|
||||
|
||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
@@ -3847,13 +3846,13 @@
|
||||
|
||||
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
|
||||
|
||||
"tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="],
|
||||
"tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
||||
|
||||
"terracotta": ["terracotta@1.1.0", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww=="],
|
||||
|
||||
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
|
||||
|
||||
"text-decoder": ["text-decoder@1.2.4", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-mzlffA3tBNhziEHPK5L5InZg1d/ElNIpJhnhbDRNUtem/edZcJ5zg5FgwKKKOyklxk+6Jt+TrSu83musmvrDlg=="],
|
||||
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
@@ -4183,9 +4182,7 @@
|
||||
|
||||
"@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
@@ -4195,25 +4192,11 @@
|
||||
|
||||
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="],
|
||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="],
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||
|
||||
"@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
@@ -4299,7 +4282,7 @@
|
||||
|
||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.21.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-26dQFi76dB8IiN/WKGQOV+yKKTTlRCxQjoi2WLt0kMcH8pvxVyvfdBDkld5GTl7W1qvBpwVOtFcsqktj3fBRpA=="],
|
||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.19.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-5uGrF82Ql7TKgIWUnuxh+OyzYbPRPwYDSgGc05JowbXRFsOkuj0dJuCdPCTBZT4mcmp2NEvj/URwDzW+lYgmVw=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
@@ -4431,9 +4414,13 @@
|
||||
|
||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||
|
||||
"@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/@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=="],
|
||||
|
||||
"@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
"@pierre/diffs/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ=="],
|
||||
|
||||
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/types": "3.19.0" } }, "sha512-e6vwrsyw+wx4OkcrDbL+FVCxwx8jgKiCoXzakVur++mIWVcgpzIi8vxf4/b4dVTYrV/nUx5RjinMf4tq8YV8Fw=="],
|
||||
|
||||
"@pierre/diffs/shiki": ["shiki@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/engine-oniguruma": "3.19.0", "@shikijs/langs": "3.19.0", "@shikijs/themes": "3.19.0", "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA=="],
|
||||
|
||||
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
@@ -4491,8 +4478,6 @@
|
||||
|
||||
"@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@tanstack/router-utils/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
|
||||
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
||||
@@ -4501,13 +4486,13 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="],
|
||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="],
|
||||
"ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
@@ -4581,7 +4566,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="],
|
||||
"glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
|
||||
|
||||
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
@@ -4619,17 +4604,15 @@
|
||||
|
||||
"mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
|
||||
|
||||
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
|
||||
"nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
||||
|
||||
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="],
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="],
|
||||
|
||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="],
|
||||
|
||||
@@ -4653,7 +4636,7 @@
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
"path-scurry/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="],
|
||||
|
||||
"pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||
|
||||
@@ -4661,8 +4644,6 @@
|
||||
|
||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"postcss-css-variables/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
@@ -4709,6 +4690,8 @@
|
||||
|
||||
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"token-types/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@@ -4979,9 +4962,23 @@
|
||||
|
||||
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@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=="],
|
||||
"@pierre/diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
|
||||
"@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=="],
|
||||
"@pierre/diffs/@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
|
||||
|
||||
"@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="],
|
||||
|
||||
"@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
|
||||
|
||||
"@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="],
|
||||
|
||||
"@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg=="],
|
||||
|
||||
"@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="],
|
||||
|
||||
"@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="],
|
||||
|
||||
"@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
|
||||
|
||||
"@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
@@ -5005,8 +5002,6 @@
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
||||
@@ -5015,8 +5010,6 @@
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||
|
||||
"ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
@@ -5031,7 +5024,7 @@
|
||||
|
||||
"astro/unstorage/h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="],
|
||||
|
||||
"astro/unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
"astro/unstorage/lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="],
|
||||
|
||||
"astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
@@ -5047,8 +5040,6 @@
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
@@ -5069,10 +5060,6 @@
|
||||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||
@@ -5087,8 +5074,6 @@
|
||||
|
||||
"readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
@@ -5325,8 +5310,6 @@
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"astro/unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
@@ -5335,22 +5318,16 @@
|
||||
|
||||
"astro/unstorage/h3/crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"js-beautify/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"js-beautify/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
@@ -5379,12 +5356,8 @@
|
||||
|
||||
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||
|
||||
"readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"rimraf/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
@@ -5407,16 +5380,10 @@
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"js-beautify/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"js-beautify/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
@@ -5427,8 +5394,6 @@
|
||||
|
||||
"rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770812194,
|
||||
"narHash": "sha256-OH+lkaIKAvPXR3nITO7iYZwew2nW9Y7Xxq0yfM/UcUU=",
|
||||
"lastModified": 1770073757,
|
||||
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8482c7ded03bae7550f3d69884f1e611e3bd19e8",
|
||||
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -145,16 +145,6 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS18"),
|
||||
new sst.Secret("ZEN_MODELS19"),
|
||||
new sst.Secret("ZEN_MODELS20"),
|
||||
new sst.Secret("ZEN_MODELS21"),
|
||||
new sst.Secret("ZEN_MODELS22"),
|
||||
new sst.Secret("ZEN_MODELS23"),
|
||||
new sst.Secret("ZEN_MODELS24"),
|
||||
new sst.Secret("ZEN_MODELS25"),
|
||||
new sst.Secret("ZEN_MODELS26"),
|
||||
new sst.Secret("ZEN_MODELS27"),
|
||||
new sst.Secret("ZEN_MODELS28"),
|
||||
new sst.Secret("ZEN_MODELS29"),
|
||||
new sst.Secret("ZEN_MODELS30"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||
|
||||
16
install
16
install
@@ -130,7 +130,7 @@ else
|
||||
needs_baseline=false
|
||||
if [ "$arch" = "x64" ]; then
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
@@ -141,20 +141,6 @@ else
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "windows" ]; then
|
||||
ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)"
|
||||
out=""
|
||||
if command -v powershell.exe >/dev/null 2>&1; then
|
||||
out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
|
||||
elif command -v pwsh >/dev/null 2>&1; then
|
||||
out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
|
||||
fi
|
||||
out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
|
||||
if [ "$out" != "true" ] && [ "$out" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
target="$os-$arch"
|
||||
|
||||
15
logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json
Normal file
15
logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1759827172859,
|
||||
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
|
||||
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
48
logs/mcp-puppeteer-2025-10-07.log
Normal file
48
logs/mcp-puppeteer-2025-10-07.log
Normal file
@@ -0,0 +1,48 @@
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
|
||||
{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"}
|
||||
{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"}
|
||||
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"}
|
||||
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-C3WIEER2XgzO85wk2sp3BzQ6dknW026zslD8nKZjo2U=",
|
||||
"aarch64-linux": "sha256-+tTJHZMZ/+8fAjI/1fUTuca8J2MZfB+5vhBoZ7jgqcE=",
|
||||
"aarch64-darwin": "sha256-vS82puFGBBToxyIBa8Zi0KLKdJYr64T6HZL2rL32mH8=",
|
||||
"x86_64-darwin": "sha256-Tr8JMTCxV6WVt3dXV7iq3PNCm2Cn+RXAbU9+o7pKKV0="
|
||||
"x86_64-linux": "sha256-pp2gb4nxiIT3VltB6Xli2wZPH32JfnMsI+BbihyU1+E=",
|
||||
"aarch64-linux": "sha256-hJwxhBICZz/pbIxQsF/sIpZTlFIgLpcAyF44O8wxMdU=",
|
||||
"aarch64-darwin": "sha256-DPONXP52XOg/ApdSnLp32a+K5XCOnDGhbTUto2Rme0g=",
|
||||
"x86_64-darwin": "sha256-KX1h5LRJSgthpbOPqWlbM/sPf8cvQrdRJvxtrz/FzBQ="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.1.0-beta.13",
|
||||
"@pierre/diffs": "1.0.2",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
@@ -103,7 +103,6 @@
|
||||
"@types/node": "catalog:"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch"
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
test("ctrl+l focuses the prompt", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
|
||||
await page.locator("main").click({ position: { x: 5, y: 5 } })
|
||||
await expect(prompt).not.toBeFocused()
|
||||
|
||||
await page.keyboard.press("Control+L")
|
||||
await expect(prompt).toBeFocused()
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { modKey } from "../utils"
|
||||
|
||||
const expanded = async (el: { getAttribute: (name: string) => Promise<string | null> }) => {
|
||||
const value = await el.getAttribute("aria-expanded")
|
||||
if (value !== "true" && value !== "false") throw new Error(`Expected aria-expanded to be true|false, got: ${value}`)
|
||||
return value === "true"
|
||||
}
|
||||
|
||||
test("review panel can be toggled via keybind", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const treeToggle = page.getByRole("button", { name: "Toggle file tree" }).first()
|
||||
await expect(treeToggle).toBeVisible()
|
||||
if (await expanded(treeToggle)) await treeToggle.click()
|
||||
await expect(treeToggle).toHaveAttribute("aria-expanded", "false")
|
||||
|
||||
const reviewToggle = page.getByRole("button", { name: "Toggle review" }).first()
|
||||
await expect(reviewToggle).toBeVisible()
|
||||
if (await expanded(reviewToggle)) await reviewToggle.click()
|
||||
await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(page.locator("#review-panel")).toHaveCount(0)
|
||||
|
||||
await page.keyboard.press(`${modKey}+Shift+R`)
|
||||
await expect(reviewToggle).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(page.locator("#review-panel")).toBeVisible()
|
||||
|
||||
await page.keyboard.press(`${modKey}+Shift+R`)
|
||||
await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(page.locator("#review-panel")).toHaveCount(0)
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { modKey } from "../utils"
|
||||
|
||||
test("mod+w closes the active file tab", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/open")
|
||||
await expect(page.locator('[data-slash-id="file.open"]').first()).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByPlaceholder(/search files/i) })
|
||||
.first()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
await dialog.getByRole("textbox").first().fill("package.json")
|
||||
const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
|
||||
await expect(item).toBeVisible({ timeout: 30_000 })
|
||||
await item.click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const tab = page.getByRole("tab", { name: "package.json" }).first()
|
||||
await expect(tab).toBeVisible()
|
||||
await tab.click()
|
||||
await expect(tab).toHaveAttribute("aria-selected", "true")
|
||||
|
||||
await page.keyboard.press(`${modKey}+W`)
|
||||
await expect(page.getByRole("tab", { name: "package.json" })).toHaveCount(0)
|
||||
})
|
||||
@@ -1,28 +1,15 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { openPalette, clickListItem } from "../actions"
|
||||
|
||||
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/open")
|
||||
|
||||
const command = page.locator('[data-slash-id="file.open"]').first()
|
||||
await expect(command).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByPlaceholder(/search files/i) })
|
||||
.first()
|
||||
await expect(dialog).toBeVisible()
|
||||
const dialog = await openPalette(page)
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
await input.fill("package.json")
|
||||
|
||||
const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
|
||||
await expect(item).toBeVisible({ timeout: 30_000 })
|
||||
await item.click()
|
||||
await clickListItem(dialog, { keyStartsWith: "file:" })
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
|
||||
@@ -1,41 +1,18 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { openPalette, clickListItem } from "../actions"
|
||||
|
||||
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/open")
|
||||
const sep = process.platform === "win32" ? "\\" : "/"
|
||||
const file = ["packages", "app", "package.json"].join(sep)
|
||||
|
||||
const command = page.locator('[data-slash-id="file.open"]').first()
|
||||
await expect(command).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByPlaceholder(/search files/i) })
|
||||
.first()
|
||||
await expect(dialog).toBeVisible()
|
||||
const dialog = await openPalette(page)
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
await input.fill("package.json")
|
||||
await input.fill(file)
|
||||
|
||||
const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
|
||||
let index = -1
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
|
||||
index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
|
||||
return index >= 0
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const item = items.nth(index)
|
||||
await expect(item).toBeVisible()
|
||||
await item.click()
|
||||
await clickListItem(dialog, { text: /packages.*app.*package.json/ })
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
@@ -45,5 +22,5 @@ test("smoke file viewer renders real file content", async ({ page, gotoSession }
|
||||
|
||||
const code = page.locator('[data-component="code"]').first()
|
||||
await expect(code).toBeVisible()
|
||||
await expect(code.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
|
||||
await expect(code.getByText("@opencode-ai/app")).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession }
|
||||
const key = await target.getAttribute("data-key")
|
||||
if (!key) throw new Error("Failed to resolve model key from list item")
|
||||
|
||||
const name = (await target.locator("span").first().innerText()).trim()
|
||||
const model = key.split(":").slice(1).join(":")
|
||||
|
||||
await input.fill(model)
|
||||
@@ -36,13 +37,6 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession }
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/model")
|
||||
await expect(command).toBeVisible()
|
||||
await command.hover()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialogAgain = page.getByRole("dialog")
|
||||
await expect(dialogAgain).toBeVisible()
|
||||
await expect(dialogAgain.locator(`[data-slot="list-item"][data-key="${key}"][data-selected="true"]`)).toBeVisible()
|
||||
const form = page.locator(promptSelector).locator("xpath=ancestor::form[1]")
|
||||
await expect(form.locator('[data-component="button"]').filter({ hasText: name }).first()).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -69,19 +69,15 @@ async function createSessionFromWorkspace(page: Page, slug: string, text: string
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
await expect(prompt).toBeEditable()
|
||||
await prompt.click()
|
||||
await expect(prompt).toBeFocused()
|
||||
await prompt.fill(text)
|
||||
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
|
||||
await prompt.press("Enter")
|
||||
await page.keyboard.type(text)
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
|
||||
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/[^/?#]+`), { timeout: 30_000 })
|
||||
|
||||
const sessionID = sessionIDFromUrl(page.url())
|
||||
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
|
||||
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${sessionID}(?:[/?#]|$)`))
|
||||
return sessionID
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,18 @@ import {
|
||||
cleanupTestProject,
|
||||
clickMenuItem,
|
||||
confirmDialog,
|
||||
openProjectMenu,
|
||||
openSidebar,
|
||||
openWorkspaceMenu,
|
||||
setWorkspacesEnabled,
|
||||
} from "../actions"
|
||||
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
|
||||
import { createSdk, dirSlug } from "../utils"
|
||||
import {
|
||||
inlineInputSelector,
|
||||
projectSwitchSelector,
|
||||
projectWorkspacesToggleSelector,
|
||||
workspaceItemSelector,
|
||||
} from "../selectors"
|
||||
import { dirSlug } from "../utils"
|
||||
|
||||
function slugFromUrl(url: string) {
|
||||
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
|
||||
@@ -137,35 +143,26 @@ test("non-git projects keep workspace mode disabled", async ({ page, withProject
|
||||
await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
|
||||
|
||||
try {
|
||||
await withProject(async () => {
|
||||
await page.goto(`/${nonGitSlug}/session`)
|
||||
await withProject(
|
||||
async () => {
|
||||
await openSidebar(page)
|
||||
|
||||
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
|
||||
const nonGitButton = page.locator(projectSwitchSelector(nonGitSlug)).first()
|
||||
await expect(nonGitButton).toBeVisible()
|
||||
await nonGitButton.click()
|
||||
await expect(page).toHaveURL(new RegExp(`/${nonGitSlug}/session`))
|
||||
|
||||
const activeDir = base64Decode(slugFromUrl(page.url()))
|
||||
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
|
||||
const menu = await openProjectMenu(page, nonGitSlug)
|
||||
const toggle = menu.locator(projectWorkspacesToggleSelector(nonGitSlug)).first()
|
||||
|
||||
await openSidebar(page)
|
||||
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
|
||||
await expect(toggle).toBeVisible()
|
||||
await expect(toggle).toBeDisabled()
|
||||
|
||||
const trigger = page.locator('[data-action="project-menu"]').first()
|
||||
const hasMenu = await trigger
|
||||
.isVisible()
|
||||
.then((x) => x)
|
||||
.catch(() => false)
|
||||
if (!hasMenu) return
|
||||
|
||||
await trigger.click({ force: true })
|
||||
|
||||
const menu = page.locator(dropdownMenuContentSelector).first()
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first()
|
||||
|
||||
await expect(toggle).toBeVisible()
|
||||
await expect(toggle).toBeDisabled()
|
||||
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
|
||||
})
|
||||
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
|
||||
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
|
||||
},
|
||||
{ extra: [nonGit] },
|
||||
)
|
||||
} finally {
|
||||
await cleanupTestProject(nonGit)
|
||||
}
|
||||
@@ -259,45 +256,14 @@ test("can delete a workspace", async ({ page, withProject }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 800 })
|
||||
|
||||
await withProject(async (project) => {
|
||||
const sdk = createSdk(project.directory)
|
||||
const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project)
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const worktrees = await sdk.worktree
|
||||
.list()
|
||||
.then((r) => r.data ?? [])
|
||||
.catch(() => [] as string[])
|
||||
return worktrees.includes(directory)
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
const { rootSlug, slug } = await setupWorkspaceTest(page, project)
|
||||
|
||||
const menu = await openWorkspaceMenu(page, slug)
|
||||
await clickMenuItem(menu, /^Delete$/i, { force: true })
|
||||
await confirmDialog(page, /^Delete workspace$/i)
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const worktrees = await sdk.worktree
|
||||
.list()
|
||||
.then((r) => r.data ?? [])
|
||||
.catch(() => [] as string[])
|
||||
return worktrees.includes(directory)
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
)
|
||||
.toBe(false)
|
||||
|
||||
await project.gotoSession()
|
||||
|
||||
await openSidebar(page)
|
||||
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
|
||||
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
|
||||
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,95 +1,40 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import type { Page } from "@playwright/test"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { withSession } from "../actions"
|
||||
|
||||
function contextButton(page: Page) {
|
||||
return page
|
||||
.locator('[data-component="button"]')
|
||||
.filter({ has: page.locator('[data-component="progress-circle"]').first() })
|
||||
.first()
|
||||
}
|
||||
|
||||
async function seedContextSession(input: { sessionID: string; sdk: Parameters<typeof withSession>[0] }) {
|
||||
await input.sdk.session.promptAsync({
|
||||
sessionID: input.sessionID,
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "seed context",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const messages = await input.sdk.session
|
||||
.messages({ sessionID: input.sessionID, limit: 1 })
|
||||
.then((r) => r.data ?? [])
|
||||
return messages.length
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
|
||||
const title = `e2e smoke context ${Date.now()}`
|
||||
|
||||
await withSession(sdk, title, async (session) => {
|
||||
await seedContextSession({ sessionID: session.id, sdk })
|
||||
await sdk.session.promptAsync({
|
||||
sessionID: session.id,
|
||||
noReply: true,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "seed context",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const messages = await sdk.session.messages({ sessionID: session.id, limit: 1 }).then((r) => r.data ?? [])
|
||||
return messages.length
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
|
||||
await gotoSession(session.id)
|
||||
|
||||
const trigger = contextButton(page)
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
const contextButton = page
|
||||
.locator('[data-component="button"]')
|
||||
.filter({ has: page.locator('[data-component="progress-circle"]').first() })
|
||||
.first()
|
||||
|
||||
await expect(contextButton).toBeVisible()
|
||||
await contextButton.click()
|
||||
|
||||
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
|
||||
await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test("context panel can be closed from the context tab close action", async ({ page, sdk, gotoSession }) => {
|
||||
await withSession(sdk, `e2e context toggle ${Date.now()}`, async (session) => {
|
||||
await seedContextSession({ sessionID: session.id, sdk })
|
||||
await gotoSession(session.id)
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
|
||||
const trigger = contextButton(page)
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
|
||||
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
|
||||
const context = tabs.getByRole("tab", { name: "Context" })
|
||||
await expect(context).toBeVisible()
|
||||
|
||||
await page.getByRole("button", { name: "Close tab" }).first().click()
|
||||
await expect(context).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
test("context panel can open file picker from context actions", async ({ page, sdk, gotoSession }) => {
|
||||
await withSession(sdk, `e2e context tabs ${Date.now()}`, async (session) => {
|
||||
await seedContextSession({ sessionID: session.id, sdk })
|
||||
await gotoSession(session.id)
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
|
||||
const trigger = contextButton(page)
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
|
||||
await expect(page.getByRole("tab", { name: "Context" })).toBeVisible()
|
||||
await page.getByRole("button", { name: "Open file" }).first().click()
|
||||
|
||||
const dialog = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByPlaceholder(/search files/i) })
|
||||
.first()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(dialog).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
import { sessionIDFromUrl } from "../actions"
|
||||
|
||||
// Regression test for Issue #12453: the synchronous POST /message endpoint holds
|
||||
// the connection open while the agent works, causing "Failed to fetch" over
|
||||
// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately.
|
||||
test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => {
|
||||
test.setTimeout(120_000)
|
||||
|
||||
// Simulate Tailscale/VPN killing the long-lived sync connection
|
||||
await page.route("**/session/*/message", (route) => route.abort("connectionfailed"))
|
||||
|
||||
await gotoSession()
|
||||
|
||||
const token = `E2E_ASYNC_${Date.now()}`
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type(`Reply with exactly: ${token}`)
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
|
||||
const sessionID = sessionIDFromUrl(page.url())!
|
||||
|
||||
try {
|
||||
// Agent response arrives via SSE despite sync endpoint being dead
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
|
||||
return messages
|
||||
.filter((m) => m.info.role === "assistant")
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toContain(token)
|
||||
} finally {
|
||||
await sdk.session.delete({ sessionID }).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
test("dropping text/plain file: uri inserts a file pill", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await prompt.click()
|
||||
|
||||
const path = process.platform === "win32" ? "C:\\opencode-e2e-drop.txt" : "/tmp/opencode-e2e-drop.txt"
|
||||
const dt = await page.evaluateHandle((text) => {
|
||||
const dt = new DataTransfer()
|
||||
dt.setData("text/plain", text)
|
||||
return dt
|
||||
}, `file:${path}`)
|
||||
|
||||
await page.dispatchEvent("body", "drop", { dataTransfer: dt })
|
||||
|
||||
const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
|
||||
await expect(pill).toBeVisible()
|
||||
await expect(pill).toHaveAttribute("data-path", path)
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
test("dropping an image file adds an attachment", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await prompt.click()
|
||||
|
||||
const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3+4uQAAAAASUVORK5CYII="
|
||||
const dt = await page.evaluateHandle((b64) => {
|
||||
const dt = new DataTransfer()
|
||||
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))
|
||||
const file = new File([bytes], "drop.png", { type: "image/png" })
|
||||
dt.items.add(file)
|
||||
return dt
|
||||
}, png)
|
||||
|
||||
await page.dispatchEvent("body", "drop", { dataTransfer: dt })
|
||||
|
||||
const img = page.locator('img[alt="drop.png"]').first()
|
||||
await expect(img).toBeVisible()
|
||||
|
||||
const remove = page.getByRole("button", { name: "Remove attachment" }).first()
|
||||
await expect(remove).toBeVisible()
|
||||
|
||||
await img.hover()
|
||||
await remove.click()
|
||||
await expect(page.locator('img[alt="drop.png"]')).toHaveCount(0)
|
||||
})
|
||||
@@ -1,18 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
test("shift+enter inserts a newline without submitting", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await expect(page).toHaveURL(/\/session\/?$/)
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
await prompt.click()
|
||||
await page.keyboard.type("line one")
|
||||
await page.keyboard.press("Shift+Enter")
|
||||
await page.keyboard.type("line two")
|
||||
|
||||
await expect(page).toHaveURL(/\/session\/?$/)
|
||||
await expect(prompt).toContainText("line one")
|
||||
await expect(prompt).toContainText("line two")
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { test, expect } from "../fixtures"
|
||||
import { promptSelector, terminalSelector } from "../selectors"
|
||||
|
||||
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const prompt = page.locator(promptSelector)
|
||||
const terminal = page.locator(terminalSelector)
|
||||
|
||||
await expect(terminal).not.toBeVisible()
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type("/terminal")
|
||||
await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
await expect(terminal).toBeVisible()
|
||||
|
||||
await prompt.click()
|
||||
await page.keyboard.type("/terminal")
|
||||
await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
await expect(terminal).not.toBeVisible()
|
||||
})
|
||||
@@ -44,6 +44,9 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession })
|
||||
)
|
||||
|
||||
.toContain(token)
|
||||
|
||||
const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first()
|
||||
await expect(reply).toBeVisible({ timeout: 90_000 })
|
||||
} finally {
|
||||
page.off("pageerror", onPageError)
|
||||
await sdk.session.delete({ sessionID }).catch(() => undefined)
|
||||
|
||||
@@ -10,11 +10,8 @@ export const settingsNotificationsAgentSelector = '[data-action="settings-notifi
|
||||
export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
|
||||
export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
|
||||
export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]'
|
||||
export const settingsSoundsAgentEnabledSelector = '[data-action="settings-sounds-agent-enabled"]'
|
||||
export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
|
||||
export const settingsSoundsPermissionsEnabledSelector = '[data-action="settings-sounds-permissions-enabled"]'
|
||||
export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
|
||||
export const settingsSoundsErrorsEnabledSelector = '[data-action="settings-sounds-errors-enabled"]'
|
||||
export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]'
|
||||
export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]'
|
||||
|
||||
@@ -30,9 +27,6 @@ export const projectMenuTriggerSelector = (slug: string) =>
|
||||
|
||||
export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
|
||||
|
||||
export const projectClearNotificationsSelector = (slug: string) =>
|
||||
`[data-action="project-clear-notifications"][data-project="${slug}"]`
|
||||
|
||||
export const projectWorkspacesToggleSelector = (slug: string) =>
|
||||
`[data-action="project-workspaces-toggle"][data-project="${slug}"]`
|
||||
|
||||
|
||||
@@ -10,26 +10,21 @@ async function seedConversation(input: {
|
||||
sessionID: string
|
||||
token: string
|
||||
}) {
|
||||
const messages = async () =>
|
||||
await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
|
||||
const seeded = await messages()
|
||||
const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
|
||||
|
||||
const prompt = input.page.locator(promptSelector)
|
||||
await expect(prompt).toBeVisible()
|
||||
await input.sdk.session.promptAsync({
|
||||
sessionID: input.sessionID,
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: input.token }],
|
||||
})
|
||||
await prompt.click()
|
||||
await input.page.keyboard.type(`Reply with exactly: ${input.token}`)
|
||||
await input.page.keyboard.press("Enter")
|
||||
|
||||
let userMessageID: string | undefined
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const users = (await messages()).filter(
|
||||
const messages = await input.sdk.session
|
||||
.messages({ sessionID: input.sessionID, limit: 50 })
|
||||
.then((r) => r.data ?? [])
|
||||
const users = messages.filter(
|
||||
(m) =>
|
||||
!userIDs.has(m.info.id) &&
|
||||
m.info.role === "user" &&
|
||||
m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
|
||||
)
|
||||
@@ -38,14 +33,21 @@ async function seedConversation(input: {
|
||||
const user = users[users.length - 1]
|
||||
if (!user) return false
|
||||
userMessageID = user.info.id
|
||||
return true
|
||||
|
||||
const assistantText = messages
|
||||
.filter((m) => m.info.role === "assistant")
|
||||
.flatMap((m) => m.parts)
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n")
|
||||
|
||||
return assistantText.includes(input.token)
|
||||
},
|
||||
{ timeout: 90_000, intervals: [250, 500, 1_000] },
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
if (!userMessageID) throw new Error("Expected a user message id")
|
||||
await expect(input.page.locator(`[data-message-id="${userMessageID}"]`).first()).toBeVisible({ timeout: 30_000 })
|
||||
return { prompt, userMessageID }
|
||||
}
|
||||
|
||||
|
||||
@@ -34,34 +34,21 @@ async function seedMessage(sdk: Sdk, sessionID: string) {
|
||||
test("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => {
|
||||
const stamp = Date.now()
|
||||
const originalTitle = `e2e rename test ${stamp}`
|
||||
const renamedTitle = `e2e renamed ${stamp}`
|
||||
const newTitle = `e2e renamed ${stamp}`
|
||||
|
||||
await withSession(sdk, originalTitle, async (session) => {
|
||||
await seedMessage(sdk, session.id)
|
||||
await gotoSession(session.id)
|
||||
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle)
|
||||
|
||||
const menu = await openSessionMoreMenu(page, session.id)
|
||||
await clickMenuItem(menu, /rename/i)
|
||||
|
||||
const input = page.locator(".session-scroller").locator(inlineInputSelector).first()
|
||||
await expect(input).toBeVisible()
|
||||
await expect(input).toBeFocused()
|
||||
await input.fill(renamedTitle)
|
||||
await expect(input).toHaveValue(renamedTitle)
|
||||
await input.fill(newTitle)
|
||||
await input.press("Enter")
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
|
||||
return data?.title
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe(renamedTitle)
|
||||
|
||||
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle)
|
||||
await expect(page.getByRole("heading", { level: 1 }).first()).toContainText(newTitle)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -129,14 +116,8 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
|
||||
await seedMessage(sdk, session.id)
|
||||
await gotoSession(session.id)
|
||||
|
||||
const shared = await openSharePopover(page)
|
||||
const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first()
|
||||
await expect(publish).toBeVisible({ timeout: 30_000 })
|
||||
await publish.click()
|
||||
|
||||
await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
})
|
||||
const { rightSection, popoverBody } = await openSharePopover(page)
|
||||
await popoverBody.getByRole("button", { name: "Publish" }).first().click()
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -148,14 +129,14 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
|
||||
)
|
||||
.not.toBeUndefined()
|
||||
|
||||
const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()
|
||||
const copyButton = rightSection.locator('button[aria-label="Copy link"]').first()
|
||||
await expect(copyButton).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
const sharedPopover = await openSharePopover(page)
|
||||
const unpublish = sharedPopover.popoverBody.getByRole("button", { name: "Unpublish" }).first()
|
||||
await expect(unpublish).toBeVisible({ timeout: 30_000 })
|
||||
await unpublish.click()
|
||||
|
||||
await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
})
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
@@ -166,8 +147,10 @@ test("session can be shared and unshared via header button", async ({ page, sdk,
|
||||
)
|
||||
.toBeUndefined()
|
||||
|
||||
const unshared = await openSharePopover(page)
|
||||
await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
|
||||
await expect(copyButton).not.toBeVisible({ timeout: 30_000 })
|
||||
|
||||
const unsharedPopover = await openSharePopover(page)
|
||||
await expect(unsharedPopover.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
settingsNotificationsPermissionsSelector,
|
||||
settingsReleaseNotesSelector,
|
||||
settingsSoundsAgentSelector,
|
||||
settingsSoundsAgentEnabledSelector,
|
||||
settingsSoundsErrorsSelector,
|
||||
settingsSoundsPermissionsSelector,
|
||||
settingsThemeSelector,
|
||||
@@ -336,30 +335,6 @@ test("changing sound agent selection persists in localStorage", async ({ page, g
|
||||
expect(stored?.sounds?.agent).not.toBe("staplebops-01")
|
||||
})
|
||||
|
||||
test("disabling agent sound disables sound selection", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
const dialog = await openSettings(page)
|
||||
const select = dialog.locator(settingsSoundsAgentSelector)
|
||||
const switchContainer = dialog.locator(settingsSoundsAgentEnabledSelector)
|
||||
const trigger = select.locator('[data-slot="select-select-trigger"]')
|
||||
await expect(select).toBeVisible()
|
||||
await expect(switchContainer).toBeVisible()
|
||||
await expect(trigger).toBeEnabled()
|
||||
|
||||
await switchContainer.locator('[data-slot="switch-control"]').click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(trigger).toBeDisabled()
|
||||
|
||||
const stored = await page.evaluate((key) => {
|
||||
const raw = localStorage.getItem(key)
|
||||
return raw ? JSON.parse(raw) : null
|
||||
}, settingsKey)
|
||||
|
||||
expect(stored?.sounds?.agentEnabled).toBe(false)
|
||||
})
|
||||
|
||||
test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.6",
|
||||
"version": "1.1.59",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@/index.css"
|
||||
import { ErrorBoundary, Show, Suspense, lazy, type JSX, type ParentProps } from "solid-js"
|
||||
import { ErrorBoundary, Show, lazy, type ParentProps } from "solid-js"
|
||||
import { Router, Route, Navigate } from "@solidjs/router"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
@@ -30,26 +30,12 @@ import { HighlightsProvider } from "@/context/highlights"
|
||||
import Layout from "@/pages/layout"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import { ErrorPage } from "./pages/error"
|
||||
import { Suspense, JSX } from "solid-js"
|
||||
|
||||
const Home = lazy(() => import("@/pages/home"))
|
||||
const Session = lazy(() => import("@/pages/session"))
|
||||
const Loading = () => <div class="size-full" />
|
||||
|
||||
const HomeRoute = () => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Home />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
const SessionRoute = () => (
|
||||
<SessionProviders>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
</SessionProviders>
|
||||
)
|
||||
|
||||
const SessionIndexRoute = () => <Navigate href="session" />
|
||||
|
||||
function UiI18nBridge(props: ParentProps) {
|
||||
const language = useLanguage()
|
||||
return <I18nProvider value={{ locale: language.locale, t: language.t }}>{props.children}</I18nProvider>
|
||||
@@ -66,71 +52,6 @@ function MarkedProviderWithNativeParser(props: ParentProps) {
|
||||
return <MarkedProvider nativeParser={platform.parseMarkdown}>{props.children}</MarkedProvider>
|
||||
}
|
||||
|
||||
function AppShellProviders(props: ParentProps) {
|
||||
return (
|
||||
<SettingsProvider>
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<ModelsProvider>
|
||||
<CommandProvider>
|
||||
<HighlightsProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</HighlightsProvider>
|
||||
</CommandProvider>
|
||||
</ModelsProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
</SettingsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionProviders(props: ParentProps) {
|
||||
return (
|
||||
<TerminalProvider>
|
||||
<FileProvider>
|
||||
<PromptProvider>
|
||||
<CommentsProvider>{props.children}</CommentsProvider>
|
||||
</PromptProvider>
|
||||
</FileProvider>
|
||||
</TerminalProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||
return (
|
||||
<AppShellProviders>
|
||||
{props.appChildren}
|
||||
{props.children}
|
||||
</AppShellProviders>
|
||||
)
|
||||
}
|
||||
|
||||
const getStoredDefaultServerUrl = (platform: ReturnType<typeof usePlatform>) => {
|
||||
if (platform.platform !== "web") return
|
||||
const result = platform.getDefaultServerUrl?.()
|
||||
if (result instanceof Promise) return
|
||||
if (!result) return
|
||||
return normalizeServerUrl(result)
|
||||
}
|
||||
|
||||
const resolveDefaultServerUrl = (props: {
|
||||
defaultUrl?: string
|
||||
storedDefaultServerUrl?: string
|
||||
hostname: string
|
||||
origin: string
|
||||
isDev: boolean
|
||||
devHost?: string
|
||||
devPort?: string
|
||||
}) => {
|
||||
if (props.defaultUrl) return props.defaultUrl
|
||||
if (props.storedDefaultServerUrl) return props.storedDefaultServerUrl
|
||||
if (props.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||
if (props.isDev) return `http://${props.devHost ?? "localhost"}:${props.devPort ?? "4096"}`
|
||||
return props.origin
|
||||
}
|
||||
|
||||
export function AppBaseProviders(props: ParentProps) {
|
||||
return (
|
||||
<MetaProvider>
|
||||
@@ -165,29 +86,80 @@ function ServerKey(props: ParentProps) {
|
||||
|
||||
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
|
||||
const platform = usePlatform()
|
||||
const storedDefaultServerUrl = getStoredDefaultServerUrl(platform)
|
||||
const defaultServerUrl = resolveDefaultServerUrl({
|
||||
defaultUrl: props.defaultUrl,
|
||||
storedDefaultServerUrl,
|
||||
hostname: location.hostname,
|
||||
origin: window.location.origin,
|
||||
isDev: import.meta.env.DEV,
|
||||
devHost: import.meta.env.VITE_OPENCODE_SERVER_HOST,
|
||||
devPort: import.meta.env.VITE_OPENCODE_SERVER_PORT,
|
||||
})
|
||||
|
||||
const stored = (() => {
|
||||
if (platform.platform !== "web") return
|
||||
const result = platform.getDefaultServerUrl?.()
|
||||
if (result instanceof Promise) return
|
||||
if (!result) return
|
||||
return normalizeServerUrl(result)
|
||||
})()
|
||||
|
||||
const defaultServerUrl = () => {
|
||||
if (props.defaultUrl) return props.defaultUrl
|
||||
if (stored) return stored
|
||||
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||
if (import.meta.env.DEV)
|
||||
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
return (
|
||||
<ServerProvider defaultUrl={defaultServerUrl} isSidecar={props.isSidecar}>
|
||||
<ServerProvider defaultUrl={defaultServerUrl()} isSidecar={props.isSidecar}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
<Router
|
||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||
root={(routerProps) => (
|
||||
<SettingsProvider>
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<ModelsProvider>
|
||||
<CommandProvider>
|
||||
<HighlightsProvider>
|
||||
<Layout>
|
||||
{props.children}
|
||||
{routerProps.children}
|
||||
</Layout>
|
||||
</HighlightsProvider>
|
||||
</CommandProvider>
|
||||
</ModelsProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
</SettingsProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={HomeRoute} />
|
||||
<Route
|
||||
path="/"
|
||||
component={() => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Home />
|
||||
</Suspense>
|
||||
)}
|
||||
/>
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={SessionIndexRoute} />
|
||||
<Route path="/session/:id?" component={SessionRoute} />
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id ?? "new"}>
|
||||
<TerminalProvider>
|
||||
<FileProvider>
|
||||
<PromptProvider>
|
||||
<CommentsProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Session />
|
||||
</Suspense>
|
||||
</CommentsProvider>
|
||||
</PromptProvider>
|
||||
</FileProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</GlobalSyncProvider>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Link } from "@/components/link"
|
||||
@@ -54,47 +55,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
type Action =
|
||||
| { type: "method.select"; index: number }
|
||||
| { type: "method.reset" }
|
||||
| { type: "auth.pending" }
|
||||
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
|
||||
| { type: "auth.error"; error: string }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
if (action.type === "method.select") {
|
||||
draft.methodIndex = action.index
|
||||
draft.authorization = undefined
|
||||
draft.state = undefined
|
||||
draft.error = undefined
|
||||
return
|
||||
}
|
||||
if (action.type === "method.reset") {
|
||||
draft.methodIndex = undefined
|
||||
draft.authorization = undefined
|
||||
draft.state = undefined
|
||||
draft.error = undefined
|
||||
return
|
||||
}
|
||||
if (action.type === "auth.pending") {
|
||||
draft.state = "pending"
|
||||
draft.error = undefined
|
||||
return
|
||||
}
|
||||
if (action.type === "auth.complete") {
|
||||
draft.state = "complete"
|
||||
draft.authorization = action.authorization
|
||||
draft.error = undefined
|
||||
return
|
||||
}
|
||||
draft.state = "error"
|
||||
draft.error = action.error
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
|
||||
|
||||
const methodLabel = (value?: { type?: string; label?: string }) => {
|
||||
@@ -103,24 +63,6 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
return value.label ?? ""
|
||||
}
|
||||
|
||||
function formatError(value: unknown, fallback: string): string {
|
||||
if (value && typeof value === "object" && "data" in value) {
|
||||
const data = (value as { data?: { message?: unknown } }).data
|
||||
if (typeof data?.message === "string" && data.message) return data.message
|
||||
}
|
||||
if (value && typeof value === "object" && "error" in value) {
|
||||
const nested = formatError((value as { error?: unknown }).error, "")
|
||||
if (nested) return nested
|
||||
}
|
||||
if (value && typeof value === "object" && "message" in value) {
|
||||
const message = (value as { message?: unknown }).message
|
||||
if (typeof message === "string" && message) return message
|
||||
}
|
||||
if (value instanceof Error && value.message) return value.message
|
||||
if (typeof value === "string" && value) return value
|
||||
return fallback
|
||||
}
|
||||
|
||||
async function selectMethod(index: number) {
|
||||
if (timer.current !== undefined) {
|
||||
clearTimeout(timer.current)
|
||||
@@ -128,10 +70,17 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
}
|
||||
|
||||
const method = methods()[index]
|
||||
dispatch({ type: "method.select", index })
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.methodIndex = index
|
||||
draft.authorization = undefined
|
||||
draft.state = undefined
|
||||
draft.error = undefined
|
||||
}),
|
||||
)
|
||||
|
||||
if (method.type === "oauth") {
|
||||
dispatch({ type: "auth.pending" })
|
||||
setStore("state", "pending")
|
||||
const start = Date.now()
|
||||
await globalSDK.client.provider.oauth
|
||||
.authorize(
|
||||
@@ -151,15 +100,18 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
timer.current = setTimeout(() => {
|
||||
timer.current = undefined
|
||||
if (!alive.value) return
|
||||
dispatch({ type: "auth.complete", authorization: x.data! })
|
||||
setStore("state", "complete")
|
||||
setStore("authorization", x.data!)
|
||||
}, delay)
|
||||
return
|
||||
}
|
||||
dispatch({ type: "auth.complete", authorization: x.data! })
|
||||
setStore("state", "complete")
|
||||
setStore("authorization", x.data!)
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!alive.value) return
|
||||
dispatch({ type: "auth.error", error: formatError(e, language.t("common.requestFailed")) })
|
||||
setStore("state", "error")
|
||||
setStore("error", String(e))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -177,6 +129,10 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
if (methods().length === 1) {
|
||||
selectMethod(0)
|
||||
}
|
||||
document.addEventListener("keydown", handleKey)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKey)
|
||||
})
|
||||
})
|
||||
|
||||
async function complete() {
|
||||
@@ -196,243 +152,17 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
return
|
||||
}
|
||||
if (store.authorization) {
|
||||
dispatch({ type: "method.reset" })
|
||||
setStore("authorization", undefined)
|
||||
setStore("methodIndex", undefined)
|
||||
return
|
||||
}
|
||||
if (store.methodIndex !== undefined) {
|
||||
dispatch({ type: "method.reset" })
|
||||
if (store.methodIndex) {
|
||||
setStore("methodIndex", undefined)
|
||||
return
|
||||
}
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
|
||||
function MethodSelection() {
|
||||
return (
|
||||
<>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.selectMethod", { provider: provider().name })}
|
||||
</div>
|
||||
<div>
|
||||
<List
|
||||
ref={(ref) => {
|
||||
listRef = ref
|
||||
}}
|
||||
items={methods}
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (selected, index) => {
|
||||
if (!selected) return
|
||||
selectMethod(index)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2">
|
||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
</div>
|
||||
<span>{methodLabel(i)}</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ApiAuthView() {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const apiKey = formData.get("apiKey") as string
|
||||
|
||||
if (!apiKey?.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.apiKey.required"))
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await globalSDK.client.auth.set({
|
||||
providerID: props.provider,
|
||||
auth: {
|
||||
type: "api",
|
||||
key: apiKey,
|
||||
},
|
||||
})
|
||||
await complete()
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<Switch>
|
||||
<Match when={provider().id === "opencode"}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line1")}</div>
|
||||
<div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line2")}</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.opencodeZen.visit.prefix")}
|
||||
<Link href="https://opencode.ai/zen" tabIndex={-1}>
|
||||
{language.t("provider.connect.opencodeZen.visit.link")}
|
||||
</Link>
|
||||
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.apiKey.description", { provider: provider().name })}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={language.t("provider.connect.apiKey.label", { provider: provider().name })}
|
||||
placeholder={language.t("provider.connect.apiKey.placeholder")}
|
||||
name="apiKey"
|
||||
value={formStore.value}
|
||||
onChange={(v) => setFormStore("value", v)}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
{language.t("common.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OAuthCodeView() {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const code = formData.get("code") as string
|
||||
|
||||
if (!code?.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.oauth.code.required"))
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
code,
|
||||
})
|
||||
.then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
if (result.ok) {
|
||||
await complete()
|
||||
return
|
||||
}
|
||||
setFormStore("error", formatError(result.error, language.t("provider.connect.oauth.code.invalid")))
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.oauth.code.visit.prefix")}
|
||||
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.code.visit.link")}</Link>
|
||||
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
|
||||
placeholder={language.t("provider.connect.oauth.code.placeholder")}
|
||||
name="code"
|
||||
value={formStore.value}
|
||||
onChange={(v) => setFormStore("value", v)}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
{language.t("common.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OAuthAutoView() {
|
||||
const code = createMemo(() => {
|
||||
const instructions = store.authorization?.instructions
|
||||
if (instructions?.includes(":")) {
|
||||
return instructions.split(":")[1]?.trim()
|
||||
}
|
||||
return instructions
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
if (store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
.then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (!alive.value) return
|
||||
|
||||
if (!result.ok) {
|
||||
const message = formatError(result.error, language.t("common.requestFailed"))
|
||||
dispatch({ type: "auth.error", error: message })
|
||||
return
|
||||
}
|
||||
|
||||
await complete()
|
||||
})()
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.oauth.auto.visit.prefix")}
|
||||
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.auto.visit.link")}</Link>
|
||||
{language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
|
||||
</div>
|
||||
<TextField
|
||||
label={language.t("provider.connect.oauth.auto.confirmationCode")}
|
||||
class="font-mono"
|
||||
value={code()}
|
||||
readOnly
|
||||
copyable
|
||||
/>
|
||||
<div class="text-14-regular text-text-base flex items-center gap-4">
|
||||
<Spinner />
|
||||
<span>{language.t("provider.connect.status.waiting")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={
|
||||
@@ -458,42 +188,267 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
||||
<div onKeyDown={handleKey} tabIndex={0} autofocus={store.methodIndex === undefined ? true : undefined}>
|
||||
<Switch>
|
||||
<Match when={store.methodIndex === undefined}>
|
||||
<MethodSelection />
|
||||
</Match>
|
||||
<Match when={store.state === "pending"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Spinner />
|
||||
<span>{language.t("provider.connect.status.inProgress")}</span>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={store.methodIndex === undefined}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.selectMethod", { provider: provider().name })}
|
||||
</div>
|
||||
<div class="">
|
||||
<List
|
||||
ref={(ref) => {
|
||||
listRef = ref
|
||||
}}
|
||||
items={methods}
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (method, index) => {
|
||||
if (!method) return
|
||||
selectMethod(index)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2">
|
||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
</div>
|
||||
<span>{methodLabel(i)}</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "pending"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Spinner />
|
||||
<span>{language.t("provider.connect.status.inProgress")}</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "error"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "error"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||
<span>{language.t("provider.connect.status.failed", { error: store.error ?? "" })}</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={method()?.type === "api"}>
|
||||
<ApiAuthView />
|
||||
</Match>
|
||||
<Match when={method()?.type === "oauth"}>
|
||||
<Switch>
|
||||
<Match when={store.authorization?.method === "code"}>
|
||||
<OAuthCodeView />
|
||||
</Match>
|
||||
<Match when={store.authorization?.method === "auto"}>
|
||||
<OAuthAutoView />
|
||||
</Match>
|
||||
</Switch>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={method()?.type === "api"}>
|
||||
{iife(() => {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const apiKey = formData.get("apiKey") as string
|
||||
|
||||
if (!apiKey?.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.apiKey.required"))
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await globalSDK.client.auth.set({
|
||||
providerID: props.provider,
|
||||
auth: {
|
||||
type: "api",
|
||||
key: apiKey,
|
||||
},
|
||||
})
|
||||
await complete()
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<Switch>
|
||||
<Match when={provider().id === "opencode"}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.opencodeZen.line1")}
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.opencodeZen.line2")}
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.opencodeZen.visit.prefix")}
|
||||
<Link href="https://opencode.ai/zen" tabIndex={-1}>
|
||||
{language.t("provider.connect.opencodeZen.visit.link")}
|
||||
</Link>
|
||||
{language.t("provider.connect.opencodeZen.visit.suffix")}
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.apiKey.description", { provider: provider().name })}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={language.t("provider.connect.apiKey.label", { provider: provider().name })}
|
||||
placeholder={language.t("provider.connect.apiKey.placeholder")}
|
||||
name="apiKey"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
{language.t("common.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
<Match when={method()?.type === "oauth"}>
|
||||
<Switch>
|
||||
<Match when={store.authorization?.method === "code"}>
|
||||
{iife(() => {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const code = formData.get("code") as string
|
||||
|
||||
if (!code?.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.oauth.code.required"))
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
code,
|
||||
})
|
||||
.then((value) =>
|
||||
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
||||
)
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
if (result.ok) {
|
||||
await complete()
|
||||
return
|
||||
}
|
||||
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
||||
setFormStore("error", message || language.t("provider.connect.oauth.code.invalid"))
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.oauth.code.visit.prefix")}
|
||||
<Link href={store.authorization!.url}>
|
||||
{language.t("provider.connect.oauth.code.visit.link")}
|
||||
</Link>
|
||||
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={language.t("provider.connect.oauth.code.label", { method: method()?.label ?? "" })}
|
||||
placeholder={language.t("provider.connect.oauth.code.placeholder")}
|
||||
name="code"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
{language.t("common.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
<Match when={store.authorization?.method === "auto"}>
|
||||
{iife(() => {
|
||||
const code = createMemo(() => {
|
||||
const instructions = store.authorization?.instructions
|
||||
if (instructions?.includes(":")) {
|
||||
return instructions?.split(":")[1]?.trim()
|
||||
}
|
||||
return instructions
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
if (store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
|
||||
const result = await globalSDK.client.provider.oauth
|
||||
.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
.then((value) =>
|
||||
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
|
||||
)
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (!alive.value) return
|
||||
|
||||
if (!result.ok) {
|
||||
const message = result.error instanceof Error ? result.error.message : String(result.error)
|
||||
setStore("state", "error")
|
||||
setStore("error", message)
|
||||
return
|
||||
}
|
||||
|
||||
await complete()
|
||||
})()
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
{language.t("provider.connect.oauth.auto.visit.prefix")}
|
||||
<Link href={store.authorization!.url}>
|
||||
{language.t("provider.connect.oauth.auto.visit.link")}
|
||||
</Link>
|
||||
{language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
|
||||
</div>
|
||||
<TextField
|
||||
label={language.t("provider.connect.oauth.auto.confirmationCode")}
|
||||
class="font-mono"
|
||||
value={code()}
|
||||
readOnly
|
||||
copyable
|
||||
/>
|
||||
<div class="text-14-regular text-text-base flex items-center gap-4">
|
||||
<Spinner />
|
||||
<span>{language.t("provider.connect.status.waiting")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { For } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { Link } from "@/components/link"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
@@ -16,147 +16,6 @@ import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
|
||||
const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
|
||||
|
||||
type Translator = ReturnType<typeof useLanguage>["t"]
|
||||
|
||||
type ModelRow = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type HeaderRow = {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type FormState = {
|
||||
providerID: string
|
||||
name: string
|
||||
baseURL: string
|
||||
apiKey: string
|
||||
models: ModelRow[]
|
||||
headers: HeaderRow[]
|
||||
saving: boolean
|
||||
}
|
||||
|
||||
type FormErrors = {
|
||||
providerID: string | undefined
|
||||
name: string | undefined
|
||||
baseURL: string | undefined
|
||||
models: Array<{ id?: string; name?: string }>
|
||||
headers: Array<{ key?: string; value?: string }>
|
||||
}
|
||||
|
||||
type ValidateArgs = {
|
||||
form: FormState
|
||||
t: Translator
|
||||
disabledProviders: string[]
|
||||
existingProviderIDs: Set<string>
|
||||
}
|
||||
|
||||
function validateCustomProvider(input: ValidateArgs) {
|
||||
const providerID = input.form.providerID.trim()
|
||||
const name = input.form.name.trim()
|
||||
const baseURL = input.form.baseURL.trim()
|
||||
const apiKey = input.form.apiKey.trim()
|
||||
|
||||
const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
|
||||
const key = apiKey && !env ? apiKey : undefined
|
||||
|
||||
const idError = !providerID
|
||||
? input.t("provider.custom.error.providerID.required")
|
||||
: !PROVIDER_ID.test(providerID)
|
||||
? input.t("provider.custom.error.providerID.format")
|
||||
: undefined
|
||||
|
||||
const nameError = !name ? input.t("provider.custom.error.name.required") : undefined
|
||||
const urlError = !baseURL
|
||||
? input.t("provider.custom.error.baseURL.required")
|
||||
: !/^https?:\/\//.test(baseURL)
|
||||
? input.t("provider.custom.error.baseURL.format")
|
||||
: undefined
|
||||
|
||||
const disabled = input.disabledProviders.includes(providerID)
|
||||
const existsError = idError
|
||||
? undefined
|
||||
: input.existingProviderIDs.has(providerID) && !disabled
|
||||
? input.t("provider.custom.error.providerID.exists")
|
||||
: undefined
|
||||
|
||||
const seenModels = new Set<string>()
|
||||
const modelErrors = input.form.models.map((m) => {
|
||||
const id = m.id.trim()
|
||||
const modelIdError = !id
|
||||
? input.t("provider.custom.error.required")
|
||||
: seenModels.has(id)
|
||||
? input.t("provider.custom.error.duplicate")
|
||||
: (() => {
|
||||
seenModels.add(id)
|
||||
return undefined
|
||||
})()
|
||||
const modelNameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
|
||||
return { id: modelIdError, name: modelNameError }
|
||||
})
|
||||
const modelsValid = modelErrors.every((m) => !m.id && !m.name)
|
||||
const models = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
|
||||
|
||||
const seenHeaders = new Set<string>()
|
||||
const headerErrors = input.form.headers.map((h) => {
|
||||
const key = h.key.trim()
|
||||
const value = h.value.trim()
|
||||
|
||||
if (!key && !value) return {}
|
||||
const keyError = !key
|
||||
? input.t("provider.custom.error.required")
|
||||
: seenHeaders.has(key.toLowerCase())
|
||||
? input.t("provider.custom.error.duplicate")
|
||||
: (() => {
|
||||
seenHeaders.add(key.toLowerCase())
|
||||
return undefined
|
||||
})()
|
||||
const valueError = !value ? input.t("provider.custom.error.required") : undefined
|
||||
return { key: keyError, value: valueError }
|
||||
})
|
||||
const headersValid = headerErrors.every((h) => !h.key && !h.value)
|
||||
const headers = Object.fromEntries(
|
||||
input.form.headers
|
||||
.map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
|
||||
.filter((h) => !!h.key && !!h.value)
|
||||
.map((h) => [h.key, h.value]),
|
||||
)
|
||||
|
||||
const errors: FormErrors = {
|
||||
providerID: idError ?? existsError,
|
||||
name: nameError,
|
||||
baseURL: urlError,
|
||||
models: modelErrors,
|
||||
headers: headerErrors,
|
||||
}
|
||||
|
||||
const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
|
||||
if (!ok) return { errors }
|
||||
|
||||
const options = {
|
||||
baseURL,
|
||||
...(Object.keys(headers).length ? { headers } : {}),
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
result: {
|
||||
providerID,
|
||||
name,
|
||||
key,
|
||||
config: {
|
||||
npm: OPENAI_COMPATIBLE,
|
||||
name,
|
||||
...(env ? { env: [env] } : {}),
|
||||
options,
|
||||
models,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
back?: "providers" | "close"
|
||||
}
|
||||
@@ -167,7 +26,7 @@ export function DialogCustomProvider(props: Props) {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const language = useLanguage()
|
||||
|
||||
const [form, setForm] = createStore<FormState>({
|
||||
const [form, setForm] = createStore({
|
||||
providerID: "",
|
||||
name: "",
|
||||
baseURL: "",
|
||||
@@ -177,12 +36,12 @@ export function DialogCustomProvider(props: Props) {
|
||||
saving: false,
|
||||
})
|
||||
|
||||
const [errors, setErrors] = createStore<FormErrors>({
|
||||
providerID: undefined,
|
||||
name: undefined,
|
||||
baseURL: undefined,
|
||||
models: [{}],
|
||||
headers: [{}],
|
||||
const [errors, setErrors] = createStore({
|
||||
providerID: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
baseURL: undefined as string | undefined,
|
||||
models: [{} as { id?: string; name?: string }],
|
||||
headers: [{} as { key?: string; value?: string }],
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
@@ -194,36 +53,169 @@ export function DialogCustomProvider(props: Props) {
|
||||
}
|
||||
|
||||
const addModel = () => {
|
||||
setForm("models", (v) => [...v, { id: "", name: "" }])
|
||||
setErrors("models", (v) => [...v, {}])
|
||||
setForm(
|
||||
"models",
|
||||
produce((draft) => {
|
||||
draft.push({ id: "", name: "" })
|
||||
}),
|
||||
)
|
||||
setErrors(
|
||||
"models",
|
||||
produce((draft) => {
|
||||
draft.push({})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const removeModel = (index: number) => {
|
||||
if (form.models.length <= 1) return
|
||||
setForm("models", (v) => v.filter((_, i) => i !== index))
|
||||
setErrors("models", (v) => v.filter((_, i) => i !== index))
|
||||
setForm(
|
||||
"models",
|
||||
produce((draft) => {
|
||||
draft.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
setErrors(
|
||||
"models",
|
||||
produce((draft) => {
|
||||
draft.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const addHeader = () => {
|
||||
setForm("headers", (v) => [...v, { key: "", value: "" }])
|
||||
setErrors("headers", (v) => [...v, {}])
|
||||
setForm(
|
||||
"headers",
|
||||
produce((draft) => {
|
||||
draft.push({ key: "", value: "" })
|
||||
}),
|
||||
)
|
||||
setErrors(
|
||||
"headers",
|
||||
produce((draft) => {
|
||||
draft.push({})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const removeHeader = (index: number) => {
|
||||
if (form.headers.length <= 1) return
|
||||
setForm("headers", (v) => v.filter((_, i) => i !== index))
|
||||
setErrors("headers", (v) => v.filter((_, i) => i !== index))
|
||||
setForm(
|
||||
"headers",
|
||||
produce((draft) => {
|
||||
draft.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
setErrors(
|
||||
"headers",
|
||||
produce((draft) => {
|
||||
draft.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
const output = validateCustomProvider({
|
||||
form,
|
||||
t: language.t,
|
||||
disabledProviders: globalSync.data.config.disabled_providers ?? [],
|
||||
existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)),
|
||||
const providerID = form.providerID.trim()
|
||||
const name = form.name.trim()
|
||||
const baseURL = form.baseURL.trim()
|
||||
const apiKey = form.apiKey.trim()
|
||||
|
||||
const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
|
||||
const key = apiKey && !env ? apiKey : undefined
|
||||
|
||||
const idError = !providerID
|
||||
? language.t("provider.custom.error.providerID.required")
|
||||
: !PROVIDER_ID.test(providerID)
|
||||
? language.t("provider.custom.error.providerID.format")
|
||||
: undefined
|
||||
|
||||
const nameError = !name ? language.t("provider.custom.error.name.required") : undefined
|
||||
const urlError = !baseURL
|
||||
? language.t("provider.custom.error.baseURL.required")
|
||||
: !/^https?:\/\//.test(baseURL)
|
||||
? language.t("provider.custom.error.baseURL.format")
|
||||
: undefined
|
||||
|
||||
const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID)
|
||||
const existingProvider = globalSync.data.provider.all.find((p) => p.id === providerID)
|
||||
const existsError = idError
|
||||
? undefined
|
||||
: existingProvider && !disabled
|
||||
? language.t("provider.custom.error.providerID.exists")
|
||||
: undefined
|
||||
|
||||
const seenModels = new Set<string>()
|
||||
const modelErrors = form.models.map((m) => {
|
||||
const id = m.id.trim()
|
||||
const modelIdError = !id
|
||||
? language.t("provider.custom.error.required")
|
||||
: seenModels.has(id)
|
||||
? language.t("provider.custom.error.duplicate")
|
||||
: (() => {
|
||||
seenModels.add(id)
|
||||
return undefined
|
||||
})()
|
||||
const modelNameError = !m.name.trim() ? language.t("provider.custom.error.required") : undefined
|
||||
return { id: modelIdError, name: modelNameError }
|
||||
})
|
||||
setErrors(output.errors)
|
||||
return output.result
|
||||
const modelsValid = modelErrors.every((m) => !m.id && !m.name)
|
||||
const models = Object.fromEntries(form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
|
||||
|
||||
const seenHeaders = new Set<string>()
|
||||
const headerErrors = form.headers.map((h) => {
|
||||
const key = h.key.trim()
|
||||
const value = h.value.trim()
|
||||
|
||||
if (!key && !value) return {}
|
||||
const keyError = !key
|
||||
? language.t("provider.custom.error.required")
|
||||
: seenHeaders.has(key.toLowerCase())
|
||||
? language.t("provider.custom.error.duplicate")
|
||||
: (() => {
|
||||
seenHeaders.add(key.toLowerCase())
|
||||
return undefined
|
||||
})()
|
||||
const valueError = !value ? language.t("provider.custom.error.required") : undefined
|
||||
return { key: keyError, value: valueError }
|
||||
})
|
||||
const headersValid = headerErrors.every((h) => !h.key && !h.value)
|
||||
const headers = Object.fromEntries(
|
||||
form.headers
|
||||
.map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
|
||||
.filter((h) => !!h.key && !!h.value)
|
||||
.map((h) => [h.key, h.value]),
|
||||
)
|
||||
|
||||
setErrors(
|
||||
produce((draft) => {
|
||||
draft.providerID = idError ?? existsError
|
||||
draft.name = nameError
|
||||
draft.baseURL = urlError
|
||||
draft.models = modelErrors
|
||||
draft.headers = headerErrors
|
||||
}),
|
||||
)
|
||||
|
||||
const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
|
||||
if (!ok) return
|
||||
|
||||
const options = {
|
||||
baseURL,
|
||||
...(Object.keys(headers).length ? { headers } : {}),
|
||||
}
|
||||
|
||||
return {
|
||||
providerID,
|
||||
name,
|
||||
key,
|
||||
config: {
|
||||
npm: OPENAI_COMPATIBLE,
|
||||
name,
|
||||
...(env ? { env: [env] } : {}),
|
||||
options,
|
||||
models,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const save = async (e: SubmitEvent) => {
|
||||
@@ -305,7 +297,7 @@ export function DialogCustomProvider(props: Props) {
|
||||
placeholder={language.t("provider.custom.field.providerID.placeholder")}
|
||||
description={language.t("provider.custom.field.providerID.description")}
|
||||
value={form.providerID}
|
||||
onChange={(v) => setForm("providerID", v)}
|
||||
onChange={setForm.bind(null, "providerID")}
|
||||
validationState={errors.providerID ? "invalid" : undefined}
|
||||
error={errors.providerID}
|
||||
/>
|
||||
@@ -313,7 +305,7 @@ export function DialogCustomProvider(props: Props) {
|
||||
label={language.t("provider.custom.field.name.label")}
|
||||
placeholder={language.t("provider.custom.field.name.placeholder")}
|
||||
value={form.name}
|
||||
onChange={(v) => setForm("name", v)}
|
||||
onChange={setForm.bind(null, "name")}
|
||||
validationState={errors.name ? "invalid" : undefined}
|
||||
error={errors.name}
|
||||
/>
|
||||
@@ -321,7 +313,7 @@ export function DialogCustomProvider(props: Props) {
|
||||
label={language.t("provider.custom.field.baseURL.label")}
|
||||
placeholder={language.t("provider.custom.field.baseURL.placeholder")}
|
||||
value={form.baseURL}
|
||||
onChange={(v) => setForm("baseURL", v)}
|
||||
onChange={setForm.bind(null, "baseURL")}
|
||||
validationState={errors.baseURL ? "invalid" : undefined}
|
||||
error={errors.baseURL}
|
||||
/>
|
||||
@@ -330,7 +322,7 @@ export function DialogCustomProvider(props: Props) {
|
||||
placeholder={language.t("provider.custom.field.apiKey.placeholder")}
|
||||
description={language.t("provider.custom.field.apiKey.description")}
|
||||
value={form.apiKey}
|
||||
onChange={(v) => setForm("apiKey", v)}
|
||||
onChange={setForm.bind(null, "apiKey")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
iconHover: false,
|
||||
})
|
||||
|
||||
let iconInput: HTMLInputElement | undefined
|
||||
|
||||
function handleFileSelect(file: File) {
|
||||
if (!file.type.startsWith("image/")) return
|
||||
const reader = new FileReader()
|
||||
@@ -74,35 +72,31 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
await Promise.resolve()
|
||||
.then(async () => {
|
||||
setStore("saving", true)
|
||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||
const start = store.startup.trim()
|
||||
setStore("saving", true)
|
||||
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||
const start = store.startup.trim()
|
||||
|
||||
if (props.project.id && props.project.id !== "global") {
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl },
|
||||
commands: { start },
|
||||
})
|
||||
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
if (props.project.id && props.project.id !== "global") {
|
||||
await globalSDK.client.project.update({
|
||||
projectID: props.project.id,
|
||||
directory: props.project.worktree,
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl },
|
||||
commands: { start },
|
||||
})
|
||||
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
|
||||
globalSync.project.meta(props.project.worktree, {
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||
commands: { start: start || undefined },
|
||||
})
|
||||
dialog.close()
|
||||
})
|
||||
.finally(() => {
|
||||
setStore("saving", false)
|
||||
})
|
||||
globalSync.project.meta(props.project.worktree, {
|
||||
name,
|
||||
icon: { color: store.color, override: store.iconUrl || undefined },
|
||||
commands: { start: start || undefined },
|
||||
})
|
||||
setStore("saving", false)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -140,7 +134,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
if (store.iconUrl && store.iconHover) {
|
||||
clearIcon()
|
||||
} else {
|
||||
iconInput?.click()
|
||||
document.getElementById("icon-upload")?.click()
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -182,16 +176,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
|
||||
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
id="icon-upload"
|
||||
ref={(el) => {
|
||||
iconInput = el
|
||||
}}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
|
||||
<span>{language.t("dialog.project.edit.icon.hint")}</span>
|
||||
<span>{language.t("dialog.project.edit.icon.recommended")}</span>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { usePrompt } from "@/context/prompt"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
@@ -67,23 +66,15 @@ export const DialogFork: Component = () => {
|
||||
attachmentName: language.t("common.attachment"),
|
||||
})
|
||||
|
||||
sdk.client.session
|
||||
.fork({ sessionID, messageID: item.id })
|
||||
.then((forked) => {
|
||||
if (!forked.data) {
|
||||
showToast({ title: language.t("common.requestFailed") })
|
||||
return
|
||||
}
|
||||
dialog.close()
|
||||
navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
|
||||
requestAnimationFrame(() => {
|
||||
prompt.set(restored)
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
dialog.close()
|
||||
|
||||
sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => {
|
||||
if (!forked.data) return
|
||||
navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
|
||||
requestAnimationFrame(() => {
|
||||
prompt.set(restored)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import type { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -18,15 +17,6 @@ export const DialogManageModels: Component = () => {
|
||||
const handleConnectProvider = () => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
const providerRank = (id: string) => popularProviders.indexOf(id)
|
||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||
const providerVisible = (providerID: string) =>
|
||||
providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id }))
|
||||
const setProviderVisibility = (providerID: string, checked: boolean) => {
|
||||
providerList(providerID).forEach((x) => {
|
||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -45,41 +35,21 @@ export const DialogManageModels: Component = () => {
|
||||
items={local.model.list()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.id}
|
||||
groupHeader={(group) => {
|
||||
const provider = group.items[0].provider
|
||||
return (
|
||||
<>
|
||||
<span>{provider.name}</span>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
|
||||
>
|
||||
<Switch
|
||||
class="-mr-1"
|
||||
checked={providerVisible(provider.id)}
|
||||
onChange={(checked) => setProviderVisibility(provider.id, checked)}
|
||||
hideLabel
|
||||
>
|
||||
{provider.name}
|
||||
</Switch>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
const aRank = providerRank(a.items[0].provider.id)
|
||||
const bRank = providerRank(b.items[0].provider.id)
|
||||
const aPopular = aRank >= 0
|
||||
const bPopular = bRank >= 0
|
||||
if (aPopular && !bPopular) return -1
|
||||
if (!aPopular && bPopular) return 1
|
||||
return aRank - bRank
|
||||
const aProvider = a.items[0].provider.id
|
||||
const bProvider = b.items[0].provider.id
|
||||
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
||||
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
||||
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
const key = { modelID: x.id, providerID: x.provider.id }
|
||||
local.model.setVisibility(key, !local.model.visible(key))
|
||||
const visible = local.model.visible({
|
||||
modelID: x.id,
|
||||
providerID: x.provider.id,
|
||||
})
|
||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
@@ -87,7 +57,12 @@ export const DialogManageModels: Component = () => {
|
||||
<span>{i.name}</span>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
|
||||
checked={
|
||||
!!local.model.visible({
|
||||
modelID: i.id,
|
||||
providerID: i.provider.id,
|
||||
})
|
||||
}
|
||||
onChange={(checked) => {
|
||||
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import { createSignal, createEffect, onMount, onCleanup } from "solid-js"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
@@ -40,6 +40,8 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
let focusTrap: HTMLDivElement | undefined
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
@@ -58,13 +60,27 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
focusTrap?.focus()
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
|
||||
})
|
||||
|
||||
// Refocus the trap when index changes to ensure escape always works
|
||||
createEffect(() => {
|
||||
index() // track index
|
||||
focusTrap?.focus()
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
size="large"
|
||||
fit
|
||||
class="w-[min(calc(100vw-40px),720px)] h-[min(calc(100vh-40px),400px)] -mt-20 min-h-0 overflow-hidden"
|
||||
>
|
||||
<div class="flex flex-1 min-w-0 min-h-0" tabIndex={0} autofocus onKeyDown={handleKeyDown}>
|
||||
{/* Hidden element to capture initial focus and handle escape */}
|
||||
<div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
|
||||
<div class="flex flex-1 min-w-0 min-h-0">
|
||||
{/* Left side - Text content */}
|
||||
<div class="flex flex-col flex-1 min-w-0 p-8">
|
||||
{/* Top section - feature content (fixed position from top) */}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import type { ListRef } from "@opencode-ai/ui/list"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { createMemo, createResource, createSignal } from "solid-js"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { ListRef } from "@opencode-ai/ui/list"
|
||||
|
||||
interface DialogSelectDirectoryProps {
|
||||
title?: string
|
||||
@@ -21,131 +21,157 @@ type Row = {
|
||||
search: string
|
||||
}
|
||||
|
||||
function cleanInput(value: string) {
|
||||
const first = (value ?? "").split(/\r?\n/)[0] ?? ""
|
||||
return first.replace(/[\u0000-\u001F\u007F]/g, "").trim()
|
||||
}
|
||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useGlobalSync()
|
||||
const sdk = useGlobalSDK()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
function normalizePath(input: string) {
|
||||
const v = input.replaceAll("\\", "/")
|
||||
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
||||
return v.replace(/\/+/g, "/")
|
||||
}
|
||||
const [filter, setFilter] = createSignal("")
|
||||
|
||||
function normalizeDriveRoot(input: string) {
|
||||
const v = normalizePath(input)
|
||||
if (/^[A-Za-z]:$/.test(v)) return v + "/"
|
||||
return v
|
||||
}
|
||||
let list: ListRef | undefined
|
||||
|
||||
function trimTrailing(input: string) {
|
||||
const v = normalizeDriveRoot(input)
|
||||
if (v === "/") return v
|
||||
if (v === "//") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
return v.replace(/\/+$/, "")
|
||||
}
|
||||
const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
|
||||
|
||||
function joinPath(base: string | undefined, rel: string) {
|
||||
const b = trimTrailing(base ?? "")
|
||||
const r = trimTrailing(rel).replace(/^\/+/, "")
|
||||
if (!b) return r
|
||||
if (!r) return b
|
||||
if (b.endsWith("/")) return b + r
|
||||
return b + "/" + r
|
||||
}
|
||||
const [fallbackPath] = createResource(
|
||||
() => (missingBase() ? true : undefined),
|
||||
async () => {
|
||||
return sdk.client.path
|
||||
.get()
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
},
|
||||
{ initialValue: undefined },
|
||||
)
|
||||
|
||||
function rootOf(input: string) {
|
||||
const v = normalizeDriveRoot(input)
|
||||
if (v.startsWith("//")) return "//"
|
||||
if (v.startsWith("/")) return "/"
|
||||
if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3)
|
||||
return ""
|
||||
}
|
||||
const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
|
||||
|
||||
function parentOf(input: string) {
|
||||
const v = trimTrailing(input)
|
||||
if (v === "/") return v
|
||||
if (v === "//") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
const start = createMemo(
|
||||
() => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory,
|
||||
)
|
||||
|
||||
const i = v.lastIndexOf("/")
|
||||
if (i <= 0) return "/"
|
||||
if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3)
|
||||
return v.slice(0, i)
|
||||
}
|
||||
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
||||
|
||||
function modeOf(input: string) {
|
||||
const raw = normalizeDriveRoot(input.trim())
|
||||
if (!raw) return "relative" as const
|
||||
if (raw.startsWith("~")) return "tilde" as const
|
||||
if (rootOf(raw)) return "absolute" as const
|
||||
return "relative" as const
|
||||
}
|
||||
|
||||
function tildeOf(absolute: string, home: string) {
|
||||
const full = trimTrailing(absolute)
|
||||
if (!home) return ""
|
||||
|
||||
const hn = trimTrailing(home)
|
||||
const lc = full.toLowerCase()
|
||||
const hc = hn.toLowerCase()
|
||||
if (lc === hc) return "~"
|
||||
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
||||
return ""
|
||||
}
|
||||
|
||||
function displayPath(path: string, input: string, home: string) {
|
||||
const full = trimTrailing(path)
|
||||
if (modeOf(input) === "absolute") return full
|
||||
return tildeOf(full, home) || full
|
||||
}
|
||||
|
||||
function toRow(absolute: string, home: string): Row {
|
||||
const full = trimTrailing(absolute)
|
||||
const tilde = tildeOf(full, home)
|
||||
const withSlash = (value: string) => {
|
||||
if (!value) return ""
|
||||
if (value.endsWith("/")) return value
|
||||
return value + "/"
|
||||
const clean = (value: string) => {
|
||||
const first = (value ?? "").split(/\r?\n/)[0] ?? ""
|
||||
return first.replace(/[\u0000-\u001F\u007F]/g, "").trim()
|
||||
}
|
||||
|
||||
const search = Array.from(
|
||||
new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)),
|
||||
).join("\n")
|
||||
return { absolute: full, search }
|
||||
}
|
||||
function normalize(input: string) {
|
||||
const v = input.replaceAll("\\", "/")
|
||||
if (v.startsWith("//") && !v.startsWith("///")) return "//" + v.slice(2).replace(/\/+/g, "/")
|
||||
return v.replace(/\/+/g, "/")
|
||||
}
|
||||
|
||||
function useDirectorySearch(args: {
|
||||
sdk: ReturnType<typeof useGlobalSDK>
|
||||
start: () => string | undefined
|
||||
home: () => string
|
||||
}) {
|
||||
const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
|
||||
let current = 0
|
||||
function normalizeDriveRoot(input: string) {
|
||||
const v = normalize(input)
|
||||
if (/^[A-Za-z]:$/.test(v)) return v + "/"
|
||||
return v
|
||||
}
|
||||
|
||||
const scoped = (value: string) => {
|
||||
const base = args.start()
|
||||
function trimTrailing(input: string) {
|
||||
const v = normalizeDriveRoot(input)
|
||||
if (v === "/") return v
|
||||
if (v === "//") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
return v.replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
function join(base: string | undefined, rel: string) {
|
||||
const b = trimTrailing(base ?? "")
|
||||
const r = trimTrailing(rel).replace(/^\/+/, "")
|
||||
if (!b) return r
|
||||
if (!r) return b
|
||||
if (b.endsWith("/")) return b + r
|
||||
return b + "/" + r
|
||||
}
|
||||
|
||||
function rootOf(input: string) {
|
||||
const v = normalizeDriveRoot(input)
|
||||
if (v.startsWith("//")) return "//"
|
||||
if (v.startsWith("/")) return "/"
|
||||
if (/^[A-Za-z]:\//.test(v)) return v.slice(0, 3)
|
||||
return ""
|
||||
}
|
||||
|
||||
function parentOf(input: string) {
|
||||
const v = trimTrailing(input)
|
||||
if (v === "/") return v
|
||||
if (v === "//") return v
|
||||
if (/^[A-Za-z]:\/$/.test(v)) return v
|
||||
|
||||
const i = v.lastIndexOf("/")
|
||||
if (i <= 0) return "/"
|
||||
if (i === 2 && /^[A-Za-z]:/.test(v)) return v.slice(0, 3)
|
||||
return v.slice(0, i)
|
||||
}
|
||||
|
||||
function modeOf(input: string) {
|
||||
const raw = normalizeDriveRoot(input.trim())
|
||||
if (!raw) return "relative" as const
|
||||
if (raw.startsWith("~")) return "tilde" as const
|
||||
if (rootOf(raw)) return "absolute" as const
|
||||
return "relative" as const
|
||||
}
|
||||
|
||||
function display(path: string, input: string) {
|
||||
const full = trimTrailing(path)
|
||||
if (modeOf(input) === "absolute") return full
|
||||
|
||||
return tildeOf(full) || full
|
||||
}
|
||||
|
||||
function tildeOf(absolute: string) {
|
||||
const full = trimTrailing(absolute)
|
||||
const h = home()
|
||||
if (!h) return ""
|
||||
|
||||
const hn = trimTrailing(h)
|
||||
const lc = full.toLowerCase()
|
||||
const hc = hn.toLowerCase()
|
||||
if (lc === hc) return "~"
|
||||
if (lc.startsWith(hc + "/")) return "~" + full.slice(hn.length)
|
||||
return ""
|
||||
}
|
||||
|
||||
function row(absolute: string): Row {
|
||||
const full = trimTrailing(absolute)
|
||||
const tilde = tildeOf(full)
|
||||
|
||||
const withSlash = (value: string) => {
|
||||
if (!value) return ""
|
||||
if (value.endsWith("/")) return value
|
||||
return value + "/"
|
||||
}
|
||||
|
||||
const search = Array.from(
|
||||
new Set([full, withSlash(full), tilde, withSlash(tilde), getFilename(full)].filter(Boolean)),
|
||||
).join("\n")
|
||||
return { absolute: full, search }
|
||||
}
|
||||
|
||||
function scoped(value: string) {
|
||||
const base = start()
|
||||
if (!base) return
|
||||
|
||||
const raw = normalizeDriveRoot(value)
|
||||
if (!raw) return { directory: trimTrailing(base), path: "" }
|
||||
|
||||
const h = args.home()
|
||||
if (raw === "~") return { directory: trimTrailing(h || base), path: "" }
|
||||
if (raw.startsWith("~/")) return { directory: trimTrailing(h || base), path: raw.slice(2) }
|
||||
const h = home()
|
||||
if (raw === "~") return { directory: trimTrailing(h ?? base), path: "" }
|
||||
if (raw.startsWith("~/")) return { directory: trimTrailing(h ?? base), path: raw.slice(2) }
|
||||
|
||||
const root = rootOf(raw)
|
||||
if (root) return { directory: trimTrailing(root), path: raw.slice(root.length) }
|
||||
return { directory: trimTrailing(base), path: raw }
|
||||
}
|
||||
|
||||
const dirs = async (dir: string) => {
|
||||
async function dirs(dir: string) {
|
||||
const key = trimTrailing(dir)
|
||||
const existing = cache.get(key)
|
||||
if (existing) return existing
|
||||
|
||||
const request = args.sdk.client.file
|
||||
const request = sdk.client.file
|
||||
.list({ directory: key, path: "" })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
@@ -162,34 +188,32 @@ function useDirectorySearch(args: {
|
||||
return request
|
||||
}
|
||||
|
||||
const match = async (dir: string, query: string, limit: number) => {
|
||||
async function match(dir: string, query: string, limit: number) {
|
||||
const items = await dirs(dir)
|
||||
if (!query) return items.slice(0, limit).map((x) => x.absolute)
|
||||
return fuzzysort.go(query, items, { key: "name", limit }).map((x) => x.obj.absolute)
|
||||
}
|
||||
|
||||
return async (filter: string) => {
|
||||
const token = ++current
|
||||
const active = () => token === current
|
||||
|
||||
const value = cleanInput(filter)
|
||||
const directories = async (filter: string) => {
|
||||
const value = clean(filter)
|
||||
const scopedInput = scoped(value)
|
||||
if (!scopedInput) return [] as string[]
|
||||
|
||||
const raw = normalizeDriveRoot(value)
|
||||
const isPath = raw.startsWith("~") || !!rootOf(raw) || raw.includes("/")
|
||||
|
||||
const query = normalizeDriveRoot(scopedInput.path)
|
||||
|
||||
const find = () =>
|
||||
args.sdk.client.find
|
||||
sdk.client.find
|
||||
.files({ directory: scopedInput.directory, query, type: "directory", limit: 50 })
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => [])
|
||||
|
||||
if (!isPath) {
|
||||
const results = await find()
|
||||
if (!active()) return []
|
||||
return results.map((rel) => joinPath(scopedInput.directory, rel)).slice(0, 50)
|
||||
|
||||
return results.map((rel) => join(scopedInput.directory, rel)).slice(0, 50)
|
||||
}
|
||||
|
||||
const segments = query.replace(/^\/+/, "").split("/")
|
||||
@@ -200,20 +224,17 @@ function useDirectorySearch(args: {
|
||||
const branch = 4
|
||||
let paths = [scopedInput.directory]
|
||||
for (const part of head) {
|
||||
if (!active()) return []
|
||||
if (part === "..") {
|
||||
paths = paths.map(parentOf)
|
||||
continue
|
||||
}
|
||||
|
||||
const next = (await Promise.all(paths.map((p) => match(p, part, branch)))).flat()
|
||||
if (!active()) return []
|
||||
paths = Array.from(new Set(next)).slice(0, cap)
|
||||
if (paths.length === 0) return [] as string[]
|
||||
}
|
||||
|
||||
const out = (await Promise.all(paths.map((p) => match(p, tail, 50)))).flat()
|
||||
if (!active()) return []
|
||||
const deduped = Array.from(new Set(out))
|
||||
const base = raw.startsWith("~") ? trimTrailing(scopedInput.directory) : ""
|
||||
const expand = !raw.endsWith("/")
|
||||
@@ -228,47 +249,13 @@ function useDirectorySearch(args: {
|
||||
if (!target) return deduped.slice(0, 50)
|
||||
|
||||
const children = await match(target, "", 30)
|
||||
if (!active()) return []
|
||||
const items = Array.from(new Set([...deduped, ...children]))
|
||||
return (base ? Array.from(new Set([base, ...items])) : items).slice(0, 50)
|
||||
}
|
||||
}
|
||||
|
||||
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
const sync = useGlobalSync()
|
||||
const sdk = useGlobalSDK()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
const [filter, setFilter] = createSignal("")
|
||||
let list: ListRef | undefined
|
||||
|
||||
const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
|
||||
const [fallbackPath] = createResource(
|
||||
() => (missingBase() ? true : undefined),
|
||||
async () => {
|
||||
return sdk.client.path
|
||||
.get()
|
||||
.then((x) => x.data)
|
||||
.catch(() => undefined)
|
||||
},
|
||||
{ initialValue: undefined },
|
||||
)
|
||||
|
||||
const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
|
||||
const start = createMemo(
|
||||
() => sync.data.path.home || sync.data.path.directory || fallbackPath()?.home || fallbackPath()?.directory,
|
||||
)
|
||||
|
||||
const directories = useDirectorySearch({
|
||||
sdk,
|
||||
home,
|
||||
start,
|
||||
})
|
||||
|
||||
const items = async (value: string) => {
|
||||
const results = await directories(value)
|
||||
return results.map((absolute) => toRow(absolute, home()))
|
||||
return results.map(row)
|
||||
}
|
||||
|
||||
function resolve(absolute: string) {
|
||||
@@ -286,7 +273,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
key={(x) => x.absolute}
|
||||
filterKeys={["search"]}
|
||||
ref={(r) => (list = r)}
|
||||
onFilter={(value) => setFilter(cleanInput(value))}
|
||||
onFilter={(value) => setFilter(clean(value))}
|
||||
onKeyEvent={(e, item) => {
|
||||
if (e.key !== "Tab") return
|
||||
if (e.shiftKey) return
|
||||
@@ -295,7 +282,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const value = displayPath(item.absolute, filter(), home())
|
||||
const value = display(item.absolute, filter())
|
||||
list?.setFilter(value.endsWith("/") ? value : value + "/")
|
||||
}}
|
||||
onSelect={(path) => {
|
||||
@@ -304,7 +291,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
|
||||
}}
|
||||
>
|
||||
{(item) => {
|
||||
const path = displayPath(item.absolute, filter(), home())
|
||||
const path = display(item.absolute, filter())
|
||||
if (path === "~") {
|
||||
return (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
|
||||
@@ -36,223 +36,6 @@ type Entry = {
|
||||
|
||||
type DialogSelectFileMode = "all" | "files"
|
||||
|
||||
const ENTRY_LIMIT = 5
|
||||
const COMMON_COMMAND_IDS = [
|
||||
"session.new",
|
||||
"workspace.new",
|
||||
"session.previous",
|
||||
"session.next",
|
||||
"terminal.toggle",
|
||||
"review.toggle",
|
||||
] as const
|
||||
|
||||
const uniqueEntries = (items: Entry[]) => {
|
||||
const seen = new Set<string>()
|
||||
const out: Entry[] = []
|
||||
for (const item of items) {
|
||||
if (seen.has(item.id)) continue
|
||||
seen.add(item.id)
|
||||
out.push(item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const createCommandEntry = (option: CommandOption, category: string): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category,
|
||||
option,
|
||||
})
|
||||
|
||||
const createFileEntry = (path: string, category: string): Entry => ({
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category,
|
||||
path,
|
||||
})
|
||||
|
||||
const createSessionEntry = (
|
||||
input: {
|
||||
directory: string
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
},
|
||||
category: string,
|
||||
): Entry => ({
|
||||
id: `session:${input.directory}:${input.id}`,
|
||||
type: "session",
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
category,
|
||||
directory: input.directory,
|
||||
sessionID: input.id,
|
||||
archived: input.archived,
|
||||
updated: input.updated,
|
||||
})
|
||||
|
||||
function createCommandEntries(props: {
|
||||
filesOnly: () => boolean
|
||||
command: ReturnType<typeof useCommand>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const allowed = createMemo(() => {
|
||||
if (props.filesOnly()) return []
|
||||
return props.command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
)
|
||||
})
|
||||
|
||||
const list = createMemo(() => {
|
||||
const category = props.language.t("palette.group.commands")
|
||||
return allowed().map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
|
||||
const picks = createMemo(() => {
|
||||
const all = allowed()
|
||||
const order = new Map<string, number>(COMMON_COMMAND_IDS.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
const category = props.language.t("palette.group.commands")
|
||||
return sorted.map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
|
||||
return { allowed, list, picks }
|
||||
}
|
||||
|
||||
function createFileEntries(props: {
|
||||
file: ReturnType<typeof useFile>
|
||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const recent = createMemo(() => {
|
||||
const all = props.tabs().all()
|
||||
const active = props.tabs().active()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const category = props.language.t("palette.group.files")
|
||||
const items: Entry[] = []
|
||||
|
||||
for (const item of order) {
|
||||
const path = props.file.pathFromTab(item)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
items.push(createFileEntry(path, category))
|
||||
}
|
||||
|
||||
return items.slice(0, ENTRY_LIMIT)
|
||||
})
|
||||
|
||||
const root = createMemo(() => {
|
||||
const category = props.language.t("palette.group.files")
|
||||
const nodes = props.file.tree.children("")
|
||||
const paths = nodes
|
||||
.filter((node) => node.type === "file")
|
||||
.map((node) => node.path)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
return paths.slice(0, ENTRY_LIMIT).map((path) => createFileEntry(path, category))
|
||||
})
|
||||
|
||||
return { recent, root }
|
||||
}
|
||||
|
||||
function createSessionEntries(props: {
|
||||
workspaces: () => string[]
|
||||
label: (directory: string) => string
|
||||
globalSDK: ReturnType<typeof useGlobalSDK>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}) {
|
||||
const state: {
|
||||
token: number
|
||||
inflight: Promise<Entry[]> | undefined
|
||||
cached: Entry[] | undefined
|
||||
} = {
|
||||
token: 0,
|
||||
inflight: undefined,
|
||||
cached: undefined,
|
||||
}
|
||||
|
||||
const sessions = (text: string) => {
|
||||
const query = text.trim()
|
||||
if (!query) {
|
||||
state.token += 1
|
||||
state.inflight = undefined
|
||||
state.cached = undefined
|
||||
return [] as Entry[]
|
||||
}
|
||||
|
||||
if (state.cached) return state.cached
|
||||
if (state.inflight) return state.inflight
|
||||
|
||||
const current = state.token
|
||||
const dirs = props.workspaces()
|
||||
if (dirs.length === 0) return [] as Entry[]
|
||||
|
||||
state.inflight = Promise.all(
|
||||
dirs.map((directory) => {
|
||||
const description = props.label(directory)
|
||||
return props.globalSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
.then((x) =>
|
||||
(x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.title ?? props.language.t("command.session.new"),
|
||||
description,
|
||||
directory,
|
||||
archived: s.time?.archived,
|
||||
updated: s.time?.updated,
|
||||
})),
|
||||
)
|
||||
.catch(
|
||||
() =>
|
||||
[] as {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
directory: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}[],
|
||||
)
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (state.token !== current) return [] as Entry[]
|
||||
const seen = new Set<string>()
|
||||
const category = props.language.t("command.category.session")
|
||||
const next = results
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const key = `${item.directory}:${item.id}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.map((item) => createSessionEntry(item, category))
|
||||
state.cached = next
|
||||
return next
|
||||
})
|
||||
.catch(() => [] as Entry[])
|
||||
.finally(() => {
|
||||
state.inflight = undefined
|
||||
})
|
||||
|
||||
return state.inflight
|
||||
}
|
||||
|
||||
return { sessions }
|
||||
}
|
||||
|
||||
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
@@ -269,8 +52,40 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const [grouped, setGrouped] = createSignal(false)
|
||||
const commandEntries = createCommandEntries({ filesOnly, command, language })
|
||||
const fileEntries = createFileEntries({ file, tabs, language })
|
||||
const common = [
|
||||
"session.new",
|
||||
"workspace.new",
|
||||
"session.previous",
|
||||
"session.next",
|
||||
"terminal.toggle",
|
||||
"review.toggle",
|
||||
]
|
||||
const limit = 5
|
||||
|
||||
const allowed = createMemo(() => {
|
||||
if (filesOnly()) return []
|
||||
return command.options.filter(
|
||||
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
)
|
||||
})
|
||||
|
||||
const commandItem = (option: CommandOption): Entry => ({
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
title: option.title,
|
||||
description: option.description,
|
||||
keybind: option.keybind,
|
||||
category: language.t("palette.group.commands"),
|
||||
option,
|
||||
})
|
||||
|
||||
const fileItem = (path: string): Entry => ({
|
||||
id: "file:" + path,
|
||||
type: "file",
|
||||
title: path,
|
||||
category: language.t("palette.group.files"),
|
||||
path,
|
||||
})
|
||||
|
||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const project = createMemo(() => {
|
||||
@@ -301,7 +116,136 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
return `${kind} : ${name || path}`
|
||||
}
|
||||
|
||||
const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language })
|
||||
const sessionItem = (input: {
|
||||
directory: string
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}): Entry => ({
|
||||
id: `session:${input.directory}:${input.id}`,
|
||||
type: "session",
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
category: language.t("command.category.session"),
|
||||
directory: input.directory,
|
||||
sessionID: input.id,
|
||||
archived: input.archived,
|
||||
updated: input.updated,
|
||||
})
|
||||
|
||||
const list = createMemo(() => allowed().map(commandItem))
|
||||
|
||||
const picks = createMemo(() => {
|
||||
const all = allowed()
|
||||
const order = new Map(common.map((id, index) => [id, index]))
|
||||
const picked = all.filter((option) => order.has(option.id))
|
||||
const base = picked.length ? picked : all.slice(0, limit)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
return sorted.map(commandItem)
|
||||
})
|
||||
|
||||
const recent = createMemo(() => {
|
||||
const all = tabs().all()
|
||||
const active = tabs().active()
|
||||
const order = active ? [active, ...all.filter((item) => item !== active)] : all
|
||||
const seen = new Set<string>()
|
||||
const items: Entry[] = []
|
||||
|
||||
for (const item of order) {
|
||||
const path = file.pathFromTab(item)
|
||||
if (!path) continue
|
||||
if (seen.has(path)) continue
|
||||
seen.add(path)
|
||||
items.push(fileItem(path))
|
||||
}
|
||||
|
||||
return items.slice(0, limit)
|
||||
})
|
||||
|
||||
const root = createMemo(() => {
|
||||
const nodes = file.tree.children("")
|
||||
const paths = nodes
|
||||
.filter((node) => node.type === "file")
|
||||
.map((node) => node.path)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
return paths.slice(0, limit).map(fileItem)
|
||||
})
|
||||
|
||||
const unique = (items: Entry[]) => {
|
||||
const seen = new Set<string>()
|
||||
const out: Entry[] = []
|
||||
for (const item of items) {
|
||||
if (seen.has(item.id)) continue
|
||||
seen.add(item.id)
|
||||
out.push(item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const sessionToken = { value: 0 }
|
||||
let sessionInflight: Promise<Entry[]> | undefined
|
||||
let sessionAll: Entry[] | undefined
|
||||
|
||||
const sessions = (text: string) => {
|
||||
const query = text.trim()
|
||||
if (!query) {
|
||||
sessionToken.value += 1
|
||||
sessionInflight = undefined
|
||||
sessionAll = undefined
|
||||
return [] as Entry[]
|
||||
}
|
||||
|
||||
if (sessionAll) return sessionAll
|
||||
if (sessionInflight) return sessionInflight
|
||||
|
||||
const current = sessionToken.value
|
||||
const dirs = workspaces()
|
||||
if (dirs.length === 0) return [] as Entry[]
|
||||
|
||||
sessionInflight = Promise.all(
|
||||
dirs.map((directory) => {
|
||||
const description = label(directory)
|
||||
return globalSDK.client.session
|
||||
.list({ directory, roots: true })
|
||||
.then((x) =>
|
||||
(x.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.map((s) => ({
|
||||
id: s.id,
|
||||
title: s.title ?? language.t("command.session.new"),
|
||||
description,
|
||||
directory,
|
||||
archived: s.time?.archived,
|
||||
updated: s.time?.updated,
|
||||
})),
|
||||
)
|
||||
.catch(() => [] as { id: string; title: string; description: string; directory: string; archived?: number }[])
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (sessionToken.value !== current) return [] as Entry[]
|
||||
const seen = new Set<string>()
|
||||
const next = results
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const key = `${item.directory}:${item.id}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.map(sessionItem)
|
||||
sessionAll = next
|
||||
return next
|
||||
})
|
||||
.catch(() => [] as Entry[])
|
||||
.finally(() => {
|
||||
sessionInflight = undefined
|
||||
})
|
||||
|
||||
return sessionInflight
|
||||
}
|
||||
|
||||
const items = async (text: string) => {
|
||||
const query = text.trim()
|
||||
@@ -310,7 +254,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
if (!query && filesOnly()) {
|
||||
const loaded = file.tree.state("")?.loaded
|
||||
const pending = loaded ? Promise.resolve() : file.tree.list("")
|
||||
const next = uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
||||
const next = unique([...recent(), ...root()])
|
||||
|
||||
if (loaded || next.length > 0) {
|
||||
void pending
|
||||
@@ -318,21 +262,19 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
}
|
||||
|
||||
await pending
|
||||
return uniqueEntries([...fileEntries.recent(), ...fileEntries.root()])
|
||||
return unique([...recent(), ...root()])
|
||||
}
|
||||
|
||||
if (!query) return [...commandEntries.picks(), ...fileEntries.recent()]
|
||||
if (!query) return [...picks(), ...recent()]
|
||||
|
||||
if (filesOnly()) {
|
||||
const files = await file.searchFiles(query)
|
||||
const category = language.t("palette.group.files")
|
||||
return files.map((path) => createFileEntry(path, category))
|
||||
return files.map(fileItem)
|
||||
}
|
||||
|
||||
const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
|
||||
const category = language.t("palette.group.files")
|
||||
const entries = files.map((path) => createFileEntry(path, category))
|
||||
return [...commandEntries.list(), ...nextSessions, ...entries]
|
||||
const entries = files.map(fileItem)
|
||||
return [...list(), ...nextSessions, ...entries]
|
||||
}
|
||||
|
||||
const handleMove = (item: Entry | undefined) => {
|
||||
@@ -347,9 +289,9 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
tabs().open(value)
|
||||
file.load(path)
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
props.onOpenFile?.(path)
|
||||
tabs().setActive(value)
|
||||
}
|
||||
|
||||
const handleSelect = (item: Entry | undefined) => {
|
||||
|
||||
@@ -6,13 +6,6 @@ import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
const statusLabels = {
|
||||
connected: "mcp.status.connected",
|
||||
failed: "mcp.status.failed",
|
||||
needs_auth: "mcp.status.needs_auth",
|
||||
disabled: "mcp.status.disabled",
|
||||
} as const
|
||||
|
||||
export const DialogSelectMcp: Component = () => {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
@@ -28,19 +21,15 @@ export const DialogSelectMcp: Component = () => {
|
||||
const toggle = async (name: string) => {
|
||||
if (loading()) return
|
||||
setLoading(name)
|
||||
try {
|
||||
const status = sync.data.mcp[name]
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
} else {
|
||||
await sdk.client.mcp.connect({ name })
|
||||
}
|
||||
|
||||
const result = await sdk.client.mcp.status()
|
||||
if (result.data) sync.set("mcp", result.data)
|
||||
} finally {
|
||||
setLoading(null)
|
||||
const status = sync.data.mcp[name]
|
||||
if (status?.status === "connected") {
|
||||
await sdk.client.mcp.disconnect({ name })
|
||||
} else {
|
||||
await sdk.client.mcp.connect({ name })
|
||||
}
|
||||
const result = await sdk.client.mcp.status()
|
||||
if (result.data) sync.set("mcp", result.data)
|
||||
setLoading(null)
|
||||
}
|
||||
|
||||
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||
@@ -65,11 +54,6 @@ export const DialogSelectMcp: Component = () => {
|
||||
{(i) => {
|
||||
const mcpStatus = () => sync.data.mcp[i.name]
|
||||
const status = () => mcpStatus()?.status
|
||||
const statusLabel = () => {
|
||||
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
|
||||
if (!key) return
|
||||
return language.t(key)
|
||||
}
|
||||
const error = () => {
|
||||
const s = mcpStatus()
|
||||
return s?.status === "failed" ? s.error : undefined
|
||||
@@ -80,8 +64,17 @@ export const DialogSelectMcp: Component = () => {
|
||||
<div class="flex flex-col gap-0.5 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={statusLabel()}>
|
||||
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
||||
<Show when={status() === "connected"}>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.connected")}</span>
|
||||
</Show>
|
||||
<Show when={status() === "failed"}>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.failed")}</span>
|
||||
</Show>
|
||||
<Show when={status() === "needs_auth"}>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.needs_auth")}</span>
|
||||
</Show>
|
||||
<Show when={status() === "disabled"}>
|
||||
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.disabled")}</span>
|
||||
</Show>
|
||||
<Show when={loading() === i.name}>
|
||||
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { type Component, Show } from "solid-js"
|
||||
import { type Component, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
@@ -21,17 +21,24 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
const language = useLanguage()
|
||||
|
||||
let listRef: ListRef | undefined
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") return
|
||||
listRef?.onKeyDown(e)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKey)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKey)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={language.t("dialog.model.select.title")}
|
||||
class="overflow-y-auto [&_[data-slot=dialog-body]]:overflow-visible [&_[data-slot=dialog-body]]:flex-none"
|
||||
>
|
||||
<div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
|
||||
<div class="flex flex-col gap-3 px-2.5">
|
||||
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
|
||||
<List
|
||||
class="[&_[data-slot=list-scroll]]:overflow-visible"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Popover as Kobalte } from "@kobalte/core/popover"
|
||||
import { Component, ComponentProps, createMemo, JSX, Show, ValidComponent } from "solid-js"
|
||||
import { Component, ComponentProps, createEffect, createMemo, JSX, onCleanup, Show, ValidComponent } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
@@ -15,9 +15,6 @@ import { DialogManageModels } from "./dialog-manage-models"
|
||||
import { ModelTooltip } from "./model-tooltip"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
const isFree = (provider: string, cost: { input: number } | undefined) =>
|
||||
provider === "opencode" && (!cost || cost.input === 0)
|
||||
|
||||
const ModelList: Component<{
|
||||
provider?: string
|
||||
class?: string
|
||||
@@ -57,7 +54,13 @@ const ModelList: Component<{
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={12}
|
||||
value={<ModelTooltip model={item} latest={item.latest} free={isFree(item.provider.id, item.cost)} />}
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
latest={item.latest}
|
||||
free={item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{node}
|
||||
</Tooltip>
|
||||
@@ -72,7 +75,7 @@ const ModelList: Component<{
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2 text-13-regular">
|
||||
<span class="truncate">{i.name}</span>
|
||||
<Show when={isFree(i.provider.id, i.cost)}>
|
||||
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
||||
<Tag>{language.t("model.tag.free")}</Tag>
|
||||
</Show>
|
||||
<Show when={i.latest}>
|
||||
@@ -95,9 +98,13 @@ export function ModelSelectorPopover(props: {
|
||||
const [store, setStore] = createStore<{
|
||||
open: boolean
|
||||
dismiss: "escape" | "outside" | null
|
||||
trigger?: HTMLElement
|
||||
content?: HTMLElement
|
||||
}>({
|
||||
open: false,
|
||||
dismiss: null,
|
||||
trigger: undefined,
|
||||
content: undefined,
|
||||
})
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -112,6 +119,54 @@ export function ModelSelectorPopover(props: {
|
||||
}
|
||||
const language = useLanguage()
|
||||
|
||||
createEffect(() => {
|
||||
if (!store.open) return
|
||||
|
||||
const inside = (node: Node | null | undefined) => {
|
||||
if (!node) return false
|
||||
const el = store.content
|
||||
if (el && el.contains(node)) return true
|
||||
const anchor = store.trigger
|
||||
if (anchor && anchor.contains(node)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Escape") return
|
||||
setStore("dismiss", "escape")
|
||||
setStore("open", false)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
const target = event.target
|
||||
if (!(target instanceof Node)) return
|
||||
if (inside(target)) return
|
||||
setStore("dismiss", "outside")
|
||||
setStore("open", false)
|
||||
}
|
||||
|
||||
const onFocusIn = (event: FocusEvent) => {
|
||||
if (!store.content) return
|
||||
const target = event.target
|
||||
if (!(target instanceof Node)) return
|
||||
if (inside(target)) return
|
||||
setStore("dismiss", "outside")
|
||||
setStore("open", false)
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown, true)
|
||||
window.addEventListener("pointerdown", onPointerDown, true)
|
||||
window.addEventListener("focusin", onFocusIn, true)
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("keydown", onKeyDown, true)
|
||||
window.removeEventListener("pointerdown", onPointerDown, true)
|
||||
window.removeEventListener("focusin", onFocusIn, true)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Kobalte
|
||||
open={store.open}
|
||||
@@ -121,13 +176,14 @@ export function ModelSelectorPopover(props: {
|
||||
}}
|
||||
modal={false}
|
||||
placement="top-start"
|
||||
gutter={4}
|
||||
gutter={8}
|
||||
>
|
||||
<Kobalte.Trigger as={props.triggerAs ?? "div"} {...props.triggerProps}>
|
||||
<Kobalte.Trigger ref={(el) => setStore("trigger", el)} as={props.triggerAs ?? "div"} {...props.triggerProps}>
|
||||
{props.children}
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content
|
||||
ref={(el) => setStore("content", el)}
|
||||
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
|
||||
onEscapeKeyDown={(event) => {
|
||||
setStore("dismiss", "escape")
|
||||
|
||||
@@ -24,12 +24,6 @@ export const DialogSelectProvider: Component = () => {
|
||||
|
||||
const popularGroup = () => language.t("dialog.provider.group.popular")
|
||||
const otherGroup = () => language.t("dialog.provider.group.other")
|
||||
const customLabel = () => language.t("settings.providers.tag.custom")
|
||||
const note = (id: string) => {
|
||||
if (id === "anthropic") return language.t("dialog.provider.anthropic.note")
|
||||
if (id === "openai") return language.t("dialog.provider.openai.note")
|
||||
if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note")
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("command.provider.connect")} transition>
|
||||
@@ -40,7 +34,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
key={(x) => x?.id}
|
||||
items={() => {
|
||||
language.locale()
|
||||
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all()]
|
||||
return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()]
|
||||
}}
|
||||
filterKeys={["id", "name"]}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
|
||||
@@ -76,7 +70,15 @@ export const DialogSelectProvider: Component = () => {
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
|
||||
</Show>
|
||||
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
|
||||
</Show>
|
||||
<Show when={i.id === "openai"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
|
||||
</Show>
|
||||
<Show when={i.id.startsWith("github-copilot")}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
|
||||
@@ -38,64 +38,6 @@ interface EditRowProps {
|
||||
onBlur: () => void
|
||||
}
|
||||
|
||||
function showRequestError(language: ReturnType<typeof useLanguage>, err: unknown) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
function useDefaultServer(platform: ReturnType<typeof usePlatform>, language: ReturnType<typeof useLanguage>) {
|
||||
const [defaultUrl, defaultUrlActions] = createResource(
|
||||
async () => {
|
||||
try {
|
||||
const url = await platform.getDefaultServerUrl?.()
|
||||
if (!url) return null
|
||||
return normalizeServerUrl(url) ?? null
|
||||
} catch (err) {
|
||||
showRequestError(language, err)
|
||||
return null
|
||||
}
|
||||
},
|
||||
{ initialValue: null },
|
||||
)
|
||||
|
||||
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
|
||||
const setDefault = async (url: string | null) => {
|
||||
try {
|
||||
await platform.setDefaultServerUrl?.(url)
|
||||
defaultUrlActions.mutate(url)
|
||||
} catch (err) {
|
||||
showRequestError(language, err)
|
||||
}
|
||||
}
|
||||
|
||||
return { defaultUrl, canDefault, setDefault }
|
||||
}
|
||||
|
||||
function useServerPreview(fetcher: typeof fetch) {
|
||||
const looksComplete = (value: string) => {
|
||||
const normalized = normalizeServerUrl(value)
|
||||
if (!normalized) return false
|
||||
const host = normalized.replace(/^https?:\/\//, "").split("/")[0]
|
||||
if (!host) return false
|
||||
if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true
|
||||
return host.includes(".") || host.includes(":")
|
||||
}
|
||||
|
||||
const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => {
|
||||
setStatus(undefined)
|
||||
if (!looksComplete(value)) return
|
||||
const normalized = normalizeServerUrl(value)
|
||||
if (!normalized) return
|
||||
const result = await checkServerHealth(normalized, fetcher)
|
||||
setStatus(result.healthy)
|
||||
}
|
||||
|
||||
return { previewStatus }
|
||||
}
|
||||
|
||||
function AddRow(props: AddRowProps) {
|
||||
return (
|
||||
<div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
|
||||
@@ -173,10 +115,6 @@ export function DialogSelectServer() {
|
||||
const platform = usePlatform()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const language = useLanguage()
|
||||
const fetcher = platform.fetch ?? globalThis.fetch
|
||||
const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language)
|
||||
const { previewStatus } = useServerPreview(fetcher)
|
||||
let listRoot: HTMLDivElement | undefined
|
||||
const [store, setStore] = createStore({
|
||||
status: {} as Record<string, ServerHealth | undefined>,
|
||||
addServer: {
|
||||
@@ -194,6 +132,43 @@ export function DialogSelectServer() {
|
||||
status: undefined as boolean | undefined,
|
||||
},
|
||||
})
|
||||
const [defaultUrl, defaultUrlActions] = createResource(
|
||||
async () => {
|
||||
try {
|
||||
const url = await platform.getDefaultServerUrl?.()
|
||||
if (!url) return null
|
||||
return normalizeServerUrl(url) ?? null
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return null
|
||||
}
|
||||
},
|
||||
{ initialValue: null },
|
||||
)
|
||||
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
|
||||
const fetcher = platform.fetch ?? globalThis.fetch
|
||||
|
||||
const looksComplete = (value: string) => {
|
||||
const normalized = normalizeServerUrl(value)
|
||||
if (!normalized) return false
|
||||
const host = normalized.replace(/^https?:\/\//, "").split("/")[0]
|
||||
if (!host) return false
|
||||
if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true
|
||||
return host.includes(".") || host.includes(":")
|
||||
}
|
||||
|
||||
const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => {
|
||||
setStatus(undefined)
|
||||
if (!looksComplete(value)) return
|
||||
const normalized = normalizeServerUrl(value)
|
||||
if (!normalized) return
|
||||
const result = await checkServerHealth(normalized, fetcher)
|
||||
setStatus(result.healthy)
|
||||
}
|
||||
|
||||
const resetAdd = () => {
|
||||
setStore("addServer", {
|
||||
@@ -288,7 +263,7 @@ export function DialogSelectServer() {
|
||||
}
|
||||
|
||||
const scrollListToBottom = () => {
|
||||
const scroll = listRoot?.querySelector<HTMLDivElement>('[data-slot="list-scroll"]')
|
||||
const scroll = document.querySelector<HTMLDivElement>('[data-component="list"] [data-slot="list-scroll"]')
|
||||
if (!scroll) return
|
||||
requestAnimationFrame(() => {
|
||||
scroll.scrollTop = scroll.scrollHeight
|
||||
@@ -388,134 +363,158 @@ export function DialogSelectServer() {
|
||||
return (
|
||||
<Dialog title={language.t("dialog.server.title")}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div ref={(el) => (listRoot = el)}>
|
||||
<List
|
||||
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x}
|
||||
onSelect={(x) => {
|
||||
if (x) select(x)
|
||||
}}
|
||||
onFilter={(value) => {
|
||||
if (value && store.addServer.showForm && !store.addServer.adding) {
|
||||
resetAdd()
|
||||
}
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0"
|
||||
add={
|
||||
store.addServer.showForm
|
||||
? {
|
||||
render: () => (
|
||||
<AddRow
|
||||
value={store.addServer.url}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
adding={store.addServer.adding}
|
||||
error={store.addServer.error}
|
||||
status={store.addServer.status}
|
||||
onChange={handleAddChange}
|
||||
onKeyDown={handleAddKey}
|
||||
onBlur={blurAdd}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
<List
|
||||
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x}
|
||||
onSelect={(x) => {
|
||||
if (x) select(x)
|
||||
}}
|
||||
onFilter={(value) => {
|
||||
if (value && store.addServer.showForm && !store.addServer.adding) {
|
||||
resetAdd()
|
||||
}
|
||||
>
|
||||
{(i) => {
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
||||
<Show
|
||||
when={store.editServer.id !== i}
|
||||
fallback={
|
||||
<EditRow
|
||||
value={store.editServer.value}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={store.editServer.busy}
|
||||
error={store.editServer.error}
|
||||
status={store.editServer.status}
|
||||
onChange={handleEditChange}
|
||||
onKeyDown={(event) => handleEditKey(event, i)}
|
||||
onBlur={() => handleEdit(i, store.editServer.value)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ServerRow
|
||||
url={i}
|
||||
status={store.status[i]}
|
||||
dimmed={store.status[i]?.healthy === false}
|
||||
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultUrl() === i}>
|
||||
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0"
|
||||
add={
|
||||
store.addServer.showForm
|
||||
? {
|
||||
render: () => (
|
||||
<AddRow
|
||||
value={store.addServer.url}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
adding={store.addServer.adding}
|
||||
error={store.addServer.error}
|
||||
status={store.addServer.status}
|
||||
onChange={handleAddChange}
|
||||
onKeyDown={handleAddKey}
|
||||
onBlur={blurAdd}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={store.editServer.id !== i}>
|
||||
<div class="flex items-center justify-center gap-5 pl-4">
|
||||
<Show when={current() === i}>
|
||||
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{(i) => {
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 group/item">
|
||||
<Show
|
||||
when={store.editServer.id !== i}
|
||||
fallback={
|
||||
<EditRow
|
||||
value={store.editServer.value}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
busy={store.editServer.busy}
|
||||
error={store.editServer.error}
|
||||
status={store.editServer.status}
|
||||
onChange={handleEditChange}
|
||||
onKeyDown={(event) => handleEditKey(event, i)}
|
||||
onBlur={() => handleEdit(i, store.editServer.value)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ServerRow
|
||||
url={i}
|
||||
status={store.status[i]}
|
||||
dimmed={store.status[i]?.healthy === false}
|
||||
class="flex items-center gap-3 px-4 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultUrl() === i}>
|
||||
<span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={store.editServer.id !== i}>
|
||||
<div class="flex items-center justify-center gap-5 pl-4">
|
||||
<Show when={current() === i}>
|
||||
<p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
|
||||
</Show>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setStore("editServer", {
|
||||
id: i,
|
||||
value: i,
|
||||
error: "",
|
||||
status: store.status[i]?.healthy,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultUrl() !== i}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setStore("editServer", {
|
||||
id: i,
|
||||
value: i,
|
||||
error: "",
|
||||
status: store.status[i]?.healthy,
|
||||
})
|
||||
onSelect={async () => {
|
||||
try {
|
||||
await platform.setDefaultServerUrl?.(i)
|
||||
defaultUrlActions.mutate(i)
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultUrl() !== i}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(i)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultUrl() === i}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultUrl() === i}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(i)}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
onSelect={async () => {
|
||||
try {
|
||||
await platform.setDefaultServerUrl?.(null)
|
||||
defaultUrlActions.mutate(null)
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</div>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(i)}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
|
||||
<div class="px-5 pb-5">
|
||||
<Button
|
||||
|
||||
@@ -67,6 +67,15 @@ export const DialogSettings: Component = () => {
|
||||
<Tabs.Content value="models" class="no-scrollbar">
|
||||
<SettingsModels />
|
||||
</Tabs.Content>
|
||||
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
|
||||
{/* <SettingsAgents /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="commands" class="no-scrollbar"> */}
|
||||
{/* <SettingsCommands /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
{/* <Tabs.Content value="mcp" class="no-scrollbar"> */}
|
||||
{/* <SettingsMcp /> */}
|
||||
{/* </Tabs.Content> */}
|
||||
</Tabs>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -15,14 +15,11 @@ import {
|
||||
Switch,
|
||||
untrack,
|
||||
type ComponentProps,
|
||||
type JSXElement,
|
||||
type ParentProps,
|
||||
} from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import type { FileNode } from "@opencode-ai/sdk/v2"
|
||||
|
||||
const MAX_DEPTH = 128
|
||||
|
||||
function pathToFileUrl(filepath: string): string {
|
||||
return `file://${encodeFilePath(filepath)}`
|
||||
}
|
||||
@@ -62,189 +59,6 @@ export function dirsToExpand(input: {
|
||||
return [...input.filter.dirs].filter((dir) => !input.expanded(dir))
|
||||
}
|
||||
|
||||
const kindLabel = (kind: Kind) => {
|
||||
if (kind === "add") return "A"
|
||||
if (kind === "del") return "D"
|
||||
return "M"
|
||||
}
|
||||
|
||||
const kindTextColor = (kind: Kind) => {
|
||||
if (kind === "add") return "color: var(--icon-diff-add-base)"
|
||||
if (kind === "del") return "color: var(--icon-diff-delete-base)"
|
||||
return "color: var(--icon-diff-modified-base)"
|
||||
}
|
||||
|
||||
const kindDotColor = (kind: Kind) => {
|
||||
if (kind === "add") return "background-color: var(--icon-diff-add-base)"
|
||||
if (kind === "del") return "background-color: var(--icon-diff-delete-base)"
|
||||
return "background-color: var(--icon-diff-modified-base)"
|
||||
}
|
||||
|
||||
const visibleKind = (node: FileNode, kinds?: ReadonlyMap<string, Kind>, marks?: Set<string>) => {
|
||||
const kind = kinds?.get(node.path)
|
||||
if (!kind) return
|
||||
if (!marks?.has(node.path)) return
|
||||
return kind
|
||||
}
|
||||
|
||||
const buildDragImage = (target: HTMLElement) => {
|
||||
const icon = target.querySelector('[data-component="file-icon"]') ?? target.querySelector("svg")
|
||||
const text = target.querySelector("span")
|
||||
if (!icon || !text) return
|
||||
|
||||
const image = document.createElement("div")
|
||||
image.className =
|
||||
"flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong"
|
||||
image.style.position = "absolute"
|
||||
image.style.top = "-1000px"
|
||||
image.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML
|
||||
return image
|
||||
}
|
||||
|
||||
const withFileDragImage = (event: DragEvent) => {
|
||||
const image = buildDragImage(event.currentTarget as HTMLElement)
|
||||
if (!image) return
|
||||
document.body.appendChild(image)
|
||||
event.dataTransfer?.setDragImage(image, 0, 12)
|
||||
setTimeout(() => document.body.removeChild(image), 0)
|
||||
}
|
||||
|
||||
const FileTreeNode = (
|
||||
p: ParentProps &
|
||||
ComponentProps<"div"> &
|
||||
ComponentProps<"button"> & {
|
||||
node: FileNode
|
||||
level: number
|
||||
active?: string
|
||||
nodeClass?: string
|
||||
draggable: boolean
|
||||
kinds?: ReadonlyMap<string, Kind>
|
||||
marks?: Set<string>
|
||||
as?: "div" | "button"
|
||||
},
|
||||
) => {
|
||||
const [local, rest] = splitProps(p, [
|
||||
"node",
|
||||
"level",
|
||||
"active",
|
||||
"nodeClass",
|
||||
"draggable",
|
||||
"kinds",
|
||||
"marks",
|
||||
"as",
|
||||
"children",
|
||||
"class",
|
||||
"classList",
|
||||
])
|
||||
const kind = () => visibleKind(local.node, local.kinds, local.marks)
|
||||
const active = () => !!kind() && !local.node.ignored
|
||||
const color = () => {
|
||||
const value = kind()
|
||||
if (!value) return
|
||||
return kindTextColor(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
component={local.as ?? "div"}
|
||||
classList={{
|
||||
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
|
||||
"bg-surface-base-active": local.node.path === local.active,
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
[local.nodeClass ?? ""]: !!local.nodeClass,
|
||||
}}
|
||||
style={`padding-left: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
|
||||
draggable={local.draggable}
|
||||
onDragStart={(event: DragEvent) => {
|
||||
if (!local.draggable) return
|
||||
event.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||
event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"
|
||||
withFileDragImage(event)
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<span
|
||||
classList={{
|
||||
"flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
|
||||
"text-text-weaker": local.node.ignored,
|
||||
"text-text-weak": !local.node.ignored && !active(),
|
||||
}}
|
||||
style={active() ? color() : undefined}
|
||||
>
|
||||
{local.node.name}
|
||||
</span>
|
||||
{(() => {
|
||||
const value = kind()
|
||||
if (!value) return null
|
||||
if (local.node.type === "file") {
|
||||
return (
|
||||
<span class="shrink-0 w-4 text-center text-12-medium" style={kindTextColor(value)}>
|
||||
{kindLabel(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={kindDotColor(value)} />
|
||||
})()}
|
||||
</Dynamic>
|
||||
)
|
||||
}
|
||||
|
||||
const FileTreeNodeTooltip = (props: { enabled: boolean; node: FileNode; kind?: Kind; children: JSXElement }) => {
|
||||
if (!props.enabled) return props.children
|
||||
|
||||
const parts = props.node.path.split("/")
|
||||
const leaf = parts[parts.length - 1] ?? props.node.path
|
||||
const head = parts.slice(0, -1).join("/")
|
||||
const prefix = head ? `${head}/` : ""
|
||||
const label =
|
||||
props.kind === "add"
|
||||
? "Additions"
|
||||
: props.kind === "del"
|
||||
? "Deletions"
|
||||
: props.kind === "mix"
|
||||
? "Modifications"
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
openDelay={2000}
|
||||
placement="bottom-start"
|
||||
class="w-full"
|
||||
contentStyle={{ "max-width": "480px", width: "fit-content" }}
|
||||
value={
|
||||
<div class="flex items-center min-w-0 whitespace-nowrap text-12-regular">
|
||||
<span
|
||||
class="min-w-0 truncate text-text-invert-base"
|
||||
style={{ direction: "rtl", "unicode-bidi": "plaintext" }}
|
||||
>
|
||||
{prefix}
|
||||
</span>
|
||||
<span class="shrink-0 text-text-invert-strong">{leaf}</span>
|
||||
<Show when={label}>
|
||||
{(text) => (
|
||||
<>
|
||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
||||
<span class="shrink-0 text-text-invert-strong">{text()}</span>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.node.type === "directory" && props.node.ignored}>
|
||||
<>
|
||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
||||
<span class="shrink-0 text-text-invert-strong">Ignored</span>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FileTree(props: {
|
||||
path: string
|
||||
class?: string
|
||||
@@ -262,20 +76,12 @@ export default function FileTree(props: {
|
||||
_marks?: Set<string>
|
||||
_deeps?: Map<string, number>
|
||||
_kinds?: ReadonlyMap<string, Kind>
|
||||
_chain?: readonly string[]
|
||||
}) {
|
||||
const file = useFile()
|
||||
const level = props.level ?? 0
|
||||
const draggable = () => props.draggable ?? true
|
||||
const tooltip = () => props.tooltip ?? true
|
||||
|
||||
const key = (p: string) =>
|
||||
file
|
||||
.normalize(p)
|
||||
.replace(/[\\/]+$/, "")
|
||||
.replaceAll("\\", "/")
|
||||
const chain = props._chain ? [...props._chain, key(props.path)] : [key(props.path)]
|
||||
|
||||
const filter = createMemo(() => {
|
||||
if (props._filter) return props._filter
|
||||
|
||||
@@ -317,45 +123,23 @@ export default function FileTree(props: {
|
||||
|
||||
const out = new Map<string, number>()
|
||||
|
||||
const root = props.path
|
||||
if (!(file.tree.state(root)?.expanded ?? false)) return out
|
||||
const visit = (dir: string, lvl: number): number => {
|
||||
const expanded = file.tree.state(dir)?.expanded ?? false
|
||||
if (!expanded) return -1
|
||||
|
||||
const seen = new Set<string>()
|
||||
const stack: { dir: string; lvl: number; i: number; kids: string[]; max: number }[] = []
|
||||
const nodes = file.tree.children(dir)
|
||||
const max = nodes.reduce((max, node) => {
|
||||
if (node.type !== "directory") return max
|
||||
const open = file.tree.state(node.path)?.expanded ?? false
|
||||
if (!open) return max
|
||||
return Math.max(max, visit(node.path, lvl + 1))
|
||||
}, lvl)
|
||||
|
||||
const push = (dir: string, lvl: number) => {
|
||||
const id = key(dir)
|
||||
if (seen.has(id)) return
|
||||
seen.add(id)
|
||||
|
||||
const kids = file.tree
|
||||
.children(dir)
|
||||
.filter((node) => node.type === "directory" && (file.tree.state(node.path)?.expanded ?? false))
|
||||
.map((node) => node.path)
|
||||
|
||||
stack.push({ dir, lvl, i: 0, kids, max: lvl })
|
||||
}
|
||||
|
||||
push(root, level - 1)
|
||||
|
||||
while (stack.length > 0) {
|
||||
const top = stack[stack.length - 1]!
|
||||
|
||||
if (top.i < top.kids.length) {
|
||||
const next = top.kids[top.i]!
|
||||
top.i++
|
||||
push(next, top.lvl + 1)
|
||||
continue
|
||||
}
|
||||
|
||||
out.set(top.dir, top.max)
|
||||
stack.pop()
|
||||
|
||||
const parent = stack[stack.length - 1]
|
||||
if (!parent) continue
|
||||
parent.max = Math.max(parent.max, top.max)
|
||||
out.set(dir, max)
|
||||
return max
|
||||
}
|
||||
|
||||
visit(props.path, level - 1)
|
||||
return out
|
||||
})
|
||||
|
||||
@@ -446,14 +230,178 @@ export default function FileTree(props: {
|
||||
return out
|
||||
})
|
||||
|
||||
const Node = (
|
||||
p: ParentProps &
|
||||
ComponentProps<"div"> &
|
||||
ComponentProps<"button"> & {
|
||||
node: FileNode
|
||||
as?: "div" | "button"
|
||||
},
|
||||
) => {
|
||||
const [local, rest] = splitProps(p, ["node", "as", "children", "class", "classList"])
|
||||
return (
|
||||
<Dynamic
|
||||
component={local.as ?? "div"}
|
||||
classList={{
|
||||
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
|
||||
"bg-surface-base-active": local.node.path === props.active,
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
[props.nodeClass ?? ""]: !!props.nodeClass,
|
||||
}}
|
||||
style={`padding-left: ${Math.max(0, 8 + level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
|
||||
draggable={draggable()}
|
||||
onDragStart={(e: DragEvent) => {
|
||||
if (!draggable()) return
|
||||
e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
|
||||
e.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
|
||||
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
|
||||
|
||||
const dragImage = document.createElement("div")
|
||||
dragImage.className =
|
||||
"flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong"
|
||||
dragImage.style.position = "absolute"
|
||||
dragImage.style.top = "-1000px"
|
||||
|
||||
const icon =
|
||||
(e.currentTarget as HTMLElement).querySelector('[data-component="file-icon"]') ??
|
||||
(e.currentTarget as HTMLElement).querySelector("svg")
|
||||
const text = (e.currentTarget as HTMLElement).querySelector("span")
|
||||
if (icon && text) {
|
||||
dragImage.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML
|
||||
}
|
||||
|
||||
document.body.appendChild(dragImage)
|
||||
e.dataTransfer?.setDragImage(dragImage, 0, 12)
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0)
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
{(() => {
|
||||
const kind = kinds()?.get(local.node.path)
|
||||
const marked = marks()?.has(local.node.path) ?? false
|
||||
const active = !!kind && marked && !local.node.ignored
|
||||
const color =
|
||||
kind === "add"
|
||||
? "color: var(--icon-diff-add-base)"
|
||||
: kind === "del"
|
||||
? "color: var(--icon-diff-delete-base)"
|
||||
: kind === "mix"
|
||||
? "color: var(--icon-warning-active)"
|
||||
: undefined
|
||||
return (
|
||||
<span
|
||||
classList={{
|
||||
"flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
|
||||
"text-text-weaker": local.node.ignored,
|
||||
"text-text-weak": !local.node.ignored && !active,
|
||||
}}
|
||||
style={active ? color : undefined}
|
||||
>
|
||||
{local.node.name}
|
||||
</span>
|
||||
)
|
||||
})()}
|
||||
{(() => {
|
||||
const kind = kinds()?.get(local.node.path)
|
||||
if (!kind) return null
|
||||
if (!marks()?.has(local.node.path)) return null
|
||||
|
||||
if (local.node.type === "file") {
|
||||
const text = kind === "add" ? "A" : kind === "del" ? "D" : "M"
|
||||
const color =
|
||||
kind === "add"
|
||||
? "color: var(--icon-diff-add-base)"
|
||||
: kind === "del"
|
||||
? "color: var(--icon-diff-delete-base)"
|
||||
: "color: var(--icon-warning-active)"
|
||||
|
||||
return (
|
||||
<span class="shrink-0 w-4 text-center text-12-medium" style={color}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (local.node.type === "directory") {
|
||||
const color =
|
||||
kind === "add"
|
||||
? "background-color: var(--icon-diff-add-base)"
|
||||
: kind === "del"
|
||||
? "background-color: var(--icon-diff-delete-base)"
|
||||
: "background-color: var(--icon-warning-active)"
|
||||
|
||||
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} />
|
||||
}
|
||||
|
||||
return null
|
||||
})()}
|
||||
</Dynamic>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||
<div class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||
<For each={nodes()}>
|
||||
{(node) => {
|
||||
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
||||
const deep = () => deeps().get(node.path) ?? -1
|
||||
const kind = () => visibleKind(node, kinds(), marks())
|
||||
const active = () => !!kind() && !node.ignored
|
||||
const Wrapper = (p: ParentProps) => {
|
||||
if (!tooltip()) return p.children
|
||||
|
||||
const parts = node.path.split("/")
|
||||
const leaf = parts[parts.length - 1] ?? node.path
|
||||
const head = parts.slice(0, -1).join("/")
|
||||
const prefix = head ? `${head}/` : ""
|
||||
|
||||
const kind = () => kinds()?.get(node.path)
|
||||
const label = () => {
|
||||
const k = kind()
|
||||
if (!k) return
|
||||
if (k === "add") return "Additions"
|
||||
if (k === "del") return "Deletions"
|
||||
return "Modifications"
|
||||
}
|
||||
|
||||
const ignored = () => node.type === "directory" && node.ignored
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
openDelay={2000}
|
||||
placement="bottom-start"
|
||||
class="w-full"
|
||||
contentStyle={{ "max-width": "480px", width: "fit-content" }}
|
||||
value={
|
||||
<div class="flex items-center min-w-0 whitespace-nowrap text-12-regular">
|
||||
<span
|
||||
class="min-w-0 truncate text-text-invert-base"
|
||||
style={{ direction: "rtl", "unicode-bidi": "plaintext" }}
|
||||
>
|
||||
{prefix}
|
||||
</span>
|
||||
<span class="shrink-0 text-text-invert-strong">{leaf}</span>
|
||||
<Show when={label()}>
|
||||
{(t: () => string) => (
|
||||
<>
|
||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
||||
<span class="shrink-0 text-text-invert-strong">{t()}</span>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={ignored()}>
|
||||
<>
|
||||
<span class="mx-1 font-bold text-text-invert-strong">•</span>
|
||||
<span class="shrink-0 text-text-invert-strong">Ignored</span>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{p.children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
@@ -467,21 +415,13 @@ export default function FileTree(props: {
|
||||
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
|
||||
>
|
||||
<Collapsible.Trigger>
|
||||
<FileTreeNodeTooltip enabled={tooltip()} node={node} kind={kind()}>
|
||||
<FileTreeNode
|
||||
node={node}
|
||||
level={level}
|
||||
active={props.active}
|
||||
nodeClass={props.nodeClass}
|
||||
draggable={draggable()}
|
||||
kinds={kinds()}
|
||||
marks={marks()}
|
||||
>
|
||||
<Wrapper>
|
||||
<Node node={node}>
|
||||
<div class="size-4 flex items-center justify-center text-icon-weak">
|
||||
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
|
||||
</div>
|
||||
</FileTreeNode>
|
||||
</FileTreeNodeTooltip>
|
||||
</Node>
|
||||
</Wrapper>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content class="relative pt-0.5">
|
||||
<div
|
||||
@@ -492,71 +432,31 @@ export default function FileTree(props: {
|
||||
}}
|
||||
style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`}
|
||||
/>
|
||||
<Show
|
||||
when={level < MAX_DEPTH && !chain.includes(key(node.path))}
|
||||
fallback={<div class="px-2 py-1 text-12-regular text-text-weak">...</div>}
|
||||
>
|
||||
<FileTree
|
||||
path={node.path}
|
||||
level={level + 1}
|
||||
allowed={props.allowed}
|
||||
modified={props.modified}
|
||||
kinds={props.kinds}
|
||||
active={props.active}
|
||||
draggable={props.draggable}
|
||||
tooltip={props.tooltip}
|
||||
onFileClick={props.onFileClick}
|
||||
_filter={filter()}
|
||||
_marks={marks()}
|
||||
_deeps={deeps()}
|
||||
_kinds={kinds()}
|
||||
_chain={chain}
|
||||
/>
|
||||
</Show>
|
||||
<FileTree
|
||||
path={node.path}
|
||||
level={level + 1}
|
||||
allowed={props.allowed}
|
||||
modified={props.modified}
|
||||
kinds={props.kinds}
|
||||
active={props.active}
|
||||
draggable={props.draggable}
|
||||
tooltip={props.tooltip}
|
||||
onFileClick={props.onFileClick}
|
||||
_filter={filter()}
|
||||
_marks={marks()}
|
||||
_deeps={deeps()}
|
||||
_kinds={kinds()}
|
||||
/>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
</Match>
|
||||
<Match when={node.type === "file"}>
|
||||
<FileTreeNodeTooltip enabled={tooltip()} node={node} kind={kind()}>
|
||||
<FileTreeNode
|
||||
node={node}
|
||||
level={level}
|
||||
active={props.active}
|
||||
nodeClass={props.nodeClass}
|
||||
draggable={draggable()}
|
||||
kinds={kinds()}
|
||||
marks={marks()}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => props.onFileClick?.(node)}
|
||||
>
|
||||
<Wrapper>
|
||||
<Node node={node} as="button" type="button" onClick={() => props.onFileClick?.(node)}>
|
||||
<div class="w-4 shrink-0" />
|
||||
<Switch>
|
||||
<Match when={node.ignored}>
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono"
|
||||
style="color: var(--icon-weak-base)"
|
||||
mono
|
||||
/>
|
||||
</Match>
|
||||
<Match when={active()}>
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono"
|
||||
style={kindTextColor(kind()!)}
|
||||
mono
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!node.ignored}>
|
||||
<span class="filetree-iconpair size-4">
|
||||
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--color" />
|
||||
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
</FileTreeNode>
|
||||
</FileTreeNodeTooltip>
|
||||
<FileIcon node={node} class="text-icon-weak size-4" />
|
||||
</Node>
|
||||
</Wrapper>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { ComponentProps, splitProps } from "solid-js"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
export interface LinkProps extends Omit<ComponentProps<"a">, "href"> {
|
||||
export interface LinkProps extends ComponentProps<"button"> {
|
||||
href: string
|
||||
}
|
||||
|
||||
export function Link(props: LinkProps) {
|
||||
const platform = usePlatform()
|
||||
const [local, rest] = splitProps(props, ["href", "children", "class"])
|
||||
const [local, rest] = splitProps(props, ["href", "children"])
|
||||
|
||||
return (
|
||||
<a
|
||||
href={local.href}
|
||||
class={`text-text-strong underline ${local.class ?? ""}`}
|
||||
onClick={(event) => {
|
||||
if (!local.href) return
|
||||
event.preventDefault()
|
||||
platform.openLink(local.href)
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}>
|
||||
{local.children}
|
||||
</a>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ import { onCleanup, onMount } from "solid-js"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { uuid } from "@/utils/uuid"
|
||||
import { getCursorPosition } from "./editor-dom"
|
||||
|
||||
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||
@@ -32,7 +31,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
const dataUrl = reader.result as string
|
||||
const attachment: ImageAttachmentPart = {
|
||||
type: "image",
|
||||
id: uuid(),
|
||||
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
dataUrl,
|
||||
@@ -89,9 +88,6 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
}
|
||||
|
||||
if (!plainText) return
|
||||
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText)
|
||||
if (inserted) return
|
||||
|
||||
input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||
}
|
||||
|
||||
|
||||
@@ -20,67 +20,61 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
||||
<Show when={props.items.length > 0}>
|
||||
<div class="flex flex-nowrap items-start gap-2 p-2 overflow-x-auto no-scrollbar">
|
||||
<For each={props.items}>
|
||||
{(item) => {
|
||||
const directory = getDirectory(item.path)
|
||||
const filename = getFilename(item.path)
|
||||
const label = getFilenameTruncated(item.path, 14)
|
||||
const selected = props.active(item)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
value={
|
||||
<span class="flex max-w-[300px]">
|
||||
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
|
||||
{directory}
|
||||
</span>
|
||||
<span class="shrink-0">{filename}</span>
|
||||
{(item) => (
|
||||
<Tooltip
|
||||
value={
|
||||
<span class="flex max-w-[300px]">
|
||||
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
|
||||
{getDirectory(item.path)}
|
||||
</span>
|
||||
}
|
||||
placement="top"
|
||||
openDelay={2000}
|
||||
<span class="shrink-0">{getFilename(item.path)}</span>
|
||||
</span>
|
||||
}
|
||||
placement="top"
|
||||
openDelay={2000}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"group shrink-0 flex flex-col rounded-[6px] pl-2 pr-1 py-1 max-w-[200px] h-12 transition-all transition-transform shadow-xs-border hover:shadow-xs-border-hover": true,
|
||||
"cursor-pointer hover:bg-surface-interactive-weak": !!item.commentID && !props.active(item),
|
||||
"cursor-pointer bg-surface-interactive-hover hover:bg-surface-interactive-hover shadow-xs-border-hover":
|
||||
props.active(item),
|
||||
"bg-background-stronger": !props.active(item),
|
||||
}}
|
||||
onClick={() => props.openComment(item)}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"group shrink-0 flex flex-col rounded-[6px] pl-2 pr-1 py-1 max-w-[200px] h-12 cursor-default transition-all transition-transform shadow-xs-border hover:shadow-xs-border-hover": true,
|
||||
"hover:bg-surface-interactive-weak": !!item.commentID && !selected,
|
||||
"bg-surface-interactive-hover hover:bg-surface-interactive-hover shadow-xs-border-hover": selected,
|
||||
"bg-background-stronger": !selected,
|
||||
}}
|
||||
onClick={() => props.openComment(item)}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
|
||||
<div class="flex items-center text-11-regular min-w-0 font-medium">
|
||||
<span class="text-text-strong whitespace-nowrap">{label}</span>
|
||||
<Show when={item.selection}>
|
||||
{(sel) => (
|
||||
<span class="text-text-weak whitespace-nowrap shrink-0">
|
||||
{sel().startLine === sel().endLine
|
||||
? `:${sel().startLine}`
|
||||
: `:${sel().startLine}-${sel().endLine}`}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="ml-auto size-3.5 text-text-weak hover:text-text-strong transition-all"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.remove(item)
|
||||
}}
|
||||
aria-label={props.t("prompt.context.removeFile")}
|
||||
/>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
|
||||
<div class="flex items-center text-11-regular min-w-0 font-medium">
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
|
||||
<Show when={item.selection}>
|
||||
{(sel) => (
|
||||
<span class="text-text-weak whitespace-nowrap shrink-0">
|
||||
{sel().startLine === sel().endLine
|
||||
? `:${sel().startLine}`
|
||||
: `:${sel().startLine}-${sel().endLine}`}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={item.comment}>
|
||||
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
|
||||
</Show>
|
||||
<IconButton
|
||||
type="button"
|
||||
icon="close-small"
|
||||
variant="ghost"
|
||||
class="ml-auto size-3.5 text-text-weak hover:text-text-strong transition-all"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.remove(item)
|
||||
}}
|
||||
aria-label={props.t("prompt.context.removeFile")}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}}
|
||||
<Show when={item.comment}>
|
||||
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
|
||||
</Show>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -6,17 +6,12 @@ type PromptDragOverlayProps = {
|
||||
label: string
|
||||
}
|
||||
|
||||
const kindToIcon = {
|
||||
image: "photo",
|
||||
"@mention": "link",
|
||||
} as const
|
||||
|
||||
export const PromptDragOverlay: Component<PromptDragOverlayProps> = (props) => {
|
||||
return (
|
||||
<Show when={props.type !== null}>
|
||||
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
|
||||
<div class="flex flex-col items-center gap-2 text-text-weak">
|
||||
<Icon name={props.type ? kindToIcon[props.type] : kindToIcon.image} class="size-8" />
|
||||
<Icon name={props.type === "@mention" ? "link" : "photo"} class="size-8" />
|
||||
<span class="text-14-regular">{props.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,26 +2,17 @@ import { describe, expect, test } from "bun:test"
|
||||
import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom"
|
||||
|
||||
describe("prompt-input editor dom", () => {
|
||||
test("createTextFragment preserves newlines with consecutive br nodes", () => {
|
||||
test("createTextFragment preserves newlines with br and zero-width placeholders", () => {
|
||||
const fragment = createTextFragment("foo\n\nbar")
|
||||
const container = document.createElement("div")
|
||||
container.appendChild(fragment)
|
||||
|
||||
expect(container.childNodes.length).toBe(4)
|
||||
expect(container.childNodes[0]?.textContent).toBe("foo")
|
||||
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
|
||||
expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR")
|
||||
expect(container.childNodes[3]?.textContent).toBe("bar")
|
||||
})
|
||||
|
||||
test("createTextFragment keeps trailing newline as terminal break", () => {
|
||||
const fragment = createTextFragment("foo\n")
|
||||
const container = document.createElement("div")
|
||||
container.appendChild(fragment)
|
||||
|
||||
expect(container.childNodes.length).toBe(2)
|
||||
expect(container.childNodes.length).toBe(5)
|
||||
expect(container.childNodes[0]?.textContent).toBe("foo")
|
||||
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
|
||||
expect(container.childNodes[2]?.textContent).toBe("\u200B")
|
||||
expect((container.childNodes[3] as HTMLElement).tagName).toBe("BR")
|
||||
expect(container.childNodes[4]?.textContent).toBe("bar")
|
||||
})
|
||||
|
||||
test("length helpers treat breaks as one char and ignore zero-width chars", () => {
|
||||
@@ -57,21 +48,4 @@ describe("prompt-input editor dom", () => {
|
||||
|
||||
container.remove()
|
||||
})
|
||||
|
||||
test("setCursorPosition and getCursorPosition round-trip across blank lines", () => {
|
||||
const container = document.createElement("div")
|
||||
container.appendChild(document.createTextNode("a"))
|
||||
container.appendChild(document.createElement("br"))
|
||||
container.appendChild(document.createElement("br"))
|
||||
container.appendChild(document.createTextNode("b"))
|
||||
document.body.appendChild(container)
|
||||
|
||||
setCursorPosition(container, 2)
|
||||
expect(getCursorPosition(container)).toBe(2)
|
||||
|
||||
setCursorPosition(container, 3)
|
||||
expect(getCursorPosition(container)).toBe(3)
|
||||
|
||||
container.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,8 @@ export function createTextFragment(content: string): DocumentFragment {
|
||||
segments.forEach((segment, index) => {
|
||||
if (segment) {
|
||||
fragment.appendChild(document.createTextNode(segment))
|
||||
} else if (segments.length > 1) {
|
||||
fragment.appendChild(document.createTextNode("\u200B"))
|
||||
}
|
||||
if (index < segments.length - 1) {
|
||||
fragment.appendChild(document.createElement("br"))
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Prompt } from "@/context/prompt"
|
||||
import {
|
||||
canNavigateHistoryAtCursor,
|
||||
clonePromptParts,
|
||||
navigatePromptHistory,
|
||||
prependHistoryEntry,
|
||||
promptLength,
|
||||
} from "./history"
|
||||
import { clonePromptParts, navigatePromptHistory, prependHistoryEntry, promptLength } from "./history"
|
||||
|
||||
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
@@ -72,29 +66,4 @@ describe("prompt-input history", () => {
|
||||
if (original[1]?.type !== "file") throw new Error("expected file")
|
||||
expect(original[1].selection?.startLine).toBe(1)
|
||||
})
|
||||
|
||||
test("canNavigateHistoryAtCursor only allows prompt boundaries", () => {
|
||||
const value = "a\nb\nc"
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
|
||||
expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false)
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
|
||||
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false)
|
||||
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false)
|
||||
|
||||
expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("down", "abc", 3, true)).toBe(true)
|
||||
expect(canNavigateHistoryAtCursor("up", "abc", 1, true)).toBe(false)
|
||||
expect(canNavigateHistoryAtCursor("down", "abc", 1, true)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,15 +4,6 @@ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
export const MAX_HISTORY = 100
|
||||
|
||||
export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) {
|
||||
const position = Math.max(0, Math.min(cursor, text.length))
|
||||
const atStart = position === 0
|
||||
const atEnd = position === text.length
|
||||
if (inHistory) return atStart || atEnd
|
||||
if (direction === "up") return position === 0
|
||||
return position === text.length
|
||||
}
|
||||
|
||||
export function clonePromptParts(prompt: Prompt): Prompt {
|
||||
return prompt.map((part) => {
|
||||
if (part.type === "text") return { ...part }
|
||||
|
||||
@@ -9,13 +9,6 @@ type PromptImageAttachmentsProps = {
|
||||
removeLabel: string
|
||||
}
|
||||
|
||||
const fallbackClass = "size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base"
|
||||
const imageClass =
|
||||
"size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors"
|
||||
const removeClass =
|
||||
"absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
|
||||
const nameClass = "absolute bottom-0 left-0 right-0 px-1 py-0.5 bg-black/50 rounded-b-md"
|
||||
|
||||
export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (props) => {
|
||||
return (
|
||||
<Show when={props.attachments.length > 0}>
|
||||
@@ -26,7 +19,7 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
|
||||
<Show
|
||||
when={attachment.mime.startsWith("image/")}
|
||||
fallback={
|
||||
<div class={fallbackClass}>
|
||||
<div class="size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base">
|
||||
<Icon name="folder" class="size-6 text-text-weak" />
|
||||
</div>
|
||||
}
|
||||
@@ -34,19 +27,19 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
|
||||
<img
|
||||
src={attachment.dataUrl}
|
||||
alt={attachment.filename}
|
||||
class={imageClass}
|
||||
class="size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors"
|
||||
onClick={() => props.onOpen(attachment)}
|
||||
/>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onRemove(attachment.id)}
|
||||
class={removeClass}
|
||||
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover"
|
||||
aria-label={props.removeLabel}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-text-weak" />
|
||||
</button>
|
||||
<div class={nameClass}>
|
||||
<div class="absolute bottom-0 left-0 right-0 px-1 py-0.5 bg-black/50 rounded-b-md">
|
||||
<span class="text-10-regular text-white truncate block">{attachment.filename}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,40 +9,27 @@ describe("promptPlaceholder", () => {
|
||||
mode: "shell",
|
||||
commentCount: 0,
|
||||
example: "example",
|
||||
suggest: true,
|
||||
t,
|
||||
})
|
||||
expect(value).toBe("prompt.placeholder.shell")
|
||||
})
|
||||
|
||||
test("returns summarize placeholders for comment context", () => {
|
||||
expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", suggest: true, t })).toBe(
|
||||
expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", t })).toBe(
|
||||
"prompt.placeholder.summarizeComment",
|
||||
)
|
||||
expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", suggest: true, t })).toBe(
|
||||
expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", t })).toBe(
|
||||
"prompt.placeholder.summarizeComments",
|
||||
)
|
||||
})
|
||||
|
||||
test("returns default placeholder with example when suggestions enabled", () => {
|
||||
test("returns default placeholder with example", () => {
|
||||
const value = promptPlaceholder({
|
||||
mode: "normal",
|
||||
commentCount: 0,
|
||||
example: "translated-example",
|
||||
suggest: true,
|
||||
t,
|
||||
})
|
||||
expect(value).toBe("prompt.placeholder.normal:translated-example")
|
||||
})
|
||||
|
||||
test("returns simple placeholder when suggestions disabled", () => {
|
||||
const value = promptPlaceholder({
|
||||
mode: "normal",
|
||||
commentCount: 0,
|
||||
example: "translated-example",
|
||||
suggest: false,
|
||||
t,
|
||||
})
|
||||
expect(value).toBe("prompt.placeholder.simple")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ type PromptPlaceholderInput = {
|
||||
mode: "normal" | "shell"
|
||||
commentCount: number
|
||||
example: string
|
||||
suggest: boolean
|
||||
t: (key: string, params?: Record<string, string>) => string
|
||||
}
|
||||
|
||||
@@ -10,6 +9,5 @@ export function promptPlaceholder(input: PromptPlaceholderInput) {
|
||||
if (input.mode === "shell") return input.t("prompt.placeholder.shell")
|
||||
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
|
||||
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
|
||||
if (!input.suggest) return input.t("prompt.placeholder.simple")
|
||||
return input.t("prompt.placeholder.normal", { example: input.example })
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
|
||||
ref={(el) => {
|
||||
if (props.popover === "slash") props.setSlashPopoverRef(el)
|
||||
}}
|
||||
class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10
|
||||
overflow-auto no-scrollbar flex flex-col p-2 rounded-[12px]
|
||||
bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]"
|
||||
class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10
|
||||
overflow-auto no-scrollbar flex flex-col p-2 rounded-md
|
||||
border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<Switch>
|
||||
@@ -52,44 +52,47 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
|
||||
fallback={<div class="text-text-weak px-2 py-1">{props.t("prompt.popover.emptyResults")}</div>}
|
||||
>
|
||||
<For each={props.atFlat.slice(0, 10)}>
|
||||
{(item) => {
|
||||
const key = props.atKey(item)
|
||||
|
||||
if (item.type === "agent") {
|
||||
return (
|
||||
<button
|
||||
class="w-full flex items-center gap-x-2 rounded-md px-2 py-0.5"
|
||||
classList={{ "bg-surface-raised-base-hover": props.atActive === key }}
|
||||
onClick={() => props.onAtSelect(item)}
|
||||
onMouseEnter={() => props.setAtActive(key)}
|
||||
>
|
||||
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">@{item.name}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const isDirectory = item.path.endsWith("/")
|
||||
const directory = isDirectory ? item.path : getDirectory(item.path)
|
||||
const filename = isDirectory ? "" : getFilename(item.path)
|
||||
|
||||
return (
|
||||
<button
|
||||
class="w-full flex items-center gap-x-2 rounded-md px-2 py-0.5"
|
||||
classList={{ "bg-surface-raised-base-hover": props.atActive === key }}
|
||||
onClick={() => props.onAtSelect(item)}
|
||||
onMouseEnter={() => props.setAtActive(key)}
|
||||
{(item) => (
|
||||
<button
|
||||
classList={{
|
||||
"w-full flex items-center gap-x-2 rounded-md px-2 py-0.5": true,
|
||||
"bg-surface-raised-base-hover": props.atActive === props.atKey(item),
|
||||
}}
|
||||
onClick={() => props.onAtSelect(item)}
|
||||
onMouseEnter={() => props.setAtActive(props.atKey(item))}
|
||||
>
|
||||
<Show
|
||||
when={item.type === "agent"}
|
||||
fallback={
|
||||
<>
|
||||
<FileIcon
|
||||
node={{ path: item.type === "file" ? item.path : "", type: "file" }}
|
||||
class="shrink-0 size-4"
|
||||
/>
|
||||
<div class="flex items-center text-14-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap truncate min-w-0">
|
||||
{item.type === "file"
|
||||
? item.path.endsWith("/")
|
||||
? item.path
|
||||
: getDirectory(item.path)
|
||||
: ""}
|
||||
</span>
|
||||
<Show when={item.type === "file" && !item.path.endsWith("/")}>
|
||||
<span class="text-text-strong whitespace-nowrap">
|
||||
{item.type === "file" ? getFilename(item.path) : ""}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular min-w-0">
|
||||
<span class="text-text-weak whitespace-nowrap truncate min-w-0">{directory}</span>
|
||||
<Show when={!isDirectory}>
|
||||
<span class="text-text-strong whitespace-nowrap">{filename}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
<Icon name="brain" size="small" class="text-icon-info-active shrink-0" />
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">
|
||||
@{item.type === "agent" ? item.name : ""}
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
@@ -80,7 +80,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
queued.abort.abort()
|
||||
queued.cleanup()
|
||||
pending.delete(sessionID)
|
||||
globalSync.todo.set(sessionID, undefined)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return sdk.client.session
|
||||
@@ -88,9 +87,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
sessionID,
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
globalSync.todo.set(sessionID, undefined)
|
||||
})
|
||||
}
|
||||
|
||||
const restoreCommentItems = (items: CommentItem[]) => {
|
||||
@@ -389,7 +385,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const send = async () => {
|
||||
const ok = await waitForWorktree()
|
||||
if (!ok) return
|
||||
await client.session.promptAsync({
|
||||
await client.session.prompt({
|
||||
sessionID: session.id,
|
||||
agent,
|
||||
model,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
||||
import { For, Show, createMemo, type Component } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
@@ -13,98 +12,25 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
||||
const language = useLanguage()
|
||||
|
||||
const questions = createMemo(() => props.request.questions)
|
||||
const total = createMemo(() => questions().length)
|
||||
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
tab: 0,
|
||||
answers: [] as QuestionAnswer[],
|
||||
custom: [] as string[],
|
||||
customOn: [] as boolean[],
|
||||
editing: false,
|
||||
sending: false,
|
||||
})
|
||||
|
||||
let root: HTMLDivElement | undefined
|
||||
|
||||
const question = createMemo(() => questions()[store.tab])
|
||||
const confirm = createMemo(() => !single() && store.tab === questions().length)
|
||||
const options = createMemo(() => question()?.options ?? [])
|
||||
const input = createMemo(() => store.custom[store.tab] ?? "")
|
||||
const on = createMemo(() => store.customOn[store.tab] === true)
|
||||
const multi = createMemo(() => question()?.multiple === true)
|
||||
|
||||
const summary = createMemo(() => {
|
||||
const n = Math.min(store.tab + 1, total())
|
||||
return `${n} of ${total()} questions`
|
||||
})
|
||||
|
||||
const last = createMemo(() => store.tab >= total() - 1)
|
||||
|
||||
const customUpdate = (value: string, selected: boolean = on()) => {
|
||||
const prev = input().trim()
|
||||
const next = value.trim()
|
||||
|
||||
setStore("custom", store.tab, value)
|
||||
if (!selected) return
|
||||
|
||||
if (multi()) {
|
||||
setStore("answers", store.tab, (current = []) => {
|
||||
const removed = prev ? current.filter((item) => item.trim() !== prev) : current
|
||||
if (!next) return removed
|
||||
if (removed.some((item) => item.trim() === next)) return removed
|
||||
return [...removed, next]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setStore("answers", store.tab, next ? [next] : [])
|
||||
}
|
||||
|
||||
const measure = () => {
|
||||
if (!root) return
|
||||
|
||||
const scroller = document.querySelector(".session-scroller")
|
||||
const head = scroller instanceof HTMLElement ? scroller.firstElementChild : undefined
|
||||
const top =
|
||||
head instanceof HTMLElement && head.classList.contains("sticky") ? head.getBoundingClientRect().bottom : 0
|
||||
if (!top) {
|
||||
root.style.removeProperty("--question-prompt-max-height")
|
||||
return
|
||||
}
|
||||
|
||||
const dock = root.closest('[data-component="session-prompt-dock"]')
|
||||
if (!(dock instanceof HTMLElement)) return
|
||||
|
||||
const dockBottom = dock.getBoundingClientRect().bottom
|
||||
const below = Math.max(0, dockBottom - root.getBoundingClientRect().bottom)
|
||||
const gap = 8
|
||||
const max = Math.max(240, Math.floor(dockBottom - top - gap - below))
|
||||
root.style.setProperty("--question-prompt-max-height", `${max}px`)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let raf: number | undefined
|
||||
const update = () => {
|
||||
if (raf !== undefined) cancelAnimationFrame(raf)
|
||||
raf = requestAnimationFrame(() => {
|
||||
raf = undefined
|
||||
measure()
|
||||
})
|
||||
}
|
||||
|
||||
update()
|
||||
window.addEventListener("resize", update)
|
||||
|
||||
const dock = root?.closest('[data-component="session-prompt-dock"]')
|
||||
const scroller = document.querySelector(".session-scroller")
|
||||
const observer = new ResizeObserver(update)
|
||||
if (dock instanceof HTMLElement) observer.observe(dock)
|
||||
if (scroller instanceof HTMLElement) observer.observe(scroller)
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", update)
|
||||
observer.disconnect()
|
||||
if (raf !== undefined) cancelAnimationFrame(raf)
|
||||
})
|
||||
const customPicked = createMemo(() => {
|
||||
const value = input()
|
||||
if (!value) return false
|
||||
return store.answers[store.tab]?.includes(value) ?? false
|
||||
})
|
||||
|
||||
const fail = (err: unknown) => {
|
||||
@@ -112,83 +38,71 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
}
|
||||
|
||||
const reply = async (answers: QuestionAnswer[]) => {
|
||||
const reply = (answers: QuestionAnswer[]) => {
|
||||
if (store.sending) return
|
||||
|
||||
setStore("sending", true)
|
||||
try {
|
||||
await sdk.client.question.reply({ requestID: props.request.id, answers })
|
||||
} catch (err) {
|
||||
fail(err)
|
||||
} finally {
|
||||
setStore("sending", false)
|
||||
}
|
||||
sdk.client.question
|
||||
.reply({ requestID: props.request.id, answers })
|
||||
.catch(fail)
|
||||
.finally(() => setStore("sending", false))
|
||||
}
|
||||
|
||||
const reject = async () => {
|
||||
const reject = () => {
|
||||
if (store.sending) return
|
||||
|
||||
setStore("sending", true)
|
||||
try {
|
||||
await sdk.client.question.reject({ requestID: props.request.id })
|
||||
} catch (err) {
|
||||
fail(err)
|
||||
} finally {
|
||||
setStore("sending", false)
|
||||
}
|
||||
sdk.client.question
|
||||
.reject({ requestID: props.request.id })
|
||||
.catch(fail)
|
||||
.finally(() => setStore("sending", false))
|
||||
}
|
||||
|
||||
const submit = () => void reply(questions().map((_, i) => store.answers[i] ?? []))
|
||||
const submit = () => {
|
||||
reply(questions().map((_, i) => store.answers[i] ?? []))
|
||||
}
|
||||
|
||||
const pick = (answer: string, custom: boolean = false) => {
|
||||
setStore("answers", store.tab, [answer])
|
||||
if (custom) setStore("custom", store.tab, answer)
|
||||
if (!custom) setStore("customOn", store.tab, false)
|
||||
setStore("editing", false)
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = [answer]
|
||||
setStore("answers", answers)
|
||||
|
||||
if (custom) {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = answer
|
||||
setStore("custom", inputs)
|
||||
}
|
||||
|
||||
if (single()) {
|
||||
reply([[answer]])
|
||||
return
|
||||
}
|
||||
|
||||
setStore("tab", store.tab + 1)
|
||||
}
|
||||
|
||||
const toggle = (answer: string) => {
|
||||
setStore("answers", store.tab, (current = []) => {
|
||||
if (current.includes(answer)) return current.filter((item) => item !== answer)
|
||||
return [...current, answer]
|
||||
})
|
||||
const existing = store.answers[store.tab] ?? []
|
||||
const next = [...existing]
|
||||
const index = next.indexOf(answer)
|
||||
if (index === -1) next.push(answer)
|
||||
if (index !== -1) next.splice(index, 1)
|
||||
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = next
|
||||
setStore("answers", answers)
|
||||
}
|
||||
|
||||
const customToggle = () => {
|
||||
if (store.sending) return
|
||||
|
||||
if (!multi()) {
|
||||
setStore("customOn", store.tab, true)
|
||||
setStore("editing", true)
|
||||
customUpdate(input(), true)
|
||||
return
|
||||
}
|
||||
|
||||
const next = !on()
|
||||
setStore("customOn", store.tab, next)
|
||||
if (next) {
|
||||
setStore("editing", true)
|
||||
customUpdate(input(), true)
|
||||
return
|
||||
}
|
||||
|
||||
const value = input().trim()
|
||||
if (value) setStore("answers", store.tab, (current = []) => current.filter((item) => item.trim() !== value))
|
||||
const selectTab = (index: number) => {
|
||||
setStore("tab", index)
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
const customOpen = () => {
|
||||
if (store.sending) return
|
||||
if (!on()) setStore("customOn", store.tab, true)
|
||||
setStore("editing", true)
|
||||
customUpdate(input(), true)
|
||||
}
|
||||
|
||||
const selectOption = (optIndex: number) => {
|
||||
if (store.sending) return
|
||||
|
||||
if (optIndex === options().length) {
|
||||
customOpen()
|
||||
setStore("editing", true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -201,225 +115,181 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
||||
pick(opt.label)
|
||||
}
|
||||
|
||||
const commitCustom = () => {
|
||||
setStore("editing", false)
|
||||
customUpdate(input())
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
const handleCustomSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
if (store.sending) return
|
||||
if (store.editing) commitCustom()
|
||||
|
||||
if (store.tab >= total() - 1) {
|
||||
submit()
|
||||
const value = input().trim()
|
||||
if (!value) {
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
|
||||
setStore("tab", store.tab + 1)
|
||||
setStore("editing", false)
|
||||
}
|
||||
if (multi()) {
|
||||
const existing = store.answers[store.tab] ?? []
|
||||
const next = [...existing]
|
||||
if (!next.includes(value)) next.push(value)
|
||||
|
||||
const back = () => {
|
||||
if (store.sending) return
|
||||
if (store.tab <= 0) return
|
||||
setStore("tab", store.tab - 1)
|
||||
setStore("editing", false)
|
||||
}
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = next
|
||||
setStore("answers", answers)
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
|
||||
const jump = (tab: number) => {
|
||||
if (store.sending) return
|
||||
setStore("tab", tab)
|
||||
pick(value, true)
|
||||
setStore("editing", false)
|
||||
}
|
||||
|
||||
return (
|
||||
<DockPrompt
|
||||
kind="question"
|
||||
ref={(el) => (root = el)}
|
||||
header={
|
||||
<>
|
||||
<div data-slot="question-header-title">{summary()}</div>
|
||||
<div data-slot="question-progress">
|
||||
<For each={questions()}>
|
||||
{(_, i) => (
|
||||
<div data-component="question-prompt">
|
||||
<Show when={!single()}>
|
||||
<div data-slot="question-tabs">
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const active = () => index() === store.tab
|
||||
const answered = () => (store.answers[index()]?.length ?? 0) > 0
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="question-progress-segment"
|
||||
data-active={i() === store.tab}
|
||||
data-answered={
|
||||
(store.answers[i()]?.length ?? 0) > 0 ||
|
||||
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
||||
}
|
||||
data-slot="question-tab"
|
||||
data-active={active()}
|
||||
data-answered={answered()}
|
||||
disabled={store.sending}
|
||||
onClick={() => jump(i())}
|
||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
|
||||
{language.t("ui.common.dismiss")}
|
||||
</Button>
|
||||
<div data-slot="question-footer-actions">
|
||||
<Show when={store.tab > 0}>
|
||||
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
|
||||
{language.t("ui.common.back")}
|
||||
</Button>
|
||||
</Show>
|
||||
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
|
||||
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div data-slot="question-text">{question()?.question}</div>
|
||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
onClick={() => selectTab(index())}
|
||||
>
|
||||
{q.header}
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<button
|
||||
data-slot="question-tab"
|
||||
data-active={confirm()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectTab(questions().length)}
|
||||
>
|
||||
{language.t("ui.common.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={picked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={picked()}
|
||||
>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={
|
||||
<Show when={!confirm()}>
|
||||
<div data-slot="question-content">
|
||||
<div data-slot="question-text">
|
||||
{question()?.question}
|
||||
{multi() ? " " + language.t("ui.question.multiHint") : ""}
|
||||
</div>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
>
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
<Show when={picked()}>
|
||||
<Icon name="check-small" size="normal" />
|
||||
</Show>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={on()}
|
||||
data-picked={customPicked()}
|
||||
disabled={store.sending}
|
||||
onClick={customOpen}
|
||||
onClick={() => selectOption(options().length)}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
customToggle()
|
||||
}}
|
||||
>
|
||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<form
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={on()}
|
||||
onMouseDown={(e) => {
|
||||
if (store.sending) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (e.target instanceof HTMLTextAreaElement) return
|
||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
customToggle()
|
||||
}}
|
||||
>
|
||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<textarea
|
||||
ref={(el) =>
|
||||
setTimeout(() => {
|
||||
el.focus()
|
||||
el.style.height = "0px"
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}, 0)
|
||||
}
|
||||
data-slot="question-custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
rows={1}
|
||||
disabled={store.sending}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
if (e.key !== "Enter" || e.shiftKey) return
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
onInput={(e) => {
|
||||
customUpdate(e.currentTarget.value)
|
||||
e.currentTarget.style.height = "0px"
|
||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
<Show when={!store.editing && input()}>
|
||||
<span data-slot="option-description">{input()}</span>
|
||||
</Show>
|
||||
<Show when={customPicked()}>
|
||||
<Icon name="check-small" size="normal" />
|
||||
</Show>
|
||||
</button>
|
||||
<Show when={store.editing}>
|
||||
<form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
|
||||
<input
|
||||
ref={(el) => setTimeout(() => el.focus(), 0)}
|
||||
type="text"
|
||||
data-slot="custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
disabled={store.sending}
|
||||
onInput={(e) => {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = e.currentTarget.value
|
||||
setStore("custom", inputs)
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
|
||||
{multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
disabled={store.sending}
|
||||
onClick={() => setStore("editing", false)}
|
||||
>
|
||||
{language.t("ui.common.cancel")}
|
||||
</Button>
|
||||
</form>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={confirm()}>
|
||||
<div data-slot="question-review">
|
||||
<div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const value = () => store.answers[index()]?.join(", ") ?? ""
|
||||
const answered = () => Boolean(value())
|
||||
return (
|
||||
<div data-slot="review-item">
|
||||
<span data-slot="review-label">{q.question}</span>
|
||||
<span data-slot="review-value" data-answered={answered()}>
|
||||
{answered() ? value() : language.t("ui.question.review.notAnswered")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div data-slot="question-actions">
|
||||
<Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
|
||||
{language.t("ui.common.dismiss")}
|
||||
</Button>
|
||||
<Show when={!single()}>
|
||||
<Show when={confirm()}>
|
||||
<Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
|
||||
{language.t("ui.common.submit")}
|
||||
</Button>
|
||||
</Show>
|
||||
<Show when={!confirm() && multi()}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => selectTab(store.tab + 1)}
|
||||
disabled={store.sending || (store.answers[store.tab]?.length ?? 0) === 0}
|
||||
>
|
||||
{language.t("ui.common.next")}
|
||||
</Button>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</DockPrompt>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { JSXElement, ParentProps, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { JSXElement, ParentProps, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { serverDisplayName } from "@/context/server"
|
||||
import type { ServerHealth } from "@/utils/server-health"
|
||||
|
||||
@@ -17,7 +17,6 @@ export function ServerRow(props: ServerRowProps) {
|
||||
const [truncated, setTruncated] = createSignal(false)
|
||||
let nameRef: HTMLSpanElement | undefined
|
||||
let versionRef: HTMLSpanElement | undefined
|
||||
const name = createMemo(() => serverDisplayName(props.url))
|
||||
|
||||
const check = () => {
|
||||
const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
|
||||
@@ -26,24 +25,25 @@ export function ServerRow(props: ServerRowProps) {
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
name()
|
||||
props.url
|
||||
props.status?.version
|
||||
queueMicrotask(check)
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
requestAnimationFrame(check)
|
||||
return
|
||||
}
|
||||
check()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
check()
|
||||
if (typeof ResizeObserver !== "function") return
|
||||
const observer = new ResizeObserver(check)
|
||||
if (nameRef) observer.observe(nameRef)
|
||||
if (versionRef) observer.observe(versionRef)
|
||||
onCleanup(() => observer.disconnect())
|
||||
if (typeof window === "undefined") return
|
||||
window.addEventListener("resize", check)
|
||||
onCleanup(() => window.removeEventListener("resize", check))
|
||||
})
|
||||
|
||||
const tooltipValue = () => (
|
||||
<span class="flex items-center gap-2">
|
||||
<span>{name()}</span>
|
||||
<span>{serverDisplayName(props.url)}</span>
|
||||
<Show when={props.status?.version}>
|
||||
<span class="text-text-invert-base">{props.status?.version}</span>
|
||||
</Show>
|
||||
@@ -62,7 +62,7 @@ export function ServerRow(props: ServerRowProps) {
|
||||
}}
|
||||
/>
|
||||
<span ref={nameRef} class={props.nameClass ?? "truncate"}>
|
||||
{name()}
|
||||
{serverDisplayName(props.url)}
|
||||
</span>
|
||||
<Show when={props.status?.version}>
|
||||
<span ref={versionRef} class={props.versionClass ?? "text-text-weak text-14-regular truncate"}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Match, Show, Switch, createMemo } from "solid-js"
|
||||
import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useParams } from "@solidjs/router"
|
||||
@@ -11,18 +11,6 @@ import { getSessionContextMetrics } from "@/components/session/session-context-m
|
||||
|
||||
interface SessionContextUsageProps {
|
||||
variant?: "button" | "indicator"
|
||||
placement?: TooltipProps["placement"]
|
||||
}
|
||||
|
||||
function openSessionContext(args: {
|
||||
view: ReturnType<ReturnType<typeof useLayout>["view"]>
|
||||
layout: ReturnType<typeof useLayout>
|
||||
tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
||||
}) {
|
||||
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
|
||||
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
||||
args.tabs.open("context")
|
||||
args.tabs.setActive("context")
|
||||
}
|
||||
|
||||
export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
@@ -53,16 +41,11 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
|
||||
const openContext = () => {
|
||||
if (!params.id) return
|
||||
|
||||
if (tabs().active() === "context") {
|
||||
tabs().close("context")
|
||||
return
|
||||
}
|
||||
openSessionContext({
|
||||
view: view(),
|
||||
layout,
|
||||
tabs: tabs(),
|
||||
})
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
layout.fileTree.open()
|
||||
layout.fileTree.setTab("all")
|
||||
tabs().open("context")
|
||||
tabs().setActive("context")
|
||||
}
|
||||
|
||||
const circle = () => (
|
||||
@@ -96,7 +79,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||
|
||||
return (
|
||||
<Show when={params.id}>
|
||||
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
||||
<Tooltip value={tooltipValue()} placement="top">
|
||||
<Switch>
|
||||
<Match when={variant() === "indicator"}>{circle()}</Match>
|
||||
<Match when={true}>
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import type { Todo } from "@opencode-ai/sdk/v2"
|
||||
import { Checkbox } from "@opencode-ai/ui/checkbox"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
function dot(status: Todo["status"]) {
|
||||
if (status !== "in_progress") return undefined
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 12 12"
|
||||
width="12"
|
||||
height="12"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="block"
|
||||
>
|
||||
<circle
|
||||
cx="6"
|
||||
cy="6"
|
||||
r="3"
|
||||
style={{
|
||||
animation: "var(--animate-pulse-scale)",
|
||||
"transform-origin": "center",
|
||||
"transform-box": "fill-box",
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionTodoDock(props: { todos: Todo[]; title: string; collapseLabel: string; expandLabel: string }) {
|
||||
const [store, setStore] = createStore({
|
||||
collapsed: false,
|
||||
})
|
||||
|
||||
const toggle = () => setStore("collapsed", (value) => !value)
|
||||
|
||||
const summary = createMemo(() => {
|
||||
const total = props.todos.length
|
||||
if (total === 0) return ""
|
||||
const completed = props.todos.filter((todo) => todo.status === "completed").length
|
||||
return `${completed} of ${total} ${props.title.toLowerCase()} completed`
|
||||
})
|
||||
|
||||
const active = createMemo(
|
||||
() =>
|
||||
props.todos.find((todo) => todo.status === "in_progress") ??
|
||||
props.todos.find((todo) => todo.status === "pending") ??
|
||||
props.todos.filter((todo) => todo.status === "completed").at(-1) ??
|
||||
props.todos[0],
|
||||
)
|
||||
|
||||
const preview = createMemo(() => active()?.content ?? "")
|
||||
|
||||
return (
|
||||
<div
|
||||
classList={{
|
||||
"bg-background-base border border-border-weak-base relative z-0 rounded-[12px] overflow-clip": true,
|
||||
"h-[78px]": store.collapsed,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="pl-3 pr-2 py-2 flex items-center gap-2"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={toggle}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
toggle()
|
||||
}}
|
||||
>
|
||||
<span class="text-14-regular text-text-strong cursor-default">{summary()}</span>
|
||||
<Show when={store.collapsed}>
|
||||
<div class="ml-1 flex-1 min-w-0">
|
||||
<Show when={preview()}>
|
||||
<div class="text-14-regular text-text-base truncate cursor-default">{preview()}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<div classList={{ "ml-auto": !store.collapsed, "ml-1": store.collapsed }}>
|
||||
<IconButton
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
classList={{ "rotate-180": !store.collapsed }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
}}
|
||||
aria-label={store.collapsed ? props.expandLabel : props.collapseLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden={store.collapsed}>
|
||||
<TodoList todos={props.todos} open={!store.collapsed} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
const [stuck, setStuck] = createSignal(false)
|
||||
const [scrolling, setScrolling] = createSignal(false)
|
||||
let scrollRef!: HTMLDivElement
|
||||
let timer: number | undefined
|
||||
|
||||
const inProgress = createMemo(() => props.todos.findIndex((todo) => todo.status === "in_progress"))
|
||||
|
||||
const ensure = () => {
|
||||
if (!props.open) return
|
||||
if (scrolling()) return
|
||||
if (!scrollRef || scrollRef.offsetParent === null) return
|
||||
|
||||
const el = scrollRef.querySelector("[data-in-progress]")
|
||||
if (!(el instanceof HTMLElement)) return
|
||||
|
||||
const topFade = 16
|
||||
const bottomFade = 44
|
||||
const container = scrollRef.getBoundingClientRect()
|
||||
const rect = el.getBoundingClientRect()
|
||||
const top = rect.top - container.top + scrollRef.scrollTop
|
||||
const bottom = rect.bottom - container.top + scrollRef.scrollTop
|
||||
const viewTop = scrollRef.scrollTop + topFade
|
||||
const viewBottom = scrollRef.scrollTop + scrollRef.clientHeight - bottomFade
|
||||
|
||||
if (top < viewTop) {
|
||||
scrollRef.scrollTop = Math.max(0, top - topFade)
|
||||
} else if (bottom > viewBottom) {
|
||||
scrollRef.scrollTop = bottom - (scrollRef.clientHeight - bottomFade)
|
||||
}
|
||||
|
||||
setStuck(scrollRef.scrollTop > 0)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on([() => props.open, inProgress], () => {
|
||||
if (!props.open || inProgress() < 0) return
|
||||
requestAnimationFrame(ensure)
|
||||
}),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (!timer) return
|
||||
window.clearTimeout(timer)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="relative">
|
||||
<div
|
||||
class="px-3 pb-11 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar"
|
||||
ref={scrollRef}
|
||||
style={{ "overflow-anchor": "none" }}
|
||||
onScroll={(e) => {
|
||||
setStuck(e.currentTarget.scrollTop > 0)
|
||||
setScrolling(true)
|
||||
if (timer) window.clearTimeout(timer)
|
||||
timer = window.setTimeout(() => {
|
||||
setScrolling(false)
|
||||
if (inProgress() < 0) return
|
||||
requestAnimationFrame(ensure)
|
||||
}, 250)
|
||||
}}
|
||||
>
|
||||
<For each={props.todos}>
|
||||
{(todo) => (
|
||||
<Checkbox
|
||||
readOnly
|
||||
checked={todo.status === "completed"}
|
||||
indeterminate={todo.status === "in_progress"}
|
||||
data-in-progress={todo.status === "in_progress" ? "" : undefined}
|
||||
icon={dot(todo.status)}
|
||||
style={{ "--checkbox-align": "flex-start", "--checkbox-offset": "1px" }}
|
||||
>
|
||||
<span
|
||||
class="text-14-regular min-w-0 break-words"
|
||||
classList={{
|
||||
"text-text-weak": todo.status === "completed" || todo.status === "cancelled",
|
||||
"text-text-strong": todo.status !== "completed" && todo.status !== "cancelled",
|
||||
}}
|
||||
style={{
|
||||
"line-height": "var(--line-height-normal)",
|
||||
"text-decoration":
|
||||
todo.status === "completed" || todo.status === "cancelled" ? "line-through" : undefined,
|
||||
}}
|
||||
>
|
||||
{todo.content}
|
||||
</span>
|
||||
</Checkbox>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 left-0 right-0 h-4 transition-opacity duration-150"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, var(--background-base), transparent)",
|
||||
opacity: stuck() ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { estimateSessionContextBreakdown } from "./session-context-breakdown"
|
||||
|
||||
const user = (id: string) => {
|
||||
return {
|
||||
id,
|
||||
role: "user",
|
||||
time: { created: 1 },
|
||||
} as unknown as Message
|
||||
}
|
||||
|
||||
const assistant = (id: string) => {
|
||||
return {
|
||||
id,
|
||||
role: "assistant",
|
||||
time: { created: 1 },
|
||||
} as unknown as Message
|
||||
}
|
||||
|
||||
describe("estimateSessionContextBreakdown", () => {
|
||||
test("estimates tokens and keeps remaining tokens as other", () => {
|
||||
const messages = [user("u1"), assistant("a1")]
|
||||
const parts = {
|
||||
u1: [{ type: "text", text: "hello world" }] as unknown as Part[],
|
||||
a1: [{ type: "text", text: "assistant response" }] as unknown as Part[],
|
||||
}
|
||||
|
||||
const output = estimateSessionContextBreakdown({
|
||||
messages,
|
||||
parts,
|
||||
input: 20,
|
||||
systemPrompt: "system prompt",
|
||||
})
|
||||
|
||||
const map = Object.fromEntries(output.map((segment) => [segment.key, segment.tokens]))
|
||||
expect(map.system).toBe(4)
|
||||
expect(map.user).toBe(3)
|
||||
expect(map.assistant).toBe(5)
|
||||
expect(map.other).toBe(8)
|
||||
})
|
||||
|
||||
test("scales segments when estimates exceed input", () => {
|
||||
const messages = [user("u1"), assistant("a1")]
|
||||
const parts = {
|
||||
u1: [{ type: "text", text: "x".repeat(400) }] as unknown as Part[],
|
||||
a1: [{ type: "text", text: "y".repeat(400) }] as unknown as Part[],
|
||||
}
|
||||
|
||||
const output = estimateSessionContextBreakdown({
|
||||
messages,
|
||||
parts,
|
||||
input: 10,
|
||||
systemPrompt: "z".repeat(200),
|
||||
})
|
||||
|
||||
const total = output.reduce((sum, segment) => sum + segment.tokens, 0)
|
||||
expect(total).toBeLessThanOrEqual(10)
|
||||
expect(output.every((segment) => segment.width <= 100)).toBeTrue()
|
||||
})
|
||||
})
|
||||
@@ -1,132 +0,0 @@
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
export type SessionContextBreakdownKey = "system" | "user" | "assistant" | "tool" | "other"
|
||||
|
||||
export type SessionContextBreakdownSegment = {
|
||||
key: SessionContextBreakdownKey
|
||||
tokens: number
|
||||
width: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
const estimateTokens = (chars: number) => Math.ceil(chars / 4)
|
||||
const toPercent = (tokens: number, input: number) => (tokens / input) * 100
|
||||
const toPercentLabel = (tokens: number, input: number) => Math.round(toPercent(tokens, input) * 10) / 10
|
||||
|
||||
const charsFromUserPart = (part: Part) => {
|
||||
if (part.type === "text") return part.text.length
|
||||
if (part.type === "file") return part.source?.text.value.length ?? 0
|
||||
if (part.type === "agent") return part.source?.value.length ?? 0
|
||||
return 0
|
||||
}
|
||||
|
||||
const charsFromAssistantPart = (part: Part) => {
|
||||
if (part.type === "text") return { assistant: part.text.length, tool: 0 }
|
||||
if (part.type === "reasoning") return { assistant: part.text.length, tool: 0 }
|
||||
if (part.type !== "tool") return { assistant: 0, tool: 0 }
|
||||
|
||||
const input = Object.keys(part.state.input).length * 16
|
||||
if (part.state.status === "pending") return { assistant: 0, tool: input + part.state.raw.length }
|
||||
if (part.state.status === "completed") return { assistant: 0, tool: input + part.state.output.length }
|
||||
if (part.state.status === "error") return { assistant: 0, tool: input + part.state.error.length }
|
||||
return { assistant: 0, tool: input }
|
||||
}
|
||||
|
||||
const build = (
|
||||
tokens: { system: number; user: number; assistant: number; tool: number; other: number },
|
||||
input: number,
|
||||
) => {
|
||||
return [
|
||||
{
|
||||
key: "system",
|
||||
tokens: tokens.system,
|
||||
},
|
||||
{
|
||||
key: "user",
|
||||
tokens: tokens.user,
|
||||
},
|
||||
{
|
||||
key: "assistant",
|
||||
tokens: tokens.assistant,
|
||||
},
|
||||
{
|
||||
key: "tool",
|
||||
tokens: tokens.tool,
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
tokens: tokens.other,
|
||||
},
|
||||
]
|
||||
.filter((x) => x.tokens > 0)
|
||||
.map((x) => ({
|
||||
key: x.key,
|
||||
tokens: x.tokens,
|
||||
width: toPercent(x.tokens, input),
|
||||
percent: toPercentLabel(x.tokens, input),
|
||||
})) as SessionContextBreakdownSegment[]
|
||||
}
|
||||
|
||||
export function estimateSessionContextBreakdown(args: {
|
||||
messages: Message[]
|
||||
parts: Record<string, Part[] | undefined>
|
||||
input: number
|
||||
systemPrompt?: string
|
||||
}) {
|
||||
if (!args.input) return []
|
||||
|
||||
const counts = args.messages.reduce(
|
||||
(acc, msg) => {
|
||||
const parts = args.parts[msg.id] ?? []
|
||||
if (msg.role === "user") {
|
||||
const user = parts.reduce((sum, part) => sum + charsFromUserPart(part), 0)
|
||||
return { ...acc, user: acc.user + user }
|
||||
}
|
||||
|
||||
if (msg.role !== "assistant") return acc
|
||||
const assistant = parts.reduce(
|
||||
(sum, part) => {
|
||||
const next = charsFromAssistantPart(part)
|
||||
return {
|
||||
assistant: sum.assistant + next.assistant,
|
||||
tool: sum.tool + next.tool,
|
||||
}
|
||||
},
|
||||
{ assistant: 0, tool: 0 },
|
||||
)
|
||||
return {
|
||||
...acc,
|
||||
assistant: acc.assistant + assistant.assistant,
|
||||
tool: acc.tool + assistant.tool,
|
||||
}
|
||||
},
|
||||
{
|
||||
system: args.systemPrompt?.length ?? 0,
|
||||
user: 0,
|
||||
assistant: 0,
|
||||
tool: 0,
|
||||
},
|
||||
)
|
||||
|
||||
const tokens = {
|
||||
system: estimateTokens(counts.system),
|
||||
user: estimateTokens(counts.user),
|
||||
assistant: estimateTokens(counts.assistant),
|
||||
tool: estimateTokens(counts.tool),
|
||||
}
|
||||
const estimated = tokens.system + tokens.user + tokens.assistant + tokens.tool
|
||||
|
||||
if (estimated <= args.input) {
|
||||
return build({ ...tokens, other: args.input - estimated }, args.input)
|
||||
}
|
||||
|
||||
const scale = args.input / estimated
|
||||
const scaled = {
|
||||
system: Math.floor(tokens.system * scale),
|
||||
user: Math.floor(tokens.user * scale),
|
||||
assistant: Math.floor(tokens.assistant * scale),
|
||||
tool: Math.floor(tokens.tool * scale),
|
||||
}
|
||||
const total = scaled.system + scaled.user + scaled.assistant + scaled.tool
|
||||
return build({ ...scaled, other: Math.max(0, args.input - total) }, args.input)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export function createSessionContextFormatter(locale: string) {
|
||||
return {
|
||||
number(value: number | null | undefined) {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toLocaleString(locale)
|
||||
},
|
||||
percent(value: number | null | undefined) {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toLocaleString(locale) + "%"
|
||||
},
|
||||
time(value: number | undefined) {
|
||||
if (!value) return "—"
|
||||
return DateTime.fromMillis(value).setLocale(locale).toLocaleString(DateTime.DATETIME_MED)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -91,11 +91,4 @@ describe("getSessionContextMetrics", () => {
|
||||
expect(two.context?.message.id).toBe("a2")
|
||||
expect(two.totalCost).toBe(1)
|
||||
})
|
||||
|
||||
test("returns empty metrics when inputs are undefined", () => {
|
||||
const metrics = getSessionContextMetrics(undefined, undefined)
|
||||
|
||||
expect(metrics.totalCost).toBe(0)
|
||||
expect(metrics.context).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ const lastAssistantWithTokens = (messages: Message[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
const build = (messages: Message[] = [], providers: Provider[] = []): Metrics => {
|
||||
const build = (messages: Message[], providers: Provider[]): Metrics => {
|
||||
const totalCost = messages.reduce((sum, msg) => sum + (msg.role === "assistant" ? msg.cost : 0), 0)
|
||||
const message = lastAssistantWithTokens(messages)
|
||||
if (!message) return { totalCost, context: undefined }
|
||||
@@ -77,6 +77,6 @@ const build = (messages: Message[] = [], providers: Provider[] = []): Metrics =>
|
||||
}
|
||||
}
|
||||
|
||||
export function getSessionContextMetrics(messages: Message[] = [], providers: Provider[] = []) {
|
||||
export function getSessionContextMetrics(messages: Message[], providers: Provider[]) {
|
||||
return build(messages, providers)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { DateTime } from "luxon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
@@ -13,8 +14,6 @@ import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getSessionContextMetrics } from "./session-context-metrics"
|
||||
import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
|
||||
import { createSessionContextFormatter } from "./session-context-format"
|
||||
|
||||
interface SessionContextTabProps {
|
||||
messages: () => Message[]
|
||||
@@ -23,74 +22,6 @@ interface SessionContextTabProps {
|
||||
info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
|
||||
}
|
||||
|
||||
const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = {
|
||||
system: "var(--syntax-info)",
|
||||
user: "var(--syntax-success)",
|
||||
assistant: "var(--syntax-property)",
|
||||
tool: "var(--syntax-warning)",
|
||||
other: "var(--syntax-comment)",
|
||||
}
|
||||
|
||||
function Stat(props: { label: string; value: JSX.Element }) {
|
||||
return (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-12-regular text-text-weak">{props.label}</div>
|
||||
<div class="text-12-medium text-text-strong">{props.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RawMessageContent(props: { message: Message; getParts: (id: string) => Part[]; onRendered: () => void }) {
|
||||
const file = createMemo(() => {
|
||||
const parts = props.getParts(props.message.id)
|
||||
const contents = JSON.stringify({ message: props.message, parts }, null, 2)
|
||||
return {
|
||||
name: `${props.message.role}-${props.message.id}.json`,
|
||||
contents,
|
||||
cacheKey: checksum(contents),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Code
|
||||
file={file()}
|
||||
overflow="wrap"
|
||||
class="select-text"
|
||||
onRendered={() => requestAnimationFrame(props.onRendered)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RawMessage(props: {
|
||||
message: Message
|
||||
getParts: (id: string) => Part[]
|
||||
onRendered: () => void
|
||||
time: (value: number | undefined) => string
|
||||
}) {
|
||||
return (
|
||||
<Accordion.Item value={props.message.id}>
|
||||
<StickyAccordionHeader>
|
||||
<Accordion.Trigger>
|
||||
<div class="flex items-center justify-between gap-2 w-full">
|
||||
<div class="min-w-0 truncate">
|
||||
{props.message.role} <span class="text-text-base">• {props.message.id}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="shrink-0 text-12-regular text-text-weak">{props.time(props.message.time.created)}</div>
|
||||
<Icon name="chevron-grabber-vertical" size="small" class="shrink-0 text-text-weak" />
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content class="bg-background-base">
|
||||
<div class="p-3">
|
||||
<RawMessageContent message={props.message} getParts={props.getParts} onRendered={props.onRendered} />
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionContextTab(props: SessionContextTabProps) {
|
||||
const params = useParams()
|
||||
const sync = useSync()
|
||||
@@ -106,7 +37,6 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
|
||||
const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all))
|
||||
const ctx = createMemo(() => metrics().context)
|
||||
const formatter = createMemo(() => createSessionContextFormatter(language.locale()))
|
||||
|
||||
const cost = createMemo(() => {
|
||||
return usd().format(metrics().totalCost)
|
||||
@@ -132,6 +62,23 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
return trimmed
|
||||
})
|
||||
|
||||
const number = (value: number | null | undefined) => {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toLocaleString(language.locale())
|
||||
}
|
||||
|
||||
const percent = (value: number | null | undefined) => {
|
||||
if (value === undefined) return "—"
|
||||
if (value === null) return "—"
|
||||
return value.toLocaleString(language.locale()) + "%"
|
||||
}
|
||||
|
||||
const time = (value: number | undefined) => {
|
||||
if (!value) return "—"
|
||||
return DateTime.fromMillis(value).setLocale(language.locale()).toLocaleString(DateTime.DATETIME_MED)
|
||||
}
|
||||
|
||||
const providerLabel = createMemo(() => {
|
||||
const c = ctx()
|
||||
if (!c) return "—"
|
||||
@@ -149,51 +96,197 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
() => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()],
|
||||
() => {
|
||||
const c = ctx()
|
||||
if (!c?.input) return []
|
||||
return estimateSessionContextBreakdown({
|
||||
messages: props.messages(),
|
||||
parts: sync.data.part as Record<string, Part[] | undefined>,
|
||||
input: c.input,
|
||||
systemPrompt: systemPrompt(),
|
||||
})
|
||||
if (!c) return []
|
||||
const input = c.input
|
||||
if (!input) return []
|
||||
|
||||
const out = {
|
||||
system: systemPrompt()?.length ?? 0,
|
||||
user: 0,
|
||||
assistant: 0,
|
||||
tool: 0,
|
||||
}
|
||||
|
||||
for (const msg of props.messages()) {
|
||||
const parts = (sync.data.part[msg.id] ?? []) as Part[]
|
||||
|
||||
if (msg.role === "user") {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") out.user += part.text.length
|
||||
if (part.type === "file") out.user += part.source?.text.value.length ?? 0
|
||||
if (part.type === "agent") out.user += part.source?.value.length ?? 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") out.assistant += part.text.length
|
||||
if (part.type === "reasoning") out.assistant += part.text.length
|
||||
if (part.type === "tool") {
|
||||
out.tool += Object.keys(part.state.input).length * 16
|
||||
if (part.state.status === "pending") out.tool += part.state.raw.length
|
||||
if (part.state.status === "completed") out.tool += part.state.output.length
|
||||
if (part.state.status === "error") out.tool += part.state.error.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const estimateTokens = (chars: number) => Math.ceil(chars / 4)
|
||||
const system = estimateTokens(out.system)
|
||||
const user = estimateTokens(out.user)
|
||||
const assistant = estimateTokens(out.assistant)
|
||||
const tool = estimateTokens(out.tool)
|
||||
const estimated = system + user + assistant + tool
|
||||
|
||||
const pct = (tokens: number) => (tokens / input) * 100
|
||||
const pctLabel = (tokens: number) => (Math.round(pct(tokens) * 10) / 10).toString() + "%"
|
||||
|
||||
const build = (tokens: { system: number; user: number; assistant: number; tool: number; other: number }) => {
|
||||
return [
|
||||
{
|
||||
key: "system",
|
||||
label: language.t("context.breakdown.system"),
|
||||
tokens: tokens.system,
|
||||
width: pct(tokens.system),
|
||||
percent: pctLabel(tokens.system),
|
||||
color: "var(--syntax-info)",
|
||||
},
|
||||
{
|
||||
key: "user",
|
||||
label: language.t("context.breakdown.user"),
|
||||
tokens: tokens.user,
|
||||
width: pct(tokens.user),
|
||||
percent: pctLabel(tokens.user),
|
||||
color: "var(--syntax-success)",
|
||||
},
|
||||
{
|
||||
key: "assistant",
|
||||
label: language.t("context.breakdown.assistant"),
|
||||
tokens: tokens.assistant,
|
||||
width: pct(tokens.assistant),
|
||||
percent: pctLabel(tokens.assistant),
|
||||
color: "var(--syntax-property)",
|
||||
},
|
||||
{
|
||||
key: "tool",
|
||||
label: language.t("context.breakdown.tool"),
|
||||
tokens: tokens.tool,
|
||||
width: pct(tokens.tool),
|
||||
percent: pctLabel(tokens.tool),
|
||||
color: "var(--syntax-warning)",
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: language.t("context.breakdown.other"),
|
||||
tokens: tokens.other,
|
||||
width: pct(tokens.other),
|
||||
percent: pctLabel(tokens.other),
|
||||
color: "var(--syntax-comment)",
|
||||
},
|
||||
].filter((x) => x.tokens > 0)
|
||||
}
|
||||
|
||||
if (estimated <= input) {
|
||||
return build({ system, user, assistant, tool, other: input - estimated })
|
||||
}
|
||||
|
||||
const scale = input / estimated
|
||||
const scaled = {
|
||||
system: Math.floor(system * scale),
|
||||
user: Math.floor(user * scale),
|
||||
assistant: Math.floor(assistant * scale),
|
||||
tool: Math.floor(tool * scale),
|
||||
}
|
||||
const scaledTotal = scaled.system + scaled.user + scaled.assistant + scaled.tool
|
||||
return build({ ...scaled, other: Math.max(0, input - scaledTotal) })
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const breakdownLabel = (key: SessionContextBreakdownKey) => {
|
||||
if (key === "system") return language.t("context.breakdown.system")
|
||||
if (key === "user") return language.t("context.breakdown.user")
|
||||
if (key === "assistant") return language.t("context.breakdown.assistant")
|
||||
if (key === "tool") return language.t("context.breakdown.tool")
|
||||
return language.t("context.breakdown.other")
|
||||
function Stat(statProps: { label: string; value: JSX.Element }) {
|
||||
return (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-12-regular text-text-weak">{statProps.label}</div>
|
||||
<div class="text-12-medium text-text-strong">{statProps.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const stats = [
|
||||
{ label: "context.stats.session", value: () => props.info()?.title ?? params.id ?? "—" },
|
||||
{ label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) },
|
||||
{ label: "context.stats.provider", value: providerLabel },
|
||||
{ label: "context.stats.model", value: modelLabel },
|
||||
{ label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) },
|
||||
{ label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) },
|
||||
{ label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) },
|
||||
{ label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) },
|
||||
{ label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.output) },
|
||||
{ label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) },
|
||||
{
|
||||
label: "context.stats.cacheTokens",
|
||||
value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`,
|
||||
},
|
||||
{ label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) },
|
||||
{ label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) },
|
||||
{ label: "context.stats.totalCost", value: cost },
|
||||
{ label: "context.stats.sessionCreated", value: () => formatter().time(props.info()?.time.created) },
|
||||
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||
] satisfies { label: string; value: () => JSX.Element }[]
|
||||
const stats = createMemo(() => {
|
||||
const c = ctx()
|
||||
const count = counts()
|
||||
return [
|
||||
{ label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" },
|
||||
{ label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) },
|
||||
{ label: language.t("context.stats.provider"), value: providerLabel() },
|
||||
{ label: language.t("context.stats.model"), value: modelLabel() },
|
||||
{ label: language.t("context.stats.limit"), value: number(c?.limit) },
|
||||
{ label: language.t("context.stats.totalTokens"), value: number(c?.total) },
|
||||
{ label: language.t("context.stats.usage"), value: percent(c?.usage) },
|
||||
{ label: language.t("context.stats.inputTokens"), value: number(c?.input) },
|
||||
{ label: language.t("context.stats.outputTokens"), value: number(c?.output) },
|
||||
{ label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) },
|
||||
{
|
||||
label: language.t("context.stats.cacheTokens"),
|
||||
value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
|
||||
},
|
||||
{ label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
|
||||
{
|
||||
label: language.t("context.stats.assistantMessages"),
|
||||
value: count.assistant.toLocaleString(language.locale()),
|
||||
},
|
||||
{ label: language.t("context.stats.totalCost"), value: cost() },
|
||||
{ label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
|
||||
{ label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },
|
||||
] satisfies { label: string; value: JSX.Element }[]
|
||||
})
|
||||
|
||||
function RawMessageContent(msgProps: { message: Message }) {
|
||||
const file = createMemo(() => {
|
||||
const parts = (sync.data.part[msgProps.message.id] ?? []) as Part[]
|
||||
const contents = JSON.stringify({ message: msgProps.message, parts }, null, 2)
|
||||
return {
|
||||
name: `${msgProps.message.role}-${msgProps.message.id}.json`,
|
||||
contents,
|
||||
cacheKey: checksum(contents),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Code file={file()} overflow="wrap" class="select-text" onRendered={() => requestAnimationFrame(restoreScroll)} />
|
||||
)
|
||||
}
|
||||
|
||||
function RawMessage(msgProps: { message: Message }) {
|
||||
return (
|
||||
<Accordion.Item value={msgProps.message.id}>
|
||||
<StickyAccordionHeader>
|
||||
<Accordion.Trigger>
|
||||
<div class="flex items-center justify-between gap-2 w-full">
|
||||
<div class="min-w-0 truncate">
|
||||
{msgProps.message.role} <span class="text-text-base">• {msgProps.message.id}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="shrink-0 text-12-regular text-text-weak">{time(msgProps.message.time.created)}</div>
|
||||
<Icon name="chevron-grabber-vertical" size="small" class="shrink-0 text-text-weak" />
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content class="bg-background-base">
|
||||
<div class="p-3">
|
||||
<RawMessageContent message={msgProps.message} />
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
const getParts = (id: string) => (sync.data.part[id] ?? []) as Part[]
|
||||
|
||||
const restoreScroll = () => {
|
||||
const el = scroll
|
||||
@@ -250,9 +343,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
>
|
||||
<div class="px-6 pt-4 flex flex-col gap-10">
|
||||
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
||||
<For each={stats}>
|
||||
{(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />}
|
||||
</For>
|
||||
<For each={stats()}>{(stat) => <Stat label={stat.label} value={stat.value} />}</For>
|
||||
</div>
|
||||
|
||||
<Show when={breakdown().length > 0}>
|
||||
@@ -265,7 +356,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
class="h-full"
|
||||
style={{
|
||||
width: `${segment.width}%`,
|
||||
"background-color": BREAKDOWN_COLOR[segment.key],
|
||||
"background-color": segment.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -275,9 +366,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
<For each={breakdown()}>
|
||||
{(segment) => (
|
||||
<div class="flex items-center gap-1 text-11-regular text-text-weak">
|
||||
<div class="size-2 rounded-sm" style={{ "background-color": BREAKDOWN_COLOR[segment.key] }} />
|
||||
<div>{breakdownLabel(segment.key)}</div>
|
||||
<div class="text-text-weaker">{segment.percent.toLocaleString(language.locale())}%</div>
|
||||
<div class="size-2 rounded-sm" style={{ "background-color": segment.color }} />
|
||||
<div>{segment.label}</div>
|
||||
<div class="text-text-weaker">{segment.percent}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
@@ -300,11 +391,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Accordion multiple>
|
||||
<For each={props.messages()}>
|
||||
{(message) => (
|
||||
<RawMessage message={message} getParts={getParts} onRendered={restoreScroll} time={formatter().time} />
|
||||
)}
|
||||
</For>
|
||||
<For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useParams } from "@solidjs/router"
|
||||
@@ -25,164 +25,6 @@ import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { StatusPopover } from "../status-popover"
|
||||
|
||||
const OPEN_APPS = [
|
||||
"vscode",
|
||||
"cursor",
|
||||
"zed",
|
||||
"textmate",
|
||||
"antigravity",
|
||||
"finder",
|
||||
"terminal",
|
||||
"iterm2",
|
||||
"ghostty",
|
||||
"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: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
|
||||
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
|
||||
{ id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
|
||||
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
|
||||
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
|
||||
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
|
||||
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
|
||||
{ id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
] as const
|
||||
|
||||
const WINDOWS_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
] as const
|
||||
|
||||
const LINUX_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
] as const
|
||||
|
||||
type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number]
|
||||
type OpenIcon = OpenApp | "file-explorer"
|
||||
const OPEN_ICON_BASE = new Set<OpenIcon>(["finder", "vscode", "cursor", "zed"])
|
||||
|
||||
const openIconSize = (id: OpenIcon) => (OPEN_ICON_BASE.has(id) ? "size-4" : "size-[19px]")
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
function useSessionShare(args: {
|
||||
globalSDK: ReturnType<typeof useGlobalSDK>
|
||||
currentSession: () =>
|
||||
| {
|
||||
id: string
|
||||
share?: {
|
||||
url?: string
|
||||
}
|
||||
}
|
||||
| undefined
|
||||
projectDirectory: () => string
|
||||
platform: ReturnType<typeof usePlatform>
|
||||
}) {
|
||||
const [state, setState] = createStore({
|
||||
share: false,
|
||||
unshare: false,
|
||||
copied: false,
|
||||
timer: undefined as number | undefined,
|
||||
})
|
||||
const shareUrl = createMemo(() => args.currentSession()?.share?.url)
|
||||
|
||||
createEffect(() => {
|
||||
const url = shareUrl()
|
||||
if (url) return
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
setState({ copied: false, timer: undefined })
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
})
|
||||
|
||||
const shareSession = () => {
|
||||
const session = args.currentSession()
|
||||
if (!session || state.share) return
|
||||
setState("share", true)
|
||||
args.globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: args.projectDirectory() })
|
||||
.catch((error) => {
|
||||
console.error("Failed to share session", error)
|
||||
})
|
||||
.finally(() => {
|
||||
setState("share", false)
|
||||
})
|
||||
}
|
||||
|
||||
const unshareSession = () => {
|
||||
const session = args.currentSession()
|
||||
if (!session || state.unshare) return
|
||||
setState("unshare", true)
|
||||
args.globalSDK.client.session
|
||||
.unshare({ sessionID: session.id, directory: args.projectDirectory() })
|
||||
.catch((error) => {
|
||||
console.error("Failed to unshare session", error)
|
||||
})
|
||||
.finally(() => {
|
||||
setState("unshare", false)
|
||||
})
|
||||
}
|
||||
|
||||
const copyLink = (onError: (error: unknown) => void) => {
|
||||
const url = shareUrl()
|
||||
if (!url) return
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
setState("copied", true)
|
||||
const timer = window.setTimeout(() => {
|
||||
setState("copied", false)
|
||||
setState("timer", undefined)
|
||||
}, 3000)
|
||||
setState("timer", timer)
|
||||
})
|
||||
.catch(onError)
|
||||
}
|
||||
|
||||
const viewShare = () => {
|
||||
const url = shareUrl()
|
||||
if (!url) return
|
||||
args.platform.openLink(url)
|
||||
}
|
||||
|
||||
return { state, shareUrl, shareSession, unshareSession, copyLink, viewShare }
|
||||
}
|
||||
|
||||
export function SessionHeader() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
@@ -211,7 +53,62 @@ export function SessionHeader() {
|
||||
const showShare = createMemo(() => shareEnabled() && !!currentSession())
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const os = createMemo(() => detectOS(platform))
|
||||
|
||||
const OPEN_APPS = [
|
||||
"vscode",
|
||||
"cursor",
|
||||
"zed",
|
||||
"textmate",
|
||||
"antigravity",
|
||||
"finder",
|
||||
"terminal",
|
||||
"iterm2",
|
||||
"ghostty",
|
||||
"xcode",
|
||||
"android-studio",
|
||||
"powershell",
|
||||
"sublime-text",
|
||||
] as const
|
||||
type OpenApp = (typeof OPEN_APPS)[number]
|
||||
|
||||
const MAC_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "Visual Studio Code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
|
||||
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
|
||||
{ id: "antigravity", label: "Antigravity", icon: "antigravity", openWith: "Antigravity" },
|
||||
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
|
||||
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
|
||||
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
|
||||
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
|
||||
{ id: "android-studio", label: "Android Studio", icon: "android-studio", openWith: "Android Studio" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
] as const
|
||||
|
||||
const WINDOWS_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "powershell", label: "PowerShell", icon: "powershell", openWith: "powershell" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
] as const
|
||||
|
||||
const LINUX_APPS = [
|
||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
||||
{ id: "sublime-text", label: "Sublime Text", icon: "sublime-text", openWith: "Sublime Text" },
|
||||
] as const
|
||||
|
||||
const os = createMemo<"macos" | "windows" | "linux" | "unknown">(() => {
|
||||
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 [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
|
||||
|
||||
@@ -257,6 +154,10 @@ export function SessionHeader() {
|
||||
] as const
|
||||
})
|
||||
|
||||
type OpenIcon = OpenApp | "file-explorer"
|
||||
const base = new Set<OpenIcon>(["finder", "vscode", "cursor", "zed"])
|
||||
const size = (id: OpenIcon) => (base.has(id) ? "size-4" : "size-[19px]")
|
||||
|
||||
const checksReady = createMemo(() => {
|
||||
if (platform.platform !== "desktop") return true
|
||||
if (!platform.checkAppExists) return true
|
||||
@@ -285,7 +186,13 @@ export function SessionHeader() {
|
||||
|
||||
const item = options().find((o) => o.id === app)
|
||||
const openWith = item && "openWith" in item ? item.openWith : undefined
|
||||
Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => showRequestError(language, err))
|
||||
Promise.resolve(platform.openPath?.(directory, openWith)).catch((err: unknown) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const copyPath = () => {
|
||||
@@ -301,15 +208,86 @@ export function SessionHeader() {
|
||||
description: directory,
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => showRequestError(language, err))
|
||||
.catch((err: unknown) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const share = useSessionShare({
|
||||
globalSDK,
|
||||
currentSession,
|
||||
projectDirectory,
|
||||
platform,
|
||||
const [state, setState] = createStore({
|
||||
share: false,
|
||||
unshare: false,
|
||||
copied: false,
|
||||
timer: undefined as number | undefined,
|
||||
})
|
||||
const shareUrl = createMemo(() => currentSession()?.share?.url)
|
||||
|
||||
createEffect(() => {
|
||||
const url = shareUrl()
|
||||
if (url) return
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
setState({ copied: false, timer: undefined })
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
})
|
||||
|
||||
function shareSession() {
|
||||
const session = currentSession()
|
||||
if (!session || state.share) return
|
||||
setState("share", true)
|
||||
globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.catch((error) => {
|
||||
console.error("Failed to share session", error)
|
||||
})
|
||||
.finally(() => {
|
||||
setState("share", false)
|
||||
})
|
||||
}
|
||||
|
||||
function unshareSession() {
|
||||
const session = currentSession()
|
||||
if (!session || state.unshare) return
|
||||
setState("unshare", true)
|
||||
globalSDK.client.session
|
||||
.unshare({ sessionID: session.id, directory: projectDirectory() })
|
||||
.catch((error) => {
|
||||
console.error("Failed to unshare session", error)
|
||||
})
|
||||
.finally(() => {
|
||||
setState("unshare", false)
|
||||
})
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
const url = shareUrl()
|
||||
if (!url) return
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
if (state.timer) window.clearTimeout(state.timer)
|
||||
setState("copied", true)
|
||||
const timer = window.setTimeout(() => {
|
||||
setState("copied", false)
|
||||
setState("timer", undefined)
|
||||
}, 3000)
|
||||
setState("timer", timer)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to copy share link", error)
|
||||
})
|
||||
}
|
||||
|
||||
function viewShare() {
|
||||
const url = shareUrl()
|
||||
if (!url) return
|
||||
platform.openLink(url)
|
||||
}
|
||||
|
||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||
@@ -321,13 +299,13 @@ export function SessionHeader() {
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[240px] max-w-full min-w-0 h-[24px] pl-0.5 pr-2 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-panel transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] max-w-full min-w-0 h-[24px] px-2 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-base bg-surface-panel transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
aria-label={language.t("session.header.searchFiles")}
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1.5 overflow-visible">
|
||||
<Icon name="magnifying-glass" size="small" class="icon-base shrink-0 size-4" />
|
||||
<span class="flex-1 min-w-0 text-12-regular text-text-weak truncate text-left">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
|
||||
<Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
|
||||
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate h-4.5 flex items-center">
|
||||
{language.t("session.header.search.placeholder", { project: name() })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -344,17 +322,17 @@ export function SessionHeader() {
|
||||
<Show when={rightMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<StatusPopover />
|
||||
<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">
|
||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-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"
|
||||
class="rounded-none h-full py-0 pr-3 pl-2 gap-2 border-none shadow-none"
|
||||
onClick={copyPath}
|
||||
aria-label={language.t("session.header.open.copyPath")}
|
||||
>
|
||||
@@ -367,10 +345,10 @@ export function SessionHeader() {
|
||||
}
|
||||
>
|
||||
<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">
|
||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-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"
|
||||
class="rounded-none h-full py-0 pr-3 pl-2 gap-1.5 border-none shadow-none"
|
||||
onClick={() => openDir(current().id)}
|
||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||
>
|
||||
@@ -379,9 +357,9 @@ export function SessionHeader() {
|
||||
</div>
|
||||
<span class="text-12-regular text-text-strong">Open</span>
|
||||
</Button>
|
||||
<div class="self-stretch w-px bg-border-weak-base" />
|
||||
<div class="self-stretch w-px bg-border-base/70" />
|
||||
<DropdownMenu
|
||||
gutter={4}
|
||||
gutter={6}
|
||||
placement="bottom-end"
|
||||
open={menu.open}
|
||||
onOpenChange={(open) => setMenu("open", open)}
|
||||
@@ -390,7 +368,7 @@ export function SessionHeader() {
|
||||
as={IconButton}
|
||||
icon="chevron-down"
|
||||
variant="ghost"
|
||||
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-hover"
|
||||
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active"
|
||||
aria-label={language.t("session.header.open.menu")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
@@ -404,25 +382,23 @@ export function SessionHeader() {
|
||||
setPrefs("app", value as OpenApp)
|
||||
}}
|
||||
>
|
||||
<For each={options()}>
|
||||
{(o) => (
|
||||
<DropdownMenu.RadioItem
|
||||
value={o.id}
|
||||
onSelect={() => {
|
||||
setMenu("open", false)
|
||||
openDir(o.id)
|
||||
}}
|
||||
>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={o.icon} class={openIconSize(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>
|
||||
{options().map((o) => (
|
||||
<DropdownMenu.RadioItem
|
||||
value={o.id}
|
||||
onSelect={() => {
|
||||
setMenu("open", false)
|
||||
openDir(o.id)
|
||||
}}
|
||||
>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={o.icon} class={size(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>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
@@ -452,11 +428,11 @@ export function SessionHeader() {
|
||||
<Popover
|
||||
title={language.t("session.share.popover.title")}
|
||||
description={
|
||||
share.shareUrl()
|
||||
shareUrl()
|
||||
? language.t("session.share.popover.description.shared")
|
||||
: language.t("session.share.popover.description.unshared")
|
||||
}
|
||||
gutter={4}
|
||||
gutter={6}
|
||||
placement="bottom-end"
|
||||
shift={-64}
|
||||
class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
|
||||
@@ -465,24 +441,24 @@ export function SessionHeader() {
|
||||
variant: "ghost",
|
||||
class:
|
||||
"rounded-md h-[24px] px-3 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active",
|
||||
classList: { "rounded-r-none": share.shareUrl() !== undefined },
|
||||
classList: { "rounded-r-none": shareUrl() !== undefined },
|
||||
style: { scale: 1 },
|
||||
}}
|
||||
trigger={<span class="text-12-regular">{language.t("session.share.action.share")}</span>}
|
||||
trigger={language.t("session.share.action.share")}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Show
|
||||
when={share.shareUrl()}
|
||||
when={shareUrl()}
|
||||
fallback={
|
||||
<div class="flex">
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-1/2"
|
||||
onClick={share.shareSession}
|
||||
disabled={share.state.share}
|
||||
onClick={shareSession}
|
||||
disabled={state.share}
|
||||
>
|
||||
{share.state.share
|
||||
{state.share
|
||||
? language.t("session.share.action.publishing")
|
||||
: language.t("session.share.action.publish")}
|
||||
</Button>
|
||||
@@ -491,7 +467,7 @@ export function SessionHeader() {
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<TextField
|
||||
value={share.shareUrl() ?? ""}
|
||||
value={shareUrl() ?? ""}
|
||||
readOnly
|
||||
copyable
|
||||
copyKind="link"
|
||||
@@ -503,10 +479,10 @@ export function SessionHeader() {
|
||||
size="large"
|
||||
variant="secondary"
|
||||
class="w-full shadow-none border border-border-weak-base"
|
||||
onClick={share.unshareSession}
|
||||
disabled={share.state.unshare}
|
||||
onClick={unshareSession}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{share.state.unshare
|
||||
{state.unshare
|
||||
? language.t("session.share.action.unpublishing")
|
||||
: language.t("session.share.action.unpublish")}
|
||||
</Button>
|
||||
@@ -514,8 +490,8 @@ export function SessionHeader() {
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-full"
|
||||
onClick={share.viewShare}
|
||||
disabled={share.state.unshare}
|
||||
onClick={viewShare}
|
||||
disabled={state.unshare}
|
||||
>
|
||||
{language.t("session.share.action.view")}
|
||||
</Button>
|
||||
@@ -524,10 +500,10 @@ export function SessionHeader() {
|
||||
</Show>
|
||||
</div>
|
||||
</Popover>
|
||||
<Show when={share.shareUrl()} fallback={<div aria-hidden="true" />}>
|
||||
<Show when={shareUrl()} fallback={<div aria-hidden="true" />}>
|
||||
<Tooltip
|
||||
value={
|
||||
share.state.copied
|
||||
state.copied
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("session.share.copy.copyLink")
|
||||
}
|
||||
@@ -535,13 +511,13 @@ export function SessionHeader() {
|
||||
gutter={8}
|
||||
>
|
||||
<IconButton
|
||||
icon={share.state.copied ? "check" : "link"}
|
||||
icon={state.copied ? "check" : "link"}
|
||||
variant="ghost"
|
||||
class="rounded-l-none h-[24px] border border-border-base bg-surface-panel shadow-none"
|
||||
onClick={() => share.copyLink((error) => showRequestError(language, error))}
|
||||
disabled={share.state.unshare}
|
||||
onClick={copyLink}
|
||||
disabled={state.unshare}
|
||||
aria-label={
|
||||
share.state.copied
|
||||
state.copied
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("session.share.copy.copyLink")
|
||||
}
|
||||
@@ -550,97 +526,94 @@ export function SessionHeader() {
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.terminal.toggle")}
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
<div class="hidden md:flex items-center gap-3 ml-2 shrink-0">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.terminal.toggle")}
|
||||
keybind={command.keybind("terminal.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle size-6 p-0"
|
||||
onClick={() => view().terminal.toggle()}
|
||||
aria-label={language.t("command.terminal.toggle")}
|
||||
aria-expanded={view().terminal.opened()}
|
||||
aria-controls="terminal-panel"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||
onClick={() => view().terminal.toggle()}
|
||||
aria-label={language.t("command.terminal.toggle")}
|
||||
aria-expanded={view().terminal.opened()}
|
||||
aria-controls="terminal-panel"
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom-partial" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-partial"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
|
||||
<TooltipKeybind
|
||||
title={language.t("command.review.toggle")}
|
||||
keybind={command.keybind("review.toggle")}
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="hidden md:block shrink-0">
|
||||
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/review-toggle size-6 p-0"
|
||||
onClick={() => view().reviewPanel.toggle()}
|
||||
aria-label={language.t("command.review.toggle")}
|
||||
aria-expanded={view().reviewPanel.opened()}
|
||||
aria-controls="review-panel"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-right"}
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-right-partial"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-partial"}
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
|
||||
<TooltipKeybind
|
||||
title={language.t("command.fileTree.toggle")}
|
||||
keybind={command.keybind("fileTree.toggle")}
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-right-partial"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="hidden md:block shrink-0">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.fileTree.toggle")}
|
||||
keybind={command.keybind("fileTree.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/file-tree-toggle size-6 p-0"
|
||||
onClick={() => layout.fileTree.toggle()}
|
||||
aria-label={language.t("command.fileTree.toggle")}
|
||||
aria-expanded={layout.fileTree.opened()}
|
||||
aria-controls="file-tree-panel"
|
||||
>
|
||||
<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 class="relative flex items-center justify-center size-4">
|
||||
<Icon
|
||||
size="small"
|
||||
name="bullet-list"
|
||||
classList={{
|
||||
"text-icon-strong": layout.fileTree.opened(),
|
||||
"text-icon-weak": !layout.fileTree.opened(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
@@ -8,8 +8,6 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
const MAIN_WORKTREE = "main"
|
||||
const CREATE_WORKTREE = "create"
|
||||
const ROOT_CLASS =
|
||||
"size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto 2xl:max-w-[1000px] px-6 pb-16"
|
||||
|
||||
interface NewSessionViewProps {
|
||||
worktree: string
|
||||
@@ -49,7 +47,7 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={ROOT_CLASS}>
|
||||
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto 2xl:max-w-[1000px] px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
|
||||
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user