diff --git a/.github/workflows/guidelines-check.yml b/.github/guidelines-check.yml similarity index 96% rename from .github/workflows/guidelines-check.yml rename to .github/guidelines-check.yml index b4da51c93..522e52a5b 100644 --- a/.github/workflows/guidelines-check.yml +++ b/.github/guidelines-check.yml @@ -1,3 +1,7 @@ +# +# This file is intentionally in the wrong dir, will move and add later.... +# + name: Guidelines Check on: diff --git a/.github/workflows/auto-label-tui.yml b/.github/workflows/auto-label-tui.yml index 8f2d80b59..0f03bb68d 100644 --- a/.github/workflows/auto-label-tui.yml +++ b/.github/workflows/auto-label-tui.yml @@ -28,14 +28,14 @@ jobs: 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) { - // Add web label - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ['web'] - }); + labels.push('web'); // Assign to adamdotdevin await github.rest.issues.addAssignees({ @@ -46,10 +46,18 @@ jobs: }); } 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: ['opentui'] + labels: labels }); } diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 11f49fb7d..23dc9d39a 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -27,12 +27,12 @@ jobs: { "bash": { "gh issue*": "allow", - "*": "deny" - }, + "*": "deny" + }, "webfetch": "deny" } run: | - opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created:' + opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:' Issue number: ${{ github.event.issue.number }} diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 402c014fd..815433f03 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -4,7 +4,7 @@ on: push: branches: - dev - - opentui + - fix-snapshot-2 - v0 concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8d867284..ccce2aa25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,3 +28,9 @@ jobs: bun turbo test env: CI: true + + - name: Check SDK is up to date + run: | + bun ./packages/sdk/js/script/build.ts + git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist + continue-on-error: false diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml new file mode 100644 index 000000000..2f0aeac20 --- /dev/null +++ b/.github/workflows/update-nix-hashes.yml @@ -0,0 +1,84 @@ +name: Update Nix Hashes + +permissions: + contents: write + +on: + workflow_dispatch: + push: + paths: + - "bun.lock" + - "package.json" + - "packages/*/package.json" + pull_request: + paths: + - "bun.lock" + - "package.json" + - "packages/*/package.json" + +jobs: + update: + runs-on: ubuntu-latest + env: + SYSTEM: x86_64-linux + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Setup Nix + uses: DeterminateSystems/nix-installer-action@v20 + + - name: Configure git + run: | + git config --global user.email "action@github.com" + git config --global user.name "Github Action" + + - name: Update flake.lock + run: | + set -euo pipefail + nix flake update + + - name: Update node_modules hash + run: | + set -euo pipefail + nix/scripts/update-hashes.sh + + - name: Commit hash changes + env: + TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + run: | + set -euo pipefail + + summarize() { + local status="$1" + { + echo "### Nix Hash Update" + echo "" + echo "- ref: ${GITHUB_REF_NAME}" + echo "- status: ${status}" + } >> "$GITHUB_STEP_SUMMARY" + if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then + echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY" + fi + echo "" >> "$GITHUB_STEP_SUMMARY" + } + + FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json) + STATUS="$(git status --short -- "${FILES[@]}" || true)" + if [ -z "$STATUS" ]; then + summarize "no changes" + echo "No changes to tracked Nix files. Hashes are already up to date." + exit 0 + fi + + git add "${FILES[@]}" + git commit -m "Update Nix flake.lock and hashes" + + BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" + git push origin HEAD:"$BRANCH" + + summarize "committed $(git rev-parse --short HEAD)" diff --git a/.gitignore b/.gitignore index f69a70796..62cb12717 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,8 @@ dist .turbo **/.serena .serena/ +/result +refs +Session.vim +opencode.json +a.out diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 2e3d759b6..9626f172c 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,5 +1,6 @@ --- description: Git commit and push +subtask: true --- commit and push diff --git a/.opencode/command/issues.md b/.opencode/command/issues.md new file mode 100644 index 000000000..793dce651 --- /dev/null +++ b/.opencode/command/issues.md @@ -0,0 +1,23 @@ +--- +description: "Find issue(s) on github" +model: opencode/claude-haiku-4-5 +--- + +Search through existing issues in sst/opencode using the gh cli to find issues matching this query: + +$ARGUMENTS + +Consider: + +1. Similar titles or descriptions +2. Same error messages or symptoms +3. Related functionality or components +4. Similar feature requests + +Please list any matching issues with: + +- Issue number and title +- Brief explanation of why it matches the query +- Link to the issue + +If no clear matches are found, say so. diff --git a/.opencode/opencode.json b/.opencode/opencode.json deleted file mode 100644 index 7da874d36..000000000 --- a/.opencode/opencode.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "plugin": ["opencode-openai-codex-auth"] -} diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc new file mode 100644 index 000000000..2a4558e42 --- /dev/null +++ b/.opencode/opencode.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["opencode-openai-codex-auth"], + "provider": { + "opencode": { + "options": { + // "baseURL": "http://localhost:8080", + }, + }, + }, +} diff --git a/.vscode/launch.example.json b/.vscode/launch.example.json new file mode 100644 index 000000000..3f8a2a760 --- /dev/null +++ b/.vscode/launch.example.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "bun", + "request": "attach", + "name": "opencode (attach)", + "url": "ws://localhost:6499/" + } + ] +} diff --git a/.vscode/settings.example.json b/.vscode/settings.example.json new file mode 100644 index 000000000..05bbf7fe1 --- /dev/null +++ b/.vscode/settings.example.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "oven.bun-vscode" + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecfa7b3d5..2fc5737d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,38 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you > [!NOTE] > After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk. +### Setting up a Debugger + +Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points. + +The most reliable way to debug OpenCode is to run it manually in a terminal via `bun run --inspect= dev ...` and attach +your debugger via that URL. Other methods can result in breakpoints being mapped incorrectly, at least in VSCode (YMMV). + +Caveats: + +- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed + via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx` + files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code + is triggered. +- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of + the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there. + +Other tips and tricks: + +- You might want to use `--inspect-wait` or `--inspect-brk` instead of `--inspect`, depending on your workflow +- Specifying `--inspect=ws://localhost:6499/` on every invocation can be tiresome, you may want to `export BUN_OPTIONS=--inspect=ws://localhost:6499/` instead + +#### VSCode Setup + +If you use VSCode, you can use our example configurations [.vscode/settings.example.json](.vscode/settings.example.json) and [.vscode/launch.example.json](.vscode/launch.example.json). + +Some debug methods that can be problematic: + +- Debug configurations with `"request": "launch"` can have breakpoints incorrectly mapped and thus unusable +- The same problem arises when running OpenCode in the VSCode `JavaScript Debug Terminal` + +With that said, you may want to try these methods, as they might work for you. + ## Pull Request Expectations - Try to keep pull requests small and focused. diff --git a/README.md b/README.md index 551456f32..799cf00a2 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ curl -fsSL https://opencode.ai/install | bash npm i -g opencode-ai@latest # or bun/pnpm/yarn scoop bucket add extras; scoop install extras/opencode # Windows choco install opencode # Windows -brew install opencode # macOS and Linux +brew install opencode # macOS and Linux paru -S opencode-bin # Arch Linux +mise use --pin -g ubi:sst/opencode # Any OS +nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch ``` > [!TIP] @@ -50,6 +52,22 @@ OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bas XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash ``` +### Agents + +OpenCode includes two built-in agents you can switch between, +you can switch between these using the `Tab` key. + +- **build** - Default, full access agent for development work +- **plan** - Read-only agent for analysis and code exploration + - Denies file edits by default + - Asks permission before running bash commands + - Ideal for exploring unfamiliar codebases or planning changes + +Also, included is a **general** subagent for complex searches and multi-step tasks. +This is used internally and can be invoked using `@general` in messages. + +Learn more about [agents](https://opencode.ai/docs/agents). + ### Documentation For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs). @@ -58,6 +76,10 @@ For more info on how to configure OpenCode [**head over to our docs**](https://o If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request. +### 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. + ### FAQ #### How is this different than Claude Code? @@ -65,7 +87,7 @@ If you're interested in contributing to OpenCode, please read our [contributing It's very similar to Claude Code in terms of capability. Here are the key differences: - 100% open source -- Not coupled to any provider. Although Anthropic is recommended, OpenCode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important. +- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important. - Out of the box LSP support - A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal. - A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients. diff --git a/STATS.md b/STATS.md index 3f8da4f21..8e34cb4cf 100644 --- a/STATS.md +++ b/STATS.md @@ -138,3 +138,11 @@ | 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) | | 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) | | 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) | +| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) | +| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) | +| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) | +| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) | +| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) | +| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) | +| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) | +| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) | diff --git a/bun.lock b/bun.lock index 3f9916ccd..a02f23989 100644 --- a/bun.lock +++ b/bun.lock @@ -29,6 +29,7 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.1.0", + "chart.js": "4.5.1", "solid-js": "catalog:", "vinxi": "^0.5.7", "zod": "catalog:", @@ -40,7 +41,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -67,7 +68,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -91,7 +92,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -115,7 +116,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -155,7 +156,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -171,7 +172,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.61", + "version": "1.0.85", "bin": { "opencode": "./bin/opencode", }, @@ -179,6 +180,7 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.5.1", + "@ai-sdk/mcp": "0.0.8", "@clack/prompts": "1.0.0-alpha.1", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", @@ -189,8 +191,8 @@ "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opentui/core": "0.1.42", - "@opentui/solid": "0.1.42", + "@opentui/core": "0.1.47", + "@opentui/solid": "0.1.47", "@parcel/watcher": "2.5.1", "@pierre/precision-diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -249,7 +251,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -269,7 +271,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.61", + "version": "1.0.85", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -280,7 +282,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -293,7 +295,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -321,9 +323,19 @@ "vite-plugin-solid": "catalog:", }, }, + "packages/util": { + "name": "@opencode-ai/util", + "version": "1.0.85", + "dependencies": { + "zod": "catalog:", + }, + "devDependencies": { + "typescript": "catalog:", + }, + }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.61", + "version": "1.0.85", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -377,7 +389,7 @@ "@types/bun": "1.3.0", "@types/node": "22.13.9", "@typescript/native-preview": "7.0.0-dev.20251014.1", - "ai": "5.0.8", + "ai": "5.0.97", "diff": "8.0.2", "fuzzysort": "3.1.0", "hono": "4.7.10", @@ -412,12 +424,14 @@ "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="], "@ai-sdk/google": ["@ai-sdk/google@2.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-dnVIgSz1DZD/0gVau6ifYN3HZFN15HZwC9VjevTFfvrfSfbEvpXj5x/k/zk/0XuQrlQ5g8JiwJtxc9bx24x2xw=="], "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.16", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.9", "@ai-sdk/google": "2.0.11", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-tStlnOCRGRqKKJSCOtXhijX4r9kYVK2v+Vs7miJnfvr3sZfO8nRS0xnNhfgu17xuNi5LMMufeCYURTz4lKxzUQ=="], + "@ai-sdk/mcp": ["@ai-sdk/mcp@0.0.8", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9y9GuGcZ9/+pMIHfpOCJgZVp+AZMv6TkjX2NVT17SQZvTF2N8LXuCXyoUPyi1PxIxzxl0n463LxxaB2O6olC+Q=="], + "@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="], @@ -870,6 +884,8 @@ "@kobalte/utils": ["@kobalte/utils@0.9.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.2.14", "@solid-primitives/keyed": "^1.2.0", "@solid-primitives/map": "^0.4.7", "@solid-primitives/media": "^2.2.4", "@solid-primitives/props": "^3.1.8", "@solid-primitives/refs": "^1.0.5", "@solid-primitives/utils": "^6.2.1" }, "peerDependencies": { "solid-js": "^1.8.8" } }, "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.0", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], @@ -962,25 +978,27 @@ "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], + "@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"], + "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.42", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.42", "@opentui/core-darwin-x64": "0.1.42", "@opentui/core-linux-arm64": "0.1.42", "@opentui/core-linux-x64": "0.1.42", "@opentui/core-win32-arm64": "0.1.42", "@opentui/core-win32-x64": "0.1.42", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oV2xHBB2HaNiGvaV6R0C8GmniNJSsLKop4APq4FrLyCYberc6vZcATSHcA5YT9krdvHbBDOOn9RI2oaVJYRbUQ=="], + "@opentui/core": ["@opentui/core@0.1.47", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.47", "@opentui/core-darwin-x64": "0.1.47", "@opentui/core-linux-arm64": "0.1.47", "@opentui/core-linux-x64": "0.1.47", "@opentui/core-win32-arm64": "0.1.47", "@opentui/core-win32-x64": "0.1.47", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-gKcYX9EJ/e5VLEwBH2kalDr5xoI9MEanzQV7uV3Sb2Z9+ndwEUShKKna3odN8g4E20c4sX2VpwmB9hhl3Tsd9w=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.42", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sk5b/kh/y8HUJ7stGA5ydkajJX/z2OiGqSm+wn6XIoqdDavxQaFoQOt1PCuCqaxqZWJcXZ6OmISDVagZPUsPuw=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.47", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0/u4VkJJPvW24cZzMaKf6Dm+VzeO1a94l6NV3AQ1Wb+pPTEyOmNWkRvj03ZrRLMCyQduaFVtlnor8DVCk6OHuQ=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.42", "", { "os": "darwin", "cpu": "x64" }, "sha512-b0FKTw+t/wlJg4u+wTurWzbQe47gExkjguaGSUua0m0vybrkkvbUvmrADr+yivCjxcPAhSZ3lOOVU3uZuWsNqw=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.47", "", { "os": "darwin", "cpu": "x64" }, "sha512-y1+c/e+IaZAj5N02GnD+oaubbb5JiW5eKgF0h58kw73iXDMfynuoGOpREz58i1rUFYOMYJGdrSjEHtXk2pD2XA=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.42", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vy8BrjJpv2f56JAsYmv4PkC+2HsCv8Gh0ErrlIJQ8L4h29oWabS44m0uxFdvjuTDgKpCJzOScsxsy1VGzSd9rw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.47", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZESHmqILtfb6FFEfi40JGKl8z0+LhOSoHgfOK1PPyuyRT9Mk8uXeQgPMF5W6Ac0pp4w+uWVC4TrFjijCCSiaUQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.42", "", { "os": "linux", "cpu": "x64" }, "sha512-cO+13E1HIAPUdV/DRdKotHFAxsLc+ipbbFKGAuu/msfvywCnnNs86w22yeMg0cEqx7aBocWWT1XfJEHDJLFOqw=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.47", "", { "os": "linux", "cpu": "x64" }, "sha512-qfvy1qshgnZMcAHQ3MS093IBjxM2pPx+kEnW7icsyud60zoJgoUugdN2kjgJiIJiYX3f3PgE68J6CVW2MCtYfQ=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.42", "", { "os": "win32", "cpu": "arm64" }, "sha512-xpLhODjOWh7gMOSrKIldb4v6hR0TGyz6kjckDKwcjUv3LGbLJuSly+3O/zuWWS60dt56G1X4A0OyjWwiGZjc0g=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.47", "", { "os": "win32", "cpu": "arm64" }, "sha512-f6OoPnaz303H6fudi8blS+iEcJtlFlcqdBoWnWnJQfN9rLmajW3Yf7RfpNOoLUlDcwxQLyTL/5EHwbcG8D4r7A=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.42", "", { "os": "win32", "cpu": "x64" }, "sha512-pao5XdAln93WWPdsTF+V+HccZ5d1ijSmv0OoBbkjkVbP+tiN41yxNqg/7jzW9IiAakYsvmpKV+3ixi/dlBEvOQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.47", "", { "os": "win32", "cpu": "x64" }, "sha512-lQnJg7FucyyTbN/ybTj5FZ7S8OAfT5KxXDR5l9Sla7R5MIDY6nBXYM3GWeF81jzDd4K4Z/0hxNFtWSopEXRFYg=="], - "@opentui/solid": ["@opentui/solid@0.1.42", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.42", "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-4TNlEtatZ4n9TcKPWSF/EoaPaLmZuFVJ4hHh9wRggNaGrmDlmJ+9N/8oEKXETt+oRDX/1CdowAaTOVfaqb1t6g=="], + "@opentui/solid": ["@opentui/solid@0.1.47", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.47", "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-azN2sf8X/6HiLkz8ip2lcY532ApNEkl+BHd+wml/HdwdgLE7nthgA6x8Pgvi7f4qkRmpeYATU+danIzB6K6B8A=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1492,6 +1510,8 @@ "@vercel/nft": ["@vercel/nft@0.30.3", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w=="], + "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], + "@vinxi/listhen": ["@vinxi/listhen@1.5.6", "", { "dependencies": { "@parcel/watcher": "^2.3.0", "@parcel/watcher-wasm": "2.3.0", "citty": "^0.1.5", "clipboardy": "^4.0.0", "consola": "^3.2.3", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.10.0", "http-shutdown": "^1.2.2", "jiti": "^1.21.0", "mlly": "^1.5.0", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.3.2", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw=="], "@vinxi/plugin-directives": ["@vinxi/plugin-directives@0.5.1", "", { "dependencies": { "@babel/parser": "^7.23.5", "acorn": "^8.10.0", "acorn-jsx": "^5.3.2", "acorn-loose": "^8.3.0", "acorn-typescript": "^1.4.3", "astring": "^1.8.6", "magicast": "^0.2.10", "recast": "^0.23.4", "tslib": "^2.6.2" }, "peerDependencies": { "vinxi": "^0.5.5" } }, "sha512-pH/KIVBvBt7z7cXrUH/9uaqcdxjegFC7+zvkZkdOyWzs+kQD5KPf3cl8kC+5ayzXHT+OMlhGhyitytqN3cGmHg=="], @@ -1526,7 +1546,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@5.0.8", "", { "dependencies": { "@ai-sdk/gateway": "1.0.4", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-qbnhj046UvG30V1S5WhjBn+RBGEAmi8PSZWqMhRsE3EPxvO5BcePXTZFA23e9MYyWS9zr4Vm8Mv3wQXwLmtIBw=="], + "ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -1684,15 +1704,15 @@ "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], - "bun-webgpu": ["bun-webgpu@0.1.3", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.3", "bun-webgpu-darwin-x64": "^0.1.3", "bun-webgpu-linux-x64": "^0.1.3", "bun-webgpu-win32-x64": "^0.1.3" } }, "sha512-IXFxaIi4rgsEEpl9n/QVDm5RajCK/0FcOXZeMb52YRjoiAR1YVYK5hLrXT8cm+KDi6LVahA9GJFqOR4yiloVCw=="], + "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=="], - "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KkNQ9gT7dxGDndQaHTTHss9miukqpczML3pO2nZJoT/nITwe9lw3ZGFJMujkW41BUQ1mDYKFgo5nBGf9xYHPAg=="], + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eDgLN9teKTfmvrCqgwwmWNsNszxYs7IZdCqk0S1DCarvMhr4wcajoSBlA/nQA0/owwLduPTS8xxCnQp4/N/gDg=="], - "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-TODWnMUbCoqD/wqzlB3oGOBIUWIFly0lqMeBFz/MBV+ndjbnkNrP9huaZJCTkCVEPKGtd1FCM3ExZUtBbnGziA=="], + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-X+PjwJUWenUmdQBP8EtdItMyieQ6Nlpn+BH518oaouDiSnWj5+b0Y7DNDZJq7Ezom4EaxmqL/uGYZK3aCQ7CXg=="], - "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-lVHORoVu1G61XVM8CRRqUsqr6w8kMlpuSpbPGpKUpmvrsoay6ymXAhT5lRPKyrGNamHUQTknmWdI59aRDCfLtQ=="], + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zMLs2YIGB+/jxrYFXaFhVKX/GBt05UTF45lc9srcHc9JXGjEj+12CIo1CHLTAWatXMTqt0Jsu6ukWEoWVT/ayA=="], - "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-vlspsFffctJlBnFfs2lW3QgDD6LyFu8VT18ryID7Qka5poTj0clGVRxz7DFRi7yva3GovEGw/82z/WVc5US8Pw=="], + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Z5yAK28xrcm8Wb5k7TZ8FJKpOI/r+aVCRdlHYAqI2SDJFN3nD4mJs900X6kNVmG/xFzb5yOuKVYWGg+6ZXWbyA=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -1726,6 +1746,8 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "cheerio": ["cheerio@1.0.0-rc.12", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "htmlparser2": "^8.0.1", "parse5": "^7.0.0", "parse5-htmlparser2-tree-adapter": "^7.0.0" } }, "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q=="], "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], @@ -3598,7 +3620,7 @@ "@ai-sdk/amazon-bedrock/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="], + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="], @@ -3606,6 +3628,8 @@ "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="], + "@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], + "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "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-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="], @@ -3814,7 +3838,7 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="], + "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..1150e2751 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1763618868, + "narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..a6614a5dc --- /dev/null +++ b/flake.nix @@ -0,0 +1,107 @@ +{ + description = "OpenCode development flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = + { + nixpkgs, + ... + }: + let + systems = [ + "aarch64-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + lib = nixpkgs.lib; + forEachSystem = lib.genAttrs systems; + pkgsFor = system: nixpkgs.legacyPackages.${system}; + packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json); + bunTarget = { + "aarch64-linux" = "bun-linux-arm64"; + "x86_64-linux" = "bun-linux-x64"; + "aarch64-darwin" = "bun-darwin-arm64"; + "x86_64-darwin" = "bun-darwin-x64"; + }; + defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + hashesFile = "${./nix}/hashes.json"; + hashesData = + if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { }; + nodeModulesHash = hashesData.nodeModules or defaultNodeModules; + modelsDev = forEachSystem ( + system: + let + pkgs = pkgsFor system; + in + pkgs."models-dev" + ); + in + { + devShells = forEachSystem ( + system: + let + pkgs = pkgsFor system; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + bun + nodejs_20 + pkg-config + openssl + git + ]; + }; + } + ); + + packages = forEachSystem ( + system: + let + pkgs = pkgsFor system; + mkNodeModules = pkgs.callPackage ./nix/node-modules.nix { + hash = nodeModulesHash; + }; + mkPackage = pkgs.callPackage ./nix/opencode.nix { }; + in + { + default = mkPackage { + version = packageJson.version; + src = ./.; + scripts = ./nix/scripts; + target = bunTarget.${system}; + modelsDev = "${modelsDev.${system}}/dist/_api.json"; + mkNodeModules = mkNodeModules; + }; + } + ); + + apps = forEachSystem ( + system: + let + pkgs = pkgsFor system; + in + { + opencode-dev = { + type = "app"; + meta = { + description = "Nix devshell shell for OpenCode"; + runtimeInputs = [ pkgs.bun ]; + }; + program = "${ + pkgs.writeShellApplication { + name = "opencode-dev"; + text = '' + exec bun run dev "$@" + ''; + } + }/bin/opencode-dev"; + }; + } + ); + }; +} diff --git a/nix/hashes.json b/nix/hashes.json new file mode 100644 index 000000000..1a68f55ae --- /dev/null +++ b/nix/hashes.json @@ -0,0 +1,3 @@ +{ + "nodeModules": "sha256-bPiUpHGtgwVxHQHXBprpc6fFeJqW6/x7dwtQZBq29oU=" +} diff --git a/nix/node-modules.nix b/nix/node-modules.nix new file mode 100644 index 000000000..7b22ef8e7 --- /dev/null +++ b/nix/node-modules.nix @@ -0,0 +1,52 @@ +{ hash, lib, stdenvNoCC, bun, cacert, curl }: +args: +stdenvNoCC.mkDerivation { + pname = "opencode-node_modules"; + version = args.version; + src = args.src; + + impureEnvVars = + lib.fetchers.proxyImpureEnvVars + ++ [ + "GIT_PROXY_COMMAND" + "SOCKS_SERVER" + ]; + + nativeBuildInputs = [ bun cacert curl ]; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + export HOME=$(mktemp -d) + export BUN_INSTALL_CACHE_DIR=$(mktemp -d) + bun install \ + --cpu="*" \ + --os="*" \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress \ + --linker=isolated + bun --bun ${args.canonicalizeScript} + bun --bun ${args.normalizeBinsScript} + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + while IFS= read -r dir; do + rel="''${dir#./}" + dest="$out/$rel" + mkdir -p "$(dirname "$dest")" + cp -R "$dir" "$dest" + done < <(find . -type d -name node_modules -prune | sort) + runHook postInstall + ''; + + dontFixup = true; + + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = hash; +} diff --git a/nix/opencode.nix b/nix/opencode.nix new file mode 100644 index 000000000..bec299760 --- /dev/null +++ b/nix/opencode.nix @@ -0,0 +1,108 @@ +{ lib, stdenv, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }: +args: +let + scripts = args.scripts; + mkModules = + attrs: + args.mkNodeModules ( + attrs + // { + canonicalizeScript = scripts + "/canonicalize-node-modules.ts"; + normalizeBinsScript = scripts + "/normalize-bun-binaries.ts"; + } + ); +in +stdenvNoCC.mkDerivation (finalAttrs: { + pname = "opencode"; + version = args.version; + + src = args.src; + + node_modules = mkModules { + version = finalAttrs.version; + src = finalAttrs.src; + }; + + nativeBuildInputs = [ + bun + makeBinaryWrapper + ]; + + configurePhase = '' + runHook preConfigure + cp -R ${finalAttrs.node_modules}/. . + runHook postConfigure + ''; + + env.MODELS_DEV_API_JSON = args.modelsDev; + env.OPENCODE_VERSION = args.version; + env.OPENCODE_CHANNEL = "stable"; + + buildPhase = '' + runHook preBuild + + cp ${scripts + "/bun-build.ts"} bun-build.ts + + substituteInPlace bun-build.ts \ + --replace '@VERSION@' "${finalAttrs.version}" + + export BUN_COMPILE_TARGET=${args.target} + bun --bun bun-build.ts + + runHook postBuild + ''; + + dontStrip = true; + + installPhase = '' + runHook preInstall + + cd packages/opencode + if [ ! -f opencode ]; then + echo "ERROR: opencode binary not found in $(pwd)" + ls -la + exit 1 + fi + if [ ! -f opencode-worker.js ]; then + echo "ERROR: opencode worker bundle not found in $(pwd)" + ls -la + exit 1 + fi + + install -Dm755 opencode $out/bin/opencode + install -Dm644 opencode-worker.js $out/bin/opencode-worker.js + if [ -f opencode-assets.manifest ]; then + while IFS= read -r asset; do + [ -z "$asset" ] && continue + if [ ! -f "$asset" ]; then + echo "ERROR: referenced asset \"$asset\" missing" + exit 1 + fi + install -Dm644 "$asset" "$out/bin/$(basename "$asset")" + done < opencode-assets.manifest + fi + runHook postInstall + ''; + + postFixup = '' + wrapProgram "$out/bin/opencode" --prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} + ''; + + meta = { + description = "AI coding agent built for the terminal"; + longDescription = '' + OpenCode is a terminal-based agent that can build anything. + It combines a TypeScript/JavaScript core with a Go-based TUI + to provide an interactive AI coding experience. + ''; + homepage = "https://github.com/sst/opencode"; + license = lib.licenses.mit; + platforms = [ + "aarch64-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; + mainProgram = "opencode"; + }; +}) diff --git a/nix/scripts/bun-build.ts b/nix/scripts/bun-build.ts new file mode 100644 index 000000000..a22708163 --- /dev/null +++ b/nix/scripts/bun-build.ts @@ -0,0 +1,115 @@ +import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin" +import path from "path" +import fs from "fs" + +const version = "@VERSION@" +const pkg = path.join(process.cwd(), "packages/opencode") +const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js")) +const worker = "./src/cli/cmd/tui/worker.ts" +const target = process.env["BUN_COMPILE_TARGET"] + +if (!target) { + throw new Error("BUN_COMPILE_TARGET not set") +} + +process.chdir(pkg) + +const manifestName = "opencode-assets.manifest" +const manifestPath = path.join(pkg, manifestName) + +const readTrackedAssets = () => { + if (!fs.existsSync(manifestPath)) return [] + return fs + .readFileSync(manifestPath, "utf8") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) +} + +const removeTrackedAssets = () => { + for (const file of readTrackedAssets()) { + const filePath = path.join(pkg, file) + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { force: true }) + } + } +} + +const assets = new Set() + +const addAsset = async (p: string) => { + const file = path.basename(p) + const dest = path.join(pkg, file) + await Bun.write(dest, Bun.file(p)) + assets.add(file) +} + +removeTrackedAssets() + +const result = await Bun.build({ + conditions: ["browser"], + tsconfig: "./tsconfig.json", + plugins: [solidPlugin], + sourcemap: "external", + entrypoints: ["./src/index.ts", parser, worker], + define: { + OPENCODE_VERSION: `'@VERSION@'`, + OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"), + OPENCODE_CHANNEL: "'latest'", + }, + compile: { + target, + outfile: "opencode", + execArgv: ["--user-agent=opencode/" + version, '--env-file=""', "--"], + windows: {}, + }, +}) + +if (!result.success) { + console.error("Build failed!") + for (const log of result.logs) { + console.error(log) + } + throw new Error("Compilation failed") +} + +const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? [] +for (const x of assetOutputs) { + await addAsset(x.path) +} + +const bundle = await Bun.build({ + entrypoints: [worker], + tsconfig: "./tsconfig.json", + plugins: [solidPlugin], + target: "bun", + outdir: "./.opencode-worker", + sourcemap: "none", +}) + +if (!bundle.success) { + console.error("Worker build failed!") + for (const log of bundle.logs) { + console.error(log) + } + throw new Error("Worker compilation failed") +} + +const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? [] +for (const x of workerAssets) { + await addAsset(x.path) +} + +const output = bundle.outputs.find((x) => x.kind === "entry-point") +if (!output) { + throw new Error("Worker build produced no entry-point output") +} + +const dest = path.join(pkg, "opencode-worker.js") +await Bun.write(dest, Bun.file(output.path)) +fs.rmSync(path.dirname(output.path), { recursive: true, force: true }) + +const list = Array.from(assets) +await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "") + +console.log("Build successful!") diff --git a/nix/scripts/canonicalize-node-modules.ts b/nix/scripts/canonicalize-node-modules.ts new file mode 100644 index 000000000..bb004f3c5 --- /dev/null +++ b/nix/scripts/canonicalize-node-modules.ts @@ -0,0 +1,96 @@ +import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" +import { join, relative } from "path" + +type SemverLike = { + valid: (value: string) => string | null + rcompare: (left: string, right: string) => number +} + +type Entry = { + dir: string + version: string + label: string +} + +const root = process.cwd() +const bunRoot = join(root, "node_modules/.bun") +const linkRoot = join(bunRoot, "node_modules") +const directories = (await readdir(bunRoot)).sort() +const versions = new Map() + +for (const entry of directories) { + const full = join(bunRoot, entry) + const info = await lstat(full) + if (!info.isDirectory()) { + continue + } + const marker = entry.lastIndexOf("@") + if (marker <= 0) { + continue + } + const slug = entry.slice(0, marker).replace(/\+/g, "/") + const version = entry.slice(marker + 1) + const list = versions.get(slug) ?? [] + list.push({ dir: full, version, label: entry }) + versions.set(slug, list) +} + +const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as + | SemverLike + | { + default: SemverLike + } +const semver = "default" in semverModule ? semverModule.default : semverModule +const selections = new Map() + +for (const [slug, list] of versions) { + list.sort((a, b) => { + const left = semver.valid(a.version) + const right = semver.valid(b.version) + if (left && right) { + const delta = semver.rcompare(left, right) + if (delta !== 0) { + return delta + } + } + if (left && !right) { + return -1 + } + if (!left && right) { + return 1 + } + return b.version.localeCompare(a.version) + }) + selections.set(slug, list[0]) +} + +await rm(linkRoot, { recursive: true, force: true }) +await mkdir(linkRoot, { recursive: true }) + +const rewrites: string[] = [] + +for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) { + const parts = slug.split("/") + const leaf = parts.pop() + if (!leaf) { + continue + } + const parent = join(linkRoot, ...parts) + await mkdir(parent, { recursive: true }) + const linkPath = join(parent, leaf) + const desired = join(entry.dir, "node_modules", slug) + const relativeTarget = relative(parent, desired) + const resolved = relativeTarget.length === 0 ? "." : relativeTarget + await rm(linkPath, { recursive: true, force: true }) + await symlink(resolved, linkPath) + rewrites.push(slug + " -> " + resolved) +} + +rewrites.sort() +console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links") +for (const line of rewrites.slice(0, 20)) { + console.log(" ", line) +} +if (rewrites.length > 20) { + console.log(" ...") +} diff --git a/nix/scripts/normalize-bun-binaries.ts b/nix/scripts/normalize-bun-binaries.ts new file mode 100644 index 000000000..531d8fd05 --- /dev/null +++ b/nix/scripts/normalize-bun-binaries.ts @@ -0,0 +1,138 @@ +import { lstat, mkdir, readdir, rm, symlink } from "fs/promises" +import { join, relative } from "path" + +type PackageManifest = { + name?: string + bin?: string | Record +} + +const root = process.cwd() +const bunRoot = join(root, "node_modules/.bun") +const bunEntries = (await safeReadDir(bunRoot)).sort() +let rewritten = 0 + +for (const entry of bunEntries) { + const modulesRoot = join(bunRoot, entry, "node_modules") + if (!(await exists(modulesRoot))) { + continue + } + const binRoot = join(modulesRoot, ".bin") + await rm(binRoot, { recursive: true, force: true }) + await mkdir(binRoot, { recursive: true }) + + const packageDirs = await collectPackages(modulesRoot) + for (const packageDir of packageDirs) { + const manifest = await readManifest(packageDir) + if (!manifest) { + continue + } + const binField = manifest.bin + if (!binField) { + continue + } + const seen = new Set() + if (typeof binField === "string") { + const fallback = manifest.name ?? packageDir.split("/").pop() + if (fallback) { + await linkBinary(binRoot, fallback, packageDir, binField, seen) + } + } else { + const entries = Object.entries(binField).sort((a, b) => a[0].localeCompare(b[0])) + for (const [name, target] of entries) { + await linkBinary(binRoot, name, packageDir, target, seen) + } + } + } +} + +console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`) + +async function collectPackages(modulesRoot: string) { + const found: string[] = [] + const topLevel = (await safeReadDir(modulesRoot)).sort() + for (const name of topLevel) { + if (name === ".bin" || name === ".bun") { + continue + } + const full = join(modulesRoot, name) + if (!(await isDirectory(full))) { + continue + } + if (name.startsWith("@")) { + const scoped = (await safeReadDir(full)).sort() + for (const child of scoped) { + const scopedDir = join(full, child) + if (await isDirectory(scopedDir)) { + found.push(scopedDir) + } + } + continue + } + found.push(full) + } + return found.sort() +} + +async function readManifest(dir: string) { + const file = Bun.file(join(dir, "package.json")) + if (!(await file.exists())) { + return null + } + const data = (await file.json()) as PackageManifest + return data +} + +async function linkBinary(binRoot: string, name: string, packageDir: string, target: string, seen: Set) { + if (!name || !target) { + return + } + const normalizedName = normalizeBinName(name) + if (seen.has(normalizedName)) { + return + } + const resolved = join(packageDir, target) + const script = Bun.file(resolved) + if (!(await script.exists())) { + return + } + seen.add(normalizedName) + const destination = join(binRoot, normalizedName) + const relativeTarget = relative(binRoot, resolved) || "." + await rm(destination, { force: true }) + await symlink(relativeTarget, destination) + rewritten++ +} + +async function exists(path: string) { + try { + await lstat(path) + return true + } catch { + return false + } +} + +async function isDirectory(path: string) { + try { + const info = await lstat(path) + return info.isDirectory() + } catch { + return false + } +} + +async function safeReadDir(path: string) { + try { + return await readdir(path) + } catch { + return [] + } +} + +function normalizeBinName(name: string) { + const slash = name.lastIndexOf("/") + if (slash >= 0) { + return name.slice(slash + 1) + } + return name +} diff --git a/nix/scripts/update-hashes.sh b/nix/scripts/update-hashes.sh new file mode 100755 index 000000000..7bf183c5b --- /dev/null +++ b/nix/scripts/update-hashes.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +SYSTEM=${SYSTEM:-x86_64-linux} +DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json} +HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE} + +if [ ! -f "$HASH_FILE" ]; then + cat >"$HASH_FILE" </dev/null 2>&1; then + if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then + git add -N "$HASH_FILE" >/dev/null 2>&1 || true + fi +fi + +export DUMMY +export NIX_KEEP_OUTPUTS=1 +export NIX_KEEP_DERIVATIONS=1 + +cleanup() { + rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}" +} + +trap cleanup EXIT + +write_node_modules_hash() { + local value="$1" + local temp + temp=$(mktemp) + jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp" + mv "$temp" "$HASH_FILE" +} + +TARGET="packages.${SYSTEM}.default" +MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" +CORRECT_HASH="" + +DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" + +echo "Setting dummy node_modules outputHash for ${SYSTEM}..." +write_node_modules_hash "$DUMMY" + +BUILD_LOG=$(mktemp) +JSON_OUTPUT=$(mktemp) + +echo "Building node_modules for ${SYSTEM} to discover correct outputHash..." +echo "Attempting to realize derivation: ${DRV_PATH}" +REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) + +BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) +if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then + echo "Realized node_modules output: $BUILD_PATH" + CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) +fi + +if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" + fi + + if [ -z "$CORRECT_HASH" ]; then + echo "Searching for kept failed build directory..." + KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1) + + if [ -z "$KEPT_DIR" ]; then + KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1) + fi + + if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then + echo "Found kept build directory: $KEPT_DIR" + if [ -d "$KEPT_DIR/build" ]; then + HASH_PATH="$KEPT_DIR/build" + else + HASH_PATH="$KEPT_DIR" + fi + + echo "Attempting to hash: $HASH_PATH" + ls -la "$HASH_PATH" || true + + if [ -d "$HASH_PATH/node_modules" ]; then + CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) + echo "Computed hash from kept build: $CORRECT_HASH" + fi + fi + fi +fi + +if [ -z "$CORRECT_HASH" ]; then + echo "Failed to determine correct node_modules hash for ${SYSTEM}." + echo "Build log:" + cat "$BUILD_LOG" + exit 1 +fi + +write_node_modules_hash "$CORRECT_HASH" + +jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null + +echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH" + +rm -f "$BUILD_LOG" +unset BUILD_LOG diff --git a/package.json b/package.json index 13df96d39..ab75f2d9b 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@solidjs/meta": "0.29.4", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", - "ai": "5.0.8", + "ai": "5.0.97", "hono": "4.7.10", "fuzzysort": "3.1.0", "luxon": "3.6.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 876875272..745bf6beb 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,19 +7,20 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "1.0.61" + "version": "1.0.85" }, "dependencies": { "@ibm/plex": "6.4.1", + "@jsx-email/render": "1.1.1", + "@kobalte/core": "catalog:", + "@openauthjs/openauth": "catalog:", "@opencode-ai/console-core": "workspace:*", "@opencode-ai/console-mail": "workspace:*", - "@openauthjs/openauth": "catalog:", - "@kobalte/core": "catalog:", - "@jsx-email/render": "1.1.1", "@opencode-ai/console-resource": "workspace:*", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.1.0", + "chart.js": "4.5.1", "solid-js": "catalog:", "vinxi": "^0.5.7", "zod": "catalog:" diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx index 0395cad52..55f0940a0 100644 --- a/packages/console/app/src/component/icon.tsx +++ b/packages/console/app/src/component/icon.tsx @@ -202,6 +202,14 @@ export function IconZai(props: JSX.SvgSVGAttributes) { ) } +export function IconGoogle(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + export function IconStealth(props: JSX.SvgSVGAttributes) { return ( @@ -212,3 +220,30 @@ export function IconStealth(props: JSX.SvgSVGAttributes) { ) } + +export function IconChevronLeft(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconChevronRight(props: JSX.SvgSVGAttributes) { + return ( + + + + ) +} + +export function IconBreakdown(props: JSX.SvgSVGAttributes) { + return ( + + + + + + + ) +} diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index 40108e968..19f15c186 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -22,8 +22,8 @@ export const config = { // Static stats (used on landing page) stats: { - contributors: "250", - commits: "3,500", + contributors: "300", + commits: "4,000", monthlyUsers: "300,000", }, } as const diff --git a/packages/console/app/src/routes/workspace/[id]/graph-section.module.css b/packages/console/app/src/routes/workspace/[id]/graph-section.module.css new file mode 100644 index 000000000..24b85be74 --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/graph-section.module.css @@ -0,0 +1,145 @@ +.root { + [data-component="empty-state"] { + padding: var(--space-20) var(--space-6); + text-align: center; + border: 1px dashed var(--color-border); + border-radius: var(--border-radius-sm); + height: 400px; + display: flex; + align-items: center; + justify-content: center; + + p { + font-size: var(--font-size-sm); + color: var(--color-text-muted); + } + } + + [data-slot="filter-container"] { + margin-bottom: 0; + display: flex; + align-items: center; + gap: var(--space-3); + + [data-component="dropdown"] { + [data-slot="trigger"] { + border: 1px solid var(--color-border); + background-color: var(--color-bg); + padding: var(--space-2) var(--space-3); + border-radius: var(--border-radius-sm); + color: var(--color-text); + font-size: var(--font-size-sm); + line-height: 1.5; + + &:hover { + border-color: var(--color-accent); + } + + &:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-alpha); + } + } + + [data-slot="chevron"] { + opacity: 0.6; + } + + [data-slot="dropdown"] { + min-width: 200px; + max-height: 300px; + overflow-y: auto; + padding: var(--space-1); + } + } + } + + [data-slot="month-picker"] { + display: flex; + align-items: center; + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + padding: 0; + } + + [data-slot="month-button"] { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none !important; + color: var(--color-text); + cursor: pointer; + padding: var(--space-2) var(--space-3); + border-radius: var(--border-radius-xs); + transition: background-color 0.2s; + line-height: 1; + + &:hover { + background-color: var(--color-bg-hover); + } + + svg { + display: block; + width: 16px; + height: 16px; + stroke-width: 2; + } + } + + [data-slot="month-label"] { + font-size: var(--font-size-sm); + font-weight: 500; + color: var(--color-text); + line-height: 1.5; + min-width: 140px; + text-align: center; + white-space: nowrap; + } + + [data-slot="model-item"] { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + cursor: pointer; + transition: background-color 0.2s; + font-size: var(--font-size-sm); + color: var(--color-text); + border: none !important; + background: none; + width: 100%; + text-align: left; + white-space: nowrap; + + &:hover { + background: var(--color-bg-hover); + } + + span { + flex: 1; + user-select: none; + } + } + + [data-slot="chart-container"] { + padding: var(--space-6); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + height: 400px; + } + + @media (max-width: 40rem) { + [data-slot="chart-container"] { + height: 300px; + padding: var(--space-4); + } + + [data-component="empty-state"] { + height: 300px; + } + } +} diff --git a/packages/console/app/src/routes/workspace/[id]/graph-section.tsx b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx new file mode 100644 index 000000000..b13309d3d --- /dev/null +++ b/packages/console/app/src/routes/workspace/[id]/graph-section.tsx @@ -0,0 +1,423 @@ +import { and, Database, eq, gte, inArray, isNull, lte, or, sql, sum } from "@opencode-ai/console-core/drizzle/index.js" +import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" +import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js" +import { createAsync, query, useParams } from "@solidjs/router" +import { createEffect, createMemo, onCleanup, Show, For } from "solid-js" +import { createStore } from "solid-js/store" +import { withActor } from "~/context/auth.withActor" +import { Dropdown } from "~/component/dropdown" +import { IconChevronLeft, IconChevronRight } from "~/component/icon" +import styles from "./graph-section.module.css" +import { + Chart, + BarController, + BarElement, + CategoryScale, + LinearScale, + Tooltip, + Legend, + type ChartConfiguration, +} from "chart.js" + +Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend) + +async function getCosts(workspaceID: string, year: number, month: number) { + "use server" + return withActor(async () => { + const startDate = new Date(year, month, 1) + const endDate = new Date(year, month + 1, 0) + + // First query: get usage data without joining keys + const usageData = await Database.use((tx) => + tx + .select({ + date: sql`DATE(${UsageTable.timeCreated})`, + model: UsageTable.model, + totalCost: sum(UsageTable.cost), + keyId: UsageTable.keyID, + }) + .from(UsageTable) + .where( + and( + eq(UsageTable.workspaceID, workspaceID), + gte(UsageTable.timeCreated, startDate), + lte(UsageTable.timeCreated, endDate), + ), + ) + .groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID) + .then((x) => + x.map((r) => ({ + ...r, + totalCost: r.totalCost ? parseInt(r.totalCost) : 0, + })), + ), + ) + + // Get unique key IDs from usage + const usageKeyIds = new Set(usageData.map((r) => r.keyId).filter((id) => id !== null)) + + // Second query: get all existing keys plus any keys from usage + const keysData = await Database.use((tx) => + tx + .select({ + keyId: KeyTable.id, + keyName: KeyTable.name, + userEmail: AuthTable.subject, + timeDeleted: KeyTable.timeDeleted, + }) + .from(KeyTable) + .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID))) + .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email"))) + .where( + and( + eq(KeyTable.workspaceID, workspaceID), + usageKeyIds.size > 0 + ? or(inArray(KeyTable.id, Array.from(usageKeyIds)), isNull(KeyTable.timeDeleted)) + : isNull(KeyTable.timeDeleted), + ), + ) + .orderBy(AuthTable.subject, KeyTable.name), + ) + + return { + usage: usageData, + keys: keysData.map((key) => ({ + id: key.keyId, + displayName: + key.timeDeleted !== null + ? `${key.userEmail} - ${key.keyName} (deleted)` + : `${key.userEmail} - ${key.keyName}`, + })), + } + }, workspaceID) +} + +const queryCosts = query(getCosts, "costs.get") + +const MODEL_COLORS: Record = { + "claude-sonnet-4-5": "#D4745C", + "claude-sonnet-4": "#E8B4A4", + "claude-opus-4": "#C8A098", + "claude-haiku-4-5": "#F0D8D0", + "claude-3-5-haiku": "#F8E8E0", + "gpt-5.1": "#4A90E2", + "gpt-5.1-codex": "#6BA8F0", + "gpt-5": "#7DB8F8", + "gpt-5-codex": "#9FCAFF", + "gpt-5-nano": "#B8D8FF", + "grok-code": "#8B5CF6", + "big-pickle": "#10B981", + "kimi-k2": "#F59E0B", + "qwen3-coder": "#EC4899", + "glm-4.6": "#14B8A6", +} + +function getModelColor(model: string): string { + if (MODEL_COLORS[model]) return MODEL_COLORS[model] + + const hash = model.split("").reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0) + const hue = Math.abs(hash) % 360 + return `hsl(${hue}, 50%, 65%)` +} + +function formatDateLabel(dateStr: string): string { + const date = new Date() + const [y, m, d] = dateStr.split("-").map(Number) + date.setFullYear(y) + date.setMonth(m - 1) + date.setDate(d) + date.setHours(0, 0, 0, 0) + const month = date.toLocaleDateString("en-US", { month: "short" }) + const day = date.getUTCDate().toString().padStart(2, "0") + return `${month} ${day}` +} + +function addOpacityToColor(color: string, opacity: number): string { + if (color.startsWith("#")) { + const r = parseInt(color.slice(1, 3), 16) + const g = parseInt(color.slice(3, 5), 16) + const b = parseInt(color.slice(5, 7), 16) + return `rgba(${r}, ${g}, ${b}, ${opacity})` + } + if (color.startsWith("hsl")) return color.replace(")", `, ${opacity})`).replace("hsl", "hsla") + return color +} + +export function GraphSection() { + let canvasRef: HTMLCanvasElement | undefined + let chartInstance: Chart | undefined + const params = useParams() + const now = new Date() + const [store, setStore] = createStore({ + data: null as Awaited> | null, + year: now.getFullYear(), + month: now.getMonth(), + key: null as string | null, + model: null as string | null, + modelDropdownOpen: false, + keyDropdownOpen: false, + }) + const initialData = createAsync(() => queryCosts(params.id!, store.year, store.month)) + + const onPreviousMonth = async () => { + const month = store.month === 0 ? 11 : store.month - 1 + const year = store.month === 0 ? store.year - 1 : store.year + const data = await getCosts(params.id!, year, month) + setStore({ month, year, data }) + } + + const onNextMonth = async () => { + const month = store.month === 11 ? 0 : store.month + 1 + const year = store.month === 11 ? store.year + 1 : store.year + setStore({ month, year, data: await getCosts(params.id!, year, month) }) + } + + const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false }) + + const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false }) + + const getData = createMemo(() => store.data ?? initialData()) + + const getModels = createMemo(() => { + const data = getData() + if (!data?.usage) return [] + return Array.from(new Set(data.usage.map((row) => row.model))).sort() + }) + + const getDates = createMemo(() => { + const daysInMonth = new Date(store.year, store.month + 1, 0).getDate() + return Array.from({ length: daysInMonth }, (_, i) => { + const date = new Date(store.year, store.month, i + 1) + return date.toISOString().split("T")[0] + }) + }) + + const getKeyName = (keyID: string | null): string => { + if (!keyID || !store.data?.keys) return "All Keys" + const found = store.data.keys.find((k) => k.id === keyID) + return found?.displayName ?? "All Keys" + } + + const formatMonthYear = () => + new Date(store.year, store.month, 1).toLocaleDateString("en-US", { month: "long", year: "numeric" }) + + const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth() + + const chartConfig = createMemo((): ChartConfiguration | null => { + const data = getData() + const dates = getDates() + if (!data?.usage?.length) return null + + const dailyData = new Map>() + for (const dateKey of dates) dailyData.set(dateKey, new Map()) + + data.usage + .filter((row) => (store.key ? row.keyId === store.key : true)) + .forEach((row) => { + const dayMap = dailyData.get(row.date) + if (!dayMap) return + dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost) + }) + + const filteredModels = store.model === null ? getModels() : [store.model] + + const datasets = filteredModels.map((model) => { + const color = getModelColor(model) + return { + label: model, + data: dates.map((date) => (dailyData.get(date)?.get(model) || 0) / 100_000_000), + backgroundColor: color, + hoverBackgroundColor: color, + borderWidth: 0, + } + }) + + return { + type: "bar", + data: { + labels: dates.map(formatDateLabel), + datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + stacked: true, + grid: { + display: false, + }, + ticks: { + maxRotation: 0, + autoSkipPadding: 20, + color: "rgba(255, 255, 255, 0.5)", + font: { + family: "monospace", + size: 11, + }, + }, + }, + y: { + stacked: true, + beginAtZero: true, + grid: { + color: "rgba(255, 255, 255, 0.1)", + }, + ticks: { + color: "rgba(255, 255, 255, 0.5)", + font: { + family: "monospace", + size: 11, + }, + callback: (value) => { + const num = Number(value) + return num >= 1000 ? `$${(num / 1000).toFixed(1)}k` : `$${num.toFixed(0)}` + }, + }, + }, + }, + plugins: { + tooltip: { + mode: "index", + intersect: false, + backgroundColor: "rgba(0, 0, 0, 0.9)", + titleColor: "rgba(255, 255, 255, 0.9)", + bodyColor: "rgba(255, 255, 255, 0.8)", + borderColor: "rgba(255, 255, 255, 0.1)", + borderWidth: 1, + padding: 12, + displayColors: true, + callbacks: { + label: (context) => { + const value = context.parsed.y + if (!value || value === 0) return + return `${context.dataset.label}: $${value.toFixed(2)}` + }, + }, + }, + legend: { + display: true, + position: "bottom", + labels: { + color: "rgba(255, 255, 255, 0.7)", + font: { + size: 12, + }, + padding: 16, + boxWidth: 16, + boxHeight: 16, + usePointStyle: false, + }, + onHover: (event, legendItem, legend) => { + const chart = legend.chart + chart.data.datasets?.forEach((dataset, i) => { + const meta = chart.getDatasetMeta(i) + const baseColor = getModelColor(dataset.label || "") + const color = i === legendItem.datasetIndex ? baseColor : addOpacityToColor(baseColor, 0.3) + meta.data.forEach((bar: any) => { + bar.options.backgroundColor = color + }) + }) + chart.update("none") + }, + onLeave: (event, legendItem, legend) => { + const chart = legend.chart + chart.data.datasets?.forEach((dataset, i) => { + const meta = chart.getDatasetMeta(i) + const baseColor = getModelColor(dataset.label || "") + meta.data.forEach((bar: any) => { + bar.options.backgroundColor = baseColor + }) + }) + chart.update("none") + }, + }, + }, + }, + } + }) + + createEffect(() => { + const config = chartConfig() + if (!config || !canvasRef) return + + if (chartInstance) chartInstance.destroy() + chartInstance = new Chart(canvasRef, config) + }) + + onCleanup(() => chartInstance?.destroy()) + + return ( +
+
+

