diff --git a/.github/workflows/auto-label-tui.yml b/.github/workflows/auto-label-tui.yml deleted file mode 100644 index c2f81a380..000000000 --- a/.github/workflows/auto-label-tui.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Auto-label TUI Issues - -on: - issues: - types: [opened] - -jobs: - auto-label: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: write - steps: - - name: Auto-label and assign issues - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const issue = context.payload.issue; - const title = issue.title; - const description = issue.body || ''; - - // Check for "opencode web" keyword - const webPattern = /(opencode web)/i; - const isWebRelated = webPattern.test(title) || webPattern.test(description); - - // Check for version patterns like v1.0.x or 1.0.x - const versionPattern = /[v]?1\.0\./i; - const isVersionRelated = versionPattern.test(title) || versionPattern.test(description); - - // Check for "nix" keyword - const nixPattern = /\bnix\b/i; - const isNixRelated = nixPattern.test(title) || nixPattern.test(description); - - const labels = []; - - if (isWebRelated) { - labels.push('web'); - - // Assign to adamdotdevin - await github.rest.issues.addAssignees({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - assignees: ['adamdotdevin'] - }); - } else if (isVersionRelated) { - // Only add opentui if NOT web-related - labels.push('opentui'); - } - - if (isNixRelated) { - labels.push('nix'); - } - - if (labels.length > 0) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: labels - }); - } diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml new file mode 100644 index 000000000..559e74176 --- /dev/null +++ b/.github/workflows/docs-update.yml @@ -0,0 +1,70 @@ +name: Docs Update + +on: + schedule: + # Run every 4 hours + - cron: "0 */4 * * *" + workflow_dispatch: # Allow manual trigger for testing + +jobs: + update-docs: + if: github.repository == 'sst/opencode' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + id-token: write + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch full history to access commits + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Get recent commits + id: commits + run: | + COMMITS=$(git log --since="4 hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "") + if [ -z "$COMMITS" ]; then + echo "No commits in the last 4 hours" + echo "has_commits=false" >> $GITHUB_OUTPUT + else + echo "has_commits=true" >> $GITHUB_OUTPUT + { + echo "list<> $GITHUB_OUTPUT + fi + + - name: Run opencode + if: steps.commits.outputs.has_commits == 'true' + uses: sst/opencode/github@latest + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + with: + model: opencode/gpt-5.2 + agent: docs + prompt: | + Review the following commits from the last 4 hours and identify any new features that may need documentation. + + + ${{ steps.commits.outputs.list }} + + + Steps: + 1. For each commit that looks like a new feature or significant change: + - Read the changed files to understand what was added + - Check if the feature is already documented in packages/web/src/content/docs/* + 2. If you find undocumented features: + - Update the relevant documentation files in packages/web/src/content/docs/* + - Follow the existing documentation style and structure + - Make sure to document the feature clearly with examples where appropriate + 3. If all new features are already documented, report that no updates are needed + 4. If you are creating a new documentation file be sure to update packages/web/astro.config.mjs too. + + Focus on user-facing features and API changes. Skip internal refactors, bug fixes, and test updates unless they affect user-facing behavior. + Don't feel the need to document every little thing. It is perfectly okay to make 0 changes at all. + Try to keep documentation only for large features or changes that already have a good spot to be documented. diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 5969d9d41..dc82d297b 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -16,6 +16,8 @@ jobs: with: fetch-depth: 1 + - uses: ./.github/actions/setup-bun + - name: Install opencode run: curl -fsSL https://opencode.ai/install | bash diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index 326090f7a..29cc98953 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -2,11 +2,8 @@ name: generate on: push: - branches-ignore: - - production - pull_request: - branches-ignore: - - production + branches: + - dev workflow_dispatch: jobs: @@ -14,6 +11,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: write + pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v4 @@ -25,14 +23,29 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun - - name: Generate SDK - run: | - bun ./packages/sdk/js/script/build.ts - (cd packages/opencode && bun dev generate > ../sdk/openapi.json) - bun x prettier --write packages/sdk/openapi.json + - name: Generate + run: ./script/generate.ts - - name: Format - run: ./script/format.ts - env: - CI: true - PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + - name: Commit and push + run: | + if [ -z "$(git status --porcelain)" ]; then + echo "No changes to commit" + exit 0 + fi + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add -A + git commit -m "chore: generate" + git push origin HEAD:${{ github.ref_name }} --no-verify + # if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then + # echo "" + # echo "============================================" + # echo "Failed to push generated code." + # echo "Please run locally and push:" + # echo "" + # echo " ./script/generate.ts" + # echo " git add -A && git commit -m \"chore: generate\" && git push" + # echo "" + # echo "============================================" + # exit 1 + # fi diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml index d12cc7d73..62577ecf0 100644 --- a/.github/workflows/notify-discord.yml +++ b/.github/workflows/notify-discord.yml @@ -2,7 +2,7 @@ name: discord on: release: - types: [published] # fires only when a release is published + types: [released] # fires when a draft release is published jobs: notify: diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 4c75ad2e0..37210191e 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -31,4 +31,4 @@ jobs: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_PERMISSION: '{"bash": "deny"}' with: - model: opencode/claude-haiku-4-5 + model: opencode/claude-opus-4-5 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 96b9280fb..9e6b49339 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ on: required: false type: string -concurrency: ${{ github.workflow }}-${{ github.ref }} +concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }} permissions: id-token: write @@ -41,21 +41,9 @@ jobs: - uses: ./.github/actions/setup-bun - - name: Setup SSH for AUR - if: inputs.bump || inputs.version - run: | - sudo apt-get update - sudo apt-get install -y pacman-package-manager - mkdir -p ~/.ssh - echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true - - name: Install OpenCode if: inputs.bump || inputs.version - run: bun i -g opencode-ai@1.0.143 + run: bun i -g opencode-ai@1.0.169 - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -64,14 +52,26 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - uses: actions/setup-node@v4 with: node-version: "24" registry-url: "https://registry.npmjs.org" + - name: Setup Git Identity + run: | + git config --global user.email "opencode@sst.dev" + git config --global user.name "opencode" + git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }} + - name: Publish id: publish - run: ./script/publish.ts + run: ./script/publish-start.ts env: OPENCODE_BUMP: ${{ inputs.bump }} OPENCODE_VERSION: ${{ inputs.version }} @@ -79,9 +79,16 @@ jobs: AUR_KEY: ${{ secrets.AUR_KEY }} GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: false + + - uses: actions/upload-artifact@v4 + with: + name: opencode-cli + path: packages/opencode/dist + outputs: - releaseId: ${{ steps.publish.outputs.releaseId }} - tagName: ${{ steps.publish.outputs.tagName }} + release: ${{ steps.publish.outputs.release }} + tag: ${{ steps.publish.outputs.tag }} + version: ${{ steps.publish.outputs.version }} publish-tauri: needs: publish @@ -98,11 +105,14 @@ jobs: target: x86_64-pc-windows-msvc - host: blacksmith-4vcpu-ubuntu-2404 target: x86_64-unknown-linux-gnu + - host: blacksmith-4vcpu-ubuntu-2404-arm + target: aarch64-unknown-linux-gnu runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + ref: ${{ needs.publish.outputs.tag }} - uses: apple-actions/import-codesign-certs@v2 if: ${{ runner.os == 'macOS' }} @@ -149,21 +159,25 @@ jobs: cd packages/tauri bun ./scripts/prepare.ts env: - OPENCODE_BUMP: ${{ inputs.bump }} - OPENCODE_VERSION: ${{ inputs.version }} - OPENCODE_CHANNEL: latest + OPENCODE_VERSION: ${{ needs.publish.outputs.version }} NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} AUR_KEY: ${{ secrets.AUR_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} RUST_TARGET: ${{ matrix.settings.target }} GH_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - - run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage + - name: Install tauri-cli from portable appimage branch if: contains(matrix.settings.host, 'ubuntu') + 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 + timeout-minutes: 20 uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -180,8 +194,40 @@ jobs: projectPath: packages/tauri uploadWorkflowArtifacts: true tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} - args: --target ${{ matrix.settings.target }} + args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose updaterJsonPreferNsis: true - releaseId: ${{ needs.publish.outputs.releaseId }} - tagName: ${{ needs.publish.outputs.tagName }} + releaseId: ${{ needs.publish.outputs.release }} + tagName: ${{ needs.publish.outputs.tag }} releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext] + releaseDraft: true + + publish-release: + needs: + - publish + - publish-tauri + if: needs.publish.outputs.tag + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ needs.publish.outputs.tag }} + + - uses: ./.github/actions/setup-bun + + - name: Setup SSH for AUR + run: | + sudo apt-get update + sudo apt-get install -y pacman-package-manager + mkdir -p ~/.ssh + echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + git config --global user.email "opencode@sst.dev" + git config --global user.name "opencode" + ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true + + - run: ./script/publish-complete.ts + env: + OPENCODE_VERSION: ${{ needs.publish.outputs.version }} + AUR_KEY: ${{ secrets.AUR_KEY }} + GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} diff --git a/.github/workflows/release-github-action.yml b/.github/workflows/release-github-action.yml new file mode 100644 index 000000000..3f5caa55c --- /dev/null +++ b/.github/workflows/release-github-action.yml @@ -0,0 +1,29 @@ +name: release-github-action + +on: + push: + branches: + - dev + paths: + - "github/**" + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: + contents: write + +jobs: + release: + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - run: git fetch --force --tags + + - name: Release + run: | + git config --global user.email "opencode@sst.dev" + git config --global user.name "opencode" + ./github/script/release diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index d974e2a76..c0e3a5deb 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -29,6 +29,8 @@ jobs: with: fetch-depth: 1 + - uses: ./.github/actions/setup-bun + - name: Install opencode run: curl -fsSL https://opencode.ai/install | bash @@ -65,6 +67,8 @@ jobs: When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts) Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block. + If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors. + Generally, write a comment instead of writing suggested change if you can help it. Command MUST be like this. \`\`\` diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml index 97e924517..57e93642b 100644 --- a/.github/workflows/stats.yml +++ b/.github/workflows/stats.yml @@ -5,8 +5,11 @@ on: - cron: "0 12 * * *" # Run daily at 12:00 UTC workflow_dispatch: # Allow manual trigger +concurrency: ${{ github.workflow }}-${{ github.ref }} + jobs: stats: + if: github.repository == 'sst/opencode' runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: write diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml new file mode 100644 index 000000000..6e1509572 --- /dev/null +++ b/.github/workflows/triage.yml @@ -0,0 +1,37 @@ +name: Issue Triage + +on: + issues: + types: [opened] + +jobs: + triage: + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Install opencode + run: curl -fsSL https://opencode.ai/install | bash + + - name: Triage issue + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + run: | + opencode run --agent triage "The following issue was just opened, triage it: + + Title: $ISSUE_TITLE + + $ISSUE_BODY" diff --git a/.gitignore b/.gitignore index 3d4f9095a..7b9c006f9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ Session.vim opencode.json a.out target +.scripts diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md new file mode 100644 index 000000000..b2db100e9 --- /dev/null +++ b/.opencode/agent/triage.md @@ -0,0 +1,77 @@ +--- +mode: primary +hidden: true +model: opencode/claude-haiku-4-5 +tools: + "*": false + "github-triage": true +--- + +You are a triage agent responsible for triaging github issues. + +Use your github-triage tool to triage issues. + +## Labels + +### windows + +Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows. + +- Use if they mention WSL too + +#### perf + +Performance-related issues: + +- Slow performance +- High RAM usage +- High CPU usage + +**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness. + +#### desktop + +Desktop app issues: + +- `opencode web` command +- The desktop app itself + +**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues. + +#### nix + +**Only** add if the issue explicitly mentions nix. + +#### zen + +**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers. + +If the issue doesn't have "zen" in it then don't add zen label + +#### docs + +Add if the issue requests better documentation or docs updates. + +#### opentui + +TUI issues potentially caused by our underlying TUI library: + +- Keybindings not working +- Scroll speed issues (too fast/slow/laggy) +- Screen flickering +- Crashes with opentui in the log + +**Do not** add for general TUI bugs. + +When assigning to people here are the following rules: + +adamdotdev: +ONLY assign adam if the issue will have the "desktop" label. + +fwang: +ONLY assign fwang if the issue will have the "zen" label. + +jayair: +ONLY assign jayair if the issue will have the "docs" label. + +In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node. diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index c318ed54b..8e9346ebc 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,6 +1,7 @@ --- description: git commit and push model: opencode/glm-4.6 +subtask: true --- commit and push diff --git a/.opencode/env.d.ts b/.opencode/env.d.ts new file mode 100644 index 000000000..f2b13a934 --- /dev/null +++ b/.opencode/env.d.ts @@ -0,0 +1,4 @@ +declare module "*.txt" { + const content: string + export default content +} diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index fe70e35fa..cbcbb0c65 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -10,4 +10,8 @@ "options": {}, }, }, + "mcp": {}, + "tools": { + "github-triage": false, + }, } diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts new file mode 100644 index 000000000..a5e6c811d --- /dev/null +++ b/.opencode/tool/github-triage.ts @@ -0,0 +1,90 @@ +/// +// import { Octokit } from "@octokit/rest" +import { tool } from "@opencode-ai/plugin" +import DESCRIPTION from "./github-triage.txt" + +function getIssueNumber(): number { + const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10) + if (!issue) throw new Error("ISSUE_NUMBER env var not set") + return issue +} + +async function githubFetch(endpoint: string, options: RequestInit = {}) { + const response = await fetch(`https://api.github.com${endpoint}`, { + ...options, + headers: { + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + Accept: "application/vnd.github+json", + "Content-Type": "application/json", + ...options.headers, + }, + }) + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +export default tool({ + description: DESCRIPTION, + args: { + assignee: tool.schema + .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", "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 = "sst" + const repo = "opencode" + + const results: string[] = [] + + if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) { + throw new Error("Only desktop issues should be assigned to adamdotdevin") + } + + if (args.assignee === "fwang" && !args.labels.includes("zen")) { + throw new Error("Only zen issues should be assigned to fwang") + } + + 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: [args.assignee] }), + }) + 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: ${args.labels.join(", ")}`) + } + + return results.join("\n") + }, +}) diff --git a/.opencode/tool/github-triage.txt b/.opencode/tool/github-triage.txt new file mode 100644 index 000000000..4c46a72c1 --- /dev/null +++ b/.opencode/tool/github-triage.txt @@ -0,0 +1,88 @@ +Use this tool to assign and/or label a Github issue. + +You can assign the following users: +- thdxr +- adamdotdevin +- fwang +- jayair +- kommander +- rekram1-node + + +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a24995e8..c16d664a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you - `packages/plugin`: Source for `@opencode-ai/plugin` > [!NOTE] -> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk. +> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files. Please try to follow the [style guide](./STYLE_GUIDE.md) diff --git a/README.md b/README.md index eb0295c9c..5295810b6 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,29 @@ scoop bucket add extras; scoop install extras/opencode # Windows choco install opencode # Windows brew install opencode # macOS and Linux paru -S opencode-bin # Arch Linux -mise use -g ubi:sst/opencode # Any OS +mise use -g github:sst/opencode # Any OS nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch ``` > [!TIP] > Remove versions older than 0.1.x before installing. +### Desktop App (BETA) + +OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download). + +| Platform | 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`, or AppImage | + +```bash +# macOS (Homebrew) +brew install --cask opencode-desktop +``` + #### Installation Directory The install script respects the following priority order for the installation path: @@ -78,7 +94,7 @@ If you're interested in contributing to OpenCode, please read our [contributing ### Building on OpenCode -If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway. +If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way. ### FAQ diff --git a/README.zh-TW.md b/README.zh-TW.md new file mode 100644 index 000000000..d3cbb263c --- /dev/null +++ b/README.zh-TW.md @@ -0,0 +1,115 @@ +

+ + + + + OpenCode logo + + +

+

開源的 AI Coding Agent。

+

+ Discord + npm + Build status +