Cost

+

Usage costs broken down by model.

+
+ + +
+
+ + {formatMonthYear()} + +
+ setStore({ modelDropdownOpen: open })} + > + <> + + + {(model) => ( + + )} + + + + setStore({ keyDropdownOpen: open })} + > + <> + + + {(key) => ( + + )} + + + +
+
+ + +

No usage data available for the selected period.

+ + } + > +
+ +
+
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx index acf29d299..e25e09645 100644 --- a/packages/console/app/src/routes/workspace/[id]/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/index.tsx @@ -5,6 +5,7 @@ import { NewUserSection } from "./new-user-section" import { UsageSection } from "./usage-section" import { ModelSection } from "./model-section" import { ProviderSection } from "./provider-section" +import { GraphSection } from "./graph-section" import { IconLogo } from "~/component/icon" import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common" @@ -66,6 +67,9 @@ export default function () {
+ + + 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 9c4f87877..8b8890667 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -5,11 +5,21 @@ import { withActor } from "~/context/auth.withActor" import { ZenData } from "@opencode-ai/console-core/model.js" import styles from "./model-section.module.css" import { querySessionInfo } from "../common" -import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon" +import { + IconAlibaba, + IconAnthropic, + IconGoogle, + IconMoonshotAI, + IconOpenAI, + IconStealth, + IconXai, + IconZai, +} from "~/component/icon" const getModelLab = (modelId: string) => { if (modelId.startsWith("claude")) return "Anthropic" if (modelId.startsWith("gpt")) return "OpenAI" + if (modelId.startsWith("gemini")) return "Google" if (modelId.startsWith("kimi")) return "Moonshot AI" if (modelId.startsWith("glm")) return "Z.ai" if (modelId.startsWith("qwen")) return "Alibaba" @@ -22,9 +32,19 @@ const getModelsInfo = query(async (workspaceID: string) => { return withActor(async () => { return { all: Object.entries(ZenData.list().models) - .filter(([id, _model]) => !["claude-3-5-haiku", "minimax-m2"].includes(id)) - .filter(([id, _model]) => !id.startsWith("an-")) - .sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name)) + .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 getPriority = (id: string) => { + const index = priority.findIndex((p) => id.startsWith(p)) + return index === -1 ? Infinity : index + } + const pA = getPriority(idA) + const pB = getPriority(idB) + if (pA !== pB) return pA - pB + return modelA.name.localeCompare(modelB.name) + }) .map(([id, model]) => ({ id, name: model.name })), disabled: await Model.listDisabled(), } @@ -96,6 +116,8 @@ export function ModelSection() { return case "Anthropic": return + case "Google": + return case "Moonshot AI": return case "Z.ai": diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css index 1a772ba87..83c783a2f 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.module.css +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.module.css @@ -1,24 +1,23 @@ .root { + /* Empty state */ [data-component="empty-state"] { padding: var(--space-20) var(--space-6); text-align: center; border: 1px dashed var(--color-border); border-radius: var(--border-radius-sm); - display: flex; - flex-direction: column; - gap: var(--space-2); p { - line-height: 1.5; font-size: var(--font-size-sm); color: var(--color-text-muted); } } + /* Table container */ [data-slot="usage-table"] { overflow-x: auto; } + /* Table element */ [data-slot="usage-table-element"] { width: 100%; border-collapse: collapse; @@ -48,7 +47,6 @@ &[data-slot="usage-model"] { font-family: var(--font-sans); - font-weight: 400; color: var(--color-text-secondary); max-width: 200px; word-break: break-word; @@ -56,33 +54,133 @@ &[data-slot="usage-cost"] { color: var(--color-text); + font-weight: 500; + } + + [data-slot="tokens-with-breakdown"] { + position: relative; + display: flex; + align-items: center; + gap: var(--space-2); + } + + [data-slot="breakdown-button"] { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + background: transparent; + border: none; + color: var(--color-text-muted); + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + color: var(--color-text); + } + + svg { + width: 16px; + height: 16px; + } + } + + [data-slot="breakdown-popup"] { + position: absolute; + left: 0; + top: 100%; + margin-top: var(--space-2); + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + padding: var(--space-2); + z-index: 10; + min-width: 180px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + font-size: var(--font-size-xs); + + @media (prefers-color-scheme: dark) { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } } } - tbody tr { - &:last-child td { - border-bottom: none; + tbody tr:last-child td { + border-bottom: none; + } + } + + /* Pagination */ + [data-slot="pagination"] { + display: flex; + justify-content: flex-end; + gap: var(--space-2); + padding: var(--space-4) 0; + border-top: 1px solid var(--color-border-muted); + margin-top: var(--space-2); + + button { + padding: var(--space-2) var(--space-4); + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + color: var(--color-text); + font-size: var(--font-size-sm); + cursor: pointer; + transition: all 0.15s ease; + + svg { + width: 16px; + height: 16px; + stroke-width: 2; + } + + &:hover:not(:disabled) { + background: var(--color-bg-tertiary); + border-color: var(--color-border-hover); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; } } + } - @media (max-width: 40rem) { + /* Mobile responsive */ + @media (max-width: 40rem) { + [data-slot="usage-table-element"] { th, td { padding: var(--space-2) var(--space-3); font-size: var(--font-size-xs); } - th { - &:nth-child(2) /* Model */ { - display: none; - } - } - - td { - &:nth-child(2) /* Model */ { - display: none; - } + /* Hide Model column on mobile */ + th:nth-child(2), + td:nth-child(2) { + display: none; } } } + + /* Breakdown popup content */ + [data-slot="breakdown-row"] { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-4); + padding: var(--space-1) 0; + } + + [data-slot="breakdown-label"] { + color: var(--color-text-muted); + font-size: var(--font-size-xs); + } + + [data-slot="breakdown-value"] { + color: var(--color-text); + font-weight: 500; + font-size: var(--font-size-xs); + } } diff --git a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx index 3618bb7e2..212904f3f 100644 --- a/packages/console/app/src/routes/workspace/[id]/usage-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/usage-section.tsx @@ -1,81 +1,69 @@ import { Billing } from "@opencode-ai/console-core/billing.js" -import { query, useParams, createAsync } from "@solidjs/router" -import { createMemo, For, Show } from "solid-js" +import { createAsync, query, useParams } from "@solidjs/router" +import { createMemo, For, Show, createEffect, createSignal } from "solid-js" import { formatDateUTC, formatDateForTable } from "../common" import { withActor } from "~/context/auth.withActor" +import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon" import styles from "./usage-section.module.css" +import { createStore } from "solid-js/store" -const getUsageInfo = query(async (workspaceID: string) => { +const PAGE_SIZE = 50 + +async function getUsageInfo(workspaceID: string, page: number) { "use server" return withActor(async () => { - return await Billing.usages() + return await Billing.usages(page, PAGE_SIZE) }, workspaceID) -}, "usage.list") +} + +const queryUsageInfo = query(getUsageInfo, "usage.list") export function UsageSection() { const params = useParams() - // ORIGINAL CODE - COMMENTED OUT FOR TESTING - const usage = createAsync(() => getUsageInfo(params.id!)) + const usage = createAsync(() => queryUsageInfo(params.id!, 0)) + const [store, setStore] = createStore({ page: 0, usage: [] as Awaited> }) + const [openBreakdownId, setOpenBreakdownId] = createSignal(null) - // DUMMY DATA FOR TESTING - // const usage = () => [ - // { - // timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 1247, - // outputTokens: 423, - // cost: 125400000, // $1.254 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago - // model: "claude-3-haiku-20240307", - // inputTokens: 892, - // outputTokens: 156, - // cost: 23500000, // $0.235 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 2134, - // outputTokens: 687, - // cost: 234700000, // $2.347 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago - // model: "gpt-4o-mini", - // inputTokens: 567, - // outputTokens: 234, - // cost: 8900000, // $0.089 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago - // model: "claude-3-opus-20240229", - // inputTokens: 1893, - // outputTokens: 945, - // cost: 445600000, // $4.456 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago - // model: "gpt-4o", - // inputTokens: 1456, - // outputTokens: 532, - // cost: 156800000, // $1.568 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago - // model: "claude-3-haiku-20240307", - // inputTokens: 634, - // outputTokens: 89, - // cost: 12300000, // $0.123 - // }, - // { - // timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago - // model: "claude-3-5-sonnet-20241022", - // inputTokens: 3245, - // outputTokens: 1123, - // cost: 387200000, // $3.872 - // }, - // ] + createEffect(() => { + setStore({ usage: usage() }) + }, [usage]) + + createEffect(() => { + if (!openBreakdownId()) return + + const handleClickOutside = (e: MouseEvent) => { + const target = e.target as HTMLElement + if (!target.closest('[data-slot="tokens-with-breakdown"]')) { + setOpenBreakdownId(null) + } + } + + document.addEventListener("click", handleClickOutside) + return () => document.removeEventListener("click", handleClickOutside) + }) + + const hasResults = createMemo(() => store.usage && store.usage.length > 0) + const canGoPrev = createMemo(() => store.page > 0) + const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE) + + const calculateTotalInputTokens = (u: Awaited>[0]) => { + return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0) + } + + const goPrev = async () => { + const usage = await getUsageInfo(params.id!, store.page - 1) + setStore({ + page: store.page - 1, + usage, + }) + } + const goNext = async () => { + const usage = await getUsageInfo(params.id!, store.page + 1) + setStore({ + page: store.page + 1, + usage, + }) + } return (
@@ -85,7 +73,7 @@ export function UsageSection() {
0} + when={hasResults()} fallback={

Make your first API call to get started.

@@ -103,16 +91,51 @@ export function UsageSection() { - - {(usage) => { + + {(usage, index) => { const date = createMemo(() => new Date(usage.timeCreated)) + const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage)) + const breakdownId = `breakdown-${index()}` + const isOpen = createMemo(() => openBreakdownId() === breakdownId) + const isClaude = usage.model.toLowerCase().includes("claude") return ( {formatDateForTable(date())} {usage.model} - {usage.inputTokens} + +
e.stopPropagation()}> + + setOpenBreakdownId(null)}>{totalInputTokens()} + +
e.stopPropagation()}> +
+ Input + {usage.inputTokens} +
+
+ Cache Read + {usage.cacheReadTokens ?? 0} +
+ +
+ Cache Write + {usage.cacheWrite5mTokens ?? 0} +
+
+
+
+
+ {usage.outputTokens} ${((usage.cost ?? 0) / 100000000).toFixed(4)} @@ -121,6 +144,16 @@ export function UsageSection() {
+ +
+ + +
+
diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 89de1e03a..3453a6d38 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -15,6 +15,7 @@ import { logger } from "./logger" import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error" import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider" import { anthropicHelper } from "./provider/anthropic" +import { googleHelper } from "./provider/google" import { openaiHelper } from "./provider/openai" import { oaCompatHelper } from "./provider/openai-compatible" import { createRateLimiter } from "./rateLimiter" @@ -30,6 +31,8 @@ export async function handler( opts: { format: ZenData.Format parseApiKey: (headers: Headers) => string | undefined + parseModel: (url: string, body: any) => string + parseIsStream: (url: string, body: any) => boolean }, ) { type AuthInfo = Awaited> @@ -43,15 +46,18 @@ export async function handler( ] try { + const url = input.request.url const body = await input.request.json() const ip = input.request.headers.get("x-real-ip") ?? "" + const model = opts.parseModel(url, body) + const isStream = opts.parseIsStream(url, body) logger.metric({ - is_tream: !!body.stream, + is_tream: isStream, session: input.request.headers.get("x-opencode-session"), request: input.request.headers.get("x-opencode-request"), }) const zenData = ZenData.list() - const modelInfo = validateModel(zenData, body.model) + const modelInfo = validateModel(zenData, model) const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip) await rateLimiter?.check() @@ -64,7 +70,7 @@ export async function handler( logger.metric({ provider: providerInfo.id }) const startTimestamp = Date.now() - const reqUrl = providerInfo.modifyUrl(providerInfo.api) + const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream) const reqBody = JSON.stringify( providerInfo.modifyBody({ ...createBodyConverter(opts.format, providerInfo.format)(body), @@ -114,7 +120,7 @@ export async function handler( logger.debug("STATUS: " + res.status + " " + res.statusText) // Handle non-streaming response - if (!body.stream) { + if (!isStream) { const responseConverter = createResponseConverter(providerInfo.format, opts.format) const json = await res.json() const body = JSON.stringify(responseConverter(json)) @@ -169,7 +175,7 @@ export async function handler( responseLength += value.length buffer += decoder.decode(value, { stream: true }) - const parts = buffer.split("\n\n") + const parts = buffer.split(providerInfo.streamSeparator) buffer = parts.pop() ?? "" for (let part of parts) { @@ -283,6 +289,7 @@ export async function handler( ...(() => { const format = zenData.providers[provider.id].format if (format === "anthropic") return anthropicHelper + if (format === "google") return googleHelper if (format === "openai") return openaiHelper return oaCompatHelper })(), @@ -291,7 +298,7 @@ export async function handler( async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) { const apiKey = opts.parseApiKey(input.request.headers) - if (!apiKey) { + if (!apiKey || apiKey === "public") { if (modelInfo.allowAnonymous) return throw new AuthError("Missing API key.") } diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts index d8d1cd741..887a6e4b5 100644 --- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -30,6 +30,7 @@ export const anthropicHelper = { service_tier: "standard_only", } }, + streamSeparator: "\n\n", createUsageParser: () => { let usage: Usage diff --git a/packages/console/app/src/routes/zen/util/provider/google.ts b/packages/console/app/src/routes/zen/util/provider/google.ts new file mode 100644 index 000000000..afde42096 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/provider/google.ts @@ -0,0 +1,74 @@ +import { ProviderHelper } from "./provider" + +/* +{ + promptTokenCount: 11453, + candidatesTokenCount: 71, + totalTokenCount: 11625, + cachedContentTokenCount: 8100, + promptTokensDetails: [ + {modality: "TEXT",tokenCount: 11453} + ], + cacheTokensDetails: [ + {modality: "TEXT",tokenCount: 8100} + ], + thoughtsTokenCount: 101 +} +*/ + +type Usage = { + promptTokenCount?: number + candidatesTokenCount?: number + totalTokenCount?: number + cachedContentTokenCount?: number + promptTokensDetails?: { modality: string; tokenCount: number }[] + cacheTokensDetails?: { modality: string; tokenCount: number }[] + thoughtsTokenCount?: number +} + +export const googleHelper = { + format: "google", + modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => + `${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`, + modifyHeaders: (headers: Headers, body: Record, apiKey: string) => { + headers.set("x-goog-api-key", apiKey) + }, + modifyBody: (body: Record) => { + return body + }, + streamSeparator: "\r\n\r\n", + createUsageParser: () => { + let usage: Usage + + return { + parse: (chunk: string) => { + if (!chunk.startsWith("data: ")) return + + let json + try { + json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage } + } catch (e) { + return + } + + if (!json.usageMetadata) return + usage = json.usageMetadata + }, + retrieve: () => usage, + } + }, + normalizeUsage: (usage: Usage) => { + const inputTokens = usage.promptTokenCount ?? 0 + const outputTokens = usage.candidatesTokenCount ?? 0 + const reasoningTokens = usage.thoughtsTokenCount ?? 0 + const cacheReadTokens = usage.cachedContentTokenCount ?? 0 + return { + inputTokens: inputTokens - cacheReadTokens, + outputTokens, + reasoningTokens, + cacheReadTokens, + cacheWrite5mTokens: undefined, + cacheWrite1hTokens: undefined, + } + }, +} satisfies ProviderHelper diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts index 8a9170ef1..5771ed4fa 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -33,6 +33,7 @@ export const oaCompatHelper = { ...(body.stream ? { stream_options: { include_usage: true } } : {}), } }, + streamSeparator: "\n\n", createUsageParser: () => { let usage: Usage diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts index e79e83579..dff6e13fb 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai.ts @@ -21,6 +21,7 @@ export const openaiHelper = { modifyBody: (body: Record) => { return body }, + streamSeparator: "\n\n", createUsageParser: () => { let usage: Usage diff --git a/packages/console/app/src/routes/zen/util/provider/provider.ts b/packages/console/app/src/routes/zen/util/provider/provider.ts index d0f123968..8366f3a63 100644 --- a/packages/console/app/src/routes/zen/util/provider/provider.ts +++ b/packages/console/app/src/routes/zen/util/provider/provider.ts @@ -26,9 +26,10 @@ import { export type ProviderHelper = { format: ZenData.Format - modifyUrl: (providerApi: string) => string + modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string modifyHeaders: (headers: Headers, body: Record, apiKey: string) => void modifyBody: (body: Record) => Record + streamSeparator: string createUsageParser: () => { parse: (chunk: string) => void retrieve: () => any diff --git a/packages/console/app/src/routes/zen/v1/chat/completions.ts b/packages/console/app/src/routes/zen/v1/chat/completions.ts index 44326e79e..655459129 100644 --- a/packages/console/app/src/routes/zen/v1/chat/completions.ts +++ b/packages/console/app/src/routes/zen/v1/chat/completions.ts @@ -5,5 +5,7 @@ export function POST(input: APIEvent) { return handler(input, { format: "oa-compat", parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1], + parseModel: (url: string, body: any) => body.model, + parseIsStream: (url: string, body: any) => !!body.stream, }) } diff --git a/packages/console/app/src/routes/zen/v1/messages.ts b/packages/console/app/src/routes/zen/v1/messages.ts index 4478b6444..54d223f95 100644 --- a/packages/console/app/src/routes/zen/v1/messages.ts +++ b/packages/console/app/src/routes/zen/v1/messages.ts @@ -5,5 +5,7 @@ export function POST(input: APIEvent) { return handler(input, { format: "anthropic", parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined, + parseModel: (url: string, body: any) => body.model, + parseIsStream: (url: string, body: any) => !!body.stream, }) } diff --git a/packages/console/app/src/routes/zen/v1/models/[model].ts b/packages/console/app/src/routes/zen/v1/models/[model].ts new file mode 100644 index 000000000..b20378e37 --- /dev/null +++ b/packages/console/app/src/routes/zen/v1/models/[model].ts @@ -0,0 +1,13 @@ +import type { APIEvent } from "@solidjs/start/server" +import { handler } from "~/routes/zen/util/handler" + +export function POST(input: APIEvent) { + return handler(input, { + format: "google", + parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined, + parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "", + parseIsStream: (url: string, body: any) => + // ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse' + url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false, + }) +} diff --git a/packages/console/app/src/routes/zen/v1/responses.ts b/packages/console/app/src/routes/zen/v1/responses.ts index eadc5bc8e..a82a667cc 100644 --- a/packages/console/app/src/routes/zen/v1/responses.ts +++ b/packages/console/app/src/routes/zen/v1/responses.ts @@ -5,5 +5,7 @@ export function POST(input: APIEvent) { return handler(input, { format: "openai", parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1], + parseModel: (url: string, body: any) => body.model, + parseIsStream: (url: string, body: any) => !!body.stream, }) } diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 781f9f5ab..1ff03db34 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.61", + "version": "1.0.85", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 348718146..049ee29bb 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -57,14 +57,15 @@ export namespace Billing { ) } - export const usages = async () => { + export const usages = async (page = 0, pageSize = 50) => { return await Database.use((tx) => tx .select() .from(UsageTable) .where(eq(UsageTable.workspaceID, Actor.workspace())) .orderBy(sql`${UsageTable.timeCreated} DESC`) - .limit(100), + .limit(pageSize) + .offset(page * pageSize), ) } diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 222bdd0f8..bff999e61 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -8,7 +8,7 @@ import { Actor } from "./actor" import { Resource } from "@opencode-ai/console-resource" export namespace ZenData { - const FormatSchema = z.enum(["anthropic", "openai", "oa-compat"]) + const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"]) export type Format = z.infer const ModelCostSchema = z.object({ diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 097dbd6d3..74588bbd1 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.61", + "version": "1.0.85", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/function/src/log-processor.ts b/packages/console/function/src/log-processor.ts index 821d2be9c..9e76e2ceb 100644 --- a/packages/console/function/src/log-processor.ts +++ b/packages/console/function/src/log-processor.ts @@ -12,7 +12,8 @@ export default { if ( url.pathname !== "/zen/v1/chat/completions" && url.pathname !== "/zen/v1/messages" && - url.pathname !== "/zen/v1/responses" + url.pathname !== "/zen/v1/responses" && + !url.pathname.startsWith("/zen/v1/models/") ) return diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 9e3aefe5a..c92fb6853 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.61", + "version": "1.0.85", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/index.html b/packages/desktop/index.html index e88b49ac8..8e91aab6d 100644 --- a/packages/desktop/index.html +++ b/packages/desktop/index.html @@ -8,14 +8,12 @@ OpenCode - - - - - - - - +
diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 143b023fe..cc05656c3 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "1.0.61", + "version": "1.0.85", "description": "", "type": "module", "scripts": { diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index c48572d76..b2e552f71 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -266,7 +266,7 @@ export const PromptInput: Component = (props) => { if (!existing) { const created = await sdk.client.session.create() existing = created.data ?? undefined - if (existing) navigate(`/session/${existing.id}`) + if (existing) navigate(existing.id) } if (!existing) return @@ -347,7 +347,7 @@ export const PromptInput: Component = (props) => {
0} fallback={
No matching files
}> @@ -366,7 +366,9 @@ export const PromptInput: Component = (props) => { {getDirectory(i)} - {getFilename(i)} + + {getFilename(i)} +
@@ -380,7 +382,7 @@ export const PromptInput: Component = (props) => { onSubmit={handleSubmit} classList={{ "bg-surface-raised-stronger-non-alpha border border-border-strong-base": true, - "rounded-2xl overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true, + "rounded-md overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true, [props.class ?? ""]: !!props.class, }} > @@ -394,17 +396,17 @@ export const PromptInput: Component = (props) => { onInput={handleInput} onKeyDown={handleKeyDown} classList={{ - "w-full p-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, + "w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&>[data-type=file]]:text-icon-info-active": true, }} /> -
+
Plan and build anything
-
+