+ +[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) + +--- + +### 安裝 + +```bash +# 直接安裝 (YOLO) +curl -fsSL https://opencode.ai/install | bash + +# 套件管理員 +npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn +scoop bucket add extras; scoop install extras/opencode # Windows +choco install opencode # Windows +brew install opencode # macOS 與 Linux +paru -S opencode-bin # Arch Linux +mise use -g github:sst/opencode # 任何作業系統 +nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支 +``` + +> [!TIP] +> 安裝前請先移除 0.1.x 以前的舊版本。 + +### 桌面應用程式 (BETA) + +OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/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 Cask) +brew install --cask opencode-desktop +``` + +#### 安裝目錄 + +安裝腳本會依據以下優先順序決定安裝路徑: + +1. `$OPENCODE_INSTALL_DIR` - 自定義安裝目錄 +2. `$XDG_BIN_DIR` - 符合 XDG 基礎目錄規範的路徑 +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 +``` + +### Agents + +OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。 + +- **build** - 預設模式,具備完整權限的 Agent,適用於開發工作。 +- **plan** - 唯讀模式,適用於程式碼分析與探索。 + - 預設禁止修改檔案。 + - 執行 bash 指令前會詢問權限。 + - 非常適合用來探索陌生的程式碼庫或規劃變更。 + +此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。 + +了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。 + +### 線上文件 + +關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。 + +### 參與貢獻 + +如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。 + +### 基於 OpenCode 進行開發 + +如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。 + +### 常見問題 (FAQ) + +#### 這跟 Claude Code 有什麼不同? + +在功能面上與 Claude Code 非常相似。以下是關鍵差異: + +- 100% 開源。 +- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。 +- 內建 LSP (語言伺服器協定) 支援。 +- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。 +- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。 + +#### 另一個同名的 Repo 是什麼? + +另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。 + +--- + +**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) diff --git a/STATS.md b/STATS.md index 8a3e0d553..7f59be1aa 100644 --- a/STATS.md +++ b/STATS.md @@ -168,3 +168,13 @@ | 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) | | 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) | | 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) | +| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) | +| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) | +| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) | +| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) | +| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) | +| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) | +| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) | +| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) | +| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) | +| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) | diff --git a/bun.lock b/bun.lock index 5bc89cf56..706347b59 100644 --- a/bun.lock +++ b/bun.lock @@ -6,11 +6,13 @@ "name": "opencode", "dependencies": { "@aws-sdk/client-s3": "3.933.0", + "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "typescript": "catalog:", }, "devDependencies": { + "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "husky": "9.1.7", "prettier": "3.6.2", @@ -20,7 +22,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -48,7 +50,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -75,7 +77,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -99,7 +101,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -123,7 +125,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -133,9 +135,10 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", - "@solid-primitives/storage": "4.3.3", + "@solid-primitives/storage": "catalog:", "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", @@ -152,6 +155,7 @@ "solid-list": "catalog:", "tailwindcss": "catalog:", "virtua": "catalog:", + "zod": "catalog:", }, "devDependencies": { "@happy-dom/global-registrator": "20.0.11", @@ -169,11 +173,11 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", - "@pierre/precision-diffs": "catalog:", + "@pierre/diffs": "catalog:", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", "@solidjs/start": "catalog:", @@ -198,10 +202,10 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@octokit/auth-app": "8.0.1", - "@octokit/rest": "22.0.0", + "@octokit/rest": "catalog:", "hono": "catalog:", "jose": "6.0.11", }, @@ -214,7 +218,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.152", + "version": "1.0.186", "bin": { "opencode": "./bin/opencode", }, @@ -237,17 +241,17 @@ "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.15.1", "@octokit/graphql": "9.0.2", - "@octokit/rest": "22.0.0", + "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.2", - "@opentui/core": "0.0.0-20251211-4403a69a", - "@opentui/solid": "0.0.0-20251211-4403a69a", + "@opentui/core": "0.1.62", + "@opentui/solid": "0.1.62", "@parcel/watcher": "2.5.1", - "@pierre/precision-diffs": "catalog:", + "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", @@ -306,7 +310,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -326,7 +330,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.152", + "version": "1.0.186", "devDependencies": { "@hey-api/openapi-ts": "0.88.1", "@tsconfig/node22": "catalog:", @@ -337,7 +341,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -350,11 +354,13 @@ }, "packages/tauri": { "name": "@opencode-ai/tauri", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@opencode-ai/desktop": "workspace:*", + "@solid-primitives/storage": "catalog:", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-os": "~2", "@tauri-apps/plugin-process": "~2", @@ -375,12 +381,12 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@pierre/precision-diffs": "catalog:", + "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", "@solid-primitives/resize-observer": "2.1.3", @@ -410,7 +416,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "zod": "catalog:", }, @@ -421,7 +427,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -469,15 +475,17 @@ "@cloudflare/workers-types": "4.20251008.0", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", + "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.6.1", + "@pierre/diffs": "1.0.0-beta.3", + "@solid-primitives/storage": "4.3.3", "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.3", + "@types/bun": "1.3.4", "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@typescript/native-preview": "7.0.0-dev.20251207.1", @@ -499,7 +507,7 @@ "zod": "4.1.8", }, "packages": { - "@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], + "@actions/artifact": ["@actions/artifact@5.0.1", "", { "dependencies": { "@actions/core": "^2.0.0", "@actions/github": "^6.0.1", "@actions/http-client": "^3.0.0", "@azure/storage-blob": "^12.29.1", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-dHJ5rHduhCKUikKTT9eXeWoUvfKia3IjR1sO/VTAV3DVAL4yMTRnl2iO5mcfiBjySHLwPNezwENAVskKYU5ymw=="], "@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="], @@ -507,7 +515,7 @@ "@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="], - "@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + "@actions/http-client": ["@actions/http-client@3.0.0", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.28.5" } }, "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ=="], "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], @@ -643,7 +651,7 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.1", "", {}, "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww=="], - "@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], + "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], @@ -659,7 +667,7 @@ "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="], - "@azure/core-tracing": ["@azure/core-tracing@1.0.0-preview.13", "", { "dependencies": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" } }, "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ=="], + "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], @@ -1081,11 +1089,11 @@ "@octokit/auth-oauth-user": ["@octokit/auth-oauth-user@6.0.2", "", { "dependencies": { "@octokit/auth-oauth-device": "^8.0.3", "@octokit/oauth-methods": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A=="], - "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + "@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], - "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + "@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="], - "@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], "@octokit/graphql": ["@octokit/graphql@9.0.2", "", { "dependencies": { "@octokit/request": "^10.0.4", "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw=="], @@ -1097,15 +1105,15 @@ "@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="], - "@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], + "@octokit/plugin-request-log": ["@octokit/plugin-request-log@1.0.4", "", { "peerDependencies": { "@octokit/core": ">=3" } }, "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA=="], "@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="], "@octokit/plugin-retry": ["@octokit/plugin-retry@3.0.9", "", { "dependencies": { "@octokit/types": "^6.0.3", "bottleneck": "^2.15.3" } }, "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ=="], - "@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], - "@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], "@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="], @@ -1155,21 +1163,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.0.0-20251211-4403a69a", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-darwin-x64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-x64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-x64": "0.0.0-20251211-4403a69a", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wTZKcokyU9yiDqyC0Pvf9eRSdT73s4Ynerkit/z8Af++tynqrTlZHZCXK3o42Ff7itCSILmijcTU94n69aEypA=="], + "@opentui/core": ["@opentui/core@0.1.62", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.62", "@opentui/core-darwin-x64": "0.1.62", "@opentui/core-linux-arm64": "0.1.62", "@opentui/core-linux-x64": "0.1.62", "@opentui/core-win32-arm64": "0.1.62", "@opentui/core-win32-x64": "0.1.62", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-T9wsXaS4rFoZF2loaEFqAeuGj5DV3pJzrk18z1um3UfUS2NNH4jyDh5rDdHPb2/YrvO1lU9hd0VoAS/7zUAq/w=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VAYjTa+Eiauy8gETXadD8y0PE6ppnKasDK1X354VoexZiWFR3r7rkL+TfDfk7whhqXDYyT44JDT1QmCAhVXRzQ=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.62", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IohPhCkD/DbZEH4M5ft1/o1pI6Vvw2pdxdyoouW/TO1g21W5G8usaWTSRDXO+16BT115Nfb9/DT69H5pzAc2Eg=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "x64" }, "sha512-n9oVMpsojlILj1soORZzZ2Mjh8Zl73ZNcY7ot0iRmOjBDccrjDTsqKfxoGjKNd/xJSphLeu1LYGlcI5O5OczWQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.62", "", { "os": "darwin", "cpu": "x64" }, "sha512-BqbjQl2sLYrJ1Pq1b3H1I2CFedRiMz0QtZX08IMbyZ5kok+J0A8eQS5tmlbfqoS/VH0de9XiEbuHjG09/nSj1A=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "arm64" }, "sha512-vf4eUjPMI4ANitK4MpTGenZFddKgQD/K21aN6cZjusnH3mTEJAoIR7GbNtMdz3qclU43ajpzTID9sAwhshwdVQ=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.62", "", { "os": "linux", "cpu": "arm64" }, "sha512-P5FleF+W8O4uGubqBvV8DB1AK0+fJhJS8HvfmTZQ2DhSSJJH9Af/WXqitD7ILQY9ltlaUP7l38BC5cVdxnWzCQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "x64" }, "sha512-61635Up0YvVJ8gZ2eMiL1c8OfA+U6wAzT++LoaurNjbmsUAlKHws6MZdqTLw7aspJJVGsRFbA6d1Y+gXFxbDrQ=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.62", "", { "os": "linux", "cpu": "x64" }, "sha512-l9ab5tgOGcdf8k3NU4TzK/3C8UC0+QuMxgLA/j60BhB1e9bwJleFeYJc+wLIktTUu9QwqCsU4YcuGHL+C2lCzA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "arm64" }, "sha512-3lUddTJGKZ6uU388eU79MY//IEbgGENCITetDrrRp7v9L1AxMntE1ihf6HniziwBvKKJcsUfqLiJWcq0WPZw2w=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.62", "", { "os": "win32", "cpu": "arm64" }, "sha512-U1zsOpQl3EGhs8BwoehKAwwVONe+XOXRnXTxMhXw8huF0WWXDWOUL5psjBvfSWPm1rLmagxkQsH84jTSWA/vLA=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "x64" }, "sha512-Xwc1gqYsn8UZNTzNKkigZozAhBNBGbfX2B/I/aSbyqL0h8+XIInOodI0urzJWc0B6aEv/IDiT6Rm3coXFikLIg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.62", "", { "os": "win32", "cpu": "x64" }, "sha512-JgLZXSaE4q7gUIQb9x6fLWFF3BYlMod2VBhOT1qGBdeveZxsM6ZAno/g+CL9IDUydWfLFadOIBjdYFDVWV2Z2w=="], - "@opentui/solid": ["@opentui/solid@0.0.0-20251211-4403a69a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251211-4403a69a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-vuLppAdd1Qgaqhie3q2TuEr+8udjT4d8uVg5arvCe1AUDVs19I8kvadVCfzGUVmtXgFIOEakbiv6AxDq5v9Zig=="], + "@opentui/solid": ["@opentui/solid@0.1.62", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.62", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-3th4oZROv3cZvcoL+IwNCEMTKLZaT1BBWKVHxH29wUD0/EPxtowLQCibnjKDqqdTuEUuFA/QtSX52WqQEioR8g=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1285,7 +1293,7 @@ "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], - "@pierre/precision-diffs": ["@pierre/precision-diffs@0.6.1", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HXafRSOly6B0rRt6fuP0yy1MimHJMQ2NNnBGcIHhHwsgK4WWs+SBWRWt1usdgz0NIuSgXdIyQn8HY3F1jKyDBQ=="], + "@pierre/diffs": ["@pierre/diffs@1.0.0-beta.3", "", { "dependencies": { "@shikijs/core": "3.19.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-W3dFWdFOBZ9OskGSOgN16aci8dsUyAavCxz3ZvbbVLTb2qRzMZ7H90qdfON13/N2l1HTyh84lkrCs1/sDvnRjQ=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -1667,6 +1675,8 @@ "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="], + "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="], @@ -1703,7 +1713,7 @@ "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -1815,19 +1825,19 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@vitest/expect": ["@vitest/expect@4.0.13", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.13", "@vitest/utils": "4.0.13", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-zYtcnNIBm6yS7Gpr7nFTmq8ncowlMdOJkWLqYvhr/zweY6tFbDkDi8BPPOeHxEtK1rSI69H7Fd4+1sqvEGli6w=="], + "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="], - "@vitest/mocker": ["@vitest/mocker@4.0.13", "", { "dependencies": { "@vitest/spy": "4.0.13", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-eNCwzrI5djoauklwP1fuslHBjrbR8rqIVbvNlAnkq1OTa6XT+lX68mrtPirNM9TnR69XUPt4puBCx2Wexseylg=="], + "@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.0.13", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-ooqfze8URWbI2ozOeLDMh8YZxWDpGXoeY3VOgcDnsUxN0jPyPWSUvjPQWqDGCBks+opWlN1E4oP1UYl3C/2EQA=="], + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="], - "@vitest/runner": ["@vitest/runner@4.0.13", "", { "dependencies": { "@vitest/utils": "4.0.13", "pathe": "^2.0.3" } }, "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A=="], + "@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="], - "@vitest/snapshot": ["@vitest/snapshot@4.0.13", "", { "dependencies": { "@vitest/pretty-format": "4.0.13", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-hb7Usvyika1huG6G6l191qu1urNPsq1iFc2hmdzQY3F5/rTgqQnwwplyf8zoYHkpt7H6rw5UfIw6i/3qf9oSxQ=="], + "@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="], - "@vitest/spy": ["@vitest/spy@4.0.13", "", {}, "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw=="], + "@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="], - "@vitest/utils": ["@vitest/utils@4.0.13", "", { "dependencies": { "@vitest/pretty-format": "4.0.13", "tinyrainbow": "^3.0.3" } }, "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA=="], + "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="], "@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="], @@ -1961,7 +1971,7 @@ "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], - "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -2009,7 +2019,7 @@ "bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="], - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], @@ -2333,7 +2343,7 @@ "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], @@ -2349,7 +2359,7 @@ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], @@ -3079,6 +3089,8 @@ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], @@ -3779,7 +3791,7 @@ "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - "vitest": ["vitest@4.0.13", "", { "dependencies": { "@vitest/expect": "4.0.13", "@vitest/mocker": "4.0.13", "@vitest/pretty-format": "4.0.13", "@vitest/runner": "4.0.13", "@vitest/snapshot": "4.0.13", "@vitest/spy": "4.0.13", "@vitest/utils": "4.0.13", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.13", "@vitest/browser-preview": "4.0.13", "@vitest/browser-webdriverio": "4.0.13", "@vitest/ui": "4.0.13", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-QSD4I0fN6uZQfftryIXuqvqgBxTvJ3ZNkF6RWECd82YGAYAfhcppBLFXzXJHQAAhVFyYEuFTrq6h0hQqjB7jIQ=="], + "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="], "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], @@ -3869,24 +3881,16 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@actions/artifact/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="], + "@actions/artifact/@actions/core": ["@actions/core@2.0.1", "", { "dependencies": { "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.0" } }, "sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg=="], - "@actions/artifact/@octokit/plugin-request-log": ["@octokit/plugin-request-log@1.0.4", "", { "peerDependencies": { "@octokit/core": ">=3" } }, "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA=="], + "@actions/core/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], - "@actions/artifact/@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], - - "@actions/artifact/@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], - - "@actions/github/@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="], + "@actions/github/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], "@actions/github/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="], "@actions/github/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="], - "@actions/github/@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="], - - "@actions/github/@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="], - "@actions/github/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], @@ -3957,40 +3961,16 @@ "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@azure/core-auth/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], + "@azure/core-http/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], - "@azure/core-client/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], - - "@azure/core-client/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], + "@azure/core-http/@azure/core-tracing": ["@azure/core-tracing@1.0.0-preview.13", "", { "dependencies": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" } }, "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ=="], "@azure/core-http/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "@azure/core-http/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], - "@azure/core-http-compat/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], - - "@azure/core-lro/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], - - "@azure/core-rest-pipeline/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], - - "@azure/core-rest-pipeline/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], - - "@azure/core-util/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], - "@azure/core-xml/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], - "@azure/storage-blob/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], - - "@azure/storage-blob/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], - - "@azure/storage-blob/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - - "@azure/storage-common/@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], - - "@azure/storage-common/@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], - - "@azure/storage-common/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -4063,36 +4043,68 @@ "@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@octokit/auth-app/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/auth-oauth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/auth-oauth-app/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/auth-oauth-device/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/auth-oauth-device/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/auth-oauth-user/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + "@octokit/auth-oauth-user/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], - "@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + "@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], - "@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - "@octokit/endpoint/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + + "@octokit/endpoint/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + + "@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + + "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], "@octokit/graphql/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="], + "@octokit/oauth-methods/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@octokit/oauth-methods/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/oauth-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/plugin-paginate-rest/@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="], "@octokit/plugin-retry/@octokit/types": ["@octokit/types@6.41.0", "", { "dependencies": { "@octokit/openapi-types": "^12.11.0" } }, "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg=="], - "@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - "@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + + "@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + + "@octokit/rest/@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], + + "@octokit/rest/@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="], "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="], "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + "@opencode-ai/tauri/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], + "@opencode-ai/tauri/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="], @@ -4111,11 +4123,13 @@ "@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], - "@pierre/precision-diffs/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], + "@pierre/diffs/@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/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/types": "3.15.0" } }, "sha512-Hmwip5ovvSkg+Kc41JTvSHHVfCYF+C8Cp1omb5AJj4Xvd+y9IXz2rKJwmFRGsuN0vpHxywcXJ1+Y4B9S7EG1/A=="], + "@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/precision-diffs/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="], + "@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=="], @@ -4185,6 +4199,8 @@ "astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "aws-sdk/events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], @@ -4295,10 +4311,6 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - "opentui-spinner/@opentui/core": ["@opentui/core@0.1.60", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="], - - "opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.60", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="], - "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], @@ -4323,8 +4335,6 @@ "readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "readable-stream/events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], @@ -4377,6 +4387,8 @@ "vite-plugin-icons-spritesheet/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], "vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -4397,46 +4409,14 @@ "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@actions/artifact/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], + "@actions/artifact/@actions/core/@actions/exec": ["@actions/exec@2.0.0", "", { "dependencies": { "@actions/io": "^2.0.0" } }, "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw=="], - "@actions/artifact/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], - - "@actions/artifact/@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - - "@actions/artifact/@octokit/core/before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], - - "@actions/artifact/@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - - "@actions/artifact/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], - - "@actions/artifact/@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - - "@actions/artifact/@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - - "@actions/artifact/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - - "@actions/github/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], - - "@actions/github/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], - - "@actions/github/@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - - "@actions/github/@octokit/core/before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], - - "@actions/github/@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], + "@actions/core/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "@actions/github/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@actions/github/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], - - "@actions/github/@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - - "@actions/github/@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - - "@actions/github/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - "@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], @@ -4627,29 +4607,93 @@ "@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "@octokit/auth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/auth-app/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/auth-app/@octokit/request-error/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/auth-oauth-app/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/auth-oauth-app/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-app/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/auth-oauth-device/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/auth-oauth-device/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-device/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/auth-oauth-user/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/auth-oauth-user/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + "@octokit/auth-oauth-user/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], + "@octokit/oauth-methods/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + "@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/plugin-paginate-rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], "@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="], - "@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - "@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + "@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + + "@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], + + "@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="], + + "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="], + + "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="], + + "@octokit/rest/@octokit/core/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + + "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + + "@opencode-ai/tauri/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="], @@ -4669,19 +4713,19 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@pierre/precision-diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "@pierre/diffs/@shikijs/core/@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/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "@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/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], + "@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/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="], + "@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/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="], + "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="], - "@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="], + "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="], - "@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="], + "@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=="], @@ -4875,22 +4919,6 @@ "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], - "opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="], - - "opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.60", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="], - - "opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.60", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="], - - "opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.60", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="], - - "opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.60", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="], - - "opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.60", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="], - - "opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], - - "opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="], - "parse-bmfont-xml/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], @@ -4923,22 +4951,12 @@ "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@actions/artifact/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - - "@actions/artifact/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - - "@actions/artifact/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - - "@actions/github/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + "@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@2.0.0", "", {}, "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg=="], "@actions/github/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], - "@actions/github/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - - "@actions/github/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="], "@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="], @@ -5021,6 +5039,26 @@ "@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "@octokit/auth-app/@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/auth-app/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/plugin-paginate-rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/plugin-rest-endpoint-methods/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="], + + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], + + "@opencode-ai/tauri/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], @@ -5069,8 +5107,6 @@ "opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], diff --git a/flake.lock b/flake.lock index 58344d82c..e1c4419dc 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1765425892, - "narHash": "sha256-jlQpSkg2sK6IJVzTQBDyRxQZgKADC2HKMRfGCSgNMHo=", + "lastModified": 1766125104, + "narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5d6bdbddb4695a62f0d00a3620b37a15275a5093", + "rev": "7d853e518814cca2a657b72eeba67ae20ebf7059", "type": "github" }, "original": { diff --git a/github/README.md b/github/README.md index 36342b409..e35860340 100644 --- a/github/README.md +++ b/github/README.md @@ -6,7 +6,7 @@ Mention `/opencode` in your comment, and opencode will execute tasks within your ## Features -#### Explain an issues +#### Explain an issue Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation. @@ -14,7 +14,7 @@ Leave the following comment on a GitHub issue. `opencode` will read the entire t /opencode explain this issue ``` -#### Fix an issues +#### Fix an issue Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes. diff --git a/github/action.yml b/github/action.yml index f52f14d80..57e26d856 100644 --- a/github/action.yml +++ b/github/action.yml @@ -9,6 +9,10 @@ inputs: description: "Model to use" required: true + agent: + description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found." + required: false + share: description: "Share the opencode session (defaults to true for public repos)" required: false @@ -17,6 +21,19 @@ inputs: description: "Custom prompt to override the default prompt" required: false + use_github_token: + description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var." + required: false + default: "false" + + mentions: + description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'" + required: false + + oidc_base_url: + description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai" + required: false + runs: using: "composite" steps: @@ -49,5 +66,9 @@ runs: run: opencode github run env: MODEL: ${{ inputs.model }} + AGENT: ${{ inputs.agent }} SHARE: ${{ inputs.share }} PROMPT: ${{ inputs.prompt }} + USE_GITHUB_TOKEN: ${{ inputs.use_github_token }} + MENTIONS: ${{ inputs.mentions }} + OIDC_BASE_URL: ${{ inputs.oidc_base_url }} diff --git a/github/index.ts b/github/index.ts index 6d826326e..2dcf6e754 100644 --- a/github/index.ts +++ b/github/index.ts @@ -318,6 +318,10 @@ function useEnvRunUrl() { return `/${repo.owner}/${repo.repo}/actions/runs/${runId}` } +function useEnvAgent() { + return process.env["AGENT"] || undefined +} + function useEnvShare() { const value = process.env["SHARE"] if (!value) return undefined @@ -570,24 +574,49 @@ async function subscribeSessionEvents() { } async function summarize(response: string) { - const payload = useContext().payload as IssueCommentEvent try { return await chat(`Summarize the following in less than 40 characters:\n\n${response}`) } catch (e) { + if (isScheduleEvent()) { + return "Scheduled task changes" + } + const payload = useContext().payload as IssueCommentEvent return `Fix issue: ${payload.issue.title}` } } +async function resolveAgent(): Promise { + const envAgent = useEnvAgent() + if (!envAgent) return undefined + + // Validate the agent exists and is a primary agent + const agents = await client.agent.list() + const agent = agents.data?.find((a) => a.name === envAgent) + + if (!agent) { + console.warn(`agent "${envAgent}" not found. Falling back to default agent`) + return undefined + } + + if (agent.mode === "subagent") { + console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`) + return undefined + } + + return envAgent +} + async function chat(text: string, files: PromptFiles = []) { console.log("Sending message to opencode...") const { providerID, modelID } = useEnvModel() + const agent = await resolveAgent() const chat = await client.session.chat({ path: session, body: { providerID, modelID, - agent: "build", + agent, parts: [ { type: "text", diff --git a/github/package.json b/github/package.json index 1a6598d6b..4d447716f 100644 --- a/github/package.json +++ b/github/package.json @@ -13,7 +13,7 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@octokit/graphql": "9.0.1", - "@octokit/rest": "22.0.0", + "@octokit/rest": "catalog:", "@opencode-ai/sdk": "workspace:*" } } diff --git a/install b/install index c6f209734..67690b9a3 100755 --- a/install +++ b/install @@ -240,22 +240,23 @@ download_with_progress() { download_and_install() { print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version" - mkdir -p opencodetmp && cd opencodetmp + local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$" + mkdir -p "$tmp_dir" - if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then - # Fallback to standard curl on Windows or if custom progress fails - curl -# -L -o "$filename" "$url" + if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then + # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails + curl -# -L -o "$tmp_dir/$filename" "$url" fi if [ "$os" = "linux" ]; then - tar -xzf "$filename" + tar -xzf "$tmp_dir/$filename" -C "$tmp_dir" else - unzip -q "$filename" + unzip -q "$tmp_dir/$filename" -d "$tmp_dir" fi - mv opencode "$INSTALL_DIR" + mv "$tmp_dir/opencode" "$INSTALL_DIR" chmod 755 "${INSTALL_DIR}/opencode" - cd .. && rm -rf opencodetmp + rm -rf "$tmp_dir" } check_version diff --git a/nix/hashes.json b/nix/hashes.json index e28f98d05..1bc7f95f1 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,3 +1,3 @@ { - "nodeModules": "sha256-nWSAnQEm/t1ESZe23dr4JnIOJQ0JLN0w4NVoMJajbVQ=" + "nodeModules": "sha256-X9r0BsxLlhhCIioG8xuDVp+mDSlr37ZfqlblvEPrOJQ=" } diff --git a/package.json b/package.json index 39733b931..2ddba2c9a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.3", + "packageManager": "bun@1.3.5", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "typecheck": "bun turbo typecheck", @@ -20,7 +20,8 @@ "packages/slack" ], "catalog": { - "@types/bun": "1.3.3", + "@types/bun": "1.3.4", + "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", "ulid": "3.0.1", "@kobalte/core": "0.13.11", @@ -30,7 +31,8 @@ "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/precision-diffs": "0.6.1", + "@pierre/diffs": "1.0.0-beta.3", + "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", "ai": "5.0.97", @@ -54,6 +56,7 @@ } }, "devDependencies": { + "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "husky": "9.1.7", "prettier": "3.6.2", @@ -64,6 +67,7 @@ "@aws-sdk/client-s3": "3.933.0", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/plugin": "workspace:*", "typescript": "catalog:" }, "repository": { diff --git a/packages/console/app/.opencode/agent/css.md b/packages/console/app/.opencode/agent/css.md index d0ec43a48..d5e68c7bf 100644 --- a/packages/console/app/.opencode/agent/css.md +++ b/packages/console/app/.opencode/agent/css.md @@ -49,7 +49,7 @@ use data attributes to represent different states of the component } ``` -this will allow jsx to control the syling +this will allow jsx to control the styling avoid selectors that just target an element type like `> span` you should assign it a slot name. it's ok to do this sometimes where it makes sense semantically diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 96cd611f4..2fe98264a 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.152", + "version": "1.0.186", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index 39e833973..7bfcc7825 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -119,8 +119,8 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
@@ -169,6 +169,25 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) { + + {" "} +
  • + {" "} + + {" "} + + {" "} + {" "} + {" "} + Free{" "} + {" "} +
  • +
    @@ -101,6 +149,12 @@ export default function Download() { [2] OpenCode Desktop (Beta)
    +
    @@ -115,7 +169,7 @@ export default function Download() { macOS (Apple Silicon)
    - + Download
    @@ -131,7 +185,7 @@ export default function Download() { macOS (Intel)
    - + Download @@ -154,7 +208,7 @@ export default function Download() { Windows (x64) - + Download @@ -170,7 +224,7 @@ export default function Download() { Linux (.deb) - + Download @@ -186,7 +240,7 @@ export default function Download() { Linux (.rpm) - + Download diff --git a/packages/console/app/src/routes/download/types.ts b/packages/console/app/src/routes/download/types.ts new file mode 100644 index 000000000..adf71880d --- /dev/null +++ b/packages/console/app/src/routes/download/types.ts @@ -0,0 +1 @@ +export type DownloadPlatform = `darwin-${"x64" | "aarch64"}-dmg` | "windows-x64-nsis" | `linux-x64-${"deb" | "rpm"}` diff --git a/packages/console/app/src/routes/enterprise/index.css b/packages/console/app/src/routes/enterprise/index.css index 496a886eb..7eebf16ce 100644 --- a/packages/console/app/src/routes/enterprise/index.css +++ b/packages/console/app/src/routes/enterprise/index.css @@ -110,10 +110,13 @@ [data-slot="cta-button"] { background: var(--color-background-strong); color: var(--color-text-inverted); - padding: 8px 16px; + padding: 8px 16px 8px 10px; border-radius: 4px; font-weight: 500; text-decoration: none; + display: flex; + align-items: center; + gap: 8px; @media (max-width: 55rem) { display: none; diff --git a/packages/console/app/src/routes/index.css b/packages/console/app/src/routes/index.css index ae329b98b..f100acf8f 100644 --- a/packages/console/app/src/routes/index.css +++ b/packages/console/app/src/routes/index.css @@ -206,6 +206,7 @@ body { [data-component="top"] { padding: 24px var(--padding); height: 80px; + min-height: 80px; position: sticky; top: 0; display: flex; diff --git a/packages/console/app/src/routes/index.tsx b/packages/console/app/src/routes/index.tsx index 9948551e4..227021b89 100644 --- a/packages/console/app/src/routes/index.tsx +++ b/packages/console/app/src/routes/index.tsx @@ -1,6 +1,6 @@ import "./index.css" import { Title, Meta, Link } from "@solidjs/meta" -// import { HttpHeader } from "@solidjs/start" +//import { HttpHeader } from "@solidjs/start" import video from "../asset/lander/opencode-min.mp4" import videoPoster from "../asset/lander/opencode-poster.png" import { IconCopy, IconCheck } from "../component/icon" @@ -52,6 +52,21 @@ export default function Home() {
    +
    + New +
    + + Desktop app available in beta on macOS, Windows, and Linux. + + + Download now + + + Download the desktop beta now + +
    +
    +
    {/*[*]

    With over {config.github.starsFormatted.full} GitHub stars,{" "} - {config.stats.contributors} contributors, and almost{" "} + {config.stats.contributors} contributors, and over{" "} {config.stats.commits} commits, OpenCode is used and trusted by over{" "} {config.stats.monthlyUsers} developers every month.

    diff --git a/packages/console/app/src/routes/legal/privacy-policy/index.css b/packages/console/app/src/routes/legal/privacy-policy/index.css new file mode 100644 index 000000000..dbc9f2aa1 --- /dev/null +++ b/packages/console/app/src/routes/legal/privacy-policy/index.css @@ -0,0 +1,343 @@ +[data-component="privacy-policy"] { + max-width: 800px; + margin: 0 auto; + line-height: 1.7; +} + +[data-component="privacy-policy"] h1 { + font-size: 2rem; + font-weight: 700; + color: var(--color-text-strong); + margin-bottom: 0.5rem; + margin-top: 0; +} + +[data-component="privacy-policy"] .effective-date { + font-size: 0.95rem; + color: var(--color-text-weak); + margin-bottom: 2rem; +} + +[data-component="privacy-policy"] h2 { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-text-strong); + margin-top: 3rem; + margin-bottom: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border-weak); +} + +[data-component="privacy-policy"] h2:first-of-type { + margin-top: 2rem; +} + +[data-component="privacy-policy"] h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-strong); + margin-top: 2rem; + margin-bottom: 1rem; +} + +[data-component="privacy-policy"] h4 { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-text-strong); + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +[data-component="privacy-policy"] p { + margin-bottom: 1rem; + color: var(--color-text); +} + +[data-component="privacy-policy"] ul, +[data-component="privacy-policy"] ol { + margin-bottom: 1rem; + padding-left: 1.5rem; + color: var(--color-text); +} + +[data-component="privacy-policy"] li { + margin-bottom: 0.5rem; + line-height: 1.7; +} + +[data-component="privacy-policy"] ul ul, +[data-component="privacy-policy"] ul ol, +[data-component="privacy-policy"] ol ul, +[data-component="privacy-policy"] ol ol { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +[data-component="privacy-policy"] a { + color: var(--color-text-strong); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + word-break: break-word; +} + +[data-component="privacy-policy"] a:hover { + text-decoration-thickness: 2px; +} + +[data-component="privacy-policy"] strong { + font-weight: 600; + color: var(--color-text-strong); +} + +[data-component="privacy-policy"] .table-wrapper { + overflow-x: auto; + margin: 1.5rem 0; +} + +[data-component="privacy-policy"] table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--color-border); +} + +[data-component="privacy-policy"] th, +[data-component="privacy-policy"] td { + padding: 0.75rem 1rem; + text-align: left; + border: 1px solid var(--color-border); + vertical-align: top; +} + +[data-component="privacy-policy"] th { + background: var(--color-background-weak); + font-weight: 600; + color: var(--color-text-strong); +} + +[data-component="privacy-policy"] td { + color: var(--color-text); +} + +[data-component="privacy-policy"] td ul { + margin: 0; + padding-left: 1.25rem; +} + +[data-component="privacy-policy"] td li { + margin-bottom: 0.25rem; +} + +/* Mobile responsiveness */ +@media (max-width: 60rem) { + [data-component="privacy-policy"] { + padding: 0; + } + + [data-component="privacy-policy"] h1 { + font-size: 1.75rem; + } + + [data-component="privacy-policy"] h2 { + font-size: 1.35rem; + margin-top: 2.5rem; + } + + [data-component="privacy-policy"] h3 { + font-size: 1.15rem; + } + + [data-component="privacy-policy"] h4 { + font-size: 1rem; + } + + [data-component="privacy-policy"] table { + font-size: 0.9rem; + } + + [data-component="privacy-policy"] th, + [data-component="privacy-policy"] td { + padding: 0.5rem 0.75rem; + } +} + +html { + scroll-behavior: smooth; +} + +[data-component="privacy-policy"] [id] { + scroll-margin-top: 100px; +} + +@media print { + @page { + margin: 2cm; + size: letter; + } + + [data-component="top"], + [data-component="footer"], + [data-component="legal"] { + display: none !important; + } + + [data-page="legal"] { + background: white !important; + padding: 0 !important; + } + + [data-component="container"] { + max-width: none !important; + border: none !important; + margin: 0 !important; + } + + [data-component="content"], + [data-component="brand-content"] { + padding: 0 !important; + margin: 0 !important; + } + + [data-component="privacy-policy"] { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + } + + [data-component="privacy-policy"] * { + color: black !important; + background: transparent !important; + } + + [data-component="privacy-policy"] h1 { + font-size: 24pt; + margin-top: 0; + margin-bottom: 12pt; + page-break-after: avoid; + } + + [data-component="privacy-policy"] h2 { + font-size: 18pt; + border-top: 2pt solid black !important; + padding-top: 12pt; + margin-top: 24pt; + margin-bottom: 8pt; + page-break-after: avoid; + page-break-before: auto; + } + + [data-component="privacy-policy"] h2:first-of-type { + margin-top: 16pt; + } + + [data-component="privacy-policy"] h3 { + font-size: 14pt; + margin-top: 16pt; + margin-bottom: 8pt; + page-break-after: avoid; + } + + [data-component="privacy-policy"] h4 { + font-size: 12pt; + margin-top: 12pt; + margin-bottom: 6pt; + page-break-after: avoid; + } + + [data-component="privacy-policy"] p { + font-size: 11pt; + line-height: 1.5; + margin-bottom: 8pt; + orphans: 3; + widows: 3; + } + + [data-component="privacy-policy"] .effective-date { + font-size: 10pt; + margin-bottom: 16pt; + } + + [data-component="privacy-policy"] ul, + [data-component="privacy-policy"] ol { + margin-bottom: 8pt; + page-break-inside: auto; + } + + [data-component="privacy-policy"] li { + font-size: 11pt; + line-height: 1.5; + margin-bottom: 4pt; + page-break-inside: avoid; + } + + [data-component="privacy-policy"] a { + color: black !important; + text-decoration: underline; + } + + [data-component="privacy-policy"] .table-wrapper { + overflow: visible !important; + margin: 12pt 0; + } + + [data-component="privacy-policy"] table { + border: 2pt solid black !important; + page-break-inside: avoid; + width: 100% !important; + font-size: 10pt; + } + + [data-component="privacy-policy"] th, + [data-component="privacy-policy"] td { + border: 1pt solid black !important; + padding: 6pt 8pt !important; + background: white !important; + } + + [data-component="privacy-policy"] th { + background: #f0f0f0 !important; + font-weight: bold; + page-break-after: avoid; + } + + [data-component="privacy-policy"] tr { + page-break-inside: avoid; + } + + [data-component="privacy-policy"] td ul { + margin: 2pt 0; + padding-left: 12pt; + } + + [data-component="privacy-policy"] td li { + margin-bottom: 2pt; + font-size: 9pt; + } + + [data-component="privacy-policy"] strong { + font-weight: bold; + color: black !important; + } + + [data-component="privacy-policy"] h1, + [data-component="privacy-policy"] h2, + [data-component="privacy-policy"] h3, + [data-component="privacy-policy"] h4 { + page-break-inside: avoid; + page-break-after: avoid; + } + + [data-component="privacy-policy"] h2 + p, + [data-component="privacy-policy"] h3 + p, + [data-component="privacy-policy"] h4 + p, + [data-component="privacy-policy"] h2 + ul, + [data-component="privacy-policy"] h3 + ul, + [data-component="privacy-policy"] h4 + ul { + page-break-before: avoid; + } + + [data-component="privacy-policy"] table, + [data-component="privacy-policy"] .table-wrapper { + page-break-inside: avoid; + } +} diff --git a/packages/console/app/src/routes/legal/privacy-policy/index.tsx b/packages/console/app/src/routes/legal/privacy-policy/index.tsx new file mode 100644 index 000000000..8b30ba14e --- /dev/null +++ b/packages/console/app/src/routes/legal/privacy-policy/index.tsx @@ -0,0 +1,1512 @@ +import "../../brand/index.css" +import "./index.css" +import { Title, Meta, Link } from "@solidjs/meta" +import { Header } from "~/component/header" +import { config } from "~/config" +import { Footer } from "~/component/footer" +import { Legal } from "~/component/legal" + +export default function PrivacyPolicy() { + return ( +
    + OpenCode | Privacy Policy + + +
    +
    + +
    +
    +
    +

    Privacy Policy

    +

    Effective date: Dec 16, 2025

    + +

    + At OpenCode, we take your privacy seriously. Please read this Privacy Policy to learn how we treat your + personal data.{" "} + + By using or accessing our Services in any manner, you acknowledge that you accept the practices and + policies outlined below, and you hereby consent that we will collect, use and disclose your + information as described in this Privacy Policy. + +

    + +

    + Remember that your use of OpenCode is at all times subject to our Terms of Use,{" "} + https://opencode.ai/legal/terms-of-service, which incorporates + this Privacy Policy. Any terms we use in this Policy without defining them have the definitions given to + them in the Terms of Use. +

    + +

    You may print a copy of this Privacy Policy by clicking the print button in your browser.

    + +

    + As we continually work to improve our Services, we may need to change this Privacy Policy from time to + time. We will alert you of material changes by placing a notice on the OpenCode website, by sending you + an email and/or by some other means. Please note that if you've opted not to receive legal notice emails + from us (or you haven't provided us with your email address), those legal notices will still govern your + use of the Services, and you are still responsible for reading and understanding them. If you use the + Services after any changes to the Privacy Policy have been posted, that means you agree to all of the + changes. +

    + +

    Privacy Policy Table of Contents

    + + +

    What this Privacy Policy Covers

    +

    + This Privacy Policy covers how we treat Personal Data that we gather when you access or use our + Services. "Personal Data" means any information that identifies or relates to a particular individual + and also includes information referred to as "personally identifiable information" or "personal + information" under applicable data privacy laws, rules or regulations. This Privacy Policy does not + cover the practices of companies we don't own or control or people we don't manage. +

    + +

    Personal Data

    + +

    Categories of Personal Data We Collect

    +

    + This chart details the categories of Personal Data that we collect and have collected over the past 12 + months: +

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Category of Personal Data (and Examples)Business or Commercial Purpose(s) for CollectionCategories of Third Parties With Whom We Disclose this Personal Data
    + Profile or Contact Data such as first and last name, email, phone number and + mailing address. + +
      +
    • Providing, Customizing and Improving the Services
    • +
    • Marketing the Services
    • +
    • Corresponding with You
    • +
    +
    +
      +
    • Service Providers
    • +
    • Business Partners
    • +
    • Parties You Authorize, Access or Authenticate
    • +
    +
    + Payment Data such as financial account information, payment card type, full + number of payment card, last 4 digits of payment card, bank account information, billing + address, billing phone number and billing email + +
      +
    • Providing, Customizing and Improving the Services
    • +
    • Marketing the Services
    • +
    • Corresponding with You
    • +
    +
    +
      +
    • Service Providers (specifically our payment processing partner)
    • +
    • Business Partners
    • +
    • Parties You Authorize, Access or Authenticate
    • +
    +
    + Device/IP Data such as IP address, device ID, domain server, type of + device/operating system/browser used to access the Services. + +
      +
    • Providing, Customizing and Improving the Services
    • +
    • Marketing the Services
    • +
    • Corresponding with You
    • +
    +
    +
      +
    • None
    • +
    • Service Providers
    • +
    • Business Partners
    • +
    • Parties You Authorize, Access or Authenticate
    • +
    +
    + Other Identifying Information that You Voluntarily Choose to Provide such as + information included in conversations or prompts that you submit to AI + +
      +
    • Providing, Customizing and Improving the Services
    • +
    • Marketing the Services
    • +
    • Corresponding with You
    • +
    +
    +
      +
    • Service Providers
    • +
    • Business Partners
    • +
    • Parties You Authorize, Access or Authenticate
    • +
    +
    +
    + +

    Our Commercial or Business Purposes for Collecting Personal Data

    + +

    Providing, Customizing and Improving the Services

    +
      +
    • Creating and managing your account or other user profiles.
    • +
    • Providing you with the products, services or information you request.
    • +
    • Meeting or fulfilling the reason you provided the information to us.
    • +
    • Providing support and assistance for the Services.
    • +
    • + Improving the Services, including testing, research, internal analytics and product development. +
    • +
    • Doing fraud protection, security and debugging.
    • +
    • + Carrying out other business purposes stated when collecting your Personal Data or as otherwise set + forth in applicable data privacy laws, such as the California Consumer Privacy Act, as amended by the + California Privacy Rights Act of 2020 (the "CCPA"), the Colorado Privacy Act (the "CPA"), the + Connecticut Data Privacy Act (the "CTDPA"), the Delaware Personal Data Privacy Act (the "DPDPA"), the + Iowa Consumer Data Protection Act (the "ICDPA"), the Montana Consumer Data Privacy Act ("MCDPA"), the + Nebraska Data Privacy Act (the "NDPA"), the New Hampshire Privacy Act (the "NHPA"), the New Jersey + Privacy Act (the "NJPA"), the Oregon Consumer Privacy Act ("OCPA"), the Texas Data Privacy and + Security Act ("TDPSA"), the Utah Consumer Privacy Act (the "UCPA"), or the Virginia Consumer Data + Protection Act (the "VCDPA") (collectively, the "State Privacy Laws"). +
    • +
    + +

    Marketing the Services

    +
      +
    • Marketing and selling the Services.
    • +
    + +

    Corresponding with You

    +
      +
    • + Responding to correspondence that we receive from you, contacting you when necessary or requested, and + sending you information about OpenCode. +
    • +
    • Sending emails and other communications according to your preferences.
    • +
    + +

    Other Permitted Purposes for Processing Personal Data

    +

    + In addition, each of the above referenced categories of Personal Data may be collected, used, and + disclosed with the government, including law enforcement, or other parties to meet certain legal + requirements and enforcing legal terms including: fulfilling our legal obligations under applicable law, + regulation, court order or other legal process, such as preventing, detecting and investigating security + incidents and potentially illegal or prohibited activities; protecting the rights, property or safety of + you, OpenCode or another party; enforcing any agreements with you; responding to claims that any posting + or other content violates third-party rights; and resolving disputes. +

    + +

    + We will not collect additional categories of Personal Data or use the Personal Data we collected for + materially different, unrelated or incompatible purposes without providing you notice or obtaining your + consent. +

    + +

    Categories of Sources of Personal Data

    +

    We collect Personal Data about you from the following categories of sources:

    + +

    You

    +
      +
    • + When you provide such information directly to us. +
        +
      • When you create an account or use our interactive tools and Services.
      • +
      • + When you voluntarily provide information in free-form text boxes through the Services or through + responses to surveys or questionnaires. +
      • +
      • When you send us an email or otherwise contact us.
      • +
      +
    • +
    • + When you use the Services and such information is collected automatically. +
        +
      • Through Cookies (defined in the "Tracking Tools and Opt-Out" section below).
      • +
      • + If you download and install certain applications and software we make available, we may receive + and collect information transmitted from your computing device for the purpose of providing you + the relevant Services, such as information regarding when you are logged on and available to + receive updates or alert notices. +
      • +
      +
    • +
    + +

    Public Records

    +
      +
    • From the government.
    • +
    + +

    Third Parties

    +
      +
    • + Vendors +
        +
      • + We may use analytics providers to analyze how you interact and engage with the Services, or third + parties may help us provide you with customer support. +
      • +
      • We may use vendors to obtain information to generate leads and create user profiles.
      • +
      +
    • +
    + +

    How We Disclose Your Personal Data

    +

    + We disclose your Personal Data to the categories of service providers and other parties listed in this + section. Depending on state laws that may be applicable to you, some of these disclosures may constitute + a "sale" of your Personal Data. For more information, please refer to the state-specific sections below. +

    + +

    Service Providers

    +

    + These parties help us provide the Services or perform business functions on our behalf. They include: +

    +
      +
    • Hosting, technology and communication providers.
    • +
    • Analytics providers for web traffic or usage of the site.
    • +
    • Security and fraud prevention consultants.
    • +
    • Support and customer service vendors.
    • +
    + +

    Business Partners

    +

    These parties partner with us in offering various services. They include:

    +
      +
    • Businesses that you have a relationship with.
    • +
    • Companies that we partner with to offer joint promotional offers or opportunities.
    • +
    + +

    Parties You Authorize, Access or Authenticate

    +
      +
    • Home buyers
    • +
    + +

    Legal Obligations

    +

    + We may disclose any Personal Data that we collect with third parties in conjunction with any of the + activities set forth under "Other Permitted Purposes for Processing Personal Data" section above. +

    + +

    Business Transfers

    +

    + All of your Personal Data that we collect may be transferred to a third party if we undergo a merger, + acquisition, bankruptcy or other transaction in which that third party assumes control of our business + (in whole or in part). +

    + +

    Data that is Not Personal Data

    +

    + We may create aggregated, de-identified or anonymized data from the Personal Data we collect, including + by removing information that makes the data personally identifiable to a particular user. We may use + such aggregated, de-identified or anonymized data and disclose it with third parties for our lawful + business purposes, including to analyze, build and improve the Services and promote our business, + provided that we will not disclose such data in a manner that could identify you. +

    + +

    Tracking Tools and Opt-Out

    +

    + The Services use cookies and similar technologies such as pixel tags, web beacons, clear GIFs and + JavaScript (collectively, "Cookies") to enable our servers to recognize your web browser, tell us how + and when you visit and use our Services, analyze trends, learn about our user base and operate and + improve our Services. Cookies are small pieces of data– usually text files – placed on your computer, + tablet, phone or similar device when you use that device to access our Services. We may also supplement + the information we collect from you with information received from third parties, including third + parties that have placed their own Cookies on your device(s). +

    + +

    + Please note that because of our use of Cookies, the Services do not support "Do Not Track" requests sent + from a browser at this time. +

    + +

    We use the following types of Cookies:

    + +
      +
    • + Essential Cookies. Essential Cookies are required for providing you with features or + services that you have requested. For example, certain Cookies enable you to log into secure areas of + our Services. Disabling these Cookies may make certain features and services unavailable. +
    • +
    • + Functional Cookies. Functional Cookies are used to record your choices and settings + regarding our Services, maintain your preferences over time and recognize you when you return to our + Services. These Cookies help us to personalize our content for you, greet you by name and remember + your preferences (for example, your choice of language or region). +
    • +
    • + Performance/Analytical Cookies. Performance/Analytical Cookies allow us to understand + how visitors use our Services. They do this by collecting information about the number of visitors to + the Services, what pages visitors view on our Services and how long visitors are viewing pages on the + Services. Performance/Analytical Cookies also help us measure the performance of our advertising + campaigns in order to help us improve our campaigns and the Services' content for those who engage + with our advertising. For example, Google LLC ("Google") uses cookies in connection with its Google + Analytics services. Google's ability to use and disclose information collected by Google Analytics + about your visits to the Services is subject to the Google Analytics Terms of Use and the Google + Privacy Policy. You have the option to opt-out of Google's use of Cookies by visiting the Google + advertising opt-out page at{" "} + www.google.com/privacy_ads.html or the Google + Analytics Opt-out Browser Add-on at{" "} + https://tools.google.com/dlpage/gaoptout/. +
    • +
    + +

    + You can decide whether or not to accept Cookies through your internet browser's settings. Most browsers + have an option for turning off the Cookie feature, which will prevent your browser from accepting new + Cookies, as well as (depending on the sophistication of your browser software) allow you to decide on + acceptance of each new Cookie in a variety of ways. You can also delete all Cookies that are already on + your device. If you do this, however, you may have to manually adjust some preferences every time you + visit our website and some of the Services and functionalities may not work. +

    + +

    + To find out more information about Cookies generally, including information about how to manage and + delete Cookies, please visit{" "} + http://www.allaboutcookies.org/. +

    + +

    Data Security

    +

    + We seek to protect your Personal Data from unauthorized access, use and disclosure using appropriate + physical, technical, organizational and administrative security measures based on the type of Personal + Data and how we are processing that data. You should also help protect your data by appropriately + selecting and protecting your password and/or other sign-on mechanism; limiting access to your computer + or device and browser; and signing off after you have finished accessing your account. Although we work + to protect the security of your account and other data that we hold in our records, please be aware that + no method of transmitting data over the internet or storing data is completely secure. +

    + +

    Data Retention

    +

    + We retain Personal Data about you for as long as necessary to provide you with our Services or to + perform our business or commercial purposes for collecting your Personal Data. When establishing a + retention period for specific categories of data, we consider who we collected the data from, our need + for the Personal Data, why we collected the Personal Data, and the sensitivity of the Personal Data. In + some cases we retain Personal Data for longer, if doing so is necessary to comply with our legal + obligations, resolve disputes or collect fees owed, or is otherwise permitted or required by applicable + law, rule or regulation. We may further retain information in an anonymous or aggregated form where that + information would not identify you personally. +

    + +

    Personal Data of Children

    +

    + As noted in the Terms of Use, we do not knowingly collect or solicit Personal Data from children under + 18 years of age; if you are a child under the age of 18, please do not attempt to register for or + otherwise use the Services or send us any Personal Data. If we learn we have collected Personal Data + from a child under 18 years of age, we will delete that information as quickly as possible. If you + believe that a child under 18 years of age may have provided Personal Data to us, please contact us at{" "} + contact@anoma.ly. +

    + +

    California Resident Rights

    +

    + If you are a California resident, you have the rights set forth in this section. Please see the + "Exercising Your Rights under the State Privacy Laws" section below for instructions regarding how to + exercise these rights. Please note that we may process Personal Data of our customers' end users or + employees in connection with our provision of certain services to our customers. If we are processing + your Personal Data as a service provider, you should contact the entity that collected your Personal + Data in the first instance to address your rights with respect to such data. Additionally, please note + that these rights are subject to certain conditions and exceptions under applicable law, which may + permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a California resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access

    +

    + You have the right to request certain information about our collection and use of your Personal Data. In + response, we will provide you with the following information in the past 12 months: +

    +
      +
    • The categories of Personal Data that we have collected about you.
    • +
    • The categories of sources from which that Personal Data was collected.
    • +
    • The business or commercial purpose for collecting or selling your Personal Data.
    • +
    • The categories of third parties with whom we have shared your Personal Data.
    • +
    • The specific pieces of Personal Data that we have collected about you.
    • +
    + +

    + If we have disclosed your Personal Data to any third parties for a business purpose over the past 12 + months, we will identify the categories of Personal Data shared with each category of third party + recipient. If we have sold your Personal Data over the past 12 months, we will identify the categories + of Personal Data sold to each category of third party recipient. +

    + +

    + You may request the above information beyond the 12-month period, but no earlier than January 1, 2022. + If you do make such a request, we are required to provide that information unless doing so proves + impossible or would involve disproportionate effort. +

    + +

    Deletion

    +

    + You have the right to request that we delete the Personal Data that we have collected from you. Under + the CCPA, this right is subject to certain exceptions: for example, we may need to retain your Personal + Data to provide you with the Services or complete a transaction or other action you have requested, or + if deletion of your Personal Data involves disproportionate effort. If your deletion request is subject + to one of these exceptions, we may deny your deletion request. +

    + +

    Correction

    +

    + You have the right to request that we correct any inaccurate Personal Data we have collected about you. + Under the CCPA, this right is subject to certain exceptions: for example, if we decide, based on the + totality of circumstances related to your Personal Data, that such data is correct. If your correction + request is subject to one of these exceptions, we may deny your request. +

    + +

    Personal Data Sales Opt-Out

    +

    + We will not sell or share your Personal Data, and have not done so over the last 12 months. To our + knowledge, we do not sell or share the Personal Data of minors under 13 years of age or of consumers + under 16 years of age. +

    + +

    Limit the Use of Sensitive Personal Information

    +

    + Consumers have certain rights over the processing of their Sensitive Personal Information. However, we + do not collect Sensitive Personal Information. +

    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the CCPA

    +

    + We will not discriminate against you for exercising your rights under the CCPA. We will not deny you our + goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the CCPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the CCPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Colorado Resident Rights

    +

    + If you are a Colorado resident, you have the rights set forth under the Colorado Privacy Act ("CPA"). + Please see the "Exercising Your Rights under the State Privacy Laws" section below for instructions + regarding how to exercise these rights. Please note that we may process Personal Data of our customers' + end users or employees in connection with our provision of certain services to our customers. If we are + processing your Personal Data as a service provider, you should contact the entity that collected your + Personal Data in the first instance to address your rights with respect to such data. Additionally, + please note that these rights are subject to certain conditions and exceptions under applicable law, + which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Colorado resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access and request a copy of your Personal Data in a machine-readable format, to the extent technically + feasible, twice within a calendar year. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data concerning you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the CPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" to make "Decisions" under the CPA. "Profiling" + means any form of automated processing performed on personal data to evaluate, analyze or predict + personal aspects related to an identified or identifiable individual's economic situation, health, + personal preferences, interests, reliability, behavior, location or movements. "Decision" means any + "Decisions that produce legal or similarly significant effects concerning a Consumer," as defined in the + CPA that concern you. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, 2) + Personal Data from a known child under 13 years of age, 3) to sell, or process Personal Data for + Targeted Advertising or Profiling after you exercise your right to opt-out, or 4) Personal Data for + Secondary Use. +

    + +

    + If you would like to withdraw your consent, please follow the instructions under the "Exercising Your + Rights under the State Privacy Laws" section. +

    + +

    We Will Not Discriminate Against You

    +

    + We will not process your personal data in violation of state and federal laws that prohibit unlawful + discrimination against consumers. +

    + +

    Connecticut Resident Rights

    +

    + If you are a Connecticut resident, you have the rights set forth under the Connecticut Data Privacy Act + ("CTDPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Connecticut resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access your Personal Data and request a copy of your Personal Data in a machine-readable format, to the + extent technically feasible. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the CTDPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" as defined under the CTDPA. "Profiling" means any + form of automated processing performed on personal data to evaluate, analyze or predict personal aspects + related to an identified or identifiable individual's economic situation, health, personal preferences, + interests, reliability, behavior, location or movements. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, 2) + Sensitive Data from a known child under 13 years of age, or 3) to sell, or process Personal Data for + Targeted Advertising of a consumer at least 13 years of age but younger than 16 years of age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the CTDPA

    +

    + We will not discriminate against you for exercising your rights under the CTDPA. We will not deny you + our goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the CTDPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the CTDPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Delaware Resident Rights

    +

    + If you are a Delaware resident, you have the rights set forth under the Delaware Personal Data Privacy + Act ("DPDPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Delaware resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access and request a copy of your Personal Data in a machine-readable format, to the extent technically + feasible. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the DPDPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" to make "Decisions" under the DPDPA. "Profiling" + means any form of automated processing performed on personal data to evaluate, analyze or predict + personal aspects related to an identified or identifiable individual's economic circumstances, health, + personal preferences, interests, reliability, behavior, location or movements. "Decision" means any + "Decisions that produce legal or similarly significant effects concerning a Consumer," as defined in the + DPDPA that concern you. To our knowledge, we do not process the Personal Data of consumers under 18 + years of age for the purpose of Profiling. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, 2) + Sensitive Data from a known child under 13 years of age, 3) or to sell, or process Personal Data for + Targeted Advertising, or Profiling of a consumer at least 13 years of age but younger than 18 years of + age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You

    +

    + We will not discriminate against you for exercising your rights under the DPDPA. We will not deny you + our goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the DPDPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the DPDPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Iowa Resident Rights

    +

    + If you are an Iowa resident, you have the rights set forth under the Iowa Consumer Data Protection Act + ("ICDPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are an Iowa resident, the portion that is more protective of Personal Data shall control to the extent + of such conflict. If you have any questions about this section or whether any of the following rights + apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access and request a copy of your Personal Data in a machine-readable format, to the extent technically + feasible, twice within a calendar year. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us.

    + +

    Opt-Out of Certain Processing Activities

    +
      +
    • Targeted Advertising: We do not process your Personal Data for targeted advertising purposes.
    • +
    • Sale of Personal Data: We do not currently sell your Personal Data as defined under the ICDPA.
    • +
    • Processing of Sensitive Personal Data: We do not process Sensitive Personal Data.
    • +
    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the ICDPA

    +

    + We will not discriminate against you for exercising your rights under the ICDPA. We will not deny you + our goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the ICDPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the ICDPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Montana Resident Rights

    +

    + If you are a Montana resident, you have the rights set forth under the Montana Consumer Data Privacy Act + ("MCDPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Montana resident, the portion that is more protective of Personal Data shall control to the extent + of such conflict. If you have any questions about this section or whether any of the following rights + apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access your Personal Data and request a copy of your Personal Data in a machine-readable format, to the + extent technically feasible. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the MCDPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" to make "Decisions" under the MCDPA. "Profiling" + means any form of automated processing performed on personal data to evaluate, analyze or predict + personal aspects related to an identified or identifiable individual's economic circumstances, health, + personal preferences, interests, reliability, behavior, location or movements. "Decision" means any + "Decisions that produce legal or similarly significant effects concerning a Consumer," as defined in the + MCDPA that concern you. To our knowledge, we do not process the Personal Data of consumers under 16 + years of age for the purpose of Profiling. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, 2) + Sensitive Data from a known child under 13 years of age, or 3) to sell, or process Personal Data for + Targeted Advertising or Profiling of a consumer at least 13 years of age but younger than 16 years of + age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the MCDPA

    +

    + We will not discriminate against you for exercising your rights under the MCDPA. We will not deny you + our goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the MCDPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the MCDPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Nebraska Resident Rights

    +

    + If you are a Nebraska resident, you have the rights set forth under the Nebraska Data Privacy Act + ("NDPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Nebraska resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access your Personal Data and request a copy of your Personal Data in a machine-readable format, to the + extent technically feasible, twice within a calendar year. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the NDPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" to make "Decisions" under the NDPA. "Profiling" + means any form of automated processing performed on personal data to evaluate, analyze or predict + personal aspects related to an identified or identifiable individual's economic circumstances, health, + personal preferences, interests, reliability, behavior, location or movements. "Decision" means any + "Decisions that produce legal or similarly significant effects concerning a Consumer," as defined in the + NDPA that concern you. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data and + 2) Sensitive Data from a known child under 13 years of age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the NDPA

    +

    + We will not discriminate against you for exercising your rights under the NDPA. We will not deny you our + goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the NDPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the NDPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    New Hampshire Resident Rights

    +

    + If you are a New Hampshire resident, you have the rights set forth under the New Hampshire Privacy Act + ("NHPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a New Hampshire resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access your Personal Data and request a copy of your Personal Data in a machine-readable format, to the + extent technically feasible. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the NHPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" to make "Decisions" under the NHPA. "Profiling" + means any form of automated processing performed on personal data to evaluate, analyze or predict + personal aspects related to an identified or identifiable individual's economic circumstances, health, + personal preferences, interests, reliability, behavior, location or movements. "Decision" means any + "Decisions that produce legal or similarly significant effects concerning a Consumer," as defined in the + NHPA that concern you. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data and + 2) Sensitive Data from a known child under 13 years of age, 3) or to sell or process Personal Data for + Targeted Advertising of a consumer at least 13 years of age but younger than 16 years of age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the NHPA

    +

    + We will not discriminate against you for exercising your rights under the NHPA. We will not deny you our + goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the NHPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the NHPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    New Jersey Resident Rights

    +

    + If you are a New Jersey resident, you have the rights set forth under the New Jersey Privacy Act + ("NJPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a New Jersey resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access and request a copy of your Personal Data in a machine-readable format, to the extent technically + feasible. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data concerning you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the NJPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" to make "Decisions" under the NJPA. "Profiling" + means any form of automated processing performed on personal data to evaluate, analyze or predict + personal aspects related to an identified or identifiable individual's economic circumstances, health, + personal preferences, interests, reliability, behavior, location or movements. "Decision" means any + "Decisions that produce legal or similarly significant effects concerning a Consumer," as defined in the + NJPA that concern you. To our knowledge, we do not process the Personal Data of consumers under 17 years + of age for the purpose of Profiling. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, 2) + Sensitive Data from a known child under 13 years of age, 3) or to sell, or process Personal Data for + Targeted Advertising, or Profiling of a consumer at least 13 years of age but younger than 17 years of + age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You

    +

    + We will not discriminate against you for exercising your rights under the NJPA. We will not deny you our + goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the NJPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the NJPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Oregon Resident Rights

    +

    + If you are an Oregon resident, you have the rights set forth under the Oregon Consumer Privacy Act + ("OCPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are an Oregon resident, the portion that is more protective of Personal Data shall control to the extent + of such conflict. If you have any questions about this section or whether any of the following rights + apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access and request a copy of your Personal Data, including a list of specific third parties, other than + natural persons, to which we have disclosed your Personal Data or any Personal Data, in a + machine-readable format, to the extent technically feasible. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the OCPA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" to make "Decisions" under the OCPA. "Profiling" + means any form of automated processing performed on personal data to evaluate, analyze or predict + personal aspects related to an identified or identifiable individual's economic circumstances, health, + personal preferences, interests, reliability, behavior, location or movements. "Decision" means any + "Decisions that produce legal or similarly significant effects concerning a Consumer," as defined in the + OCPA that concern you. To our knowledge, we do not process the Personal Data of consumers under 16 years + of age for the purpose of Profiling. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, 2) + Sensitive Data from a known child under 13 years of age, or 3) to sell, or process Personal Data for + Targeted Advertising, or Profiling of a consumer at least 13 years of age but younger than 16 years of + age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You

    +

    + We will not discriminate against you for exercising your rights under the OCPA. We will not deny you our + goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the OCPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the OCPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Texas Resident Rights

    +

    + If you are a Texas resident, you have the rights set forth under the Texas Data Privacy and Security Act + ("TDPSA"). Please see the "Exercising Your Rights under the State Privacy Laws" section below for + instructions regarding how to exercise these rights. Please note that we may process Personal Data of + our customers' end users or employees in connection with our provision of certain services to our + customers. If we are processing your Personal Data as a service provider, you should contact the entity + that collected your Personal Data in the first instance to address your rights with respect to such + data. Additionally, please note that these rights are subject to certain conditions and exceptions under + applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Texas resident, the portion that is more protective of Personal Data shall control to the extent + of such conflict. If you have any questions about this section or whether any of the following rights + apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access your Personal Data and request a copy of your Personal Data in a machine-readable format, to the + extent technically feasible, twice within a calendar year. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Personal Data Sales Opt-Out

    +

    + We do not currently sell or process for the purposes of targeted advertising your Personal Data as + defined under the TDPSA. +

    + +

    Profiling Opt-Out

    +

    + We do not process your Personal Data for "Profiling" as defined under the TDPSA. "Profiling" means any + form of solely automated processing performed on personal data to evaluate, analyze, or predict personal + aspects related to an identified or identifiable individual's economic situation, health, personal + preferences, interests, reliability, behavior, location, or movements. +

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, or + 2) Sensitive Data from a known child under 13 years of age. +

    + +

    However, we currently do not collect or process your Personal Data as described above.

    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the TDPSA

    +

    + We will not discriminate against you for exercising your rights under the TDPSA. We will not deny you + our goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the TDPSA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the TDPSA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Utah Resident Rights

    +

    + If you are a Utah resident, you have the rights set forth under the Utah Consumer Privacy Act ("UCPA"). + Please see the "Exercising Your Rights under the State Privacy Laws" section below for instructions + regarding how to exercise these rights. Please note that we may process Personal Data of our customers' + end users or employees in connection with our provision of certain services to our customers. If we are + processing your Personal Data as a service provider, you should contact the entity that collected your + Personal Data in the first instance to address your rights with respect to such data. Additionally, + please note that these rights are subject to certain conditions and exceptions under applicable law, + which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Utah resident, the portion that is more protective of Personal Data shall control to the extent of + such conflict. If you have any questions about this section or whether any of the following rights apply + to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access your Personal Data and request a copy of your Personal Data in a machine-readable format, to the + extent technically feasible. +

    + +

    Deletion

    +

    You have the right to delete Personal Data that you have provided to us.

    + +

    Opt-Out of Certain Processing Activities

    +
      +
    • Targeted Advertising: We do not process your Personal Data for targeted advertising purposes.
    • +
    • Sale of Personal Data: We do not currently sell your Personal Data as defined under the UCPA.
    • +
    • Processing of Sensitive Personal Data: We do not process Sensitive Personal Data.
    • +
    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the UCPA

    +

    + We will not discriminate against you for exercising your rights under the UCPA. We will not deny you our + goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the UCPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the UCPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Virginia Resident Rights

    +

    + If you are a Virginia resident, you have the rights set forth under the Virginia Consumer Data + Protection Act ("VCDPA"). Please see the "Exercising Your Rights under the State Privacy Laws" section + below for instructions regarding how to exercise these rights. Please note that we may process Personal + Data of our customers' end users or employees in connection with our provision of certain services to + our customers. If we are processing your Personal Data as a service provider, you should contact the + entity that collected your Personal Data in the first instance to address your rights with respect to + such data. Additionally, please note that these rights are subject to certain conditions and exceptions + under applicable law, which may permit or require us to deny your request. +

    + +

    + If there are any conflicts between this section and any other provision of this Privacy Policy and you + are a Virginia resident, the portion that is more protective of Personal Data shall control to the + extent of such conflict. If you have any questions about this section or whether any of the following + rights apply to you, please contact us at contact@anoma.ly. +

    + +

    Access and Portability

    +

    + You have the right to request confirmation of whether or not we are processing your Personal Data and to + access your Personal Data, and request a copy of your Personal Data in a machine-readable format, to the + extent technically feasible. +

    + +

    Correction

    +

    + You have the right to correct inaccuracies in your Personal Data, to the extent such correction is + appropriate in consideration of the nature of such data and our purposes of processing your Personal + Data. +

    + +

    Deletion

    +

    You have the right to delete Personal Data you have provided to us or we have obtained about you.

    + +

    Consent or "Opt-in" Required and How to Withdraw

    +

    + We may seek your consent to collect or process certain Personal Data, including: 1) Sensitive Data, or + 2) Sensitive Data from a known child under 13 years of age. +

    + +

    However, we currently do not collect or process Personal data as described above.

    + +

    Opt-Out of Certain Processing Activities

    +
      +
    • Targeted Advertising: We do not process your Personal Data for targeted advertising purposes.
    • +
    • Sale of Personal Data: We do not currently sell your Personal Data as defined under the VDCPA.
    • +
    • + Processing for Profiling Purposes: We do not currently process your Personal Data for the purposes of + profiling. +
    • +
    + +

    + To exercise any of your rights for these certain processing activities, please follow the instructions + under the "Exercising Your Rights under the State Privacy Laws" section. +

    + +

    We Will Not Discriminate Against You for Exercising Your Rights Under the VCDPA

    +

    + We will not discriminate against you for exercising your rights under the VCDPA. We will not deny you + our goods or services, charge you different prices or rates, or provide you a lower quality of goods and + services if you exercise your rights under the VCDPA. However, we may offer different tiers of our + Services as allowed by applicable data privacy laws (including the VCDPA) with varying prices, rates or + levels of quality of the goods or services you receive related to the value of Personal Data that we + receive from you. +

    + +

    Exercising Your Rights under the State Privacy Laws

    +

    + To exercise the rights described in this Privacy Policy, you or, if you are a California, Colorado, + Connecticut, Delaware, Montana, Nebraska, New Hampshire, New Jersey, Oregon or Texas resident, your + Authorized Agent (defined below) must send us a request that (1) provides sufficient information to + allow us to verify that you are the person about whom we have collected Personal Data, and (2) describes + your request in sufficient detail to allow us to understand, evaluate and respond to it. Each request + that meets both of these criteria will be considered a "Valid Request." We may not respond to requests + that do not meet these criteria. We will only use Personal Data provided in a Valid Request to verify + your identity and complete your request. You do not need an account to submit a Valid Request. +

    + +

    + We will work to respond to your Valid Request within the time period required by applicable law. We will + not charge you a fee for making a Valid Request unless your Valid Request(s) is excessive, repetitive or + manifestly unfounded. If we determine that your Valid Request warrants a fee, we will notify you of the + fee and explain that decision before completing your request. +

    + +

    Request to Withdraw Consent to Certain Processing Activities

    +

    + If you are a California resident, you may withdraw your consent allowing us: 1) to sell or share your + Personal Data, by using the following method: +

    + + +

    Request to Access, Delete, or Correct

    +

    + You may submit a Valid Request for any other rights afforded to you in this Privacy Policy by using the + following methods: +

    + + +

    + If you are a California, Colorado, Connecticut, Delaware, Montana, Nebraska, New Hampshire, New Jersey, + Oregon or Texas resident, you may also authorize an agent (an "Authorized Agent") to exercise your + rights on your behalf. To do this, you must provide your Authorized Agent with written permission to + exercise your rights on your behalf, and we may request a copy of this written permission from your + Authorized Agent when they make a request on your behalf. +

    + +

    Appealing a Denial

    +

    + If you are a Colorado, Connecticut, Delaware, Iowa, Montana, Nebraska, New Hampshire, New Jersey, + Oregon, Texas or Virginia resident and we refuse to take action on your request within a reasonable + period of time after receiving your request in accordance with this section, you may appeal our + decision. In such appeal, you must (1) provide sufficient information to allow us to verify that you are + the person about whom the original request pertains and to identify the original request, and (2) + provide a description of the basis of your appeal. Please note that your appeal will be subject to your + rights and obligations afforded to you under the State Privacy Laws (as applicable). We will respond to + your appeal within the time period required under the applicable law. You can submit a Verified Request + to appeal by the following methods: +

    + + +

    + If we deny your appeal, you have the right to contact the Attorney General of your State, including by + the following links: Colorado, Connecticut, Delaware, Iowa, Montana, Nebraska, New Hampshire, New + Jersey, Oregon, Texas and Virginia. +

    + +

    Other State Law Privacy Rights

    + +

    California Resident Rights

    +

    + Under California Civil Code Sections 1798.83-1798.84, California residents are entitled to contact us to + prevent disclosure of Personal Data to third parties for such third parties' direct marketing purposes; + in order to submit such a request, please contact us at{" "} + contact@anoma.ly. +

    + +

    + Your browser may offer you a "Do Not Track" option, which allows you to signal to operators of websites + and web applications and services that you do not wish such operators to track certain of your online + activities over time and across different websites. Our Services do not support Do Not Track requests at + this time. To find out more about "Do Not Track," you can visit{" "} + www.allaboutdnt.com. +

    + +

    Nevada Resident Rights

    +

    + Please note that we do not currently sell your Personal Data as sales are defined in Nevada Revised + Statutes Chapter 603A. +

    + +

    Contact Information

    +

    + If you have any questions or comments about this Privacy Policy, the ways in which we collect and use + your Personal Data or your choices and rights regarding such collection and use, please do not hesitate + to contact us at: +

    + +
    +
    +
    +
    +
    + +
    + ) +} diff --git a/packages/console/app/src/routes/legal/terms-of-service/index.css b/packages/console/app/src/routes/legal/terms-of-service/index.css new file mode 100644 index 000000000..709b3a9c3 --- /dev/null +++ b/packages/console/app/src/routes/legal/terms-of-service/index.css @@ -0,0 +1,254 @@ +[data-component="terms-of-service"] { + max-width: 800px; + margin: 0 auto; + line-height: 1.7; +} + +[data-component="terms-of-service"] h1 { + font-size: 2rem; + font-weight: 700; + color: var(--color-text-strong); + margin-bottom: 0.5rem; + margin-top: 0; +} + +[data-component="terms-of-service"] .effective-date { + font-size: 0.95rem; + color: var(--color-text-weak); + margin-bottom: 2rem; +} + +[data-component="terms-of-service"] h2 { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-text-strong); + margin-top: 3rem; + margin-bottom: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border-weak); +} + +[data-component="terms-of-service"] h2:first-of-type { + margin-top: 2rem; +} + +[data-component="terms-of-service"] h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-strong); + margin-top: 2rem; + margin-bottom: 1rem; +} + +[data-component="terms-of-service"] h4 { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-text-strong); + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +[data-component="terms-of-service"] p { + margin-bottom: 1rem; + color: var(--color-text); +} + +[data-component="terms-of-service"] ul, +[data-component="terms-of-service"] ol { + margin-bottom: 1rem; + padding-left: 1.5rem; + color: var(--color-text); +} + +[data-component="terms-of-service"] li { + margin-bottom: 0.5rem; + line-height: 1.7; +} + +[data-component="terms-of-service"] ul ul, +[data-component="terms-of-service"] ul ol, +[data-component="terms-of-service"] ol ul, +[data-component="terms-of-service"] ol ol { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +[data-component="terms-of-service"] a { + color: var(--color-text-strong); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + word-break: break-word; +} + +[data-component="terms-of-service"] a:hover { + text-decoration-thickness: 2px; +} + +[data-component="terms-of-service"] strong { + font-weight: 600; + color: var(--color-text-strong); +} + +@media (max-width: 60rem) { + [data-component="terms-of-service"] { + padding: 0; + } + + [data-component="terms-of-service"] h1 { + font-size: 1.75rem; + } + + [data-component="terms-of-service"] h2 { + font-size: 1.35rem; + margin-top: 2.5rem; + } + + [data-component="terms-of-service"] h3 { + font-size: 1.15rem; + } + + [data-component="terms-of-service"] h4 { + font-size: 1rem; + } +} + +html { + scroll-behavior: smooth; +} + +[data-component="terms-of-service"] [id] { + scroll-margin-top: 100px; +} + +@media print { + @page { + margin: 2cm; + size: letter; + } + + [data-component="top"], + [data-component="footer"], + [data-component="legal"] { + display: none !important; + } + + [data-page="legal"] { + background: white !important; + padding: 0 !important; + } + + [data-component="container"] { + max-width: none !important; + border: none !important; + margin: 0 !important; + } + + [data-component="content"], + [data-component="brand-content"] { + padding: 0 !important; + margin: 0 !important; + } + + [data-component="terms-of-service"] { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + } + + [data-component="terms-of-service"] * { + color: black !important; + background: transparent !important; + } + + [data-component="terms-of-service"] h1 { + font-size: 24pt; + margin-top: 0; + margin-bottom: 12pt; + page-break-after: avoid; + } + + [data-component="terms-of-service"] h2 { + font-size: 18pt; + border-top: 2pt solid black !important; + padding-top: 12pt; + margin-top: 24pt; + margin-bottom: 8pt; + page-break-after: avoid; + page-break-before: auto; + } + + [data-component="terms-of-service"] h2:first-of-type { + margin-top: 16pt; + } + + [data-component="terms-of-service"] h3 { + font-size: 14pt; + margin-top: 16pt; + margin-bottom: 8pt; + page-break-after: avoid; + } + + [data-component="terms-of-service"] h4 { + font-size: 12pt; + margin-top: 12pt; + margin-bottom: 6pt; + page-break-after: avoid; + } + + [data-component="terms-of-service"] p { + font-size: 11pt; + line-height: 1.5; + margin-bottom: 8pt; + orphans: 3; + widows: 3; + } + + [data-component="terms-of-service"] .effective-date { + font-size: 10pt; + margin-bottom: 16pt; + } + + [data-component="terms-of-service"] ul, + [data-component="terms-of-service"] ol { + margin-bottom: 8pt; + page-break-inside: auto; + } + + [data-component="terms-of-service"] li { + font-size: 11pt; + line-height: 1.5; + margin-bottom: 4pt; + page-break-inside: avoid; + } + + [data-component="terms-of-service"] a { + color: black !important; + text-decoration: underline; + } + + [data-component="terms-of-service"] strong { + font-weight: bold; + color: black !important; + } + + [data-component="terms-of-service"] h1, + [data-component="terms-of-service"] h2, + [data-component="terms-of-service"] h3, + [data-component="terms-of-service"] h4 { + page-break-inside: avoid; + page-break-after: avoid; + } + + [data-component="terms-of-service"] h2 + p, + [data-component="terms-of-service"] h3 + p, + [data-component="terms-of-service"] h4 + p, + [data-component="terms-of-service"] h2 + ul, + [data-component="terms-of-service"] h3 + ul, + [data-component="terms-of-service"] h4 + ul, + [data-component="terms-of-service"] h2 + ol, + [data-component="terms-of-service"] h3 + ol, + [data-component="terms-of-service"] h4 + ol { + page-break-before: avoid; + } +} diff --git a/packages/console/app/src/routes/legal/terms-of-service/index.tsx b/packages/console/app/src/routes/legal/terms-of-service/index.tsx new file mode 100644 index 000000000..f0d7be61c --- /dev/null +++ b/packages/console/app/src/routes/legal/terms-of-service/index.tsx @@ -0,0 +1,512 @@ +import "../../brand/index.css" +import "./index.css" +import { Title, Meta, Link } from "@solidjs/meta" +import { Header } from "~/component/header" +import { config } from "~/config" +import { Footer } from "~/component/footer" +import { Legal } from "~/component/legal" + +export default function TermsOfService() { + return ( +
    + OpenCode | Terms of Service + + +
    +
    + +
    +
    +
    +

    Terms of Use

    +

    Effective date: Dec 16, 2025

    + +

    + Welcome to OpenCode. Please read on to learn the rules and restrictions that govern your use of OpenCode + (the "Services"). If you have any questions, comments, or concerns regarding these terms or the + Services, please contact us at: +

    + +

    + Email: contact@anoma.ly +

    + +

    + These Terms of Use (the "Terms") are a binding contract between you and{" "} + ANOMALY INNOVATIONS, INC. ("OpenCode," "we" and "us"). Your use of the Services in any + way means that you agree to all of these Terms, and these Terms will remain in effect while you use the + Services. These Terms include the provisions in this document as well as those in the Privacy Policy{" "} + https://opencode.ai/legal/privacy-policy.{" "} + + Your use of or participation in certain Services may also be subject to additional policies, rules + and/or conditions ("Additional Terms"), which are incorporated herein by reference, and you understand + and agree that by using or participating in any such Services, you agree to also comply with these + Additional Terms. + +

    + +

    + Please read these Terms carefully. They cover important information about Services provided to you and + any charges, taxes, and fees we bill you. These Terms include information about{" "} + future changes to these Terms,{" "} + automatic renewals,{" "} + limitations of liability,{" "} + a class action waiver and{" "} + resolution of disputes by arbitration instead of in court.{" "} + + PLEASE NOTE THAT YOUR USE OF AND ACCESS TO OUR SERVICES ARE SUBJECT TO THE FOLLOWING TERMS; IF YOU DO + NOT AGREE TO ALL OF THE FOLLOWING, YOU MAY NOT USE OR ACCESS THE SERVICES IN ANY MANNER. + +

    + +

    + ARBITRATION NOTICE AND CLASS ACTION WAIVER: EXCEPT FOR CERTAIN TYPES OF DISPUTES + DESCRIBED IN THE ARBITRATION AGREEMENT SECTION BELOW, YOU AGREE + THAT DISPUTES BETWEEN YOU AND US WILL BE RESOLVED BY BINDING, INDIVIDUAL ARBITRATION AND YOU WAIVE YOUR + RIGHT TO PARTICIPATE IN A CLASS ACTION LAWSUIT OR CLASS-WIDE ARBITRATION. +

    + +

    What is OpenCode?

    +

    + OpenCode is an AI-powered coding agent that helps you write, understand, and modify code using large + language models. Certain of these large language models are provided by third parties ("Third Party + Models") and certain of these models are provided directly by us if you use the OpenCode Zen paid + offering ("Zen"). Regardless of whether you use Third Party Models or Zen, OpenCode enables you to + access the functionality of models through a coding agent running within your terminal. +

    + +

    Will these Terms ever change?

    +

    + We are constantly trying to improve our Services, so these Terms may need to change along with our + Services. We reserve the right to change the Terms at any time, but if we do, we will place a notice on + our site located at opencode.ai, send you an email, and/or notify you by some other means. +

    + +

    + If you don't agree with the new Terms, you are free to reject them; unfortunately, that means you will + no longer be able to use the Services. If you use the Services in any way after a change to the Terms is + effective, that means you agree to all of the changes. +

    + +

    + Except for changes by us as described here, no other amendment or modification of these Terms will be + effective unless in writing and signed by both you and us. +

    + +

    What about my privacy?

    +

    + OpenCode takes the privacy of its users very seriously. For the current OpenCode Privacy Policy, please + click here{" "} + https://opencode.ai/legal/privacy-policy. +

    + +

    Children's Online Privacy Protection Act

    +

    + The Children's Online Privacy Protection Act ("COPPA") requires that online service providers obtain + parental consent before they knowingly collect personally identifiable information online from children + who are under 13 years of age. We do not knowingly collect or solicit personally identifiable + information from children under 13 years of age; if you are a child under 13 years of age, please do not + attempt to register for or otherwise use the Services or send us any personal information. If we learn + we have collected personal information from a child under 13 years of age, we will delete that + information as quickly as possible. If you believe that a child under 13 years of age may have provided + us personal information, please contact us at contact@anoma.ly. +

    + +

    What are the basics of using OpenCode?

    +

    + You represent and warrant that you are an individual of legal age to form a binding contract (or if not, + you've received your parent's or guardian's permission to use the Services and have gotten your parent + or guardian to agree to these Terms on your behalf). If you're agreeing to these Terms on behalf of an + organization or entity, you represent and warrant that you are authorized to agree to these Terms on + that organization's or entity's behalf and bind them to these Terms (in which case, the references to + "you" and "your" in these Terms, except for in this sentence, refer to that organization or entity). +

    + +

    + You will only use the Services for your own internal use, and not on behalf of or for the benefit of any + third party, and only in a manner that complies with all laws that apply to you. If your use of the + Services is prohibited by applicable laws, then you aren't authorized to use the Services. We can't and + won't be responsible for your using the Services in a way that breaks the law. +

    + +

    Are there restrictions in how I can use the Services?

    +

    + You represent, warrant, and agree that you will not provide or contribute anything, including any + Content (as that term is defined below), to the Services, or otherwise use or interact with the + Services, in a manner that: +

    + +
      +
    1. + infringes or violates the intellectual property rights or any other rights of anyone else (including + OpenCode); +
    2. +
    3. + violates any law or regulation, including, without limitation, any applicable export control laws, + privacy laws or any other purpose not reasonably intended by OpenCode; +
    4. +
    5. + is dangerous, harmful, fraudulent, deceptive, threatening, harassing, defamatory, obscene, or + otherwise objectionable; +
    6. +
    7. automatically or programmatically extracts data or Output (defined below);
    8. +
    9. Represent that the Output was human-generated when it was not;
    10. +
    11. + uses Output to develop artificial intelligence models that compete with the Services or any Third + Party Models; +
    12. +
    13. + attempts, in any manner, to obtain the password, account, or other security information from any other + user; +
    14. +
    15. + violates the security of any computer network, or cracks any passwords or security encryption codes; +
    16. +
    17. + runs Maillist, Listserv, any form of auto-responder or "spam" on the Services, or any processes that + run or are activated while you are not logged into the Services, or that otherwise interfere with the + proper working of the Services (including by placing an unreasonable load on the Services' + infrastructure); +
    18. +
    19. + "crawls," "scrapes," or "spiders" any page, data, or portion of or relating to the Services or Content + (through use of manual or automated means); +
    20. +
    21. copies or stores any significant portion of the Content; or
    22. +
    23. + decompiles, reverse engineers, or otherwise attempts to obtain the source code or underlying ideas or + information of or relating to the Services. +
    24. +
    + +

    + A violation of any of the foregoing is grounds for termination of your right to use or access the + Services. +

    + +

    Who Owns the Services and Content?

    + +

    Our IP

    +

    + We retain all right, title and interest in and to the Services. Except as expressly set forth herein, no + rights to the Services or Third Party Models are granted to you. +

    + +

    Your IP

    +

    + You may provide input to the Services ("Input"), and receive output from the Services based on the Input + ("Output"). Input and Output are collectively "Content." You are responsible for Content, including + ensuring that it does not violate any applicable law or these Terms. You represent and warrant that you + have all rights, licenses, and permissions needed to provide Input to our Services. +

    + +

    + As between you and us, and to the extent permitted by applicable law, you (a) retain your ownership + rights in Input and (b) own the Output. We hereby assign to you all our right, title, and interest, if + any, in and to Output. +

    + +

    + Due to the nature of our Services and artificial intelligence generally, output may not be unique and + other users may receive similar output from our Services. Our assignment above does not extend to other + users' output. +

    + +

    + We use Content to provide our Services, comply with applicable law, enforce our terms and policies, and + keep our Services safe. In addition, if you are using the Services through an unpaid account, we may use + Content to further develop and improve our Services. +

    + +

    + If you use OpenCode with Third Party Models, then your Content will be subject to the data retention + policies of the providers of such Third Party Models. Although we will not retain your Content, we + cannot and do not control the retention practices of Third Party Model providers. You should review the + terms and conditions applicable to any Third Party Model for more information about the data use and + retention policies applicable to such Third Party Models. +

    + +

    What about Third Party Models?

    +

    + The Services enable you to access and use Third Party Models, which are not owned or controlled by + OpenCode. Your ability to access Third Party Models is contingent on you having API keys or otherwise + having the right to access such Third Party Models. +

    + +

    + OpenCode has no control over, and assumes no responsibility for, the content, accuracy, privacy + policies, or practices of any providers of Third Party Models. We encourage you to read the terms and + conditions and privacy policy of each provider of a Third Party Model that you choose to utilize. By + using the Services, you release and hold us harmless from any and all liability arising from your use of + any Third Party Model. +

    + +

    Will OpenCode ever change the Services?

    +

    + We're always trying to improve our Services, so they may change over time. We may suspend or discontinue + any part of the Services, or we may introduce new features or impose limits on certain features or + restrict access to parts or all of the Services. +

    + +

    Do the Services cost anything?

    +

    + The Services may be free or we may charge a fee for using the Services. If you are using a free version + of the Services, we will notify you before any Services you are then using begin carrying a fee, and if + you wish to continue using such Services, you must pay all applicable fees for such Services. Any and + all such charges, fees or costs are your sole responsibility. You should consult with your +

    + +

    Paid Services

    +

    + Certain of our Services, including Zen, may be subject to payments now or in the future (the "Paid + Services"). Please see our Paid Services page https://opencode.ai/zen for a + description of the current Paid Services. Please note that any payment terms presented to you in the + process of using or signing up for a Paid Service are deemed part of these Terms. +

    + +

    Billing

    +

    + We use a third-party payment processor (the "Payment Processor") to bill you through a payment account + linked to your account on the Services (your "Billing Account") for use of the Paid Services. The + processing of payments will be subject to the terms, conditions and privacy policies of the Payment + Processor in addition to these Terms. Currently, we use Stripe, Inc. as our Payment Processor. You can + access Stripe's Terms of Service at{" "} + https://stripe.com/us/checkout/legal and their + Privacy Policy at https://stripe.com/us/privacy. We are not + responsible for any error by, or other acts or omissions of, the Payment Processor. By choosing to use + Paid Services, you agree to pay us, through the Payment Processor, all charges at the prices then in + effect for any use of such Paid Services in accordance with the applicable payment terms, and you + authorize us, through the Payment Processor, to charge your chosen payment provider (your "Payment + Method"). You agree to make payment using that selected Payment Method. We reserve the right to correct + any errors or mistakes that the Payment Processor makes even if it has already requested or received + payment. +

    + +

    Payment Method

    +

    + The terms of your payment will be based on your Payment Method and may be determined by agreements + between you and the financial institution, credit card issuer or other provider of your chosen Payment + Method. If we, through the Payment Processor, do not receive payment from you, you agree to pay all + amounts due on your Billing Account upon demand. +

    + +

    Recurring Billing

    +

    + Some of the Paid Services may consist of an initial period, for which there is a one-time charge, + followed by recurring period charges as agreed to by you. By choosing a recurring payment plan, you + acknowledge that such Services have an initial and recurring payment feature and you accept + responsibility for all recurring charges prior to cancellation. WE MAY SUBMIT PERIODIC CHARGES (E.G., + MONTHLY) WITHOUT FURTHER AUTHORIZATION FROM YOU, UNTIL YOU PROVIDE PRIOR NOTICE (RECEIPT OF WHICH IS + CONFIRMED BY US) THAT YOU HAVE TERMINATED THIS AUTHORIZATION OR WISH TO CHANGE YOUR PAYMENT METHOD. SUCH + NOTICE WILL NOT AFFECT CHARGES SUBMITTED BEFORE WE REASONABLY COULD ACT. TO TERMINATE YOUR AUTHORIZATION + OR CHANGE YOUR PAYMENT METHOD, GO TO ACCOUNT SETTINGS{" "} + https://opencode.ai/auth. +

    + +

    Free Trials and Other Promotions

    +

    + Any free trial or other promotion that provides access to a Paid Service must be used within the + specified time of the trial. You must stop using a Paid Service before the end of the trial period in + order to avoid being charged for that Paid Service. If you cancel prior to the end of the trial period + and are inadvertently charged for a Paid Service, please contact us at{" "} + contact@anoma.ly. +

    + +

    What if I want to stop using the Services?

    +

    + You're free to do that at any time; please refer to our Privacy Policy{" "} + https://opencode.ai/legal/privacy-policy, as well as the licenses + above, to understand how we treat information you provide to us after you have stopped using our + Services. +

    + +

    + OpenCode is also free to terminate (or suspend access to) your use of the Services for any reason in our + discretion, including your breach of these Terms. OpenCode has the sole right to decide whether you are + in violation of any of the restrictions set forth in these Terms. +

    + +

    + Provisions that, by their nature, should survive termination of these Terms shall survive termination. + By way of example, all of the following will survive termination: any obligation you have to pay us or + indemnify us, any limitations on our liability, any terms regarding ownership or intellectual property + rights, and terms regarding disputes between us, including without limitation the arbitration agreement. +

    + +

    What else do I need to know?

    + +

    Warranty Disclaimer

    +

    + OpenCode and its licensors, suppliers, partners, parent, subsidiaries or affiliated entities, and each + of their respective officers, directors, members, employees, consultants, contract employees, + representatives and agents, and each of their respective successors and assigns (OpenCode and all such + parties together, the "OpenCode Parties") make no representations or warranties concerning the Services, + including without limitation regarding any Content contained in or accessed through the Services, and + the OpenCode Parties will not be responsible or liable for the accuracy, copyright compliance, legality, + or decency of material contained in or accessed through the Services or any claims, actions, suits + procedures, costs, expenses, damages or liabilities arising out of use of, or in any way related to your + participation in, the Services. The OpenCode Parties make no representations or warranties regarding + suggestions or recommendations of services or products offered or purchased through or in connection + with the Services. THE SERVICES AND CONTENT ARE PROVIDED BY OPENCODE (AND ITS LICENSORS AND SUPPLIERS) + ON AN "AS-IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT + LIMITATION, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, + OR THAT USE OF THE SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE. SOME STATES DO NOT ALLOW LIMITATIONS ON + HOW LONG AN IMPLIED WARRANTY LASTS, SO THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU. +

    + +

    Limitation of Liability

    +

    + TO THE FULLEST EXTENT ALLOWED BY APPLICABLE LAW, UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY + (INCLUDING, WITHOUT LIMITATION, TORT, CONTRACT, STRICT LIABILITY, OR OTHERWISE) SHALL ANY OF THE + OPENCODE PARTIES BE LIABLE TO YOU OR TO ANY OTHER PERSON FOR (A) ANY INDIRECT, SPECIAL, INCIDENTAL, + PUNITIVE OR CONSEQUENTIAL DAMAGES OF ANY KIND, INCLUDING DAMAGES FOR LOST PROFITS, BUSINESS + INTERRUPTION, LOSS OF DATA, LOSS OF GOODWILL, WORK STOPPAGE, ACCURACY OF RESULTS, OR COMPUTER FAILURE OR + MALFUNCTION, (B) ANY SUBSTITUTE GOODS, SERVICES OR TECHNOLOGY, (C) ANY AMOUNT, IN THE AGGREGATE, IN + EXCESS OF THE GREATER OF (I) ONE-HUNDRED ($100) DOLLARS OR (II) THE AMOUNTS PAID AND/OR PAYABLE BY YOU + TO OPENCODE IN CONNECTION WITH THE SERVICES IN THE TWELVE (12) MONTH PERIOD PRECEDING THIS APPLICABLE + CLAIM OR (D) ANY MATTER BEYOND OUR REASONABLE CONTROL. SOME STATES DO NOT ALLOW THE EXCLUSION OR + LIMITATION OF INCIDENTAL OR CONSEQUENTIAL OR CERTAIN OTHER DAMAGES, SO THE ABOVE LIMITATION AND + EXCLUSIONS MAY NOT APPLY TO YOU. +

    + +

    Indemnity

    +

    + You agree to indemnify and hold the OpenCode Parties harmless from and against any and all claims, + liabilities, damages (actual and consequential), losses and expenses (including attorneys' fees) arising + from or in any way related to any claims relating to (a) your use of the Services, and (b) your + violation of these Terms. In the event of such a claim, suit, or action ("Claim"), we will attempt to + provide notice of the Claim to the contact information we have for your account (provided that failure + to deliver such notice shall not eliminate or reduce your indemnification obligations hereunder). +

    + +

    Assignment

    +

    + You may not assign, delegate or transfer these Terms or your rights or obligations hereunder, or your + Services account, in any way (by operation of law or otherwise) without OpenCode's prior written + consent. We may transfer, assign, or delegate these Terms and our rights and obligations without + consent. +

    + +

    Choice of Law

    +

    + These Terms are governed by and will be construed under the Federal Arbitration Act, applicable federal + law, and the laws of the State of Delaware, without regard to the conflicts of laws provisions thereof. +

    + +

    Arbitration Agreement

    +

    + Please read the following ARBITRATION AGREEMENT carefully because it requires you to arbitrate certain + disputes and claims with OpenCode and limits the manner in which you can seek relief from OpenCode. Both + you and OpenCode acknowledge and agree that for the purposes of any dispute arising out of or relating + to the subject matter of these Terms, OpenCode's officers, directors, employees and independent + contractors ("Personnel") are third-party beneficiaries of these Terms, and that upon your acceptance of + these Terms, Personnel will have the right (and will be deemed to have accepted the right) to enforce + these Terms against you as the third-party beneficiary hereof. +

    + +

    Arbitration Rules; Applicability of Arbitration Agreement

    +

    + The parties shall use their best efforts to settle any dispute, claim, question, or disagreement arising + out of or relating to the subject matter of these Terms directly through good-faith negotiations, which + shall be a precondition to either party initiating arbitration. If such negotiations do not resolve the + dispute, it shall be finally settled by binding arbitration in New Castle County, Delaware. The + arbitration will proceed in the English language, in accordance with the JAMS Streamlined Arbitration + Rules and Procedures (the "Rules") then in effect, by one commercial arbitrator with substantial + experience in resolving intellectual property and commercial contract disputes. The arbitrator shall be + selected from the appropriate list of JAMS arbitrators in accordance with such Rules. Judgment upon the + award rendered by such arbitrator may be entered in any court of competent jurisdiction. +

    + +

    Costs of Arbitration

    +

    + The Rules will govern payment of all arbitration fees. OpenCode will pay all arbitration fees for claims + less than seventy-five thousand ($75,000) dollars. OpenCode will not seek its attorneys' fees and costs + in arbitration unless the arbitrator determines that your claim is frivolous. +

    + +

    Small Claims Court; Infringement

    +

    + Either you or OpenCode may assert claims, if they qualify, in small claims court in New Castle County, + Delaware or any United States county where you live or work. Furthermore, notwithstanding the foregoing + obligation to arbitrate disputes, each party shall have the right to pursue injunctive or other + equitable relief at any time, from any court of competent jurisdiction, to prevent the actual or + threatened infringement, misappropriation or violation of a party's copyrights, trademarks, trade + secrets, patents or other intellectual property rights. +

    + +

    Waiver of Jury Trial

    +

    + YOU AND OPENCODE WAIVE ANY CONSTITUTIONAL AND STATUTORY RIGHTS TO GO TO COURT AND HAVE A TRIAL IN FRONT + OF A JUDGE OR JURY. You and OpenCode are instead choosing to have claims and disputes resolved by + arbitration. Arbitration procedures are typically more limited, more efficient, and less costly than + rules applicable in court and are subject to very limited review by a court. In any litigation between + you and OpenCode over whether to vacate or enforce an arbitration award, YOU AND OPENCODE WAIVE ALL + RIGHTS TO A JURY TRIAL, and elect instead to have the dispute be resolved by a judge. +

    + +

    Waiver of Class or Consolidated Actions

    +

    + ALL CLAIMS AND DISPUTES WITHIN THE SCOPE OF THIS ARBITRATION AGREEMENT MUST BE ARBITRATED OR LITIGATED + ON AN INDIVIDUAL BASIS AND NOT ON A CLASS BASIS. CLAIMS OF MORE THAN ONE CUSTOMER OR USER CANNOT BE + ARBITRATED OR LITIGATED JOINTLY OR CONSOLIDATED WITH THOSE OF ANY OTHER CUSTOMER OR USER. If however, + this waiver of class or consolidated actions is deemed invalid or unenforceable, neither you nor + OpenCode is entitled to arbitration; instead all claims and disputes will be resolved in a court as set + forth in (g) below. +

    + +

    Opt-out

    +

    + You have the right to opt out of the provisions of this Section by sending written notice of your + decision to opt out to the following address: [ADDRESS], [CITY], Canada [ZIP CODE] postmarked within + thirty (30) days of first accepting these Terms. You must include (i) your name and residence address, + (ii) the email address and/or telephone number associated with your account, and (iii) a clear statement + that you want to opt out of these Terms' arbitration agreement. +

    + +

    Exclusive Venue

    +

    + If you send the opt-out notice in (f), and/or in any circumstances where the foregoing arbitration + agreement permits either you or OpenCode to litigate any dispute arising out of or relating to the + subject matter of these Terms in court, then the foregoing arbitration agreement will not apply to + either party, and both you and OpenCode agree that any judicial proceeding (other than small claims + actions) will be brought in the state or federal courts located in, respectively, New Castle County, + Delaware, or the federal district in which that county falls. +

    + +

    Severability

    +

    + If the prohibition against class actions and other claims brought on behalf of third parties contained + above is found to be unenforceable, then all of the preceding language in this Arbitration Agreement + section will be null and void. This arbitration agreement will survive the termination of your + relationship with OpenCode. +

    + +

    Miscellaneous

    +

    + You will be responsible for paying, withholding, filing, and reporting all taxes, duties, and other + governmental assessments associated with your activity in connection with the Services, provided that + the OpenCode may, in its sole discretion, do any of the foregoing on your behalf or for itself as it + sees fit. The failure of either you or us to exercise, in any way, any right herein shall not be deemed + a waiver of any further rights hereunder. If any provision of these Terms are found to be unenforceable + or invalid, that provision will be limited or eliminated, to the minimum extent necessary, so that these + Terms shall otherwise remain in full force and effect and enforceable. You and OpenCode agree that these + Terms are the complete and exclusive statement of the mutual understanding between you and OpenCode, and + that these Terms supersede and cancel all previous written and oral agreements, communications and other + understandings relating to the subject matter of these Terms. You hereby acknowledge and agree that you + are not an employee, agent, partner, or joint venture of OpenCode, and you do not have any authority of + any kind to bind OpenCode in any respect whatsoever. +

    + +

    + Except as expressly set forth in the section above regarding the arbitration agreement, you and OpenCode + agree there are no third-party beneficiaries intended under these Terms. +

    +
    +
    +
    +
    +
    + +
    + ) +} diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index e760ccea2..38813d276 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -9,6 +9,7 @@ import { IconAlibaba, IconAnthropic, IconGemini, + IconMiniMax, IconMoonshotAI, IconOpenAI, IconStealth, @@ -23,6 +24,7 @@ const getModelLab = (modelId: string) => { if (modelId.startsWith("kimi")) return "Moonshot AI" if (modelId.startsWith("glm")) return "Z.ai" if (modelId.startsWith("qwen")) return "Alibaba" + if (modelId.startsWith("minimax")) return "MiniMax" if (modelId.startsWith("grok")) return "xAI" return "Stealth" } @@ -35,7 +37,7 @@ const getModelsInfo = query(async (workspaceID: string) => { .filter(([id, _model]) => !["claude-3-5-haiku"].includes(id)) .filter(([id, _model]) => !id.startsWith("alpha-")) .sort(([idA, modelA], [idB, modelB]) => { - const priority = ["big-pickle", "grok", "claude", "gpt", "gemini"] + const priority = ["big-pickle", "minimax", "grok", "claude", "gpt", "gemini"] const getPriority = (id: string) => { const index = priority.findIndex((p) => id.startsWith(p)) return index === -1 ? Infinity : index @@ -129,6 +131,8 @@ export function ModelSection() { return case "xAI": return + case "MiniMax": + return default: return } diff --git a/packages/console/app/src/routes/zen/index.tsx b/packages/console/app/src/routes/zen/index.tsx index 6b163315c..5708c238c 100644 --- a/packages/console/app/src/routes/zen/index.tsx +++ b/packages/console/app/src/routes/zen/index.tsx @@ -1,7 +1,7 @@ import "./index.css" import { createAsync, query, redirect } from "@solidjs/router" import { Title, Meta, Link } from "@solidjs/meta" -// import { HttpHeader } from "@solidjs/start" +//import { HttpHeader } from "@solidjs/start" import zenLogoLight from "../../asset/zen-ornate-light.svg" import { config } from "~/config" import zenLogoDark from "../../asset/zen-ornate-dark.svg" diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 5e9c877cc..bef44d3e4 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -112,13 +112,21 @@ export async function handler( headers.delete("content-length") headers.delete("x-opencode-request") headers.delete("x-opencode-session") + headers.delete("x-opencode-project") + headers.delete("x-opencode-client") return headers })(), body: reqBody, }) // Try another provider => stop retrying if using fallback provider - if (res.status !== 200 && modelInfo.fallbackProvider && providerInfo.id !== modelInfo.fallbackProvider) { + if ( + res.status !== 200 && + // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found. + res.status !== 404 && + modelInfo.fallbackProvider && + providerInfo.id !== modelInfo.fallbackProvider + ) { return retriableRequest({ excludeProviders: [...retry.excludeProviders, providerInfo.id], retryCount: retry.retryCount + 1, @@ -137,6 +145,9 @@ export async function handler( // Store sticky provider await stickyTracker?.set(providerInfo.id) + // Temporarily change 404 to 400 status code b/c solid start automatically override 404 response + const resStatus = res.status === 404 ? 400 : res.status + // Scrub response headers const resHeaders = new Headers() const keepHeaders = ["content-type", "cache-control"] @@ -162,7 +173,7 @@ export async function handler( await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo) await reload(authInfo) return new Response(body, { - status: res.status, + status: resStatus, statusText: res.statusText, headers: resHeaders, }) @@ -240,7 +251,7 @@ export async function handler( }) return new Response(stream, { - status: res.status, + status: resStatus, statusText: res.statusText, headers: resHeaders, }) diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 6fd87c2f8..e1971f3ef 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.0.152", + "version": "1.0.186", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 22322aa24..8c9daf340 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.152", + "version": "1.0.186", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index f26d54d35..2ce541a3b 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.152", + "version": "1.0.186", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 91e04af08..5f38ac60d 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "1.0.152", + "version": "1.0.186", "description": "", "type": "module", "exports": { @@ -37,9 +37,10 @@ "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", "@solid-primitives/event-bus": "1.1.2", + "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", - "@solid-primitives/storage": "4.3.3", + "@solid-primitives/storage": "catalog:", "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", @@ -55,6 +56,7 @@ "solid-js": "catalog:", "solid-list": "catalog:", "tailwindcss": "catalog:", - "virtua": "catalog:" + "virtua": "catalog:", + "zod": "catalog:" } } diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx index bf9dfd3b7..9d000b8ff 100644 --- a/packages/desktop/src/app.tsx +++ b/packages/desktop/src/app.tsx @@ -1,20 +1,27 @@ import "@/index.css" +import { ErrorBoundary, Show } from "solid-js" import { Router, Route, Navigate } from "@solidjs/router" import { MetaProvider } from "@solidjs/meta" import { Font } from "@opencode-ai/ui/font" import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { DiffComponentProvider } from "@opencode-ai/ui/context/diff" +import { CodeComponentProvider } from "@opencode-ai/ui/context/code" import { Diff } from "@opencode-ai/ui/diff" -import { GlobalSyncProvider } from "./context/global-sync" +import { Code } from "@opencode-ai/ui/code" +import { GlobalSyncProvider } from "@/context/global-sync" +import { LayoutProvider } from "@/context/layout" +import { GlobalSDKProvider } from "@/context/global-sdk" +import { TerminalProvider } from "@/context/terminal" +import { PromptProvider } from "@/context/prompt" +import { NotificationProvider } from "@/context/notification" +import { DialogProvider } from "@opencode-ai/ui/context/dialog" +import { CommandProvider } from "@/context/command" import Layout from "@/pages/layout" import Home from "@/pages/home" import DirectoryLayout from "@/pages/directory-layout" import Session from "@/pages/session" -import { LayoutProvider } from "./context/layout" -import { GlobalSDKProvider } from "./context/global-sdk" -import { SessionProvider } from "./context/session" -import { Show } from "solid-js" -import { NotificationProvider } from "./context/notification" +import { ErrorPage } from "./pages/error" +import { iife } from "@opencode-ai/util/iife" declare global { interface Window { @@ -22,47 +29,64 @@ declare global { } } -const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1" -const port = window.__OPENCODE__?.port ?? import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096" +const url = iife(() => { + const param = new URLSearchParams(document.location.search).get("url") + if (param) return param -const url = - new URLSearchParams(document.location.search).get("url") || - (location.hostname.includes("opencode.ai") || location.hostname.includes("localhost") - ? `http://${host}:${port}` - : "/") + if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" + if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}` + if (import.meta.env.VITE_OPENCODE_SERVER) + return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` + + return "/" +}) export function App() { return ( - - - - - - - - - - - - } /> - ( - - - - - - )} - /> - - - - - - - - - + + + }> + + + + + + + + + ( + + {props.children} + + )} + > + + + } /> + ( + + + + + + + + )} + /> + + + + + + + + + + + + ) } diff --git a/packages/desktop/src/components/dialog-connect-provider.tsx b/packages/desktop/src/components/dialog-connect-provider.tsx new file mode 100644 index 000000000..789a5d3b7 --- /dev/null +++ b/packages/desktop/src/components/dialog-connect-provider.tsx @@ -0,0 +1,383 @@ +import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client" +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import type { IconName } from "@opencode-ai/ui/icons/provider" +import { List, type ListRef } from "@opencode-ai/ui/list" +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" +import { useGlobalSDK } from "@/context/global-sdk" +import { useGlobalSync } from "@/context/global-sync" +import { usePlatform } from "@/context/platform" +import { DialogSelectModel } from "./dialog-select-model" +import { DialogSelectProvider } from "./dialog-select-provider" + +export function DialogConnectProvider(props: { provider: string }) { + const dialog = useDialog() + const globalSync = useGlobalSync() + const globalSDK = useGlobalSDK() + const platform = usePlatform() + const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!) + const methods = createMemo( + () => + globalSync.data.provider_auth[props.provider] ?? [ + { + type: "api", + label: "API key", + }, + ], + ) + const [store, setStore] = createStore({ + methodIndex: undefined as undefined | number, + authorization: undefined as undefined | ProviderAuthAuthorization, + state: "pending" as undefined | "pending" | "complete" | "error", + error: undefined as string | undefined, + }) + + const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined)) + + async function selectMethod(index: number) { + const method = methods()[index] + setStore( + produce((draft) => { + draft.methodIndex = index + draft.authorization = undefined + draft.state = undefined + draft.error = undefined + }), + ) + + if (method.type === "oauth") { + setStore("state", "pending") + const start = Date.now() + await globalSDK.client.provider.oauth + .authorize( + { + providerID: props.provider, + method: index, + }, + { throwOnError: true }, + ) + .then((x) => { + const elapsed = Date.now() - start + const delay = 1000 - elapsed + + if (delay > 0) { + setTimeout(() => { + setStore("state", "complete") + setStore("authorization", x.data!) + }, delay) + return + } + setStore("state", "complete") + setStore("authorization", x.data!) + }) + .catch((e) => { + setStore("state", "error") + setStore("error", String(e)) + }) + } + } + + let listRef: ListRef | undefined + function handleKey(e: KeyboardEvent) { + if (e.key === "Enter" && e.target instanceof HTMLInputElement) { + return + } + if (e.key === "Escape") return + listRef?.onKeyDown(e) + } + + onMount(() => { + if (methods().length === 1) { + selectMethod(0) + } + document.addEventListener("keydown", handleKey) + onCleanup(() => { + document.removeEventListener("keydown", handleKey) + }) + }) + + async function complete() { + await globalSDK.client.global.dispose() + dialog.close() + showToast({ + variant: "success", + icon: "circle-check", + title: `${provider().name} connected`, + description: `${provider().name} models are now available to use.`, + }) + } + + function goBack() { + if (methods().length === 1) { + dialog.show(() => ) + return + } + if (store.authorization) { + setStore("authorization", undefined) + setStore("methodIndex", undefined) + return + } + if (store.methodIndex) { + setStore("methodIndex", undefined) + return + } + dialog.show(() => ) + } + + return ( + }> +
    +
    + +
    + + + Login with Claude Pro/Max + + Connect {provider().name} + +
    +
    +
    + + +
    Select login method for {provider().name}.
    +
    + { + listRef = ref + }} + items={methods} + key={(m) => m?.label} + onSelect={async (method, index) => { + if (!method) return + selectMethod(index) + }} + > + {(i) => ( +
    +
    + + {i.label} +
    + )} + +
    + + +
    +
    + + Authorization in progress... +
    +
    +
    + +
    +
    + + Authorization failed: {store.error} +
    +
    +
    + + {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", "API key is required") + return + } + + setFormStore("error", undefined) + await globalSDK.client.auth.set({ + providerID: props.provider, + auth: { + type: "api", + key: apiKey, + }, + }) + await complete() + } + + return ( +
    + + +
    +
    + OpenCode Zen gives you access to a curated set of reliable optimized models for coding + agents. +
    +
    + With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more. +
    +
    + Visit{" "} + + opencode.ai/zen + {" "} + to collect your API key. +
    +
    +
    + +
    + Enter your {provider().name} API key to connect your account and use {provider().name} models + in OpenCode. +
    +
    +
    +
    + + + +
    + ) + })} +
    + + + + {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", "Authorization code is required") + return + } + + setFormStore("error", undefined) + const { error } = await globalSDK.client.provider.oauth.callback({ + providerID: props.provider, + method: store.methodIndex, + code, + }) + if (!error) { + await complete() + return + } + setFormStore("error", "Invalid authorization code") + } + + return ( +
    +
    + Visit this link to collect your authorization + code to connect your account and use {provider().name} models in OpenCode. +
    +
    + + + +
    + ) + })} +
    + + {iife(() => { + const code = createMemo(() => { + const instructions = store.authorization?.instructions + if (instructions?.includes(":")) { + return instructions?.split(":")[1]?.trim() + } + return instructions + }) + + onMount(async () => { + const result = await globalSDK.client.provider.oauth.callback({ + providerID: props.provider, + method: store.methodIndex, + }) + if (result.error) { + // TODO: show error + dialog.close() + return + } + await complete() + }) + + return ( +
    +
    + Visit this link and enter the code below to + connect your account and use {provider().name} models in OpenCode. +
    + +
    + + Waiting for authorization... +
    +
    + ) + })} +
    +
    +
    + +
    +
    +
    + ) +} diff --git a/packages/desktop/src/components/dialog-manage-models.tsx b/packages/desktop/src/components/dialog-manage-models.tsx new file mode 100644 index 000000000..66d125288 --- /dev/null +++ b/packages/desktop/src/components/dialog-manage-models.tsx @@ -0,0 +1,57 @@ +import { Dialog } from "@opencode-ai/ui/dialog" +import { List } from "@opencode-ai/ui/list" +import { Switch } from "@opencode-ai/ui/switch" +import type { Component } from "solid-js" +import { useLocal } from "@/context/local" +import { popularProviders } from "@/hooks/use-providers" + +export const DialogManageModels: Component = () => { + const local = useLocal() + return ( + + `${x?.provider?.id}:${x?.id}`} + items={local.model.list()} + filterKeys={["provider.name", "name", "id"]} + sortBy={(a, b) => a.name.localeCompare(b.name)} + groupBy={(x) => x.provider.name} + sortGroupsBy={(a, b) => { + 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 visible = local.model.visible({ + modelID: x.id, + providerID: x.provider.id, + }) + local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible) + }} + > + {(i) => ( +
    + {i.name} +
    e.stopPropagation()}> + { + local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked) + }} + /> +
    +
    + )} +
    +
    + ) +} diff --git a/packages/desktop/src/components/dialog-select-file.tsx b/packages/desktop/src/components/dialog-select-file.tsx new file mode 100644 index 000000000..b27afdc8b --- /dev/null +++ b/packages/desktop/src/components/dialog-select-file.tsx @@ -0,0 +1,48 @@ +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 { getDirectory, getFilename } from "@opencode-ai/util/path" +import { useParams } from "@solidjs/router" +import { createMemo } from "solid-js" +import { useLayout } from "@/context/layout" +import { useLocal } from "@/context/local" + +export function DialogSelectFile() { + const layout = useLayout() + const local = useLocal() + const dialog = useDialog() + const params = useParams() + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const tabs = createMemo(() => layout.tabs(sessionKey())) + return ( + + x} + onSelect={(path) => { + if (path) { + tabs().open("file://" + path) + } + dialog.close() + }} + > + {(i) => ( +
    +
    + +
    + + {getDirectory(i)} + + {getFilename(i)} +
    +
    +
    + )} +
    +
    + ) +} diff --git a/packages/desktop/src/components/dialog-select-model-unpaid.tsx b/packages/desktop/src/components/dialog-select-model-unpaid.tsx new file mode 100644 index 000000000..24ec8092d --- /dev/null +++ b/packages/desktop/src/components/dialog-select-model-unpaid.tsx @@ -0,0 +1,110 @@ +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import type { IconName } from "@opencode-ai/ui/icons/provider" +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 { 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" +import { DialogSelectProvider } from "./dialog-select-provider" + +export const DialogSelectModelUnpaid: Component = () => { + const local = useLocal() + const dialog = useDialog() + const providers = useProviders() + + let listRef: ListRef | undefined + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") return + listRef?.onKeyDown(e) + } + + onMount(() => { + document.addEventListener("keydown", handleKey) + onCleanup(() => { + document.removeEventListener("keydown", handleKey) + }) + }) + + return ( + +
    +
    Free models provided by OpenCode
    + (listRef = ref)} + items={local.model.list} + current={local.model.current()} + key={(x) => `${x.provider.id}:${x.id}`} + onSelect={(x) => { + local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { + recent: true, + }) + dialog.close() + }} + > + {(i) => ( +
    + {i.name} + Free + + Latest + +
    + )} +
    +
    +
    +
    +
    +
    +
    +
    Add more models from popular providers
    +
    + x?.id} + items={providers.popular} + activeIcon="plus-small" + sortBy={(a, b) => { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + onSelect={(x) => { + if (!x) return + dialog.show(() => ) + }} + > + {(i) => ( +
    + + {i.name} + + Recommended + + +
    Connect with Claude Pro/Max or API key
    +
    +
    + )} +
    + +
    +
    +
    +
    +
    + ) +} diff --git a/packages/desktop/src/components/dialog-select-model.tsx b/packages/desktop/src/components/dialog-select-model.tsx new file mode 100644 index 000000000..54783386a --- /dev/null +++ b/packages/desktop/src/components/dialog-select-model.tsx @@ -0,0 +1,83 @@ +import { Component, createMemo, Show } from "solid-js" +import { useLocal } from "@/context/local" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { popularProviders } from "@/hooks/use-providers" +import { Button } from "@opencode-ai/ui/button" +import { Tag } from "@opencode-ai/ui/tag" +import { Dialog } from "@opencode-ai/ui/dialog" +import { List } from "@opencode-ai/ui/list" +import { DialogSelectProvider } from "./dialog-select-provider" +import { DialogManageModels } from "./dialog-manage-models" + +export const DialogSelectModel: Component<{ provider?: string }> = (props) => { + const local = useLocal() + const dialog = useDialog() + + const models = createMemo(() => + local.model + .list() + .filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id })) + .filter((m) => (props.provider ? m.provider.id === props.provider : true)), + ) + + return ( + dialog.show(() => )} + > + Connect provider + + } + > + `${x.provider.id}:${x.id}`} + items={models} + current={local.model.current()} + filterKeys={["provider.name", "name", "id"]} + sortBy={(a, b) => a.name.localeCompare(b.name)} + groupBy={(x) => x.provider.name} + sortGroupsBy={(a, b) => { + if (a.category === "Recent" && b.category !== "Recent") return -1 + if (b.category === "Recent" && a.category !== "Recent") return 1 + 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) => { + local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { + recent: true, + }) + dialog.close() + }} + > + {(i) => ( +
    + {i.name} + + Free + + + Latest + +
    + )} +
    + +
    + ) +} diff --git a/packages/desktop/src/components/dialog-select-provider.tsx b/packages/desktop/src/components/dialog-select-provider.tsx new file mode 100644 index 000000000..5bbde5d41 --- /dev/null +++ b/packages/desktop/src/components/dialog-select-provider.tsx @@ -0,0 +1,54 @@ +import { Component, Show } from "solid-js" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { Dialog } from "@opencode-ai/ui/dialog" +import { List } from "@opencode-ai/ui/list" +import { Tag } from "@opencode-ai/ui/tag" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { IconName } from "@opencode-ai/ui/icons/provider" +import { DialogConnectProvider } from "./dialog-connect-provider" + +export const DialogSelectProvider: Component = () => { + const dialog = useDialog() + const providers = useProviders() + + return ( + + x?.id} + items={providers.all} + filterKeys={["id", "name"]} + groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")} + sortBy={(a, b) => { + if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) + return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) + return a.name.localeCompare(b.name) + }} + sortGroupsBy={(a, b) => { + if (a.category === "Popular" && b.category !== "Popular") return -1 + if (b.category === "Popular" && a.category !== "Popular") return 1 + return 0 + }} + onSelect={(x) => { + if (!x) return + dialog.show(() => ) + }} + > + {(i) => ( +
    + + {i.name} + + Recommended + + +
    Connect with Claude Pro/Max or API key
    +
    +
    + )} +
    +
    + ) +} diff --git a/packages/desktop/src/components/header.tsx b/packages/desktop/src/components/header.tsx index cc4d01816..c5ecd9871 100644 --- a/packages/desktop/src/components/header.tsx +++ b/packages/desktop/src/components/header.tsx @@ -1,35 +1,48 @@ import { useGlobalSync } from "@/context/global-sync" +import { useGlobalSDK } from "@/context/global-sdk" import { useLayout } from "@/context/layout" import { Session } from "@opencode-ai/sdk/v2/client" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Mark } from "@opencode-ai/ui/logo" +import { Popover } from "@opencode-ai/ui/popover" import { Select } from "@opencode-ai/ui/select" +import { TextField } from "@opencode-ai/ui/text-field" import { Tooltip } from "@opencode-ai/ui/tooltip" import { base64Decode } from "@opencode-ai/util/encode" +import { useCommand } from "@/context/command" import { getFilename } from "@opencode-ai/util/path" import { A, useParams } from "@solidjs/router" -import { createMemo, Show } from "solid-js" +import { createMemo, createResource, Show } from "solid-js" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { iife } from "@opencode-ai/util/iife" export function Header(props: { navigateToProject: (directory: string) => void navigateToSession: (session: Session | undefined) => void + onMobileMenuToggle?: () => void }) { const globalSync = useGlobalSync() + const globalSDK = useGlobalSDK() const layout = useLayout() const params = useParams() - const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) - const store = createMemo(() => globalSync.child(currentDirectory())[0]) - const sessions = createMemo(() => store().session ?? []) - const currentSession = createMemo(() => sessions().find((s) => s.id === params.id)) + const command = useCommand() return (
    +
    - 0}> -
    -
    - project.worktree)} + current={currentDirectory()} + label={(x) => getFilename(x)} + onSelect={(x) => (x ? props.navigateToProject(x) : undefined)} + class="text-14-regular text-text-base" + variant="ghost" + > + {/* @ts-ignore */} + {(i) => ( +
    + +
    {getFilename(i)}
    +
    + )} + +
    /
    +
    + -
    /
    - agent.name)} - current={local.agent.current().name} - onSelect={local.agent.set} - class="capitalize" - variant="ghost" - /> - - - - 0}> - {iife(() => { - const models = createMemo(() => - local.model - .list() - .filter((m) => - layout.connect.state() === "complete" ? m.provider.id === layout.connect.provider() : true, - ), - ) - return ( - { - if (open) { - layout.dialog.open("model") - } else { - layout.dialog.close("model") - } - }} - title="Select model" - placeholder="Search models" - emptyMessage="No model results" - key={(x) => `${x.provider.id}:${x.id}`} - items={models} - current={local.model.current()} - filterKeys={["provider.name", "name", "id"]} - // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)} - groupBy={(x) => x.provider.name} - sortGroupsBy={(a, b) => { - if (a.category === "Recent" && b.category !== "Recent") return -1 - if (b.category === "Recent" && a.category !== "Recent") return 1 - 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) => - local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { - recent: true, - }) - } - actions={ - - } - > - {(i) => ( -
    - {i.name} - - Free - - - Latest - -
    - )} -
    - ) - })} -
    - - {iife(() => { - let listRef: ListRef | undefined - const handleKey = (e: KeyboardEvent) => { - if (e.key === "Escape") return - listRef?.onKeyDown(e) + + +
    + + Shell + esc to exit +
    +
    + + + Cycle agent + {command.keybind("agent.cycle")} +
    + } + > + { + const file = e.currentTarget.files?.[0] + if (file) addImageAttachment(file) + e.currentTarget.value = "" + }} /> - + + + fileInputRef.click()} + /> + + + + +
    + Stop + ESC +
    +
    + +
    + Send + +
    +
    + + } + > + +
    +
    diff --git a/packages/desktop/src/components/session-context-usage.tsx b/packages/desktop/src/components/session-context-usage.tsx new file mode 100644 index 000000000..5474005c7 --- /dev/null +++ b/packages/desktop/src/components/session-context-usage.tsx @@ -0,0 +1,64 @@ +import { createMemo, Show } from "solid-js" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { ProgressCircle } from "@opencode-ai/ui/progress-circle" +import { useSync } from "@/context/sync" +import { useParams } from "@solidjs/router" +import { AssistantMessage } from "@opencode-ai/sdk/v2" + +export function SessionContextUsage() { + const sync = useSync() + const params = useParams() + const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) + + const cost = createMemo(() => { + const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(total) + }) + + const context = createMemo(() => { + const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage + if (!last) return + const total = + last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write + const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID] + return { + tokens: total.toLocaleString(), + percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null, + } + }) + + return ( + + {(ctx) => ( + +
    + Tokens + {ctx().tokens} +
    +
    + Usage + {ctx().percentage ?? 0}% +
    +
    + Cost + {cost()} +
    + + } + placement="top" + > +
    + {`${ctx().percentage ?? 0}%`} + +
    +
    + )} +
    + ) +} diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx index 15302f152..c05ddfbf6 100644 --- a/packages/desktop/src/components/terminal.tsx +++ b/packages/desktop/src/components/terminal.tsx @@ -2,7 +2,8 @@ import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web" import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js" import { useSDK } from "@/context/sdk" import { SerializeAddon } from "@/addons/serialize" -import { LocalPTY } from "@/context/session" +import { LocalPTY } from "@/context/terminal" +import { usePrefersDark } from "@solid-primitives/media" export interface TerminalProps extends ComponentProps<"div"> { pty: LocalPTY @@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => { let serializeAddon: SerializeAddon let fitAddon: FitAddon let handleResize: () => void + const prefersDark = usePrefersDark() onMount(async () => { ghostty = await Ghostty.load() @@ -29,12 +31,19 @@ export const Terminal = (props: TerminalProps) => { term = new Term({ cursorBlink: true, fontSize: 14, - fontFamily: "TX-02, monospace", + fontFamily: "IBM Plex Mono, monospace", allowTransparency: true, - theme: { - background: "#191515", - foreground: "#d4d4d4", - }, + theme: prefersDark() + ? { + background: "#191515", + foreground: "#d4d4d4", + cursor: "#d4d4d4", + } + : { + background: "#fcfcfc", + foreground: "#211e1e", + cursor: "#211e1e", + }, scrollback: 10_000, ghostty, }) @@ -139,6 +148,7 @@ export const Terminal = (props: TerminalProps) => {
    void +} + +export function parseKeybind(config: string): Keybind[] { + if (!config || config === "none") return [] + + return config.split(",").map((combo) => { + const parts = combo.trim().toLowerCase().split("+") + const keybind: Keybind = { + key: "", + ctrl: false, + meta: false, + shift: false, + alt: false, + } + + for (const part of parts) { + switch (part) { + case "ctrl": + case "control": + keybind.ctrl = true + break + case "meta": + case "cmd": + case "command": + keybind.meta = true + break + case "mod": + if (IS_MAC) keybind.meta = true + else keybind.ctrl = true + break + case "alt": + case "option": + keybind.alt = true + break + case "shift": + keybind.shift = true + break + default: + keybind.key = part + break + } + } + + return keybind + }) +} + +export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean { + const eventKey = event.key.toLowerCase() + + for (const kb of keybinds) { + const keyMatch = kb.key === eventKey + const ctrlMatch = kb.ctrl === (event.ctrlKey || false) + const metaMatch = kb.meta === (event.metaKey || false) + const shiftMatch = kb.shift === (event.shiftKey || false) + const altMatch = kb.alt === (event.altKey || false) + + if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) { + return true + } + } + + return false +} + +export function formatKeybind(config: string): string { + if (!config || config === "none") return "" + + const keybinds = parseKeybind(config) + if (keybinds.length === 0) return "" + + const kb = keybinds[0] + const parts: string[] = [] + + if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl") + if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt") + if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift") + if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta") + + if (kb.key) { + const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1) + parts.push(displayKey) + } + + return IS_MAC ? parts.join("") : parts.join("+") +} + +function DialogCommand(props: { options: CommandOption[] }) { + const dialog = useDialog() + + return ( + + props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)} + key={(x) => x?.id} + filterKeys={["title", "description", "category"]} + groupBy={(x) => x.category ?? ""} + onSelect={(option) => { + if (option) { + dialog.close() + option.onSelect?.("palette") + } + }} + > + {(option) => ( +
    +
    + {option.title} + + {option.description} + +
    + + {formatKeybind(option.keybind!)} + +
    + )} +
    +
    + ) +} + +export const { use: useCommand, provider: CommandProvider } = createSimpleContext({ + name: "Command", + init: () => { + const [registrations, setRegistrations] = createSignal[]>([]) + const [suspendCount, setSuspendCount] = createSignal(0) + const dialog = useDialog() + + const options = createMemo(() => { + const all = registrations().flatMap((x) => x()) + const suggested = all.filter((x) => x.suggested && !x.disabled) + return [ + ...suggested.map((x) => ({ + ...x, + id: "suggested." + x.id, + category: "Suggested", + })), + ...all, + ] + }) + + const suspended = () => suspendCount() > 0 + + const showPalette = () => { + if (!dialog.active) { + dialog.show(() => !x.disabled)} />) + } + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (suspended()) return + + const paletteKeybinds = parseKeybind("mod+shift+p") + if (matchKeybind(paletteKeybinds, event)) { + event.preventDefault() + showPalette() + return + } + + for (const option of options()) { + if (option.disabled) continue + if (!option.keybind) continue + + const keybinds = parseKeybind(option.keybind) + if (matchKeybind(keybinds, event)) { + event.preventDefault() + option.onSelect?.("keybind") + return + } + } + } + + onMount(() => { + document.addEventListener("keydown", handleKeyDown) + }) + + onCleanup(() => { + document.removeEventListener("keydown", handleKeyDown) + }) + + return { + register(cb: () => CommandOption[]) { + const results = createMemo(cb) + setRegistrations((arr) => [results, ...arr]) + onCleanup(() => { + setRegistrations((arr) => arr.filter((x) => x !== results)) + }) + }, + trigger(id: string, source?: "palette" | "keybind" | "slash") { + for (const option of options()) { + if (option.id === id || option.id === "suggested." + id) { + option.onSelect?.(source) + return + } + } + }, + keybind(id: string) { + const option = options().find((x) => x.id === id || x.id === "suggested." + id) + if (!option?.keybind) return "" + return formatKeybind(option.keybind) + }, + show: showPalette, + keybinds(enabled: boolean) { + setSuspendCount((count) => count + (enabled ? -1 : 1)) + }, + suspended, + get options() { + return options() + }, + } + }, +}) diff --git a/packages/desktop/src/context/global-sdk.tsx b/packages/desktop/src/context/global-sdk.tsx index 34e731ac9..3732ca085 100644 --- a/packages/desktop/src/context/global-sdk.tsx +++ b/packages/desktop/src/context/global-sdk.tsx @@ -1,30 +1,32 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { onCleanup } from "solid-js" +import { usePlatform } from "./platform" export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({ name: "GlobalSDK", init: (props: { url: string }) => { - const abort = new AbortController() - const sdk = createOpencodeClient({ + const eventSdk = createOpencodeClient({ baseUrl: props.url, - signal: abort.signal, + // signal: AbortSignal.timeout(1000 * 60 * 10), }) - const emitter = createGlobalEmitter<{ [key: string]: Event }>() - sdk.global.event().then(async (events) => { + eventSdk.global.event().then(async (events) => { for await (const event of events.stream) { // console.log("event", event) emitter.emit(event.directory ?? "global", event.payload) } }) - onCleanup(() => { - abort.abort() + const platform = usePlatform() + const sdk = createOpencodeClient({ + baseUrl: props.url, + signal: AbortSignal.timeout(1000 * 60 * 10), + fetch: platform.fetch, + throwOnError: true, }) return { url: props.url, client: sdk, event: emitter } diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx index 8151a2c6f..27a89e7bc 100644 --- a/packages/desktop/src/context/global-sync.tsx +++ b/packages/desktop/src/context/global-sync.tsx @@ -13,17 +13,22 @@ import { type SessionStatus, type ProviderListResponse, type ProviderAuthResponse, + type Command, createOpencodeClient, } from "@opencode-ai/sdk/v2/client" import { createStore, produce, reconcile } from "solid-js/store" import { Binary } from "@opencode-ai/util/binary" -import { createSimpleContext } from "@opencode-ai/ui/context" +import { retry } from "@opencode-ai/util/retry" import { useGlobalSDK } from "./global-sdk" -import { onMount } from "solid-js" +import { ErrorPage, type InitError } from "../pages/error" +import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js" +import { showToast } from "@opencode-ai/ui/toast" +import { getFilename } from "@opencode-ai/util/path" type State = { ready: boolean agent: Agent[] + command: Command[] project: string provider: ProviderListResponse config: Config @@ -49,233 +54,314 @@ type State = { changes: File[] } -export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({ - name: "GlobalSync", - init: () => { - const globalSDK = useGlobalSDK() - const [globalStore, setGlobalStore] = createStore<{ - ready: boolean - path: Path - project: Project[] - provider: ProviderListResponse - provider_auth: ProviderAuthResponse - children: Record - }>({ - ready: false, - path: { state: "", config: "", worktree: "", directory: "", home: "" }, - project: [], - provider: { all: [], connected: [], default: {} }, - provider_auth: {}, - children: {}, - }) +function createGlobalSync() { + const globalSDK = useGlobalSDK() + const [globalStore, setGlobalStore] = createStore<{ + ready: boolean + error?: InitError + path: Path + project: Project[] + provider: ProviderListResponse + provider_auth: ProviderAuthResponse + children: Record + }>({ + ready: false, + path: { state: "", config: "", worktree: "", directory: "", home: "" }, + project: [], + provider: { all: [], connected: [], default: {} }, + provider_auth: {}, + children: {}, + }) - const children: Record>> = {} - function child(directory: string) { - if (!children[directory]) { - setGlobalStore("children", directory, { - project: "", - provider: { all: [], connected: [], default: {} }, - config: {}, - path: { state: "", config: "", worktree: "", directory: "", home: "" }, - ready: false, - agent: [], - session: [], - session_status: {}, - session_diff: {}, - todo: {}, - limit: 5, - message: {}, - part: {}, - node: [], - changes: [], - }) - children[directory] = createStore(globalStore.children[directory]) - bootstrapInstance(directory) - } - return children[directory] + const children: Record>> = {} + function child(directory: string) { + if (!directory) console.error("No directory provided") + if (!children[directory]) { + setGlobalStore("children", directory, { + project: "", + provider: { all: [], connected: [], default: {} }, + config: {}, + path: { state: "", config: "", worktree: "", directory: "", home: "" }, + ready: false, + agent: [], + command: [], + session: [], + session_status: {}, + session_diff: {}, + todo: {}, + limit: 5, + message: {}, + part: {}, + node: [], + changes: [], + }) + children[directory] = createStore(globalStore.children[directory]) + bootstrapInstance(directory) } + return children[directory] + } - async function loadSessions(directory: string) { - globalSDK.client.session.list({ directory }).then((x) => { - const sessions = (x.data ?? []) + async function loadSessions(directory: string) { + const [store, setStore] = child(directory) + globalSDK.client.session + .list({ directory }) + .then((x) => { + const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000 + const nonArchived = (x.data ?? []) .slice() .filter((s) => !s.time.archived) .sort((a, b) => a.id.localeCompare(b.id)) - .slice(0, 5) - const [, setStore] = child(directory) + // Include up to the limit, plus any updated in the last 4 hours + const sessions = nonArchived.filter((s, i) => { + if (i < store.limit) return true + const updated = new Date(s.time.updated).getTime() + return updated > fourHoursAgo + }) setStore("session", sessions) }) - } - - async function bootstrapInstance(directory: string) { - const [, setStore] = child(directory) - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - directory, + .catch((err) => { + console.error("Failed to load sessions", err) + const project = getFilename(directory) + showToast({ title: `Failed to load sessions for ${project}`, description: err.message }) }) - const load = { - project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), - provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)), - path: () => sdk.path.get().then((x) => setStore("path", x.data!)), - agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), - session: () => loadSessions(directory), - status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)), - config: () => sdk.config.get().then((x) => setStore("config", x.data!)), - changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)), - node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), + } + + async function bootstrapInstance(directory: string) { + if (!directory) return + const [, setStore] = child(directory) + const sdk = createOpencodeClient({ + baseUrl: globalSDK.url, + directory, + throwOnError: true, + }) + const load = { + project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), + provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)), + path: () => sdk.path.get().then((x) => setStore("path", x.data!)), + agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), + command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])), + session: () => loadSessions(directory), + status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)), + config: () => sdk.config.get().then((x) => setStore("config", x.data!)), + changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)), + node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)), + } + await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e)))) + .then(() => setStore("ready", true)) + .catch((e) => setGlobalStore("error", e)) + } + + globalSDK.event.listen((e) => { + const directory = e.name + const event = e.details + + if (directory === "global") { + switch (event?.type) { + case "global.disposed": { + bootstrap() + break + } + case "project.updated": { + const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) + if (result.found) { + setGlobalStore("project", result.index, reconcile(event.properties)) + return + } + setGlobalStore( + "project", + produce((draft) => { + draft.splice(result.index, 0, event.properties) + }), + ) + break + } } - await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) + return } - globalSDK.event.listen((e) => { - const directory = e.name - const event = e.details - - if (directory === "global") { - switch (event.type) { - case "global.disposed": { - bootstrap() - break - } - case "project.updated": { - const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id) - if (result.found) { - setGlobalStore("project", result.index, reconcile(event.properties)) - return - } - setGlobalStore( - "project", + const [store, setStore] = child(directory) + switch (event.type) { + case "server.instance.disposed": { + bootstrapInstance(directory) + break + } + case "session.updated": { + const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) + if (event.properties.info.time.archived) { + if (result.found) { + setStore( + "session", produce((draft) => { - draft.splice(result.index, 0, event.properties) + draft.splice(result.index, 1) }), ) - break } + break } - return + if (result.found) { + setStore("session", result.index, reconcile(event.properties.info)) + break + } + setStore( + "session", + produce((draft) => { + draft.splice(result.index, 0, event.properties.info) + }), + ) + break } - - const [store, setStore] = child(directory) - switch (event.type) { - case "server.instance.disposed": { - bootstrapInstance(directory) + case "session.diff": + setStore("session_diff", event.properties.sessionID, event.properties.diff) + break + case "todo.updated": + setStore("todo", event.properties.sessionID, event.properties.todos) + break + case "session.status": { + setStore("session_status", event.properties.sessionID, event.properties.status) + break + } + case "message.updated": { + const messages = store.message[event.properties.info.sessionID] + if (!messages) { + setStore("message", event.properties.info.sessionID, [event.properties.info]) break } - case "session.updated": { - const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) - if (event.properties.info.time.archived) { - if (result.found) { - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 1) - }), - ) - } - break - } - if (result.found) { - setStore("session", result.index, reconcile(event.properties.info)) - break - } - setStore( - "session", - produce((draft) => { - draft.splice(result.index, 0, event.properties.info) - }), - ) + const result = Binary.search(messages, event.properties.info.id, (m) => m.id) + if (result.found) { + setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info)) break } - case "session.diff": - setStore("session_diff", event.properties.sessionID, event.properties.diff) - break - case "todo.updated": - setStore("todo", event.properties.sessionID, event.properties.todos) - break - case "session.status": { - setStore("session_status", event.properties.sessionID, event.properties.status) - break - } - case "message.updated": { - const messages = store.message[event.properties.info.sessionID] - if (!messages) { - setStore("message", event.properties.info.sessionID, [event.properties.info]) - break - } - const result = Binary.search(messages, event.properties.info.id, (m) => m.id) - if (result.found) { - setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info)) - break - } + setStore( + "message", + event.properties.info.sessionID, + produce((draft) => { + draft.splice(result.index, 0, event.properties.info) + }), + ) + break + } + case "message.removed": { + const messages = store.message[event.properties.sessionID] + if (!messages) break + const result = Binary.search(messages, event.properties.messageID, (m) => m.id) + if (result.found) { setStore( "message", - event.properties.info.sessionID, + event.properties.sessionID, produce((draft) => { - draft.splice(result.index, 0, event.properties.info) + draft.splice(result.index, 1) }), ) + } + break + } + case "message.part.updated": { + const part = event.properties.part + const parts = store.part[part.messageID] + if (!parts) { + setStore("part", part.messageID, [part]) break } - case "message.part.updated": { - const part = event.properties.part - const parts = store.part[part.messageID] - if (!parts) { - setStore("part", part.messageID, [part]) - break - } - const result = Binary.search(parts, part.id, (p) => p.id) - if (result.found) { - setStore("part", part.messageID, result.index, reconcile(part)) - break - } + const result = Binary.search(parts, part.id, (p) => p.id) + if (result.found) { + setStore("part", part.messageID, result.index, reconcile(part)) + break + } + setStore( + "part", + part.messageID, + produce((draft) => { + draft.splice(result.index, 0, part) + }), + ) + break + } + case "message.part.removed": { + const parts = store.part[event.properties.messageID] + if (!parts) break + const result = Binary.search(parts, event.properties.partID, (p) => p.id) + if (result.found) { setStore( "part", - part.messageID, + event.properties.messageID, produce((draft) => { - draft.splice(result.index, 0, part) + draft.splice(result.index, 1) }), ) - break } + break } - }) + } + }) - async function bootstrap() { - return Promise.all([ + async function bootstrap() { + return Promise.all([ + retry(() => globalSDK.client.path.get().then((x) => { setGlobalStore("path", x.data!) }), + ), + retry(() => globalSDK.client.project.list().then(async (x) => { setGlobalStore( "project", - x - .data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs) - .sort((a, b) => a.id.localeCompare(b.id)), + x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)), ) }), + ), + retry(() => globalSDK.client.provider.list().then((x) => { setGlobalStore("provider", x.data ?? {}) }), + ), + retry(() => globalSDK.client.provider.auth().then((x) => { setGlobalStore("provider_auth", x.data ?? {}) }), - ]).then(() => setGlobalStore("ready", true)) - } + ), + ]) + .then(() => setGlobalStore("ready", true)) + .catch((e) => setGlobalStore("error", e)) + } - onMount(() => { - bootstrap() - }) + onMount(() => { + bootstrap() + }) - return { - data: globalStore, - get ready() { - return globalStore.ready - }, - child, - bootstrap, - project: { - loadSessions, - }, - } - }, -}) + return { + data: globalStore, + get ready() { + return globalStore.ready + }, + get error() { + return globalStore.error + }, + child, + bootstrap, + project: { + loadSessions, + }, + } +} + +const GlobalSyncContext = createContext>() + +export function GlobalSyncProvider(props: ParentProps) { + const value = createGlobalSync() + return ( + + + + + + {props.children} + + + ) +} + +export function useGlobalSync() { + const context = useContext(GlobalSyncContext) + if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider") + return context +} diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx index 3d5cad761..17cd4785c 100644 --- a/packages/desktop/src/context/layout.tsx +++ b/packages/desktop/src/context/layout.tsx @@ -1,10 +1,10 @@ import { createStore, produce } from "solid-js/store" import { batch, createMemo, onMount } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" -import { makePersisted } from "@solid-primitives/storage" import { useGlobalSync } from "./global-sync" import { useGlobalSDK } from "./global-sdk" import { Project } from "@opencode-ai/sdk/v2" +import { persisted } from "@/utils/persist" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number] @@ -22,14 +22,20 @@ export function getAvatarColors(key?: string) { } } -type Dialog = "provider" | "model" | "connect" +type SessionTabs = { + active?: string + all: string[] +} + +export type LocalProject = Partial & { worktree: string; expanded: boolean } export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ name: "Layout", init: () => { const globalSdk = useGlobalSDK() const globalSync = useGlobalSync() - const [store, setStore] = makePersisted( + const [store, setStore, _, ready] = persisted( + "layout.v3", createStore({ projects: [] as { worktree: string; expanded: boolean }[], sidebar: { @@ -40,27 +46,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( opened: false, height: 280, }, - review: { - state: "pane" as "pane" | "tab", + session: { + width: 600, }, + sessionTabs: {} as Record, }), - { - name: "layout.v1", - }, ) - const [ephemeral, setEphemeral] = createStore<{ - connect: { - provider?: string - state?: "pending" | "complete" | "error" - error?: string - } - dialog: { - open?: Dialog - } - }>({ - connect: {}, - dialog: {}, - }) + const usedColors = new Set() function pickAvailableColor(): AvatarColorKey { @@ -71,21 +63,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( function enrich(project: { worktree: string; expanded: boolean }) { const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree) - if (!metadata) return [] return [ { ...project, - ...metadata, + ...(metadata ?? {}), }, ] } - function colorize(project: Project & { expanded: boolean }) { + function colorize(project: LocalProject) { if (project.icon?.color) return project const color = pickAvailableColor() usedColors.add(color) project.icon = { ...project.icon, color } - globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + if (project.id) { + globalSdk.client.project.update({ projectID: project.id, icon: { color } }) + } return project } @@ -101,10 +94,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }) return { + ready, projects: { list, open(directory: string) { - if (store.projects.find((x) => x.worktree === directory)) return + if (store.projects.find((x) => x.worktree === directory)) { + return + } globalSync.project.loadSessions(directory) setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x]) }, @@ -112,10 +108,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("projects", (x) => x.filter((x) => x.worktree !== directory)) }, expand(directory: string) { - setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x))) + const index = store.projects.findIndex((x) => x.worktree === directory) + if (index !== -1) setStore("projects", index, "expanded", true) }, collapse(directory: string) { - setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x))) + const index = store.projects.findIndex((x) => x.worktree === directory) + if (index !== -1) setStore("projects", index, "expanded", false) }, move(directory: string, toIndex: number) { setStore("projects", (projects) => { @@ -160,66 +158,87 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("terminal", "height", height) }, }, - review: { - state: createMemo(() => store.review?.state ?? "closed"), - pane() { - setStore("review", "state", "pane") - }, - tab() { - setStore("review", "state", "tab") - }, - }, - dialog: { - opened: createMemo(() => ephemeral.dialog?.open), - open(dialog: Dialog) { - batch(() => { - // if (dialog !== "connect") { - // setEphemeral("connect", {}) - // } - setEphemeral("dialog", "open", dialog) - }) - }, - close(dialog: Dialog) { - if (ephemeral.dialog.open === dialog) { - setEphemeral( - produce((state) => { - state.dialog.open = undefined - state.connect = {} - }), - ) + session: { + width: createMemo(() => store.session?.width ?? 600), + resize(width: number) { + if (!store.session) { + setStore("session", { width }) + } else { + setStore("session", "width", width) } }, - connect(provider: string) { - setEphemeral( - produce((state) => { - state.dialog.open = "connect" - state.connect = { provider, state: "pending" } - }), - ) - }, }, - connect: { - provider: createMemo(() => ephemeral.connect.provider), - state: createMemo(() => ephemeral.connect.state), - complete() { - setEphemeral( - produce((state) => { - state.dialog.open = "model" - state.connect.state = "complete" - }), - ) - }, - error(message: string) { - setEphemeral( - produce((state) => { - state.connect.state = "error" - state.connect.error = message - }), - ) - }, - clear() { - setEphemeral("connect", {}) - }, + tabs(sessionKey: string) { + const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] }) + return { + tabs, + active: createMemo(() => tabs().active), + all: createMemo(() => tabs().all), + setActive(tab: string | undefined) { + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all: [], active: tab }) + } else { + setStore("sessionTabs", sessionKey, "active", tab) + } + }, + setAll(all: string[]) { + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all, active: undefined }) + } else { + setStore("sessionTabs", sessionKey, "all", all) + } + }, + async open(tab: string) { + const current = store.sessionTabs[sessionKey] ?? { all: [] } + if (tab !== "review") { + if (!current.all.includes(tab)) { + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all: [tab], active: tab }) + } else { + setStore("sessionTabs", sessionKey, "all", [...current.all, tab]) + setStore("sessionTabs", sessionKey, "active", tab) + } + return + } + } + if (!store.sessionTabs[sessionKey]) { + setStore("sessionTabs", sessionKey, { all: [], active: tab }) + } else { + setStore("sessionTabs", sessionKey, "active", tab) + } + }, + close(tab: string) { + const current = store.sessionTabs[sessionKey] + if (!current) return + batch(() => { + setStore( + "sessionTabs", + sessionKey, + "all", + current.all.filter((x) => x !== tab), + ) + if (current.active === tab) { + const index = current.all.findIndex((f) => f === tab) + const previous = current.all[Math.max(0, index - 1)] + setStore("sessionTabs", sessionKey, "active", previous) + } + }) + }, + move(tab: string, to: number) { + const current = store.sessionTabs[sessionKey] + if (!current) return + const index = current.all.findIndex((f) => f === tab) + if (index === -1) return + setStore( + "sessionTabs", + sessionKey, + "all", + produce((opened) => { + opened.splice(to, 0, opened.splice(index, 1)[0]) + }), + ) + }, + } }, } }, diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 39fd1f987..69807a2f4 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -1,12 +1,14 @@ import { createStore, produce, reconcile } from "solid-js/store" import { batch, createEffect, createMemo } from "solid-js" -import { uniqueBy } from "remeda" +import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" import { useSDK } from "./sdk" import { useSync } from "./sync" import { base64Encode } from "@opencode-ai/util/encode" import { useProviders } from "@/hooks/use-providers" +import { DateTime } from "luxon" +import { persisted } from "@/utils/persist" export type LocalFile = FileNode & Partial<{ @@ -78,7 +80,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const agent = (() => { - const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent")) + const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const [store, setStore] = createStore<{ current: string }>({ @@ -108,30 +110,62 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ })() const model = (() => { - const [store, setStore] = createStore<{ + const [store, setStore, _, modelReady] = persisted( + "model.v1", + createStore<{ + user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[] + recent: ModelKey[] + }>({ + user: [], + recent: [], + }), + ) + + const [ephemeral, setEphemeral] = createStore<{ model: Record - recent: ModelKey[] }>({ model: {}, - recent: [], }) - const value = localStorage.getItem("model") - setStore("recent", JSON.parse(value ?? "[]")) - createEffect(() => { - localStorage.setItem("model", JSON.stringify(store.recent)) - }) - - const list = createMemo(() => + const available = createMemo(() => providers.connected().flatMap((p) => Object.values(p.models).map((m) => ({ ...m, - name: m.name.replace("(latest)", "").trim(), provider: p, - latest: m.name.includes("(latest)"), })), ), ) + + const latest = createMemo(() => + pipe( + available(), + filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6), + groupBy((x) => x.provider.id), + mapValues((models) => + pipe( + models, + groupBy((x) => x.family), + values(), + (groups) => + groups.flatMap((g) => { + const first = firstBy(g, [(x) => x.release_date, "desc"]) + return first ? [{ modelID: first.id, providerID: first.provider.id }] : [] + }), + ), + ), + values(), + flat(), + ), + ) + + const list = createMemo(() => + available().map((m) => ({ + ...m, + name: m.name.replace("(latest)", "").trim(), + latest: m.name.includes("(latest)"), + })), + ) + const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID) const fallbackModel = createMemo(() => { @@ -163,10 +197,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ throw new Error("No default model found") }) - const currentModel = createMemo(() => { + const current = createMemo(() => { const a = agent.current() const key = getFirstValidModel( - () => store.model[a.name], + () => ephemeral.model[a.name], () => a.model, fallbackModel, )! @@ -177,10 +211,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const cycle = (direction: 1 | -1) => { const recentList = recent() - const current = currentModel() - if (!current) return + const currentModel = current() + if (!currentModel) return - const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id) + const index = recentList.findIndex( + (x) => x?.provider.id === currentModel.provider.id && x?.id === currentModel.id, + ) if (index === -1) return let next = index + direction @@ -196,14 +232,25 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) } + function updateVisibility(model: ModelKey, visibility: "show" | "hide") { + const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) + if (index >= 0) { + setStore("user", index, { visibility }) + } else { + setStore("user", store.user.length, { ...model, visibility }) + } + } + return { - current: currentModel, + ready: modelReady, + current, recent, list, cycle, set(model: ModelKey | undefined, options?: { recent?: boolean }) { batch(() => { - setStore("model", agent.current().name, model ?? fallbackModel()) + setEphemeral("model", agent.current().name, model ?? fallbackModel()) + if (model) updateVisibility(model, "show") if (options?.recent && model) { const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) if (uniq.length > 5) uniq.pop() @@ -211,6 +258,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } }) }, + visible(model: ModelKey) { + const user = store.user.find((x) => x.modelID === model.modelID && x.providerID === model.providerID) + return ( + user?.visibility !== "hide" && + (latest().find((x) => x.modelID === model.modelID && x.providerID === model.providerID) || + user?.visibility === "show") + ) + }, + setVisibility(model: ModelKey, visible: boolean) { + updateVisibility(model, visible ? "show" : "hide") + }, } })() @@ -279,6 +337,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const load = async (path: string) => { const relativePath = relative(path) await sdk.client.file.read({ path: relativePath }).then((x) => { + if (!store.node[relativePath]) return setStore( "node", relativePath, @@ -301,7 +360,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const init = async (path: string) => { const relativePath = relative(path) if (!store.node[relativePath]) await fetch(path) - if (store.node[relativePath].loaded) return + if (store.node[relativePath]?.loaded) return return load(relativePath) } @@ -321,7 +380,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ context.addActive() if (options?.pinned) setStore("node", path, "pinned", true) if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view) - if (store.node[relativePath].loaded) return + if (store.node[relativePath]?.loaded) return return load(relativePath) } @@ -349,7 +408,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ case "file.watcher.updated": const relativePath = relative(event.properties.file) if (relativePath.startsWith(".git/")) return - load(relativePath) + if (store.node[relativePath]) load(relativePath) break } }) @@ -367,7 +426,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ init, expand(path: string) { setStore("node", path, "expanded", true) - if (store.node[path].loaded) return + if (store.node[path]?.loaded) return setStore("node", path, "loaded", true) list(path) }, diff --git a/packages/desktop/src/context/notification.tsx b/packages/desktop/src/context/notification.tsx index 744e4fdf3..2b258ebd6 100644 --- a/packages/desktop/src/context/notification.tsx +++ b/packages/desktop/src/context/notification.tsx @@ -1,10 +1,13 @@ import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" -import { makePersisted } from "@solid-primitives/storage" import { useGlobalSDK } from "./global-sdk" +import { useGlobalSync } from "./global-sync" +import { Binary } from "@opencode-ai/util/binary" import { EventSessionError } from "@opencode-ai/sdk/v2" import { makeAudioPlayer } from "@solid-primitives/audio" import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac" +import errorSound from "@opencode-ai/ui/audio/nope-03.aac" +import { persisted } from "@/utils/persist" type NotificationBase = { directory?: string @@ -28,23 +31,26 @@ export type Notification = TurnCompleteNotification | ErrorNotification export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({ name: "Notification", init: () => { - const idlePlayer = makeAudioPlayer(idleSound) - const globalSDK = useGlobalSDK() + let idlePlayer: ReturnType | undefined + let errorPlayer: ReturnType | undefined - const [store, setStore] = makePersisted( + try { + idlePlayer = makeAudioPlayer(idleSound) + errorPlayer = makeAudioPlayer(errorSound) + } catch (err) { + console.log("Failed to load audio", err) + } + + const globalSDK = useGlobalSDK() + const globalSync = useGlobalSync() + + const [store, setStore, _, ready] = persisted( + "notification.v1", createStore({ list: [] as Notification[], }), - { - name: "notification.v1", - }, ) - // onMount(() => { - // const daysToKeep = 7 - // // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now())) - // }) - globalSDK.event.listen((e) => { const directory = e.name const event = e.details @@ -55,22 +61,36 @@ export const { use: useNotification, provider: NotificationProvider } = createSi } switch (event.type) { case "session.idle": { - idlePlayer.play() - const session = event.properties.sessionID + const sessionID = event.properties.sessionID + const [syncStore] = globalSync.child(directory) + const match = Binary.search(syncStore.session, sessionID, (s) => s.id) + const isChild = match.found && syncStore.session[match.index].parentID + if (isChild) break + try { + idlePlayer?.play() + } catch {} setStore("list", store.list.length, { ...base, type: "turn-complete", - session, + session: sessionID, }) break } case "session.error": { - const session = event.properties.sessionID ?? "global" - // errorPlayer.play() + const sessionID = event.properties.sessionID + if (sessionID) { + const [syncStore] = globalSync.child(directory) + const match = Binary.search(syncStore.session, sessionID, (s) => s.id) + const isChild = match.found && syncStore.session[match.index].parentID + if (isChild) break + } + try { + errorPlayer?.play() + } catch {} setStore("list", store.list.length, { ...base, type: "error", - session, + session: sessionID ?? "global", error: "error" in event.properties ? event.properties.error : undefined, }) break @@ -79,6 +99,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi }) return { + ready, session: { all(session: string) { return store.list.filter((n) => n.session === session) diff --git a/packages/desktop/src/context/platform.tsx b/packages/desktop/src/context/platform.tsx index 21be49cbd..73d4c7f3e 100644 --- a/packages/desktop/src/context/platform.tsx +++ b/packages/desktop/src/context/platform.tsx @@ -1,9 +1,16 @@ import { createSimpleContext } from "@opencode-ai/ui/context" +import { AsyncStorage, SyncStorage } from "@solid-primitives/storage" export type Platform = { /** Platform discriminator */ platform: "web" | "tauri" + /** Open a URL in the default browser */ + openLink(url: string): void + + /** Restart the app */ + restart(): Promise + /** Open native directory picker dialog (Tauri only) */ openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise @@ -13,8 +20,17 @@ export type Platform = { /** Save file picker dialog (Tauri only) */ saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise - /** Open a URL in the default browser */ - openLink(url: string): void + /** Storage mechanism, defaults to localStorage */ + storage?: (name?: string) => SyncStorage | AsyncStorage + + /** Check for updates (Tauri only) */ + checkUpdate?(): Promise<{ updateAvailable: boolean; version?: string }> + + /** Install updates (Tauri only) */ + update?(): Promise + + /** Fetch override */ + fetch?: typeof fetch } export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ diff --git a/packages/desktop/src/context/prompt.tsx b/packages/desktop/src/context/prompt.tsx new file mode 100644 index 000000000..8d3590cd9 --- /dev/null +++ b/packages/desktop/src/context/prompt.tsx @@ -0,0 +1,111 @@ +import { createStore } from "solid-js/store" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { batch, createMemo } from "solid-js" +import { useParams } from "@solidjs/router" +import { TextSelection } from "./local" +import { persisted } from "@/utils/persist" + +interface PartBase { + content: string + start: number + end: number +} + +export interface TextPart extends PartBase { + type: "text" +} + +export interface FileAttachmentPart extends PartBase { + type: "file" + path: string + selection?: TextSelection +} + +export interface ImageAttachmentPart { + type: "image" + id: string + filename: string + mime: string + dataUrl: string +} + +export type ContentPart = TextPart | FileAttachmentPart | ImageAttachmentPart +export type Prompt = ContentPart[] + +export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] + +export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean { + if (promptA.length !== promptB.length) return false + for (let i = 0; i < promptA.length; i++) { + const partA = promptA[i] + const partB = promptB[i] + if (partA.type !== partB.type) return false + if (partA.type === "text" && partA.content !== (partB as TextPart).content) { + return false + } + if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) { + return false + } + if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) { + return false + } + } + return true +} + +function cloneSelection(selection?: TextSelection) { + if (!selection) return undefined + return { ...selection } +} + +function clonePart(part: ContentPart): ContentPart { + if (part.type === "text") return { ...part } + if (part.type === "image") return { ...part } + return { + ...part, + selection: cloneSelection(part.selection), + } +} + +function clonePrompt(prompt: Prompt): Prompt { + return prompt.map(clonePart) +} + +export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({ + name: "Prompt", + init: () => { + const params = useParams() + const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`) + + const [store, setStore, _, ready] = persisted( + name(), + createStore<{ + prompt: Prompt + cursor?: number + }>({ + prompt: clonePrompt(DEFAULT_PROMPT), + cursor: undefined, + }), + ) + + return { + ready, + current: createMemo(() => store.prompt), + cursor: createMemo(() => store.cursor), + dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)), + set(prompt: Prompt, cursorPosition?: number) { + const next = clonePrompt(prompt) + batch(() => { + setStore("prompt", next) + if (cursorPosition !== undefined) setStore("cursor", cursorPosition) + }) + }, + reset() { + batch(() => { + setStore("prompt", clonePrompt(DEFAULT_PROMPT)) + setStore("cursor", 0) + }) + }, + } + }, +}) diff --git a/packages/desktop/src/context/sdk.tsx b/packages/desktop/src/context/sdk.tsx index 764b01f8a..4d1c797c9 100644 --- a/packages/desktop/src/context/sdk.tsx +++ b/packages/desktop/src/context/sdk.tsx @@ -1,18 +1,20 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { onCleanup } from "solid-js" import { useGlobalSDK } from "./global-sdk" +import { usePlatform } from "./platform" export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", init: (props: { directory: string }) => { + const platform = usePlatform() const globalSDK = useGlobalSDK() - const abort = new AbortController() const sdk = createOpencodeClient({ baseUrl: globalSDK.url, - signal: abort.signal, + signal: AbortSignal.timeout(1000 * 60 * 10), + fetch: platform.fetch, directory: props.directory, + throwOnError: true, }) const emitter = createGlobalEmitter<{ @@ -23,10 +25,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ emitter.emit(event.type, event) }) - onCleanup(() => { - abort.abort() - }) - return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url } }, }) diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx deleted file mode 100644 index 860c1a14f..000000000 --- a/packages/desktop/src/context/session.tsx +++ /dev/null @@ -1,321 +0,0 @@ -import { createStore, produce } from "solid-js/store" -import { createSimpleContext } from "@opencode-ai/ui/context" -import { batch, createEffect, createMemo } from "solid-js" -import { useSync } from "./sync" -import { makePersisted } from "@solid-primitives/storage" -import { TextSelection } from "./local" -import { pipe, sumBy } from "remeda" -import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2" -import { useParams } from "@solidjs/router" -import { useSDK } from "./sdk" - -export type LocalPTY = { - id: string - title: string - rows?: number - cols?: number - buffer?: string - scrollY?: number -} - -export const { use: useSession, provider: SessionProvider } = createSimpleContext({ - name: "Session", - init: () => { - const sdk = useSDK() - const params = useParams() - const sync = useSync() - const name = createMemo(() => `${params.dir}/session${params.id ? "/" + params.id : ""}.v3`) - - const [store, setStore] = makePersisted( - createStore<{ - messageId?: string - tabs: { - active?: string - all: string[] - } - prompt: Prompt - cursor?: number - terminals: { - active?: string - all: LocalPTY[] - } - }>({ - tabs: { - all: [], - }, - prompt: clonePrompt(DEFAULT_PROMPT), - cursor: undefined, - terminals: { all: [] }, - }), - { - name: name(), - }, - ) - - createEffect(() => { - if (!params.id) return - sync.session.sync(params.id) - }) - - const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) - const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) - const userMessages = createMemo(() => - messages() - .filter((m) => m.role === "user") - .sort((a, b) => a.id.localeCompare(b.id)), - ) - const lastUserMessage = createMemo(() => { - return userMessages()?.at(-1) - }) - const activeMessage = createMemo(() => { - if (!store.messageId) return lastUserMessage() - return userMessages()?.find((m) => m.id === store.messageId) - }) - const status = createMemo( - () => - sync.data.session_status[params.id ?? ""] ?? { - type: "idle", - }, - ) - const working = createMemo(() => status()?.type !== "idle") - - const cost = createMemo(() => { - const total = pipe( - messages(), - sumBy((x) => (x.role === "assistant" ? x.cost : 0)), - ) - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(total) - }) - - const last = createMemo( - () => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage, - ) - const model = createMemo(() => - last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined, - ) - const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) - - const tokens = createMemo(() => { - if (!last()) return - const tokens = last().tokens - return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write - }) - - const context = createMemo(() => { - const total = tokens() - const limit = model()?.limit.context - if (!total || !limit) return 0 - return Math.round((total / limit) * 100) - }) - - return { - get id() { - return params.id - }, - info, - status, - working, - diffs, - prompt: { - current: createMemo(() => store.prompt), - cursor: createMemo(() => store.cursor), - dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)), - set(prompt: Prompt, cursorPosition?: number) { - const next = clonePrompt(prompt) - batch(() => { - setStore("prompt", next) - if (cursorPosition !== undefined) setStore("cursor", cursorPosition) - }) - }, - }, - messages: { - all: messages, - user: userMessages, - last: lastUserMessage, - active: activeMessage, - setActive(message: UserMessage | undefined) { - setStore("messageId", message?.id) - }, - }, - usage: { - tokens, - cost, - context, - }, - layout: { - tabs: store.tabs, - setActiveTab(tab: string | undefined) { - setStore("tabs", "active", tab) - }, - setOpenedTabs(tabs: string[]) { - setStore("tabs", "all", tabs) - }, - async openTab(tab: string) { - if (tab === "chat") { - setStore("tabs", "active", undefined) - return - } - if (tab !== "review") { - if (!store.tabs.all.includes(tab)) { - setStore("tabs", "all", [...store.tabs.all, tab]) - } - } - setStore("tabs", "active", tab) - }, - closeTab(tab: string) { - batch(() => { - setStore( - "tabs", - "all", - store.tabs.all.filter((x) => x !== tab), - ) - if (store.tabs.active === tab) { - const index = store.tabs.all.findIndex((f) => f === tab) - const previous = store.tabs.all[Math.max(0, index - 1)] - setStore("tabs", "active", previous) - } - }) - }, - moveTab(tab: string, to: number) { - const index = store.tabs.all.findIndex((f) => f === tab) - if (index === -1) return - setStore( - "tabs", - "all", - produce((opened) => { - opened.splice(to, 0, opened.splice(index, 1)[0]) - }), - ) - }, - }, - terminal: { - all: createMemo(() => Object.values(store.terminals.all)), - active: createMemo(() => store.terminals.active), - new() { - sdk.client.pty.create({ title: `Terminal ${store.terminals.all.length + 1}` }).then((pty) => { - const id = pty.data?.id - if (!id) return - setStore("terminals", "all", [ - ...store.terminals.all, - { - id, - title: pty.data?.title ?? "Terminal", - }, - ]) - setStore("terminals", "active", id) - }) - }, - update(pty: Partial & { id: string }) { - setStore("terminals", "all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x))) - sdk.client.pty.update({ - ptyID: pty.id, - title: pty.title, - size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, - }) - }, - async clone(id: string) { - const index = store.terminals.all.findIndex((x) => x.id === id) - const pty = store.terminals.all[index] - if (!pty) return - const clone = await sdk.client.pty.create({ - title: pty.title, - }) - if (!clone.data) return - setStore("terminals", "all", index, { - ...pty, - ...clone.data, - }) - if (store.terminals.active === pty.id) { - setStore("terminals", "active", clone.data.id) - } - }, - open(id: string) { - setStore("terminals", "active", id) - }, - async close(id: string) { - batch(() => { - setStore( - "terminals", - "all", - store.terminals.all.filter((x) => x.id !== id), - ) - if (store.terminals.active === id) { - const index = store.terminals.all.findIndex((f) => f.id === id) - const previous = store.tabs.all[Math.max(0, index - 1)] - setStore("terminals", "active", previous) - } - }) - await sdk.client.pty.remove({ ptyID: id }) - }, - move(id: string, to: number) { - const index = store.terminals.all.findIndex((f) => f.id === id) - if (index === -1) return - setStore( - "terminals", - "all", - produce((all) => { - all.splice(to, 0, all.splice(index, 1)[0]) - }), - ) - }, - }, - } - }, -}) - -interface PartBase { - content: string - start: number - end: number -} - -export interface TextPart extends PartBase { - type: "text" -} - -export interface FileAttachmentPart extends PartBase { - type: "file" - path: string - selection?: TextSelection -} - -export type ContentPart = TextPart | FileAttachmentPart -export type Prompt = ContentPart[] - -export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] - -export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean { - if (promptA.length !== promptB.length) return false - for (let i = 0; i < promptA.length; i++) { - const partA = promptA[i] - const partB = promptB[i] - if (partA.type !== partB.type) return false - if (partA.type === "text" && partA.content !== (partB as TextPart).content) { - return false - } - if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) { - return false - } - } - return true -} - -function cloneSelection(selection?: TextSelection) { - if (!selection) return undefined - return { ...selection } -} - -function clonePart(part: ContentPart): ContentPart { - if (part.type === "text") return { ...part } - return { - ...part, - selection: cloneSelection(part.selection), - } -} - -function clonePrompt(prompt: Prompt): Prompt { - return prompt.map(clonePart) -} diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx index 2ab54b3ae..941b8b629 100644 --- a/packages/desktop/src/context/sync.tsx +++ b/packages/desktop/src/context/sync.tsx @@ -1,9 +1,11 @@ import { produce } from "solid-js/store" import { createMemo } from "solid-js" import { Binary } from "@opencode-ai/util/binary" +import { retry } from "@opencode-ai/util/retry" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSync } from "./global-sync" import { useSDK } from "./sdk" +import type { Message, Part } from "@opencode-ai/sdk/v2/client" export const { use: useSync, provider: SyncProvider } = createSimpleContext({ name: "Sync", @@ -30,12 +32,40 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (match.found) return store.session[match.index] return undefined }, + addOptimisticMessage(input: { + sessionID: string + messageID: string + parts: Part[] + agent: string + model: { providerID: string; modelID: string } + }) { + const message: Message = { + id: input.messageID, + sessionID: input.sessionID, + role: "user", + time: { created: Date.now() }, + agent: input.agent, + model: input.model, + } + setStore( + produce((draft) => { + const messages = draft.message[input.sessionID] + if (!messages) { + draft.message[input.sessionID] = [message] + } else { + const result = Binary.search(messages, input.messageID, (m) => m.id) + messages.splice(result.index, 0, message) + } + draft.part[input.messageID] = input.parts.slice() + }), + ) + }, async sync(sessionID: string, _isRetry = false) { const [session, messages, todo, diff] = await Promise.all([ - sdk.client.session.get({ sessionID }, { throwOnError: true }), - sdk.client.session.messages({ sessionID, limit: 100 }), - sdk.client.session.todo({ sessionID }), - sdk.client.session.diff({ sessionID }), + retry(() => sdk.client.session.get({ sessionID })), + retry(() => sdk.client.session.messages({ sessionID, limit: 100 })), + retry(() => sdk.client.session.todo({ sessionID })), + retry(() => sdk.client.session.diff({ sessionID })), ]) setStore( produce((draft) => { diff --git a/packages/desktop/src/context/terminal.tsx b/packages/desktop/src/context/terminal.tsx new file mode 100644 index 000000000..6f7c11dea --- /dev/null +++ b/packages/desktop/src/context/terminal.tsx @@ -0,0 +1,105 @@ +import { createStore, produce } from "solid-js/store" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { batch, createMemo } from "solid-js" +import { useParams } from "@solidjs/router" +import { useSDK } from "./sdk" +import { persisted } from "@/utils/persist" + +export type LocalPTY = { + id: string + title: string + rows?: number + cols?: number + buffer?: string + scrollY?: number +} + +export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({ + name: "Terminal", + init: () => { + const sdk = useSDK() + const params = useParams() + const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`) + + const [store, setStore, _, ready] = persisted( + name(), + createStore<{ + active?: string + all: LocalPTY[] + }>({ + all: [], + }), + ) + + return { + ready, + all: createMemo(() => Object.values(store.all)), + active: createMemo(() => store.active), + new() { + sdk.client.pty.create({ title: `Terminal ${store.all.length + 1}` }).then((pty) => { + const id = pty.data?.id + if (!id) return + setStore("all", [ + ...store.all, + { + id, + title: pty.data?.title ?? "Terminal", + }, + ]) + setStore("active", id) + }) + }, + update(pty: Partial & { id: string }) { + setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x))) + sdk.client.pty.update({ + ptyID: pty.id, + title: pty.title, + size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, + }) + }, + async clone(id: string) { + const index = store.all.findIndex((x) => x.id === id) + const pty = store.all[index] + if (!pty) return + const clone = await sdk.client.pty.create({ + title: pty.title, + }) + if (!clone.data) return + setStore("all", index, { + ...pty, + ...clone.data, + }) + if (store.active === pty.id) { + setStore("active", clone.data.id) + } + }, + open(id: string) { + setStore("active", id) + }, + async close(id: string) { + batch(() => { + setStore( + "all", + store.all.filter((x) => x.id !== id), + ) + if (store.active === id) { + const index = store.all.findIndex((f) => f.id === id) + const previous = store.all[Math.max(0, index - 1)] + setStore("active", previous?.id) + } + }) + await sdk.client.pty.remove({ ptyID: id }) + }, + move(id: string, to: number) { + const index = store.all.findIndex((f) => f.id === id) + if (index === -1) return + setStore( + "all", + produce((all) => { + all.splice(to, 0, all.splice(index, 1)[0]) + }), + ) + }, + } + }, +}) diff --git a/packages/desktop/src/entry.tsx b/packages/desktop/src/entry.tsx index eec6396e9..ecbce9815 100644 --- a/packages/desktop/src/entry.tsx +++ b/packages/desktop/src/entry.tsx @@ -15,6 +15,9 @@ const platform: Platform = { openLink(url: string) { window.open(url, "_blank") }, + restart: async () => { + window.location.reload() + }, } render( diff --git a/packages/desktop/src/hooks/use-providers.ts b/packages/desktop/src/hooks/use-providers.ts index 501ff9d0c..4a73fa055 100644 --- a/packages/desktop/src/hooks/use-providers.ts +++ b/packages/desktop/src/hooks/use-providers.ts @@ -6,8 +6,8 @@ import { createMemo } from "solid-js" export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"] export function useProviders() { - const params = useParams() const globalSync = useGlobalSync() + const params = useParams() const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) const providers = createMemo(() => { if (currentDirectory()) { diff --git a/packages/desktop/src/pages/error.tsx b/packages/desktop/src/pages/error.tsx new file mode 100644 index 000000000..c7330c298 --- /dev/null +++ b/packages/desktop/src/pages/error.tsx @@ -0,0 +1,133 @@ +import { TextField } from "@opencode-ai/ui/text-field" +import { Logo } from "@opencode-ai/ui/logo" +import { Button } from "@opencode-ai/ui/button" +import { Component } from "solid-js" +import { usePlatform } from "@/context/platform" +import { Icon } from "@opencode-ai/ui/icon" + +export type InitError = { + name: string + data: Record +} + +function isInitError(error: unknown): error is InitError { + return ( + typeof error === "object" && + error !== null && + "name" in error && + "data" in error && + typeof (error as InitError).data === "object" + ) +} + +function formatInitError(error: InitError): string { + const data = error.data + switch (error.name) { + case "MCPFailed": + return `MCP server "${data.name}" failed. Note, opencode does not support MCP authentication yet.` + case "ProviderModelNotFoundError": { + const { providerID, modelID, suggestions } = data as { + providerID: string + modelID: string + suggestions?: string[] + } + return [ + `Model not found: ${providerID}/${modelID}`, + ...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), + `Check your config (opencode.json) provider/model names`, + ].join("\n") + } + case "ProviderInitError": + return `Failed to initialize provider "${data.providerID}". Check credentials and configuration.` + case "ConfigJsonError": + return `Config file at ${data.path} is not valid JSON(C)` + (data.message ? `: ${data.message}` : "") + case "ConfigDirectoryTypoError": + return `Directory "${data.dir}" in ${data.path} is not valid. Rename the directory to "${data.suggestion}" or remove it. This is a common typo.` + case "ConfigFrontmatterError": + return `Failed to parse frontmatter in ${data.path}:\n${data.message}` + case "ConfigInvalidError": { + const issues = Array.isArray(data.issues) + ? data.issues.map( + (issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."), + ) + : [] + return [`Config file at ${data.path} is invalid` + (data.message ? `: ${data.message}` : ""), ...issues].join( + "\n", + ) + } + case "UnknownError": + return String(data.message) + default: + return data.message ? String(data.message) : JSON.stringify(data, null, 2) + } +} + +function formatErrorChain(error: unknown, depth = 0): string { + if (!error) return "Unknown error" + + const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : "" + + if (isInitError(error)) { + return indent + formatInitError(error) + } + + if (error instanceof Error) { + const parts = [indent + `${error.name}: ${error.message}`] + if (error.stack) { + parts.push(error.stack) + } + if (error.cause) { + parts.push(formatErrorChain(error.cause, depth + 1)) + } + return parts.join("\n\n") + } + + if (typeof error === "string") return indent + error + return indent + JSON.stringify(error, null, 2) +} + +function formatError(error: unknown): string { + return formatErrorChain(error, 0) +} + +interface ErrorPageProps { + error: unknown +} + +export const ErrorPage: Component = (props) => { + const platform = usePlatform() + return ( +
    +
    + +
    +

    Something went wrong

    +

    An error occurred while loading the application.

    +
    + + +
    + Please report this error to the OpenCode team + +
    +
    +
    + ) +} diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 7da920c5f..489899f88 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -13,21 +13,21 @@ import { } from "solid-js" import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" -import { useLayout, getAvatarColors } from "@/context/layout" +import { useLayout, getAvatarColors, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { base64Decode, base64Encode } from "@opencode-ai/util/encode" import { Avatar } from "@opencode-ai/ui/avatar" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DiffChanges } from "@opencode-ai/ui/diff-changes" +import { Spinner } from "@opencode-ai/ui/spinner" import { getFilename } from "@opencode-ai/util/path" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" -import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client" +import { Session } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { createStore, produce } from "solid-js/store" import { @@ -37,31 +37,47 @@ import { SortableProvider, closestCenter, createSortable, - useDragDropContext, } from "@thisbeyond/solid-dnd" -import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" -import { SelectDialog } from "@opencode-ai/ui/select-dialog" -import { Tag } from "@opencode-ai/ui/tag" -import { IconName } from "@opencode-ai/ui/icons/provider" -import { popularProviders, useProviders } from "@/hooks/use-providers" -import { Dialog } from "@opencode-ai/ui/dialog" -import { iife } from "@opencode-ai/util/iife" -import { Link } from "@/components/link" -import { List, ListRef } from "@opencode-ai/ui/list" -import { TextField } from "@opencode-ai/ui/text-field" +import type { DragEvent } from "@thisbeyond/solid-dnd" +import { useProviders } from "@/hooks/use-providers" import { showToast, Toast } from "@opencode-ai/ui/toast" import { useGlobalSDK } from "@/context/global-sdk" -import { Spinner } from "@opencode-ai/ui/spinner" import { useNotification } from "@/context/notification" import { Binary } from "@opencode-ai/util/binary" import { Header } from "@/components/header" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogSelectProvider } from "@/components/dialog-select-provider" +import { useCommand } from "@/context/command" +import { ConstrainDragXAxis } from "@/utils/solid-dnd" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ lastSession: {} as { [directory: string]: string }, activeDraggable: undefined as string | undefined, + mobileSidebarOpen: false, + mobileProjectsExpanded: {} as Record, }) + const mobileSidebar = { + open: () => store.mobileSidebarOpen, + show: () => setStore("mobileSidebarOpen", true), + hide: () => setStore("mobileSidebarOpen", false), + toggle: () => setStore("mobileSidebarOpen", (x) => !x), + } + + const mobileProjects = { + expanded: (directory: string) => store.mobileProjectsExpanded[directory] ?? true, + expand: (directory: string) => setStore("mobileProjectsExpanded", directory, true), + collapse: (directory: string) => setStore("mobileProjectsExpanded", directory, false), + } + + let scrollContainerRef: HTMLDivElement | undefined + const xlQuery = window.matchMedia("(min-width: 1280px)") + const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches) + const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches) + xlQuery.addEventListener("change", handleViewportChange) + onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange)) + const params = useParams() const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() @@ -70,16 +86,218 @@ export default function Layout(props: ParentProps) { const notification = useNotification() const navigate = useNavigate() const providers = useProviders() + const dialog = useDialog() + const command = useCommand() + + onMount(async () => { + if (platform.checkUpdate && platform.update && platform.restart) { + const { updateAvailable, version } = await platform.checkUpdate() + if (updateAvailable) { + showToast({ + persistent: true, + icon: "download", + title: "Update available", + description: `A new version of OpenCode (${version}) is now available to install.`, + actions: [ + { + label: "Install and restart", + onClick: async () => { + await platform.update!() + await platform.restart!() + }, + }, + { + label: "Not yet", + onClick: "dismiss", + }, + ], + }) + } + } + }) + + function flattenSessions(sessions: Session[]): Session[] { + const childrenMap = new Map() + for (const session of sessions) { + if (session.parentID) { + const children = childrenMap.get(session.parentID) ?? [] + children.push(session) + childrenMap.set(session.parentID, children) + } + } + const result: Session[] = [] + function visit(session: Session) { + result.push(session) + for (const child of childrenMap.get(session.id) ?? []) { + visit(child) + } + } + for (const session of sessions) { + if (!session.parentID) visit(session) + } + return result + } + + function scrollToSession(sessionId: string) { + if (!scrollContainerRef) return + const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`) + if (element) { + element.scrollIntoView({ block: "center", behavior: "smooth" }) + } + } + + function projectSessions(directory: string) { + if (!directory) return [] + const sessions = globalSync + .child(directory)[0] + .session.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) + return flattenSessions(sessions ?? []) + } + + const currentSessions = createMemo(() => { + if (!params.dir) return [] + const directory = base64Decode(params.dir) + return projectSessions(directory) + }) + + function navigateSessionByOffset(offset: number) { + const projects = layout.projects.list() + if (projects.length === 0) return + + const currentDirectory = params.dir ? base64Decode(params.dir) : undefined + const projectIndex = currentDirectory ? projects.findIndex((p) => p.worktree === currentDirectory) : -1 + + if (projectIndex === -1) { + const targetProject = offset > 0 ? projects[0] : projects[projects.length - 1] + if (targetProject) navigateToProject(targetProject.worktree) + return + } + + const sessions = currentSessions() + const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1 + + let targetIndex: number + if (sessionIndex === -1) { + targetIndex = offset > 0 ? 0 : sessions.length - 1 + } else { + targetIndex = sessionIndex + offset + } + + if (targetIndex >= 0 && targetIndex < sessions.length) { + const session = sessions[targetIndex] + navigateToSession(session) + queueMicrotask(() => scrollToSession(session.id)) + return + } + + const nextProjectIndex = projectIndex + (offset > 0 ? 1 : -1) + const nextProject = projects[nextProjectIndex] + if (!nextProject) return + + const nextProjectSessions = projectSessions(nextProject.worktree) + if (nextProjectSessions.length === 0) { + navigateToProject(nextProject.worktree) + return + } + + const targetSession = offset > 0 ? nextProjectSessions[0] : nextProjectSessions[nextProjectSessions.length - 1] + navigate(`/${base64Encode(nextProject.worktree)}/session/${targetSession.id}`) + queueMicrotask(() => scrollToSession(targetSession.id)) + } + + async function archiveSession(session: Session) { + const [store, setStore] = globalSync.child(session.directory) + const sessions = store.session ?? [] + const index = sessions.findIndex((s) => s.id === session.id) + const nextSession = sessions[index + 1] ?? sessions[index - 1] + + await globalSDK.client.session.update({ + directory: session.directory, + sessionID: session.id, + time: { archived: Date.now() }, + }) + setStore( + produce((draft) => { + const match = Binary.search(draft.session, session.id, (s) => s.id) + if (match.found) draft.session.splice(match.index, 1) + }), + ) + if (session.id === params.id) { + if (nextSession) { + navigate(`/${params.dir}/session/${nextSession.id}`) + } else { + navigate(`/${params.dir}/session`) + } + } + } + + command.register(() => [ + { + id: "sidebar.toggle", + title: "Toggle sidebar", + category: "View", + keybind: "mod+b", + onSelect: () => layout.sidebar.toggle(), + }, + ...(platform.openDirectoryPickerDialog + ? [ + { + id: "project.open", + title: "Open project", + category: "Project", + keybind: "mod+o", + onSelect: () => chooseProject(), + }, + ] + : []), + { + id: "provider.connect", + title: "Connect provider", + category: "Provider", + onSelect: () => connectProvider(), + }, + { + id: "session.previous", + title: "Previous session", + category: "Session", + keybind: "alt+arrowup", + onSelect: () => navigateSessionByOffset(-1), + }, + { + id: "session.next", + title: "Next session", + category: "Session", + keybind: "alt+arrowdown", + onSelect: () => navigateSessionByOffset(1), + }, + { + id: "session.archive", + title: "Archive session", + category: "Session", + keybind: "mod+shift+backspace", + disabled: !params.dir || !params.id, + onSelect: () => { + const session = currentSessions().find((s) => s.id === params.id) + if (session) archiveSession(session) + }, + }, + ]) + + function connectProvider() { + dialog.show(() => ) + } function navigateToProject(directory: string | undefined) { if (!directory) return const lastSession = store.lastSession[directory] navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`) + mobileSidebar.hide() } function navigateToSession(session: Session | undefined) { if (!session) return navigate(`/${params.dir}/session/${session?.id}`) + mobileSidebar.hide() } function openProject(directory: string, navigate = true) { @@ -110,10 +328,6 @@ export default function Layout(props: ParentProps) { } } - async function connectProvider() { - layout.dialog.open("provider") - } - createEffect(() => { if (!params.dir || !params.id) return const directory = base64Decode(params.dir) @@ -122,8 +336,12 @@ export default function Layout(props: ParentProps) { }) createEffect(() => { - const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48 - document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) + if (isLargeViewport()) { + const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48 + document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) + } else { + document.documentElement.style.setProperty("--dialog-left-margin", "0px") + } }) function getDraggableId(event: unknown): string | undefined { @@ -156,30 +374,8 @@ export default function Layout(props: ParentProps) { setStore("activeDraggable", undefined) } - const ConstrainDragXAxis = (): JSX.Element => { - const context = useDragDropContext() - if (!context) return <> - const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context - const transformer: Transformer = { - id: "constrain-x-axis", - order: 100, - callback: (transform) => ({ ...transform, x: 0 }), - } - onDragStart((event) => { - const id = getDraggableId(event) - if (!id) return - addTransformer("draggables", id, transformer) - }) - onDragEnd((event) => { - const id = getDraggableId(event) - if (!id) return - removeTransformer("draggables", id, transformer.id) - }) - return <> - } - const ProjectAvatar = (props: { - project: Project + project: LocalProject class?: string expandable?: boolean notify?: boolean @@ -189,11 +385,13 @@ export default function Layout(props: ParentProps) { const hasError = createMemo(() => notifications().some((n) => n.type === "error")) const name = createMemo(() => getFilename(props.project.worktree)) const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)" + const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" + return ( -
    +