Compare commits

...

84 Commits

Author SHA1 Message Date
opencode 9f5db46911 release: v1.0.173
Update Nix Hashes / update (push) Has been cancelled
2025-12-20 03:48:15 +00:00
Ariane Emory 755ddbb223 feat(tui): reinsert forked message text in prompt text input box when forking session (resolves #5495) (#5545)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 21:45:33 -06:00
GitHub Action 701d470d01 chore: generate 2025-12-20 03:42:00 +00:00
Aiden Cline 1d9058d26b ci: fix archive 2025-12-19 21:41:18 -06:00
opencode 39e2a5f595 release: v1.0.172
Update Nix Hashes / update (push) Has been cancelled
2025-12-20 03:29:07 +00:00
Aiden Cline f862ab6722 ci: fix undefined 2025-12-19 21:22:49 -06:00
Aiden Cline 129d4f0b1b ci: fix release notes
Update Nix Hashes / update (push) Has been cancelled
2025-12-19 17:49:07 -06:00
Aiden Cline 3a1e50d1f8 Revert "tweak: better release notes (grouped changelog) (#5768)"
This reverts commit b99afdad91.
2025-12-19 17:46:38 -06:00
Aiden Cline e2fb690d8e Revert "tweak: DevEx to run changelog independently (#5774)"
This reverts commit 7f8e799392.
2025-12-19 17:46:36 -06:00
Aiden Cline 0a7f58a811 Revert "ci: gemini 3 flash doesnt exist in pinned cicd version (#5776)"
This reverts commit 382905602c.
2025-12-19 17:46:35 -06:00
Aiden Cline dae0168ed8 test: fixture cleanup 2025-12-19 17:00:23 -06:00
Aiden Cline edfe2e4f1c test: fix test case 2025-12-19 16:37:36 -06:00
Kaspar 1bc1ea8b47 tweak: Make LSP message more accurate when LSPs disabled (#5814) 2025-12-19 16:34:04 -06:00
Aiden Cline dacbbe3184 chore: rm dead code 2025-12-19 16:20:17 -06:00
Aiden Cline 89285d8f5f ci: fix publish auth failure 2025-12-19 16:00:41 -06:00
1XD 2e853911c3 docs: replace deprecated mise ubi backend with github backend (#5811) 2025-12-19 15:51:09 -06:00
Github Action 695fdecf23 Update Nix flake.lock and hashes 2025-12-19 20:40:07 +00:00
Frank 054d22791d zen: sync 2025-12-19 15:38:31 -05:00
Frank 4a57cc69d8 zen: sync 2025-12-19 15:25:35 -05:00
Frank 7e0c8db029 zen: sync 2025-12-19 15:21:57 -05:00
Cameron ba4cc3bf86 Desktop file encoding issue (#5490) 2025-12-19 14:18:37 -06:00
Adam b19a424c85 chore: cleanup 2025-12-19 13:11:08 -06:00
Adam 1689281c35 fix(desktop): auto-scroll and session perf 2025-12-19 13:07:53 -06:00
Adam cdbb59fae8 fix(desktop): don't use tauri http for sse events 2025-12-19 13:07:53 -06:00
Adam 4eb311e98f fix(desktop): error height 2025-12-19 13:07:52 -06:00
Github Action 80eac96258 Update Nix flake.lock and hashes 2025-12-19 18:29:37 +00:00
Aiden Cline 4bad6f9f1b tweak: use fetch instead of octokit for now 2025-12-19 12:28:11 -06:00
Steven T. Cramer d7db57e8e1 docs: add Windows Terminal Shift+Enter configuration guide (#5788) 2025-12-19 12:27:21 -06:00
Brendan Allan 943fbf39a3 ci: separate standalone publishing from dependent publishing (#5634)
Co-authored-by: GitHub Action <action@github.com>
2025-12-19 13:22:17 -05:00
Dax Raad d8a34c2fcc core: prevent file system scanning when in root directory to avoid unnecessary operations 2025-12-19 13:15:11 -05:00
Aiden Cline 5720ed1f44 ci: change token for gh release to allow discord job to actually trigger see: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow 2025-12-19 10:45:26 -06:00
GitHub Action bb20a359e4 chore: generate 2025-12-19 14:56:47 +00:00
José Valim 0d472a49a0 Do not include hidden agents in ACP (#5791) 2025-12-19 08:56:12 -06:00
Github Action 203581e82f Update Nix flake.lock and hashes 2025-12-19 13:42:49 +00:00
Sebastian Herrlinger 677631916c upgrade opentui to v0.1.62, enabling textarea mouse scroll and cursor set 2025-12-19 13:42:49 +00:00
Github Action 1aa1e8c904 Update Nix flake.lock and hashes 2025-12-19 13:42:49 +00:00
opencode 55d62fbd9f release: v1.0.170
Update Nix Hashes / update (push) Has been cancelled
2025-12-19 13:42:48 +00:00
Adam e1ad2a355c fix(desktop): error handling 2025-12-19 07:38:38 -06:00
Adam 4f318f913e chore: logging 2025-12-19 07:38:38 -06:00
Adam 2d814b6db2 fix(desktop): separate prompt history for shell 2025-12-19 07:38:38 -06:00
Adam e561f1ad68 fix(desktop): don't navigate prompt history if dirty 2025-12-19 07:38:37 -06:00
Sebastian Herrlinger ebfb985215 user messages as markdown with toggle 2025-12-19 13:51:26 +01:00
GitHub Action 2646da50df ignore: update download stats 2025-12-19 2025-12-19 12:04:33 +00:00
Github Action 50a5f6e53b Update Nix flake.lock and hashes 2025-12-19 11:49:40 +00:00
Brendan Allan d03fac52e7 Update SolidStart and bring back HttpHeader usage (#5355)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-19 05:46:33 -06:00
Sherlock Holmes 6a802c01cd feat(tui): implement smooth scrolling for autocomplete dropdown navigation (#5559)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-19 00:40:16 -06:00
Eric Shirley 14146428dd lsp: add oxlint server (#5570) 2025-12-19 00:17:20 -06:00
Aiden Cline 26d0280f70 docs: contributing 2025-12-18 22:18:34 -06:00
Aiden Cline 3274a5813e ci: only run generate for dev 2025-12-18 22:17:36 -06:00
Luke Parker 382905602c ci: gemini 3 flash doesnt exist in pinned cicd version (#5776) 2025-12-18 21:59:46 -06:00
GitHub Action 8b5cea7899 chore: generate 2025-12-19 03:59:14 +00:00
Matt Silverlock 100c31cbb1 fix: use correct octokit API for PR review comment reactions (#5778) 2025-12-18 21:58:41 -06:00
GitHub Action 0b286f1b84 chore: generate 2025-12-19 02:12:35 +00:00
Brendan Allan 2f6ca958fe tauri: remove pinch-to-zoom on window 2025-12-19 02:12:35 +00:00
Basit Mustafa 5218e7a546 docs(ecosystem): add opencode-zellij-namer plugin (#5771) 2025-12-19 02:12:35 +00:00
Luke Parker 7f8e799392 tweak: DevEx to run changelog independently (#5774) 2025-12-19 02:12:35 +00:00
opencode 289f4abaaa release: v1.0.169
Update Nix Hashes / update (push) Has been cancelled
2025-12-19 02:12:34 +00:00
Adam 7ce898ce43 fix(desktop): shell mode 2025-12-18 20:06:53 -06:00
Adam 0dd716a75e fix(desktop): extra reqs 2025-12-18 19:53:38 -06:00
Aiden Cline 87171467fa ci: better err msg for generate workflow 2025-12-18 19:03:16 -06:00
Luke Parker b99afdad91 tweak: better release notes (grouped changelog) (#5768) 2025-12-18 18:49:37 -06:00
Aiden Cline 4fd576f3af fix: better api call error msgs in some cases 2025-12-18 18:46:25 -06:00
GitHub Action 2f41d0bedd chore: generate 2025-12-19 00:18:07 +00:00
Rohan Godha 5f03290534 feat(tui): click on subagents to open them (#5761)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-18 18:17:34 -06:00
opencode 427157c683 release: v1.0.168
Update Nix Hashes / update (push) Has been cancelled
2025-12-18 21:55:29 +00:00
Github Action a0ab3d98b7 Update Nix flake.lock and hashes 2025-12-18 21:48:53 +00:00
GitHub Action c8de766913 chore: generate 2025-12-18 21:47:59 +00:00
Adam d57b963141 fix: id 2025-12-18 15:47:21 -06:00
Adam 0ebcaff927 fix(desktop): expanded states 2025-12-18 15:47:20 -06:00
Adam 15931fa170 chore: cleanup 2025-12-18 15:47:20 -06:00
Adam af4087d7b5 fix(desktop): smaller max-width when review open 2025-12-18 15:47:20 -06:00
Aiden Cline 323ea1040c ci: fix generate 2025-12-18 15:23:23 -06:00
Aiden Cline 1fe87b0233 ci: fix file perm 2025-12-18 14:39:44 -06:00
Aiden Cline 8d11df1b3b ci: handle case where generate.yml fails better 2025-12-18 14:33:40 -06:00
Aiden Cline ecc5050838 tweak: more retry cases 2025-12-18 13:59:37 -06:00
Aiden Cline 606cf3b6f2 chore: rm dead code 2025-12-18 13:59:37 -06:00
GitHub Action 67cfd7f06b chore: format code 2025-12-18 19:38:25 +00:00
OpeOginni ab9ac7c87a feat: add experimental support for Ty language server (#5575) 2025-12-18 13:37:48 -06:00
Adam ee9f979613 fix(desktop): markdown styles 2025-12-18 13:03:14 -06:00
Adam 228b6444f8 fix(desktop): don't show image button in shell mode 2025-12-18 13:03:14 -06:00
Frank 9998efdae2 zen: cleanup headers 2025-12-18 13:47:31 -05:00
Aiden Cline 9427f56e1a rm interleaved thinking filter for certain kimi k2 thinking model providers that were bugged 2025-12-18 12:26:27 -06:00
Adam a6dd35d73d fix(desktop): submit prompt 2025-12-18 12:03:21 -06:00
GitHub Action faeaafa5f5 chore: format code 2025-12-18 17:31:49 +00:00
84 changed files with 2048 additions and 1101 deletions
+28 -15
View File
@@ -2,11 +2,8 @@ name: generate
on:
push:
branches-ignore:
- production
pull_request:
branches-ignore:
- production
branches:
- dev
workflow_dispatch:
jobs:
@@ -14,6 +11,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -25,14 +23,29 @@ jobs:
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Generate SDK
run: |
bun ./packages/sdk/js/script/build.ts
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
bun x prettier --write packages/sdk/openapi.json
- name: Generate
run: ./script/generate.ts
- name: Format
run: ./script/format.ts
env:
CI: true
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
- name: Commit and push
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "No changes to commit"
exit 0
fi
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add -A
git commit -m "chore: generate"
git push origin HEAD:${{ github.ref_name }} --no-verify
# if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then
# echo ""
# echo "============================================"
# echo "Failed to push generated code."
# echo "Please run locally and push:"
# echo ""
# echo " ./script/generate.ts"
# echo " git add -A && git commit -m \"chore: generate\" && git push"
# echo ""
# echo "============================================"
# exit 1
# fi
+27 -16
View File
@@ -41,21 +41,9 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Setup SSH for AUR
if: inputs.bump || inputs.version
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai@1.0.143
run: bun i -g opencode-ai@1.0.169
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -75,9 +63,15 @@ jobs:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup Git Identity
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
- name: Publish
id: publish
run: ./script/publish.ts
run: ./script/publish-start.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
@@ -212,6 +206,23 @@ jobs:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tagName }}
- run: gh release edit ${{ needs.publish.outputs.tagName }} --draft=false
- uses: ./.github/actions/setup-bun
- name: Setup SSH for AUR
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- run: ./script/publish-complete.ts
env:
GH_TOKEN: ${{ github.token }}
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
-2
View File
@@ -63,8 +63,6 @@ TUI issues potentially caused by our underlying TUI library:
**Do not** add for general TUI bugs.
---
When assigning to people here are the following rules:
adamdotdev:
+36 -12
View File
@@ -1,5 +1,5 @@
/// <reference path="../env.d.ts" />
import { Octokit } from "@octokit/rest"
// import { Octokit } from "@octokit/rest"
import { tool } from "@opencode-ai/plugin"
import DESCRIPTION from "./github-triage.txt"
@@ -9,6 +9,22 @@ function getIssueNumber(): number {
return issue
}
async function githubFetch(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`https://api.github.com${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
Accept: "application/vnd.github+json",
"Content-Type": "application/json",
...options.headers,
},
})
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
}
return response.json()
}
export default tool({
description: DESCRIPTION,
args: {
@@ -23,7 +39,7 @@ export default tool({
},
async execute(args) {
const issue = getIssueNumber()
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
// const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
const owner = "sst"
const repo = "opencode"
@@ -41,22 +57,30 @@ export default tool({
throw new Error("Only opentui issues should be assigned to kommander")
}
await octokit.rest.issues.addAssignees({
owner,
repo,
issue_number: issue,
assignees: [args.assignee],
// await octokit.rest.issues.addAssignees({
// owner,
// repo,
// issue_number: issue,
// assignees: [args.assignee],
// })
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
method: "POST",
body: JSON.stringify({ assignees: [args.assignee] }),
})
results.push(`Assigned @${args.assignee} to issue #${issue}`)
const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label))
if (labels.length > 0) {
await octokit.rest.issues.addLabels({
owner,
repo,
issue_number: issue,
labels,
// await octokit.rest.issues.addLabels({
// owner,
// repo,
// issue_number: issue,
// labels,
// })
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
method: "POST",
body: JSON.stringify({ labels }),
})
results.push(`Added labels: ${args.labels.join(", ")}`)
}
+1 -1
View File
@@ -40,7 +40,7 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
- `packages/plugin`: Source for `@opencode-ai/plugin`
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
Please try to follow the [style guide](./STYLE_GUIDE.md)
+1 -1
View File
@@ -30,7 +30,7 @@ scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
mise use -g ubi:sst/opencode # Any OS
mise use -g github:sst/opencode # Any OS
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
```
+1
View File
@@ -174,3 +174,4 @@
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
+49 -70
View File
@@ -6,7 +6,6 @@
"name": "opencode",
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@octokit/rest": "22.0.1",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"typescript": "catalog:",
@@ -21,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -49,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -76,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -100,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -124,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -154,6 +153,7 @@
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:",
"zod": "catalog:",
},
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
@@ -171,7 +171,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -200,7 +200,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -216,7 +216,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.167",
"version": "1.0.173",
"bin": {
"opencode": "./bin/opencode",
},
@@ -246,8 +246,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.61",
"@opentui/solid": "0.1.61",
"@opentui/core": "0.1.62",
"@opentui/solid": "0.1.62",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -308,7 +308,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -328,7 +328,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.167",
"version": "1.0.173",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -339,7 +339,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -352,12 +352,13 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-process": "~2",
@@ -378,7 +379,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -413,7 +414,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"zod": "catalog:",
},
@@ -424,7 +425,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1100,11 +1101,11 @@
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
"@octokit/plugin-retry": ["@octokit/plugin-retry@3.0.9", "", { "dependencies": { "@octokit/types": "^6.0.3", "bottleneck": "^2.15.3" } }, "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ=="],
@@ -1112,7 +1113,7 @@
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
@@ -1160,21 +1161,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.61", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.61", "@opentui/core-darwin-x64": "0.1.61", "@opentui/core-linux-arm64": "0.1.61", "@opentui/core-linux-x64": "0.1.61", "@opentui/core-win32-arm64": "0.1.61", "@opentui/core-win32-x64": "0.1.61", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-WrVbdki0tnsgmWCB3Iix6n8eXGXUheTqr/tcnBN7gLA/TqT9udcX+DW3/qRdgtTNJS1sVBVeuwSTYU3eqDSUJQ=="],
"@opentui/core": ["@opentui/core@0.1.62", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.62", "@opentui/core-darwin-x64": "0.1.62", "@opentui/core-linux-arm64": "0.1.62", "@opentui/core-linux-x64": "0.1.62", "@opentui/core-win32-arm64": "0.1.62", "@opentui/core-win32-x64": "0.1.62", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-T9wsXaS4rFoZF2loaEFqAeuGj5DV3pJzrk18z1um3UfUS2NNH4jyDh5rDdHPb2/YrvO1lU9hd0VoAS/7zUAq/w=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.61", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zX7EK8PwBJFwsZ2tDnScLFD0GbBfHE7sqpzGDXP2luMnBZJ0OOO95a4Hzu9dQWqxEr4RgfGDT8uIRhgimKNQEg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.62", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IohPhCkD/DbZEH4M5ft1/o1pI6Vvw2pdxdyoouW/TO1g21W5G8usaWTSRDXO+16BT115Nfb9/DT69H5pzAc2Eg=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.61", "", { "os": "darwin", "cpu": "x64" }, "sha512-xfvl8EnyN0XwlYpyTskVhHOpbMdgt++ntcuTh7M7IEFYQGzJux19NBwJl17mOxB1McG+KTa7kNx5/zu0VB9eVQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.62", "", { "os": "darwin", "cpu": "x64" }, "sha512-BqbjQl2sLYrJ1Pq1b3H1I2CFedRiMz0QtZX08IMbyZ5kok+J0A8eQS5tmlbfqoS/VH0de9XiEbuHjG09/nSj1A=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.61", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ghg7j4H6bz7CLxhgDcWx3Ann3AblDIjKFUu4vFrVysuiwfmDHwdKm8awLj8tnmC/0y8juG4ODUQbR0BXBIkE+Q=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.62", "", { "os": "linux", "cpu": "arm64" }, "sha512-P5FleF+W8O4uGubqBvV8DB1AK0+fJhJS8HvfmTZQ2DhSSJJH9Af/WXqitD7ILQY9ltlaUP7l38BC5cVdxnWzCQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.61", "", { "os": "linux", "cpu": "x64" }, "sha512-Xs9czMEOuHtnX4tigC4fNb1MU7+Gaohbk+k4teraulIgYZf19nRHIKNvXissDjOfqvOGygCkxMQIG0zeUFsPEA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.62", "", { "os": "linux", "cpu": "x64" }, "sha512-l9ab5tgOGcdf8k3NU4TzK/3C8UC0+QuMxgLA/j60BhB1e9bwJleFeYJc+wLIktTUu9QwqCsU4YcuGHL+C2lCzA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.61", "", { "os": "win32", "cpu": "arm64" }, "sha512-2CYAEPqArJqE36LkSRAs0csRzWwVJY99S/7EuY7abBm58BIL6RUw5kSw1r75oDo4I3W6v6WwW0u8B5Ik98m0Kg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.62", "", { "os": "win32", "cpu": "arm64" }, "sha512-U1zsOpQl3EGhs8BwoehKAwwVONe+XOXRnXTxMhXw8huF0WWXDWOUL5psjBvfSWPm1rLmagxkQsH84jTSWA/vLA=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.61", "", { "os": "win32", "cpu": "x64" }, "sha512-c0OK5YwcKH51Qj6wPmwTZP3X8LHA0I0dKz4fO4mOh4f+OqgU9WOG4hpbf7lv0bVlHoTvgP4zDUsjmtIVA8l6Lg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.62", "", { "os": "win32", "cpu": "x64" }, "sha512-JgLZXSaE4q7gUIQb9x6fLWFF3BYlMod2VBhOT1qGBdeveZxsM6ZAno/g+CL9IDUydWfLFadOIBjdYFDVWV2Z2w=="],
"@opentui/solid": ["@opentui/solid@0.1.61", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.61", "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-CiZHduIoeABoS0ev+eGeHA/LiRl/SpdL6io4jrwiwFi/rToKtc7YgJ8MWxIgeHScHUbpQnIr1v7jzsGI3DAYvw=="],
"@opentui/solid": ["@opentui/solid@0.1.62", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.62", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-3th4oZROv3cZvcoL+IwNCEMTKLZaT1BBWKVHxH29wUD0/EPxtowLQCibnjKDqqdTuEUuFA/QtSX52WqQEioR8g=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1672,6 +1673,8 @@
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
@@ -1820,19 +1823,19 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
"@vitest/expect": ["@vitest/expect@4.0.13", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.13", "@vitest/utils": "4.0.13", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-zYtcnNIBm6yS7Gpr7nFTmq8ncowlMdOJkWLqYvhr/zweY6tFbDkDi8BPPOeHxEtK1rSI69H7Fd4+1sqvEGli6w=="],
"@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="],
"@vitest/mocker": ["@vitest/mocker@4.0.13", "", { "dependencies": { "@vitest/spy": "4.0.13", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-eNCwzrI5djoauklwP1fuslHBjrbR8rqIVbvNlAnkq1OTa6XT+lX68mrtPirNM9TnR69XUPt4puBCx2Wexseylg=="],
"@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.13", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-ooqfze8URWbI2ozOeLDMh8YZxWDpGXoeY3VOgcDnsUxN0jPyPWSUvjPQWqDGCBks+opWlN1E4oP1UYl3C/2EQA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="],
"@vitest/runner": ["@vitest/runner@4.0.13", "", { "dependencies": { "@vitest/utils": "4.0.13", "pathe": "^2.0.3" } }, "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A=="],
"@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="],
"@vitest/snapshot": ["@vitest/snapshot@4.0.13", "", { "dependencies": { "@vitest/pretty-format": "4.0.13", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-hb7Usvyika1huG6G6l191qu1urNPsq1iFc2hmdzQY3F5/rTgqQnwwplyf8zoYHkpt7H6rw5UfIw6i/3qf9oSxQ=="],
"@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="],
"@vitest/spy": ["@vitest/spy@4.0.13", "", {}, "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw=="],
"@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="],
"@vitest/utils": ["@vitest/utils@4.0.13", "", { "dependencies": { "@vitest/pretty-format": "4.0.13", "tinyrainbow": "^3.0.3" } }, "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA=="],
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
@@ -2354,7 +2357,7 @@
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
@@ -3084,6 +3087,8 @@
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
@@ -3784,7 +3789,7 @@
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"vitest": ["vitest@4.0.13", "", { "dependencies": { "@vitest/expect": "4.0.13", "@vitest/mocker": "4.0.13", "@vitest/pretty-format": "4.0.13", "@vitest/runner": "4.0.13", "@vitest/snapshot": "4.0.13", "@vitest/spy": "4.0.13", "@vitest/utils": "4.0.13", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.13", "@vitest/browser-preview": "4.0.13", "@vitest/browser-webdriverio": "4.0.13", "@vitest/ui": "4.0.13", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-QSD4I0fN6uZQfftryIXuqvqgBxTvJ3ZNkF6RWECd82YGAYAfhcppBLFXzXJHQAAhVFyYEuFTrq6h0hQqjB7jIQ=="],
"vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
@@ -4084,9 +4089,9 @@
"@octokit/oauth-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@octokit/plugin-retry/@octokit/types": ["@octokit/types@6.41.0", "", { "dependencies": { "@octokit/openapi-types": "^12.11.0" } }, "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg=="],
@@ -4098,8 +4103,6 @@
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
"@opencode-ai/function/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
"@opencode-ai/tauri/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
@@ -4292,8 +4295,6 @@
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bpYruxVLhrTbVH6CCq48zMJNeHu6FmHtEedl9FXckEgcIEAi036idFhJlcRwC1jNCwlacbzb8dPD7OAH1EKJaQ=="],
"opencode/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
@@ -4384,6 +4385,8 @@
"vite-plugin-icons-spritesheet/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
@@ -4648,9 +4651,9 @@
"@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="],
@@ -4658,10 +4661,6 @@
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
@@ -4876,10 +4875,6 @@
"opencode/@ai-sdk/openai-compatible/@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=="],
"opencode/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
@@ -5020,10 +5015,6 @@
"@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
@@ -5044,10 +5035,6 @@
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
@@ -5140,18 +5127,10 @@
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"opencontrol/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1765934234,
"narHash": "sha256-pJjWUzNnjbIAMIc5gRFUuKCDQ9S1cuh3b2hKgA7Mc4A=",
"lastModified": 1766025857,
"narHash": "sha256-Lav5jJazCW4mdg1iHcROpuXqmM94BWJvabLFWaJVJp0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "af84f9d270d404c17699522fab95bbf928a2d92f",
"rev": "def3da69945bbe338c373fddad5a1bb49cf199ce",
"type": "github"
},
"original": {
+1 -1
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-g6XHWk9IoDoeXbvENs+U2fqk185xKMLb0BRopCbXaIk="
"nodeModules": "sha256-cpXmqJQJeFj3eED/aOb4YLUdkZFV//7u4f0STBxzUhk="
}
-1
View File
@@ -64,7 +64,6 @@
},
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@octokit/rest": "22.0.1",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"typescript": "catalog:"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.167",
"version": "1.0.173",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,6 +1,6 @@
import "./index.css"
import { Title, Meta, Link } from "@solidjs/meta"
// import { HttpHeader } from "@solidjs/start"
//import { HttpHeader } from "@solidjs/start"
import video from "../asset/lander/opencode-min.mp4"
import videoPoster from "../asset/lander/opencode-poster.png"
import { IconCopy, IconCheck } from "../component/icon"
@@ -1,7 +1,7 @@
import "./index.css"
import { createAsync, query, redirect } from "@solidjs/router"
import { Title, Meta, Link } from "@solidjs/meta"
// import { HttpHeader } from "@solidjs/start"
//import { HttpHeader } from "@solidjs/start"
import zenLogoLight from "../../asset/zen-ornate-light.svg"
import { config } from "~/config"
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
@@ -112,6 +112,8 @@ export async function handler(
headers.delete("content-length")
headers.delete("x-opencode-request")
headers.delete("x-opencode-session")
headers.delete("x-opencode-project")
headers.delete("x-opencode-client")
return headers
})(),
body: reqBody,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.167",
"version": "1.0.173",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.167",
"version": "1.0.173",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.167",
"version": "1.0.173",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.167",
"version": "1.0.173",
"description": "",
"type": "module",
"exports": {
@@ -56,6 +56,7 @@
"solid-js": "catalog:",
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:"
"virtua": "catalog:",
"zod": "catalog:"
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ export function App() {
return (
<MetaProvider>
<Font />
<ErrorBoundary fallback={ErrorPage}>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
+105 -95
View File
@@ -24,11 +24,6 @@ export function Header(props: {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => store().session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
@@ -45,101 +40,116 @@ export function Header(props: {
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<Show when={params.dir && layout.projects.list().length > 0}>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<Show when={layout.projects.list().length > 0 && params.dir}>
{(directory) => {
const currentDirectory = createMemo(() => base64Decode(directory()))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => store().session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={props.navigateToSession}
class="text-14-regular text-text-base max-w-md"
variant="ghost"
/>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={props.navigateToSession}
class="text-14-regular text-text-base max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Show>
</div>
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Show>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: currentDirectory() })
.then((r) => r.data?.share?.url)
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
return shareURL
},
)
return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
})}
</Popover>
</Show>
</div>
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: currentDirectory() })
.then((r) => r.data?.share?.url)
}
return shareURL
},
)
return (
<Show when={url()}>
{(url) => <TextField value={url()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
</>
)
}}
</Show>
</div>
</header>
@@ -21,7 +21,7 @@ import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid
import { useProviders } from "@/hooks/use-providers"
import { useCommand, formatKeybind } from "@/context/command"
import { persisted } from "@/utils/persist"
import { Identifier } from "@opencode-ai/util/identifier"
import { Identifier } from "@/utils/id"
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
@@ -102,6 +102,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttachments: ImageAttachmentPart[]
mode: "normal" | "shell"
applyingHistory: boolean
userHasEdited: boolean
}>({
popover: null,
historyIndex: -1,
@@ -111,6 +112,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttachments: [],
mode: "normal",
applyingHistory: false,
userHasEdited: false,
})
const MAX_HISTORY = 100
@@ -122,6 +124,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
entries: [],
}),
)
const [shellHistory, setShellHistory] = persisted(
"prompt-history-shell.v1",
createStore<{
entries: Prompt[]
}>({
entries: [],
}),
)
const clonePromptParts = (prompt: Prompt): Prompt =>
prompt.map((part) => {
@@ -139,6 +149,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
const length = position === "start" ? 0 : promptLength(p)
setStore("applyingHistory", true)
setStore("userHasEdited", false)
prompt.set(p, length)
requestAnimationFrame(() => {
editorRef.focus()
@@ -440,6 +451,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (shouldReset) {
setStore("popover", null)
setStore("userHasEdited", false)
if (store.historyIndex >= 0 && !store.applyingHistory) {
setStore("historyIndex", -1)
setStore("savedPrompt", null)
@@ -474,6 +486,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("savedPrompt", null)
}
if (!store.applyingHistory) {
setStore("userHasEdited", true)
}
prompt.set(rawParts, cursorPosition)
}
@@ -547,7 +563,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
sessionID: params.id!,
})
const addToHistory = (prompt: Prompt) => {
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
const text = prompt
.map((p) => ("content" in p ? p.content : ""))
.join("")
@@ -555,17 +571,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!text) return
const entry = clonePromptParts(prompt)
const lastEntry = history.entries[0]
const currentHistory = mode === "shell" ? shellHistory : history
const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
const lastEntry = currentHistory.entries[0]
if (lastEntry) {
const lastText = lastEntry.map((p) => ("content" in p ? p.content : "")).join("")
if (lastText === text) return
}
setHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
setCurrentHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
}
const navigateHistory = (direction: "up" | "down") => {
const entries = history.entries
if (store.userHasEdited) return false
const entries = store.mode === "shell" ? shellHistory.entries : history.entries
const current = store.historyIndex
if (direction === "up") {
@@ -693,9 +713,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return
}
addToHistory(currentPrompt)
addToHistory(currentPrompt, store.mode)
setStore("historyIndex", -1)
setStore("savedPrompt", null)
setStore("userHasEdited", false)
let existing = info()
if (!existing) {
@@ -1004,15 +1025,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
e.currentTarget.value = ""
}}
/>
<Tooltip placement="top" value="Attach image">
<IconButton
type="button"
icon="photo"
variant="ghost"
class="h-10 w-8"
onClick={() => fileInputRef.click()}
/>
</Tooltip>
<Show when={store.mode === "normal"}>
<Tooltip placement="top" value="Attach image">
<IconButton
type="button"
icon="photo"
variant="ghost"
class="h-10 w-8"
onClick={() => fileInputRef.click()}
/>
</Tooltip>
</Show>
<Tooltip
placement="top"
inactive={!prompt.dirty() && !working()}
+10 -8
View File
@@ -1,31 +1,33 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
import { usePlatform } from "./platform"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
init: (props: { url: string }) => {
const abort = new AbortController()
const sdk = createOpencodeClient({
const eventSdk = createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
signal: AbortSignal.timeout(1000 * 60 * 10),
throwOnError: true,
})
const emitter = createGlobalEmitter<{
[key: string]: Event
}>()
sdk.global.event().then(async (events) => {
eventSdk.global.event().then(async (events) => {
for await (const event of events.stream) {
// console.log("event", event)
emitter.emit(event.directory ?? "global", event.payload)
}
})
onCleanup(() => {
abort.abort()
const platform = usePlatform()
const sdk = createOpencodeClient({
baseUrl: props.url,
signal: AbortSignal.timeout(1000 * 60 * 10),
fetch: platform.fetch,
throwOnError: true,
})
return { url: props.url, client: sdk, event: emitter }
+6 -1
View File
@@ -21,6 +21,8 @@ import { Binary } from "@opencode-ai/util/binary"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
type State = {
ready: boolean
@@ -72,6 +74,7 @@ function createGlobalSync() {
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
setGlobalStore("children", directory, {
project: "",
@@ -117,11 +120,13 @@ function createGlobalSync() {
})
.catch((err) => {
console.error("Failed to load sessions", err)
setGlobalStore("error", err)
const project = getFilename(directory)
showToast({ title: `Failed to load sessions for ${project}`, description: err.message })
})
}
async function bootstrapInstance(directory: string) {
if (!directory) return
const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
+9 -3
View File
@@ -5,6 +5,12 @@ export type Platform = {
/** Platform discriminator */
platform: "web" | "tauri"
/** Open a URL in the default browser */
openLink(url: string): void
/** Restart the app */
restart(): Promise<void>
/** Open native directory picker dialog (Tauri only) */
openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
@@ -14,9 +20,6 @@ export type Platform = {
/** Save file picker dialog (Tauri only) */
saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise<string | null>
/** Open a URL in the default browser */
openLink(url: string): void
/** Storage mechanism, defaults to localStorage */
storage?: (name?: string) => SyncStorage | AsyncStorage
@@ -25,6 +28,9 @@ export type Platform = {
/** Install updates (Tauri only) */
update?(): Promise<void>
/** Fetch override */
fetch?: typeof fetch
}
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
+4 -7
View File
@@ -1,17 +1,18 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
import { useGlobalSDK } from "./global-sdk"
import { usePlatform } from "./platform"
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK",
init: (props: { directory: string }) => {
const platform = usePlatform()
const globalSDK = useGlobalSDK()
const abort = new AbortController()
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
signal: abort.signal,
signal: AbortSignal.timeout(1000 * 60 * 10),
fetch: platform.fetch,
directory: props.directory,
throwOnError: true,
})
@@ -24,10 +25,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
emitter.emit(event.type, event)
})
onCleanup(() => {
abort.abort()
})
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
},
})
+3
View File
@@ -15,6 +15,9 @@ const platform: Platform = {
openLink(url: string) {
window.open(url, "_blank")
},
restart: async () => {
window.location.reload()
},
}
render(
+23 -3
View File
@@ -1,5 +1,6 @@
import { TextField } from "@opencode-ai/ui/text-field"
import { Logo } from "@opencode-ai/ui/logo"
import { Button } from "@opencode-ai/ui/button"
import { Component } from "solid-js"
import { usePlatform } from "@/context/platform"
import { Icon } from "@opencode-ai/ui/icon"
@@ -9,9 +10,17 @@ export type InitError = {
data: Record<string, unknown>
}
function formatError(error: InitError | undefined): string {
if (!error) return "Unknown error"
function isInitError(error: unknown): error is InitError {
return (
typeof error === "object" &&
error !== null &&
"name" in error &&
"data" in error &&
typeof (error as InitError).data === "object"
)
}
function formatInitError(error: InitError): string {
const data = error.data
switch (error.name) {
case "MCPFailed":
@@ -53,8 +62,16 @@ function formatError(error: InitError | undefined): string {
}
}
function formatError(error: unknown): string {
if (!error) return "Unknown error"
if (isInitError(error)) return formatInitError(error)
if (error instanceof Error) return `${error.name}: ${error.message}\n\n${error.stack}`
if (typeof error === "string") return error
return JSON.stringify(error, null, 2)
}
interface ErrorPageProps {
error: InitError | undefined
error: unknown
}
export const ErrorPage: Component<ErrorPageProps> = (props) => {
@@ -76,6 +93,9 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
label="Error Details"
hideLabel
/>
<Button size="large" onClick={platform.restart}>
Restart
</Button>
<div class="flex items-center justify-center gap-1">
Please report this error to the OpenCode team
<button
+5 -2
View File
@@ -69,7 +69,7 @@ export default function Layout(props: ParentProps) {
const command = useCommand()
onMount(async () => {
if (platform.checkUpdate && platform.update) {
if (platform.checkUpdate && platform.update && platform.restart) {
const { updateAvailable, version } = await platform.checkUpdate()
if (updateAvailable) {
showToast({
@@ -80,7 +80,10 @@ export default function Layout(props: ParentProps) {
actions: [
{
label: "Install and restart",
onClick: () => platform!.update!(),
onClick: async () => {
await platform.update!()
await platform.restart!()
},
},
{
label: "Not yet",
+40 -4
View File
@@ -1,4 +1,17 @@
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect, on } from "solid-js"
import {
For,
onCleanup,
onMount,
Show,
Match,
Switch,
createResource,
createMemo,
createEffect,
on,
createRenderEffect,
batch,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
@@ -130,7 +143,8 @@ export default function Page() {
clickTimer: undefined as number | undefined,
activeDraggable: undefined as string | undefined,
activeTerminalDraggable: undefined as string | undefined,
stepsExpanded: false,
userInteracted: false,
stepsExpanded: true,
})
let inputRef!: HTMLDivElement
@@ -159,7 +173,28 @@ export default function Page() {
),
)
createEffect(() => {
params.id
const status = sync.data.session_status[params.id ?? ""] ?? { type: "idle" }
batch(() => {
setStore("userInteracted", false)
setStore("stepsExpanded", status.type !== "idle")
})
})
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
createRenderEffect((prev) => {
const isWorking = working()
if (!prev && isWorking) {
setStore("stepsExpanded", true)
}
if (prev && !isWorking && !store.userInteracted) {
setStore("stepsExpanded", false)
}
return isWorking
}, working())
command.register(() => [
{
@@ -602,7 +637,7 @@ export default function Page() {
<div
classList={{
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
"max-w-200 mx-auto": !wide(),
"max-w-146 mx-auto": !wide(),
}}
>
<Switch>
@@ -619,7 +654,8 @@ export default function Page() {
sessionID={params.id!}
messageID={activeMessage()!.id}
stepsExpanded={store.stepsExpanded}
onStepsExpandedChange={(expanded) => setStore("stepsExpanded", expanded)}
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
onUserInteracted={() => setStore("userInteracted", true)}
classes={{
root: "pb-20 flex-1 min-w-0",
content: "pb-20",
+99
View File
@@ -0,0 +1,99 @@
import z from "zod"
const prefixes = {
session: "ses",
message: "msg",
permission: "per",
user: "usr",
part: "prt",
pty: "pty",
} as const
const LENGTH = 26
let lastTimestamp = 0
let counter = 0
type Prefix = keyof typeof prefixes
export namespace Identifier {
export function schema(prefix: Prefix) {
return z.string().startsWith(prefixes[prefix])
}
export function ascending(prefix: Prefix, given?: string) {
return generateID(prefix, false, given)
}
export function descending(prefix: Prefix, given?: string) {
return generateID(prefix, true, given)
}
}
function generateID(prefix: Prefix, descending: boolean, given?: string): string {
if (!given) {
return create(prefix, descending)
}
if (!given.startsWith(prefixes[prefix])) {
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
}
return given
}
function create(prefix: Prefix, descending: boolean, timestamp?: number): string {
const currentTimestamp = timestamp ?? Date.now()
if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp
counter = 0
}
counter += 1
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
if (descending) {
now = ~now
}
const timeBytes = new Uint8Array(6)
for (let i = 0; i < 6; i += 1) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
return prefixes[prefix] + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12)
}
function bytesToHex(bytes: Uint8Array): string {
let hex = ""
for (let i = 0; i < bytes.length; i += 1) {
hex += bytes[i].toString(16).padStart(2, "0")
}
return hex
}
function randomBase62(length: number): string {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const bytes = getRandomBytes(length)
let result = ""
for (let i = 0; i < length; i += 1) {
result += chars[bytes[i] % 62]
}
return result
}
function getRandomBytes(length: number): Uint8Array {
const bytes = new Uint8Array(length)
const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : undefined
if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
cryptoObj.getRandomValues(bytes)
return bytes
}
for (let i = 0; i < length; i += 1) {
bytes[i] = Math.floor(Math.random() * 256)
}
return bytes
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.167",
"version": "1.0.173",
"private": true,
"type": "module",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.167"
version = "1.0.173"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.167/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.173/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.167/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.173/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.167/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.173/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.167/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.173/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.167/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.173/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.167",
"version": "1.0.173",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.167",
"version": "1.0.173",
"name": "opencode",
"type": "module",
"private": true,
@@ -71,8 +71,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.61",
"@opentui/solid": "0.1.61",
"@opentui/core": "0.1.62",
"@opentui/solid": "0.1.62",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -0,0 +1,187 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { Script } from "@opencode-ai/script"
if (!Script.preview) {
// Calculate SHA values
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/sst/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
// Source-based PKGBUILD for opencode
const sourcePkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/sst/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode-bin')",
"depends=('ripgrep')",
"makedepends=('git' 'bun-bin' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
` cd "opencode-\${pkgver}"`,
` bun install`,
" cd ./packages/opencode",
` OPENCODE_CHANNEL=latest OPENCODE_VERSION=${pkgver} bun run ./script/build.ts --single`,
"}",
"",
"package() {",
` cd "opencode-\${pkgver}/packages/opencode"`,
' mkdir -p "${pkgdir}/usr/bin"',
' target_arch="x64"',
' case "$CARCH" in',
' x86_64) target_arch="x64" ;;',
' aarch64) target_arch="arm64" ;;',
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
" esac",
' libc=""',
" if command -v ldd >/dev/null 2>&1; then",
" if ldd --version 2>&1 | grep -qi musl; then",
' libc="-musl"',
" fi",
" fi",
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
' libc="-musl"',
" fi",
' base=""',
' if [ "$target_arch" = "x64" ]; then',
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
' base="-baseline"',
" fi",
" fi",
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
' if [ ! -f "$bin" ]; then',
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
" return 1",
" fi",
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
for (const [pkg, pkgbuild] of [
["opencode-bin", binaryPkgbuild],
["opencode", sourcePkgbuild],
]) {
for (let i = 0; i < 30; i++) {
try {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch (e) {
continue
}
}
}
// Homebrew formula
const homebrewFormula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/sst/opencode"`,
` version "${Script.version.split("-")[0]}"`,
"",
` depends_on "ripgrep"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"end",
"",
"",
].join("\n")
await $`rm -rf ./dist/homebrew-tap`
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
}
+3 -184
View File
@@ -53,196 +53,15 @@ for (const tag of tags) {
}
if (!Script.preview) {
// Create archives for GitHub release
for (const key of Object.keys(binaries)) {
if (key.includes("linux")) {
await $`cd dist/${key}/bin && tar -czf ../../${key}.tar.gz *`
await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
} else {
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
}
}
// Calculate SHA values
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/sst/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
// Source-based PKGBUILD for opencode
const sourcePkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${pkgver}`,
`_subver=${_subver}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/sst/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode-bin')",
"depends=('ripgrep')",
"makedepends=('git' 'bun-bin' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
` cd "opencode-\${pkgver}"`,
` bun install`,
" cd ./packages/opencode",
` OPENCODE_CHANNEL=latest OPENCODE_VERSION=${pkgver} bun run ./script/build.ts --single`,
"}",
"",
"package() {",
` cd "opencode-\${pkgver}/packages/opencode"`,
' mkdir -p "${pkgdir}/usr/bin"',
' target_arch="x64"',
' case "$CARCH" in',
' x86_64) target_arch="x64" ;;',
' aarch64) target_arch="arm64" ;;',
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
" esac",
' libc=""',
" if command -v ldd >/dev/null 2>&1; then",
" if ldd --version 2>&1 | grep -qi musl; then",
' libc="-musl"',
" fi",
" fi",
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
' libc="-musl"',
" fi",
' base=""',
' if [ "$target_arch" = "x64" ]; then',
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
' base="-baseline"',
" fi",
" fi",
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
' if [ ! -f "$bin" ]; then',
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
" return 1",
" fi",
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
for (const [pkg, pkgbuild] of [
["opencode-bin", binaryPkgbuild],
["opencode", sourcePkgbuild],
]) {
for (let i = 0; i < 30; i++) {
try {
await $`rm -rf ./dist/aur-${pkg}`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}`
await $`cd ./dist/aur-${pkg} && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch (e) {
continue
}
}
}
// Homebrew formula
const homebrewFormula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/sst/opencode"`,
` version "${Script.version.split("-")[0]}"`,
"",
` depends_on "ripgrep"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"end",
"",
"",
].join("\n")
await $`rm -rf ./dist/homebrew-tap`
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
const image = "ghcr.io/sst/opencode"
const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${Script.version}`, `${image}:latest`]
+1 -1
View File
@@ -698,7 +698,7 @@ export namespace ACP {
})
const availableModes = agents
.filter((agent) => agent.mode !== "subagent")
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
.map((agent) => ({
id: agent.name,
name: agent.name,
-18
View File
@@ -111,22 +111,4 @@ export namespace BunProc {
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
return mod
}
export async function resolve(pkg: string) {
const local = workspace(pkg)
if (local) return local
const dir = path.join(Global.Path.cache, "node_modules", pkg)
const pkgjson = Bun.file(path.join(dir, "package.json"))
const exists = await pkgjson.exists()
if (exists) return dir
}
function workspace(pkg: string) {
try {
const target = req.resolve(`${pkg}/package.json`)
return path.dirname(target)
} catch {
return
}
}
}
+37 -8
View File
@@ -418,6 +418,7 @@ export const GithubRunCommand = cmd({
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
const triggerCommentId = payload.comment.id
const useGithubToken = normalizeUseGithubToken()
const commentType = context.eventName === "pull_request_review_comment" ? "pr_review" : "issue"
try {
if (useGithubToken) {
@@ -443,7 +444,7 @@ export const GithubRunCommand = cmd({
}
await assertPermissions()
await addReaction()
await addReaction(commentType)
// Setup opencode session
const repoData = await fetchRepo()
@@ -476,7 +477,7 @@ export const GithubRunCommand = cmd({
}
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
await createComment(`${response}${footer({ image: !hasShared })}`)
await removeReaction()
await removeReaction(commentType)
}
// Fork PR
else {
@@ -491,7 +492,7 @@ export const GithubRunCommand = cmd({
}
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
await createComment(`${response}${footer({ image: !hasShared })}`)
await removeReaction()
await removeReaction(commentType)
}
}
// Issue
@@ -512,10 +513,10 @@ export const GithubRunCommand = cmd({
`${response}\n\nCloses #${issueId}${footer({ image: true })}`,
)
await createComment(`Created PR #${pr}${footer({ image: true })}`)
await removeReaction()
await removeReaction(commentType)
} else {
await createComment(`${response}${footer({ image: true })}`)
await removeReaction()
await removeReaction(commentType)
}
}
} catch (e: any) {
@@ -528,7 +529,7 @@ export const GithubRunCommand = cmd({
msg = e.message
}
await createComment(`${msg}${footer()}`)
await removeReaction()
await removeReaction(commentType)
core.setFailed(msg)
// Also output the clean error message for the action to capture
//core.setOutput("prepare_error", e.message);
@@ -977,8 +978,16 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
}
async function addReaction() {
async function addReaction(commentType: "issue" | "pr_review") {
console.log("Adding reaction...")
if (commentType === "pr_review") {
return await octoRest.rest.reactions.createForPullRequestReviewComment({
owner,
repo,
comment_id: triggerCommentId,
content: AGENT_REACTION,
})
}
return await octoRest.rest.reactions.createForIssueComment({
owner,
repo,
@@ -987,8 +996,28 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
})
}
async function removeReaction() {
async function removeReaction(commentType: "issue" | "pr_review") {
console.log("Removing reaction...")
if (commentType === "pr_review") {
const reactions = await octoRest.rest.reactions.listForPullRequestReviewComment({
owner,
repo,
comment_id: triggerCommentId,
content: AGENT_REACTION,
})
const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME)
if (!eyesReaction) return
await octoRest.rest.reactions.deleteForPullRequestComment({
owner,
repo,
comment_id: triggerCommentId,
reaction_id: eyesReaction.id,
})
return
}
const reactions = await octoRest.rest.reactions.listForIssueComment({
owner,
repo,
@@ -1,4 +1,4 @@
import type { BoxRenderable, TextareaRenderable, KeyEvent } from "@opentui/core"
import type { BoxRenderable, TextareaRenderable, KeyEvent, ScrollBoxRenderable } from "@opentui/core"
import fuzzysort from "fuzzysort"
import { firstBy } from "remeda"
import { createMemo, createResource, createEffect, onMount, onCleanup, For, Show, createSignal } from "solid-js"
@@ -369,7 +369,7 @@ export function Autocomplete(props: {
store.visible === "@" ? [...agents(), ...(files() || [])] : [...commands()]
).filter((x) => x.disabled !== true)
const currentFilter = filter()
if (!currentFilter) return mixed.slice(0, 10)
if (!currentFilter) return mixed
const result = fuzzysort.go(currentFilter, mixed, {
keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""],
limit: 10,
@@ -395,7 +395,19 @@ export function Autocomplete(props: {
let next = store.selected + direction
if (next < 0) next = options().length - 1
if (next >= options().length) next = 0
moveTo(next)
}
function moveTo(next: number) {
setStore("selected", next)
if (!scroll) return
const viewportHeight = Math.min(height(), options().length)
const scrollBottom = scroll.scrollTop + viewportHeight
if (next < scroll.scrollTop) {
scroll.scrollBy(next - scroll.scrollTop)
} else if (next + 1 > scrollBottom) {
scroll.scrollBy(next + 1 - scrollBottom)
}
}
function select() {
@@ -497,6 +509,8 @@ export function Autocomplete(props: {
return 1
})
let scroll: ScrollBoxRenderable
return (
<box
visible={store.visible !== false}
@@ -508,7 +522,12 @@ export function Autocomplete(props: {
{...SplitBorder}
borderColor={theme.border}
>
<box backgroundColor={theme.backgroundMenu} height={height()}>
<scrollbox
ref={(r: ScrollBoxRenderable) => (scroll = r)}
backgroundColor={theme.backgroundMenu}
height={height()}
scrollbarOptions={{ visible: false }}
>
<For
each={options()}
fallback={
@@ -535,7 +554,7 @@ export function Autocomplete(props: {
</box>
)}
</For>
</box>
</scrollbox>
</box>
)
}
@@ -10,6 +10,7 @@ export type HomeRoute = {
export type SessionRoute = {
type: "session"
sessionID: string
initialPrompt?: PromptInfo
}
export type Route = HomeRoute | SessionRoute
@@ -6,6 +6,7 @@ import { Locale } from "@/util/locale"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import { useDialog } from "../../ui/dialog"
import type { PromptInfo } from "@tui/component/prompt/history"
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
const sync = useSync()
@@ -35,9 +36,21 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
sessionID: props.sessionID,
messageID: message.id,
})
const parts = sync.data.part[message.id] ?? []
const initialPrompt = parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
route.navigate({
sessionID: forked.data!.id,
type: "session",
initialPrompt,
})
dialog.clear()
},
@@ -80,9 +80,25 @@ export function DialogMessage(props: {
sessionID: props.sessionID,
messageID: props.messageID,
})
const initialPrompt = (() => {
const msg = message()
if (!msg) return undefined
const parts = sync.data.part[msg.id]
return parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
})()
route.navigate({
sessionID: result.data!.id,
type: "session",
initialPrompt,
})
dialog.clear()
},
@@ -0,0 +1,26 @@
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
export function DialogSubagent(props: { sessionID: string }) {
const route = useRoute()
return (
<DialogSelect
title="Subagent Actions"
options={[
{
title: "Open",
value: "subagent.view",
description: "open the subagent's session",
onSelect: (dialog) => {
route.navigate({
type: "session",
sessionID: props.sessionID,
})
dialog.clear()
},
},
]}
/>
)
}
@@ -66,6 +66,7 @@ import stripAnsi from "strip-ansi"
import { Footer } from "./footer.tsx"
import { usePromptRef } from "../../context/prompt"
import { Filesystem } from "@/util/filesystem"
import { DialogSubagent } from "./dialog-subagent.tsx"
addDefaultParsers(parsers.parsers)
@@ -86,6 +87,7 @@ const context = createContext<{
showTimestamps: () => boolean
usernameVisible: () => boolean
showDetails: () => boolean
userMessageMarkdown: () => boolean
diffWrapMode: () => "word" | "none"
sync: ReturnType<typeof useSync>
}>()
@@ -123,6 +125,7 @@ export function Session() {
const [usernameVisible, setUsernameVisible] = createSignal(kv.get("username_visible", true))
const [showDetails, setShowDetails] = createSignal(kv.get("tool_details_visibility", true))
const [showScrollbar, setShowScrollbar] = createSignal(kv.get("scrollbar_visible", false))
const [userMessageMarkdown, setUserMessageMarkdown] = createSignal(kv.get("user_message_markdown", true))
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
const wide = createMemo(() => dimensions().width > 120)
@@ -165,6 +168,13 @@ export function Session() {
const toast = useToast()
const sdk = useSDK()
// Handle initial prompt from fork
createEffect(() => {
if (route.initialPrompt && prompt) {
prompt.set(route.initialPrompt)
}
})
// Auto-navigate to whichever session currently needs permission input
createEffect(() => {
const currentSession = session()
@@ -514,6 +524,19 @@ export function Session() {
dialog.clear()
},
},
{
title: userMessageMarkdown() ? "Disable user message markdown" : "Enable user message markdown",
value: "session.toggle.user_message_markdown",
category: "Session",
onSelect: (dialog) => {
setUserMessageMarkdown((prev) => {
const next = !prev
kv.set("user_message_markdown", next)
return next
})
dialog.clear()
},
},
{
title: "Page up",
value: "session.page.up",
@@ -851,6 +874,7 @@ export function Session() {
showTimestamps,
usernameVisible,
showDetails,
userMessageMarkdown,
diffWrapMode,
sync,
}}
@@ -1024,7 +1048,7 @@ function UserMessage(props: {
const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
const sync = useSync()
const { theme } = useTheme()
const { theme, syntax } = useTheme()
const [hover, setHover] = createSignal(false)
const queued = createMemo(() => props.pending && props.message.id > props.pending)
const color = createMemo(() => (queued() ? theme.accent : local.agent.color(props.message.agent)))
@@ -1055,7 +1079,22 @@ function UserMessage(props: {
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
flexShrink={0}
>
<text fg={theme.text}>{text()?.text}</text>
<Switch>
<Match when={ctx.userMessageMarkdown()}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={false}
syntaxStyle={syntax()}
content={text()?.text ?? ""}
conceal={ctx.conceal()}
fg={theme.text}
/>
</Match>
<Match when={!ctx.userMessageMarkdown()}>
<text fg={theme.text}>{text()?.text}</text>
</Match>
</Switch>
<Show when={files().length}>
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
@@ -1528,13 +1567,33 @@ ToolRegistry.register<typeof ListTool>({
ToolRegistry.register<typeof TaskTool>({
name: "task",
container: "block",
container: "inline",
render(props) {
const { theme } = useTheme()
const keybind = useKeybind()
const dialog = useDialog()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
return (
<>
<box
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
marginTop={1}
gap={1}
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
onMouseOver={() => setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
const id = props.metadata.sessionId
if (renderer.getSelection()?.getSelectedText() || !id) return
dialog.replace(() => <DialogSubagent sessionID={id} />)
}}
>
<ToolTitle icon="◉" fallback="Delegating..." when={props.input.subagent_type ?? props.input.description}>
{Locale.titlecase(props.input.subagent_type ?? "unknown")} Task "{props.input.description}"
</ToolTitle>
@@ -1557,7 +1616,7 @@ ToolRegistry.register<typeof TaskTool>({
{keybind.print("session_child_cycle")}, {keybind.print("session_child_cycle_reverse")}
<span style={{ fg: theme.textMuted }}> to navigate between subagent sessions</span>
</text>
</>
</box>
)
},
})
@@ -154,7 +154,11 @@ export function Sidebar(props: { sessionID: string }) {
</box>
<Show when={sync.data.lsp.length <= 2 || expanded.lsp}>
<Show when={sync.data.lsp.length === 0}>
<text fg={theme.textMuted}>LSPs will activate as files are read</text>
<text fg={theme.textMuted}>
{sync.data.config.lsp === false
? "LSPs have been disabled in settings"
: "LSPs will activate as files are read"}
</text>
</Show>
<For each={sync.data.lsp}>
{(item) => (
+2 -2
View File
@@ -87,8 +87,6 @@ export namespace File {
const tops = ["image", "audio", "video", "font", "model", "multipart"]
if (tops.includes(top)) return true
if (type === "application/octet-stream") return true
const bins = [
"zip",
"gzip",
@@ -125,6 +123,8 @@ export namespace File {
let cache: Entry = { files: [], dirs: [] }
let fetching = false
const fn = async (result: Entry) => {
// Disable scanning if in root of file system
if (Instance.directory === path.parse(Instance.directory).root) return
fetching = true
const set = new Set<string>()
for await (const file of Ripgrep.files({ cwd: Instance.directory })) {
+1
View File
@@ -29,6 +29,7 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
export const OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX = number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX")
export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT")
export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
+64 -10
View File
@@ -1,19 +1,73 @@
import { Identifier as SharedIdentifier } from "@opencode-ai/util/identifier"
import z from "zod"
import { randomBytes } from "crypto"
export namespace Identifier {
export type Prefix = SharedIdentifier.Prefix
const prefixes = {
session: "ses",
message: "msg",
permission: "per",
user: "usr",
part: "prt",
pty: "pty",
} as const
export const schema = (prefix: Prefix) => SharedIdentifier.schema(prefix)
export function ascending(prefix: Prefix, given?: string) {
return SharedIdentifier.ascending(prefix, given)
export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix])
}
export function descending(prefix: Prefix, given?: string) {
return SharedIdentifier.descending(prefix, given)
const LENGTH = 26
// State for monotonic ID generation
let lastTimestamp = 0
let counter = 0
export function ascending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, false, given)
}
export function create(prefix: Prefix, descending: boolean, timestamp?: number) {
return SharedIdentifier.createPrefixed(prefix, descending, timestamp)
export function descending(prefix: keyof typeof prefixes, given?: string) {
return generateID(prefix, true, given)
}
function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
if (!given) {
return create(prefix, descending)
}
if (!given.startsWith(prefixes[prefix])) {
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
}
return given
}
function randomBase62(length: number): string {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let result = ""
const bytes = randomBytes(length)
for (let i = 0; i < length; i++) {
result += chars[bytes[i] % 62]
}
return result
}
export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string {
const currentTimestamp = timestamp ?? Date.now()
if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp
counter = 0
}
counter++
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
now = descending ? ~now : now
const timeBytes = Buffer.alloc(6)
for (let i = 0; i < 6; i++) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
}
}
+20
View File
@@ -9,6 +9,7 @@ import z from "zod"
import { Config } from "../config/config"
import { spawn } from "child_process"
import { Instance } from "../project/instance"
import { Flag } from "@/flag/flag"
export namespace LSP {
const log = Log.create({ service: "lsp" })
@@ -60,6 +61,21 @@ export namespace LSP {
})
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
const filterExperimentalServers = (servers: Record<string, LSPServer.Info>) => {
if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
// If experimental flag is enabled, disable pyright
if (servers["pyright"]) {
log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled")
delete servers["pyright"]
}
} else {
// If experimental flag is disabled, disable ty
if (servers["ty"]) {
delete servers["ty"]
}
}
}
const state = Instance.state(
async () => {
const clients: LSPClient.Info[] = []
@@ -79,6 +95,9 @@ export namespace LSP {
for (const server of Object.values(LSPServer)) {
servers[server.id] = server
}
filterExperimentalServers(servers)
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
const existing = servers[name]
if (item.disabled) {
@@ -204,6 +223,7 @@ export namespace LSP {
for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue
const root = await server.root(file)
if (!root) continue
if (s.broken.has(root + server.id)) continue
+136 -1
View File
@@ -4,7 +4,7 @@ import os from "os"
import { Global } from "../global"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { $ } from "bun"
import { $, readableStreamToText } from "bun"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
@@ -216,6 +216,77 @@ export namespace LSPServer {
},
}
export const Oxlint: Info = {
id: "oxlint",
root: NearestRoot([
".oxlintrc.json",
"package-lock.json",
"bun.lockb",
"bun.lock",
"pnpm-lock.yaml",
"yarn.lock",
"package.json",
]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"],
async spawn(root) {
const ext = process.platform === "win32" ? ".cmd" : ""
const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext)
const lintTarget = path.join("node_modules", ".bin", "oxlint" + ext)
const resolveBin = async (target: string) => {
const localBin = path.join(root, target)
if (await Bun.file(localBin).exists()) return localBin
const candidates = Filesystem.up({
targets: [target],
start: root,
stop: Instance.worktree,
})
const first = await candidates.next()
await candidates.return()
if (first.value) return first.value
return undefined
}
let lintBin = await resolveBin(lintTarget)
if (!lintBin) {
const found = Bun.which("oxlint")
if (found) lintBin = found
}
if (lintBin) {
const proc = Bun.spawn([lintBin, "--help"], { stdout: "pipe" })
await proc.exited
const help = await readableStreamToText(proc.stdout)
if (help.includes("--lsp")) {
return {
process: spawn(lintBin, ["--lsp"], {
cwd: root,
}),
}
}
}
let serverBin = await resolveBin(serverTarget)
if (!serverBin) {
const found = Bun.which("oxc_language_server")
if (found) serverBin = found
}
if (serverBin) {
return {
process: spawn(serverBin, [], {
cwd: root,
}),
}
}
log.info("oxlint not found, please install oxlint")
return
},
}
export const Biome: Info = {
id: "biome",
root: NearestRoot([
@@ -361,6 +432,70 @@ export namespace LSPServer {
},
}
export const Ty: Info = {
id: "ty",
extensions: [".py", ".pyi"],
root: NearestRoot([
"pyproject.toml",
"ty.toml",
"setup.py",
"setup.cfg",
"requirements.txt",
"Pipfile",
"pyrightconfig.json",
]),
async spawn(root) {
if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
return undefined
}
let binary = Bun.which("ty")
const initialization: Record<string, string> = {}
const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
(p): p is string => p !== undefined,
)
for (const venvPath of potentialVenvPaths) {
const isWindows = process.platform === "win32"
const potentialPythonPath = isWindows
? path.join(venvPath, "Scripts", "python.exe")
: path.join(venvPath, "bin", "python")
if (await Bun.file(potentialPythonPath).exists()) {
initialization["pythonPath"] = potentialPythonPath
break
}
}
if (!binary) {
for (const venvPath of potentialVenvPaths) {
const isWindows = process.platform === "win32"
const potentialTyPath = isWindows
? path.join(venvPath, "Scripts", "ty.exe")
: path.join(venvPath, "bin", "ty")
if (await Bun.file(potentialTyPath).exists()) {
binary = potentialTyPath
break
}
}
}
if (!binary) {
log.error("ty not found, please install ty first")
return
}
const proc = spawn(binary, ["server"], {
cwd: root,
})
return {
process: proc,
initialization,
}
},
}
export const Pyright: Info = {
id: "pyright",
extensions: [".py", ".pyi"],
+3 -10
View File
@@ -74,17 +74,10 @@ export namespace ProviderTransform {
return result
}
// TODO: rm later
const bugged =
(model.id === "kimi-k2-thinking" && model.providerID === "opencode") ||
(model.id === "moonshotai/Kimi-K2-Thinking" && model.providerID === "baseten")
if (
model.providerID === "deepseek" ||
model.api.id.toLowerCase().includes("deepseek") ||
(model.capabilities.interleaved &&
typeof model.capabilities.interleaved === "object" &&
model.capabilities.interleaved.field === "reasoning_content" &&
!bugged)
model.capabilities.interleaved &&
typeof model.capabilities.interleaved === "object" &&
model.capabilities.interleaved.field === "reasoning_content"
) {
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
+9 -1
View File
@@ -612,6 +612,14 @@ export namespace MessageV2 {
case APICallError.isInstance(e):
const message = iife(() => {
let msg = e.message
if (msg === "") {
if (e.responseBody) return e.responseBody
if (e.statusCode) {
const err = STATUS_CODES[e.statusCode]
if (err) return err
}
return "Unknown error"
}
const transformed = ProviderTransform.error(ctx.providerID, e)
if (transformed !== msg) {
return transformed
@@ -630,7 +638,7 @@ export namespace MessageV2 {
} catch {}
return `${msg}: ${e.responseBody}`
})
}).trim()
return new MessageV2.APIError(
{
+3 -2
View File
@@ -65,7 +65,7 @@ export namespace SessionRetry {
if (json.type === "error" && json.error?.type === "too_many_requests") {
return "Too Many Requests"
}
if (json.code === "Some resource has been exhausted") {
if (json.code.includes("exhausted") || json.code.includes("unavailable")) {
return "Provider is overloaded"
}
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
@@ -73,7 +73,8 @@ export namespace SessionRetry {
}
if (
json.error?.message?.includes("no_kv_space") ||
(json.type === "error" && json.error?.type === "server_error")
(json.type === "error" && json.error?.type === "server_error") ||
!!json.error
) {
return "Provider Server Error"
}
+5
View File
@@ -3,9 +3,14 @@
import os from "os"
import path from "path"
import fs from "fs/promises"
import fsSync from "fs"
import { afterAll } from "bun:test"
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
await fs.mkdir(dir, { recursive: true })
afterAll(() => {
fsSync.rmSync(dir, { recursive: true, force: true })
})
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
@@ -200,7 +200,9 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
interleaved: {
field: "reasoning_content",
},
},
cost: {
input: 0.001,
@@ -229,58 +231,6 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
})
test("DeepSeek model ID containing 'deepseek' matches (case insensitive)", () => {
const msgs = [
{
role: "assistant",
content: [
{ type: "reasoning", text: "Thinking..." },
{
type: "tool-call",
toolCallId: "test",
toolName: "get_weather",
input: { location: "Hangzhou" },
},
],
},
] as any[]
const result = ProviderTransform.message(msgs, {
id: "someprovider/deepseek-reasoner",
providerID: "someprovider",
api: {
id: "deepseek-reasoner",
url: "https://api.someprovider.com",
npm: "@ai-sdk/openai-compatible",
},
name: "SomeProvider DeepSeek Reasoner",
capabilities: {
temperature: true,
reasoning: true,
attachment: false,
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.001,
output: 0.002,
cache: { read: 0.0001, write: 0.0002 },
},
limit: {
context: 128000,
output: 8192,
},
status: "active",
options: {},
headers: {},
release_date: "2023-04-01",
})
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Thinking...")
})
test("Non-DeepSeek providers leave reasoning content unchanged", () => {
const msgs = [
{
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.167",
"version": "1.0.173",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.167",
"version": "1.0.173",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -29,4 +29,4 @@
"publishConfig": {
"directory": "dist"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.167",
"version": "1.0.173",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/tauri",
"private": true,
"version": "1.0.167",
"version": "1.0.173",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
@@ -22,6 +22,7 @@
"@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-store": "~2",
"@tauri-apps/plugin-updater": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-window-state": "~2",
"solid-js": "catalog:"
},
+153 -3
View File
@@ -553,10 +553,39 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
dependencies = [
"cookie",
"document-features",
"idna",
"log",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -580,7 +609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.10.0",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"libc",
@@ -593,7 +622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
"core-foundation",
"core-foundation 0.10.1",
"libc",
]
@@ -718,6 +747,12 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "data-url"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "deranged"
version = "0.5.5"
@@ -844,6 +879,15 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -1532,6 +1576,25 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "h2"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap 2.12.1",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "half"
version = "2.7.1"
@@ -1650,6 +1713,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"h2",
"http",
"http-body",
"httparse",
@@ -1697,9 +1761,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -2111,6 +2177,12 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -2685,6 +2757,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-process",
@@ -3143,6 +3216,22 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psl-types"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
dependencies = [
"idna",
"psl-types",
]
[[package]]
name = "pxfm"
version = "0.1.27"
@@ -3439,8 +3528,12 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
@@ -3449,6 +3542,7 @@ dependencies = [
"hyper-util",
"js-sys",
"log",
"mime",
"percent-encoding",
"pin-project-lite",
"quinn",
@@ -4113,6 +4207,27 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -4134,7 +4249,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch",
@@ -4380,6 +4495,30 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-http"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00685aceab12643cf024f712ab0448ba8fcadf86f2391d49d2e5aa732aacc70"
dependencies = [
"bytes",
"cookie_store",
"data-url",
"http",
"regex",
"reqwest",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.17",
"tokio",
"url",
"urlpattern",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.2"
@@ -5621,6 +5760,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
+1
View File
@@ -27,6 +27,7 @@ tauri-plugin-process = "2"
tauri-plugin-store = "2"
tauri-plugin-window-state = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-http = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -14,6 +14,10 @@
"process:default",
"store:default",
"window-state:default",
"os:default"
"os:default",
{
"identifier": "http:default",
"allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }]
}
]
}
+1
View File
@@ -190,6 +190,7 @@ pub fn run() {
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_http::init())
.plugin(PinchZoomDisablePlugin)
.invoke_handler(tauri::generate_handler![
kill_sidecar,
+14 -1
View File
@@ -5,6 +5,8 @@ import { open, save } from "@tauri-apps/plugin-dialog"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { AsyncStorage } from "@solid-primitives/storage"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { Store } from "@tauri-apps/plugin-store"
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
@@ -57,7 +59,7 @@ const platform: Platform = {
storage: (name = "default.dat") => {
const api: AsyncStorage = {
_store: null,
_getStore: async () => api._store || (api._store = (await import("@tauri-apps/plugin-store")).Store.load(name)),
_getStore: async () => api._store || (api._store = Store.load(name)),
getItem: async (key: string) => (await (await api._getStore()).get(key)) ?? null,
setItem: async (key: string, value: string) => await (await api._getStore()).set(key, value),
removeItem: async (key: string) => await (await api._getStore()).delete(key),
@@ -82,13 +84,24 @@ const platform: Platform = {
update: async () => {
if (!UPDATER_ENABLED || !update) return
await update.install()
},
restart: async () => {
await invoke("kill_sidecar")
await relaunch()
},
// @ts-expect-error
fetch: tauriFetch,
}
createMenu()
// Stops mousewheel events from reaching Tauri's pinch-to-zoom handler
root?.addEventListener("mousewheel", (e) => {
e.stopPropagation()
})
render(() => {
return (
<PlatformProvider value={platform}>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.167",
"version": "1.0.173",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
+105 -62
View File
@@ -1,84 +1,103 @@
[data-component="markdown"] {
/* Reset & Base Typography */
min-width: 0;
max-width: 100%;
overflow: hidden;
overflow-wrap: break-word;
color: var(--text-base);
text-wrap: pretty;
strong {
font-weight: var(--font-weight-medium);
}
/* text-12-regular */
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
font-style: normal;
font-weight: var(--font-weight-regular);
font-size: var(--font-size-base); /* 14px */
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
h1 {
margin-top: 40px;
font-weight: var(--font-weight-medium);
/* Spacing for flow */
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
/* Headings: Same size, distinguished by color and spacing */
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: var(--font-size-base);
color: var(--text-strong);
strong {
font-weight: var(--font-weight-medium);
}
}
h2 {
margin-top: 32px;
font-weight: var(--font-weight-medium);
color: var(--text-strong);
strong {
font-weight: var(--font-weight-medium);
}
margin-top: 2rem;
margin-bottom: 0.75rem;
line-height: var(--line-height-large);
}
h3 {
margin-top: 24px;
/* Emphasis & Strong: Neutral strong color */
strong,
b {
color: var(--text-strong);
font-weight: var(--font-weight-medium);
color: var(--text-strong);
strong {
font-weight: var(--font-weight-medium);
}
}
h1 {
font-size: 15px;
}
/* Paragraphs */
p {
margin-top: 16px;
margin-bottom: 8px;
margin-bottom: 1rem;
}
/* Links */
a {
color: var(--text-interactive-base);
text-decoration: none;
font-weight: inherit;
}
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Lists */
ul,
ol {
margin-top: 16px;
li {
margin-bottom: 12px;
line-height: var(--line-height-large);
}
li:last-child {
margin-bottom: 0;
}
margin-top: 0.5rem;
margin-bottom: 1rem;
padding-left: 0;
list-style-position: inside;
}
li {
margin-bottom: 0.25rem;
}
li::marker {
color: var(--text-weak);
}
/* Nested lists spacing */
li > ul,
li > ol {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
padding-left: 1rem; /* Minimal indent for nesting only */
}
/* Blockquotes */
blockquote {
border-left: 2px solid var(--border-weak-base);
margin: 1.5rem 0;
padding-left: 0.5rem;
color: var(--text-weak);
font-style: normal;
}
/* Horizontal Rule - Invisible spacing only */
hr {
margin-top: 8px;
margin-bottom: 16px;
border-color: var(--border-weaker-base);
border: none;
height: 0;
margin: 2.5rem 0;
}
.shiki {
font-size: 13px;
background: var(--surface-raised-base) !important; /* temporary fix to test style */
padding: 8px 12px;
border-radius: 4px;
border: 0.5px solid var(--border-weak-base);
@@ -99,19 +118,43 @@
font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings);
font-size: 13px;
/* background-color: var(--surface-base-strong); */
/* padding: 0.15em 0.35em; */
/* border-radius: var(--radius-sm); */
padding: 2px 2px;
margin: 0 1.5px;
border-radius: 2px;
background: var(--surface-base);
box-shadow: 0 0 0 0.5px var(--border-weak-base);
}
/* &::before, */
/* &::after { */
/* content: "\`"; */
/* } */
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: var(--font-size-base);
}
th,
td {
/* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */
border-bottom: 1px solid var(--border-weaker-base);
padding: 0.75rem 0.5rem;
text-align: left;
vertical-align: top;
}
th {
color: var(--text-strong);
font-weight: var(--font-weight-medium);
border-bottom: 1px solid var(--border-weak-base);
}
/* Images */
img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 1.5rem 0;
display: block;
}
}
@@ -319,5 +319,10 @@
[data-slot="diagnostic-message"] {
color: var(--text-on-critical-base);
word-break: break-word;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
}
}
+4 -2
View File
@@ -531,12 +531,14 @@ ToolRegistry.register({
<Dynamic
component={diffComponent}
before={{
name: getFilename(props.metadata.filediff.path),
name: props.metadata.filediff.path,
contents: props.metadata.filediff.before,
cacheKey: checksum(props.metadata.filediff.before),
}}
after={{
name: getFilename(props.metadata.filediff.path),
name: props.metadata.filediff.path,
contents: props.metadata.filediff.after,
cacheKey: checksum(props.metadata.filediff.after),
}}
/>
</div>
+437 -303
View File
@@ -1,9 +1,9 @@
import { AssistantMessage, ToolPart } from "@opencode-ai/sdk/v2/client"
import { AssistantMessage, Part as PartType, TextPart, ToolPart } from "@opencode-ai/sdk/v2/client"
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
import { batch, createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { DiffChanges } from "./diff-changes"
import { Typewriter } from "./typewriter"
@@ -20,12 +20,52 @@ import { Spinner } from "./spinner"
import { createStore } from "solid-js/store"
import { DateTime, DurationUnit, Interval } from "luxon"
function computeStatusFromPart(part: PartType | undefined): string | undefined {
if (!part) return undefined
if (part.type === "tool") {
switch (part.tool) {
case "task":
return "Delegating work"
case "todowrite":
case "todoread":
return "Planning next steps"
case "read":
return "Gathering context"
case "list":
case "grep":
case "glob":
return "Searching the codebase"
case "webfetch":
return "Searching the web"
case "edit":
case "write":
return "Making edits"
case "bash":
return "Running commands"
default:
return undefined
}
}
if (part.type === "reasoning") {
const text = part.text ?? ""
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
if (match) return `Thinking · ${match[1].trim()}`
return "Thinking"
}
if (part.type === "text") {
return "Gathering thoughts"
}
return undefined
}
export function SessionTurn(
props: ParentProps<{
sessionID: string
messageID: string
stepsExpanded?: boolean
onStepsExpandedChange?: (expanded: boolean) => void
onStepsExpandedToggle?: () => void
onUserInteracted?: () => void
classes?: {
root?: string
content?: string
@@ -35,118 +75,152 @@ export function SessionTurn(
) {
const data = useData()
const diffComponent = useDiffComponent()
const messages = createMemo(() => data.store.message[props.sessionID] ?? [])
const userMessages = createMemo(() =>
messages()
.filter((m) => m.role === "user")
.sort((a, b) => a.id.localeCompare(b.id)),
)
const lastUserMessage = createMemo(() => userMessages().at(-1)!)
const message = createMemo(() => userMessages().find((m) => m.id === props.messageID)!)
const derived = createMemo(() => {
const allMessages = data.store.message[props.sessionID] ?? []
const userMessages = allMessages.filter((m) => m.role === "user").sort((a, b) => a.id.localeCompare(b.id))
const lastUserMessage = userMessages.at(-1)
const message = userMessages.find((m) => m.id === props.messageID)
if (!message) {
return {
message: undefined,
parts: [] as PartType[],
assistantMessages: [] as AssistantMessage[],
assistantParts: [] as PartType[],
lastAssistantMessage: undefined as AssistantMessage | undefined,
lastTextPart: undefined as PartType | undefined,
error: undefined,
hasSteps: false,
isShellMode: false,
rawStatus: undefined as string | undefined,
isLastUserMessage: false,
}
}
const parts = data.store.part[message.id] ?? []
const assistantMessages = allMessages.filter(
(m) => m.role === "assistant" && m.parentID === message.id,
) as AssistantMessage[]
const assistantParts: PartType[] = []
for (const m of assistantMessages) {
const msgParts = data.store.part[m.id]
if (msgParts) {
for (const p of msgParts) {
if (p) assistantParts.push(p)
}
}
}
const lastAssistantMessage = assistantMessages.at(-1)
const error = assistantMessages.find((m) => m.error)?.error
let lastTextPart: PartType | undefined
for (let i = assistantParts.length - 1; i >= 0; i--) {
if (assistantParts[i]?.type === "text") {
lastTextPart = assistantParts[i]
break
}
}
const hasSteps = assistantParts.some((p) => p?.type === "tool")
let isShellMode = false
if (parts.every((p) => p?.type === "text" && p?.synthetic) && assistantParts.length === 1) {
const assistantPart = assistantParts[0]
if (assistantPart?.type === "tool" && assistantPart?.tool === "bash") {
isShellMode = true
}
}
let resolvedParts = assistantParts
const currentTask = assistantParts.findLast(
(p) =>
p &&
p.type === "tool" &&
p.tool === "task" &&
p.state &&
"metadata" in p.state &&
p.state.metadata &&
p.state.metadata.sessionId &&
p.state.status === "running",
) as ToolPart | undefined
if (currentTask?.state && "metadata" in currentTask.state && currentTask.state.metadata?.sessionId) {
const taskMessages = data.store.message[currentTask.state.metadata.sessionId as string]?.filter(
(m) => m.role === "assistant",
)
if (taskMessages) {
const taskParts: PartType[] = []
for (const m of taskMessages) {
const msgParts = data.store.part[m.id]
if (msgParts) {
for (const p of msgParts) {
if (p) taskParts.push(p)
}
}
}
if (taskParts.length > 0) {
resolvedParts = taskParts
}
}
}
const lastPart = resolvedParts.at(-1)
const rawStatus = computeStatusFromPart(lastPart)
return {
message,
parts,
assistantMessages,
assistantParts,
lastAssistantMessage,
lastTextPart,
error,
hasSteps,
isShellMode,
rawStatus,
isLastUserMessage: message.id === lastUserMessage?.id,
}
})
const message = () => derived().message
const parts = () => derived().parts
const assistantMessages = () => derived().assistantMessages
const assistantParts = () => derived().assistantParts
const lastAssistantMessage = () => derived().lastAssistantMessage
const lastTextPart = () => derived().lastTextPart
const error = () => derived().error
const hasSteps = () => derived().hasSteps
const isShellMode = () => derived().isShellMode
const rawStatus = () => derived().rawStatus
const status = createMemo(
() =>
data.store.session_status[props.sessionID] ?? {
type: "idle",
},
)
const working = createMemo(() => status().type !== "idle" && message().id === lastUserMessage().id)
const working = createMemo(() => status().type !== "idle" && derived().isLastUserMessage)
const retry = createMemo(() => {
const s = status()
if (s.type !== "retry") return
return s
})
const assistantMessages = createMemo(() => {
return messages().filter((m) => m.role === "assistant" && m.parentID == message().id) as AssistantMessage[]
})
const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
const error = createMemo(() => assistantMessages().find((m) => m.error)?.error)
const parts = createMemo(() => data.store.part[message().id])
const lastTextPart = createMemo(() =>
assistantParts()
.filter((p) => p?.type === "text")
.at(-1),
)
const summary = createMemo(() => message().summary?.body)
const response = createMemo(() => lastTextPart()?.text)
const currentTask = createMemo(
() =>
assistantParts().findLast(
(p) =>
p &&
p.type === "tool" &&
p.tool === "task" &&
p.state &&
"metadata" in p.state &&
p.state.metadata &&
p.state.metadata.sessionId &&
p.state.status === "running",
) as ToolPart,
)
const resolvedParts = createMemo(() => {
let resolved = assistantParts()
const task = currentTask()
if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
const messages = data.store.message[task.state.metadata.sessionId as string]?.filter(
(m) => m.role === "assistant",
)
resolved = messages?.flatMap((m) => data.store.part[m.id]) ?? assistantParts()
}
return resolved
})
const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0))
const rawStatus = createMemo(() => {
const last = lastPart()
if (!last) return undefined
if (last.type === "tool") {
switch (last.tool) {
case "task":
return "Delegating work"
case "todowrite":
case "todoread":
return "Planning next steps"
case "read":
return "Gathering context"
case "list":
case "grep":
case "glob":
return "Searching the codebase"
case "webfetch":
return "Searching the web"
case "edit":
case "write":
return "Making edits"
case "bash":
return "Running commands"
default:
break
}
} else if (last.type === "reasoning") {
const text = last.text ?? ""
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
if (match) return `Thinking · ${match[1].trim()}`
return "Thinking"
} else if (last.type === "text") {
return "Gathering thoughts"
}
return undefined
})
const hasDiffs = createMemo(() => message().summary?.diffs?.length)
const isShellMode = createMemo(() => {
if (parts().some((p) => p.type !== "text" || !p.synthetic)) return false
if (assistantParts().length !== 1) return false
const assistantPart = assistantParts()[0]
if (assistantPart.type !== "tool") return false
if (assistantPart.tool !== "bash") return false
return true
})
const summary = () => message()?.summary?.body
const response = () => {
const part = lastTextPart()
return part?.type === "text" ? (part as TextPart).text : undefined
}
const hasDiffs = () => message()?.summary?.diffs?.length
function duration() {
const msg = message()
if (!msg) return ""
const completed = lastAssistantMessage()?.time.completed
const from = DateTime.fromMillis(message().time.created)
const from = DateTime.fromMillis(msg.time.created)
const to = completed ? DateTime.fromMillis(completed) : DateTime.now()
const interval = Interval.fromDateTimes(from, to)
const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"]
@@ -160,17 +234,19 @@ export function SessionTurn(
}
let scrollRef: HTMLDivElement | undefined
let lastScrollTop = 0
const [store, setStore] = createStore({
contentRef: undefined as HTMLDivElement | undefined,
stickyTitleRef: undefined as HTMLDivElement | undefined,
stickyTriggerRef: undefined as HTMLDivElement | undefined,
lastScrollTop: 0,
lastScrollHeight: 0,
lastContainerWidth: 0,
autoScrolled: false,
userScrolled: false,
reflowing: false,
stickyHeaderHeight: 0,
retrySeconds: 0,
status: rawStatus(),
stepsExpanded: props.stepsExpanded ?? working(),
duration: duration(),
})
@@ -191,18 +267,60 @@ export function SessionTurn(
function handleScroll() {
if (!scrollRef || store.autoScrolled) return
const { scrollTop } = scrollRef
// only mark as user scrolled if they actively scrolled upward
// content growth increases scrollHeight but never decreases scrollTop
const scrolledUp = scrollTop < lastScrollTop - 10
const scrollTop = scrollRef.scrollTop
const scrollHeight = scrollRef.scrollHeight
if (store.reflowing) {
batch(() => {
setStore("lastScrollTop", scrollTop)
setStore("lastScrollHeight", scrollHeight)
})
return
}
const scrollHeightChanged = Math.abs(scrollHeight - store.lastScrollHeight) > 10
const scrollTopDelta = scrollTop - store.lastScrollTop
if (scrollHeightChanged && scrollTopDelta < 0) {
const heightRatio = store.lastScrollHeight > 0 ? scrollHeight / store.lastScrollHeight : 1
const expectedScrollTop = store.lastScrollTop * heightRatio
if (Math.abs(scrollTop - expectedScrollTop) < 100) {
batch(() => {
setStore("lastScrollTop", scrollTop)
setStore("lastScrollHeight", scrollHeight)
})
return
}
}
const reset = scrollTop <= 0 && store.lastScrollTop > 0 && working() && !store.userScrolled
if (reset) {
batch(() => {
setStore("lastScrollTop", scrollTop)
setStore("lastScrollHeight", scrollHeight)
})
requestAnimationFrame(scrollToBottom)
return
}
const scrolledUp = scrollTop < store.lastScrollTop - 50 && !scrollHeightChanged
if (scrolledUp && working()) {
setStore("userScrolled", true)
props.onUserInteracted?.()
}
lastScrollTop = scrollTop
batch(() => {
setStore("lastScrollTop", scrollTop)
setStore("lastScrollHeight", scrollHeight)
})
}
function handleInteraction() {
if (working()) setStore("userScrolled", true)
if (working()) {
setStore("userScrolled", true)
props.onUserInteracted?.()
}
}
function scrollToBottom() {
@@ -211,13 +329,35 @@ export function SessionTurn(
requestAnimationFrame(() => {
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "smooth" })
requestAnimationFrame(() => {
lastScrollTop = scrollRef?.scrollTop ?? 0
setStore("autoScrolled", false)
batch(() => {
setStore("lastScrollTop", scrollRef?.scrollTop ?? 0)
setStore("lastScrollHeight", scrollRef?.scrollHeight ?? 0)
setStore("autoScrolled", false)
})
})
})
}
createResizeObserver(() => store.contentRef, scrollToBottom)
createResizeObserver(
() => store.contentRef,
({ width }) => {
const widthChanged = Math.abs(width - store.lastContainerWidth) > 5
if (widthChanged && store.lastContainerWidth > 0) {
setStore("reflowing", true)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setStore("reflowing", false)
if (working() && !store.userScrolled) {
scrollToBottom()
}
})
})
} else if (!store.reflowing) {
scrollToBottom()
}
setStore("lastContainerWidth", width)
},
)
createEffect(() => {
if (!working()) setStore("userScrolled", false)
@@ -239,12 +379,6 @@ export function SessionTurn(
},
)
createEffect(() => {
if (props.stepsExpanded !== undefined) {
setStore("stepsExpanded", props.stepsExpanded)
}
})
createEffect(() => {
const timer = setInterval(() => {
setStore("duration", duration())
@@ -259,7 +393,6 @@ export function SessionTurn(
if (newStatus === store.status || !newStatus) return
const timeSinceLastChange = Date.now() - lastStatusChange
if (timeSinceLastChange >= 2500) {
setStore("status", newStatus)
lastStatusChange = Date.now()
@@ -277,204 +410,205 @@ export function SessionTurn(
}
})
createEffect((prev) => {
const isWorking = working()
if (!prev && isWorking) {
setStore("stepsExpanded", true)
props.onStepsExpandedChange?.(true)
}
if (prev && !isWorking && !store.userScrolled) {
setStore("stepsExpanded", false)
props.onStepsExpandedChange?.(false)
}
return isWorking
}, working())
return (
<div data-component="session-turn" class={props.classes?.root}>
<div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
<div onClick={handleInteraction}>
<div
ref={(el) => setStore("contentRef", el)}
data-message={message().id}
data-slot="session-turn-message-container"
class={props.classes?.container}
style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }}
>
<Switch>
<Match when={isShellMode()}>
<Part part={assistantParts()[0]} message={message()} defaultOpen />
</Match>
<Match when={true}>
{/* Title (sticky) */}
<div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
<div data-slot="session-turn-message-header">
<div data-slot="session-turn-message-title">
<Switch>
<Match when={working()}>
<Typewriter as="h1" text={message().summary?.title} data-slot="session-turn-typewriter" />
</Match>
<Match when={true}>
<h1>{message().summary?.title ?? "New message"}</h1>
</Match>
</Switch>
</div>
</div>
</div>
{/* User Message */}
<div data-slot="session-turn-message-content">
<Message message={message()} parts={parts()} />
</div>
{/* Trigger (sticky) */}
<div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
<Button
data-expandable={assistantMessages().length > 0}
data-slot="session-turn-collapsible-trigger-content"
variant="ghost"
size="small"
onClick={() => {
if (assistantMessages().length === 0) return
const next = !store.stepsExpanded
setStore("stepsExpanded", next)
props.onStepsExpandedChange?.(next)
}}
>
<Show when={working()}>
<Spinner />
</Show>
<Switch>
<Match when={retry()}>
<span data-slot="session-turn-retry-message">
{(() => {
const r = retry()
if (!r) return ""
return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
})()}
</span>
<span data-slot="session-turn-retry-seconds">
· retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""}
</span>
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
</Match>
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
<Match when={store.stepsExpanded}>Hide steps</Match>
<Match when={!store.stepsExpanded}>Show steps</Match>
</Switch>
<span>·</span>
<span>{store.duration}</span>
<Show when={assistantMessages().length > 0}>
<Icon name="chevron-grabber-vertical" size="small" />
</Show>
</Button>
</div>
{/* Response */}
<Show when={store.stepsExpanded && assistantMessages().length > 0}>
<div data-slot="session-turn-collapsible-content-inner">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => data.store.part[assistantMessage.id] ?? [])
const last = createMemo(() =>
parts()
.filter((p) => p?.type === "text")
.at(-1),
)
return (
<Show when={message()}>
{(msg) => (
<div
ref={(el) => setStore("contentRef", el)}
data-message={msg().id}
data-slot="session-turn-message-container"
class={props.classes?.container}
style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }}
>
<Switch>
<Match when={isShellMode()}>
<Part part={assistantParts()[0]} message={msg()} defaultOpen />
</Match>
<Match when={true}>
{/* Title (sticky) */}
<div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
<div data-slot="session-turn-message-header">
<div data-slot="session-turn-message-title">
<Switch>
<Match when={response() && lastTextPart()?.id === last()?.id}>
<Message message={assistantMessage} parts={parts().filter((p) => p?.id !== last()?.id)} />
<Match when={working()}>
<Typewriter as="h1" text={msg().summary?.title} data-slot="session-turn-typewriter" />
</Match>
<Match when={true}>
<Message message={assistantMessage} parts={parts()} />
<h1>{msg().summary?.title}</h1>
</Match>
</Switch>
)
}}
</For>
<Show when={error()}>
</div>
</div>
</div>
{/* User Message */}
<div data-slot="session-turn-message-content">
<Message message={msg()} parts={parts()} />
</div>
{/* Trigger (sticky) */}
<Show when={working() || hasSteps()}>
<div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
<Button
data-expandable={assistantMessages().length > 0}
data-slot="session-turn-collapsible-trigger-content"
variant="ghost"
size="small"
onClick={props.onStepsExpandedToggle ?? (() => {})}
>
<Show when={working()}>
<Spinner />
</Show>
<Switch>
<Match when={retry()}>
<span data-slot="session-turn-retry-message">
{(() => {
const r = retry()
if (!r) return ""
return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
})()}
</span>
<span data-slot="session-turn-retry-seconds">
· retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""}
</span>
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
</Match>
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
<Match when={props.stepsExpanded}>Hide steps</Match>
<Match when={!props.stepsExpanded}>Show steps</Match>
</Switch>
<span>·</span>
<span>{store.duration}</span>
<Show when={assistantMessages().length > 0}>
<Icon name="chevron-grabber-vertical" size="small" />
</Show>
</Button>
</div>
</Show>
{/* Response */}
<Show when={props.stepsExpanded && assistantMessages().length > 0}>
<div data-slot="session-turn-collapsible-content-inner">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => data.store.part[assistantMessage.id] ?? [])
const last = createMemo(() =>
parts()
.filter((p) => p?.type === "text")
.at(-1),
)
return (
<Switch>
<Match when={response() && lastTextPart()?.id === last()?.id}>
<Message
message={assistantMessage}
parts={parts().filter((p) => p?.id !== last()?.id)}
/>
</Match>
<Match when={true}>
<Message message={assistantMessage} parts={parts()} />
</Match>
</Switch>
)
}}
</For>
<Show when={error()}>
<Card variant="error" class="error-card">
{error()?.data?.message as string}
</Card>
</Show>
</div>
</Show>
{/* Summary */}
<Show when={!working()}>
<div data-slot="session-turn-summary-section">
<div data-slot="session-turn-summary-header">
<Switch>
<Match when={summary()}>
{(summary) => (
<>
<h2 data-slot="session-turn-summary-title">Summary</h2>
<Markdown
data-slot="session-turn-markdown"
data-diffs={hasDiffs()}
text={summary()}
/>
</>
)}
</Match>
<Match when={response()}>
{(response) => (
<>
<h2 data-slot="session-turn-summary-title">Response</h2>
<Markdown
data-slot="session-turn-markdown"
data-diffs={hasDiffs()}
text={response()}
/>
</>
)}
</Match>
</Switch>
</div>
<Accordion data-slot="session-turn-accordion" multiple>
<For each={msg().summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<StickyAccordionHeader>
<Accordion.Trigger>
<div data-slot="session-turn-accordion-trigger-content">
<div data-slot="session-turn-file-info">
<FileIcon
node={{ path: diff.file, type: "file" }}
data-slot="session-turn-file-icon"
/>
<div data-slot="session-turn-file-path">
<Show when={diff.file.includes("/")}>
<span data-slot="session-turn-directory">
{getDirectory(diff.file)}&lrm;
</span>
</Show>
<span data-slot="session-turn-filename">{getFilename(diff.file)}</span>
</div>
</div>
<div data-slot="session-turn-accordion-actions">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content data-slot="session-turn-accordion-content">
<Dynamic
component={diffComponent}
before={{
name: diff.file!,
contents: diff.before!,
cacheKey: checksum(diff.before!),
}}
after={{
name: diff.file!,
contents: diff.after!,
cacheKey: checksum(diff.after!),
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</Show>
<Show when={error() && !props.stepsExpanded}>
<Card variant="error" class="error-card">
{error()?.data?.message as string}
</Card>
</Show>
</div>
</Show>
{/* Summary */}
<Show when={!working()}>
<div data-slot="session-turn-summary-section">
<div data-slot="session-turn-summary-header">
<Switch>
<Match when={summary()}>
{(summary) => (
<>
<h2 data-slot="session-turn-summary-title">Summary</h2>
<Markdown data-slot="session-turn-markdown" data-diffs={hasDiffs()} text={summary()} />
</>
)}
</Match>
<Match when={response()}>
{(response) => (
<>
<h2 data-slot="session-turn-summary-title">Response</h2>
<Markdown data-slot="session-turn-markdown" data-diffs={hasDiffs()} text={response()} />
</>
)}
</Match>
</Switch>
</div>
<Accordion data-slot="session-turn-accordion" multiple>
<For each={message().summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<StickyAccordionHeader>
<Accordion.Trigger>
<div data-slot="session-turn-accordion-trigger-content">
<div data-slot="session-turn-file-info">
<FileIcon
node={{ path: diff.file, type: "file" }}
data-slot="session-turn-file-icon"
/>
<div data-slot="session-turn-file-path">
<Show when={diff.file.includes("/")}>
<span data-slot="session-turn-directory">{getDirectory(diff.file)}&lrm;</span>
</Show>
<span data-slot="session-turn-filename">{getFilename(diff.file)}</span>
</div>
</div>
<div data-slot="session-turn-accordion-actions">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content data-slot="session-turn-accordion-content">
<Dynamic
component={diffComponent}
before={{
name: diff.file!,
contents: diff.before!,
cacheKey: checksum(diff.before!),
}}
after={{
name: diff.file!,
contents: diff.after!,
cacheKey: checksum(diff.after!),
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</Show>
<Show when={error() && !store.stepsExpanded}>
<Card variant="error" class="error-card">
{error()?.data?.message as string}
</Card>
</Show>
</Match>
</Switch>
</div>
</Match>
</Switch>
</div>
)}
</Show>
{props.children}
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.167",
"version": "1.0.173",
"private": true,
"type": "module",
"exports": {
+22 -73
View File
@@ -1,99 +1,48 @@
import z from "zod"
import { randomBytes } from "crypto"
export namespace Identifier {
const prefixes = {
session: "ses",
message: "msg",
permission: "per",
user: "usr",
part: "prt",
pty: "pty",
} as const
export type Prefix = keyof typeof prefixes
type CryptoLike = {
getRandomValues<T extends ArrayBufferView>(array: T): T
}
const TOTAL_LENGTH = 26
const RANDOM_LENGTH = TOTAL_LENGTH - 12
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const LENGTH = 26
// State for monotonic ID generation
let lastTimestamp = 0
let counter = 0
const fillRandomBytes = (buffer: Uint8Array) => {
const cryptoLike = (globalThis as { crypto?: CryptoLike }).crypto
if (cryptoLike?.getRandomValues) {
cryptoLike.getRandomValues(buffer)
return buffer
}
for (let i = 0; i < buffer.length; i++) {
buffer[i] = Math.floor(Math.random() * 256)
}
return buffer
export function ascending() {
return create(false)
}
const randomBase62 = (length: number) => {
const bytes = fillRandomBytes(new Uint8Array(length))
export function descending() {
return create(true)
}
function randomBase62(length: number): string {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let result = ""
const bytes = randomBytes(length)
for (let i = 0; i < length; i++) {
result += BASE62[bytes[i] % BASE62.length]
result += chars[bytes[i] % 62]
}
return result
}
const createSuffix = (descending: boolean, timestamp?: number) => {
export function create(descending: boolean, timestamp?: number): string {
const currentTimestamp = timestamp ?? Date.now()
if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp
counter = 0
}
counter += 1
counter++
let value = BigInt(currentTimestamp) * 0x1000n + BigInt(counter)
if (descending) value = ~value
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
const timeBytes = new Uint8Array(6)
now = descending ? ~now : now
const timeBytes = Buffer.alloc(6)
for (let i = 0; i < 6; i++) {
timeBytes[i] = Number((value >> BigInt(40 - 8 * i)) & 0xffn)
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
const hex = Array.from(timeBytes)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
return hex + randomBase62(RANDOM_LENGTH)
}
const generateID = (prefix: Prefix, descending: boolean, given?: string, timestamp?: number) => {
if (given) {
const expected = `${prefixes[prefix]}_`
if (!given.startsWith(expected)) throw new Error(`ID ${given} does not start with ${expected}`)
return given
}
return `${prefixes[prefix]}_${createSuffix(descending, timestamp)}`
}
export const schema = (prefix: Prefix) => z.string().startsWith(`${prefixes[prefix]}_`)
export function ascending(): string
export function ascending(prefix: Prefix, given?: string): string
export function ascending(prefix?: Prefix, given?: string) {
if (prefix) return generateID(prefix, false, given)
return create(false)
}
export function descending(): string
export function descending(prefix: Prefix, given?: string): string
export function descending(prefix?: Prefix, given?: string) {
if (prefix) return generateID(prefix, true, given)
return create(true)
}
export function create(descending: boolean, timestamp?: number) {
return createSuffix(descending, timestamp)
}
export function createPrefixed(prefix: Prefix, descending: boolean, timestamp?: number) {
return generateID(prefix, descending, undefined, timestamp)
return timeBytes.toString("hex") + randomBase62(LENGTH - 12)
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.167",
"version": "1.0.173",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+2 -2
View File
@@ -133,9 +133,9 @@ if (image) {
</div>
<div class="col4">
<h3>Mise</h3>
<button class="command" data-command="mise use -g ubi:sst/opencode">
<button class="command" data-command="mise use -g github:sst/opencode">
<code>
<span>mise use -g</span> <span class="highlight">ubi:sst/opencode</span>
<span>mise use -g</span> <span class="highlight">github:sst/opencode</span>
</code>
<span class="copy">
<CopyIcon />
@@ -30,6 +30,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
---
+1 -1
View File
@@ -109,7 +109,7 @@ You can also install it with the following commands:
- **Using Mise**
```bash
mise use -g ubi:sst/opencode
mise use -g github:sst/opencode
```
- **Using Docker**
@@ -112,3 +112,44 @@ You can disable a keybind by adding the key to your config with a value of "none
}
}
```
---
## Shift+Enter
Some terminals don't send modifier keys with Enter by default. You may need to configure your terminal to send `Shift+Enter` as an escape sequence.
### Windows Terminal
Open your `settings.json` at:
```
%LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json
```
Add this to the root-level `actions` array:
```json
"actions": [
{
"command": {
"action": "sendInput",
"input": "\u001b[13;2u"
},
"id": "User.sendInput.ShiftEnterCustom"
}
]
```
Add this to the root-level `keybindings` array:
```json
"keybindings": [
{
"keys": "shift+enter",
"id": "User.sendInput.ShiftEnterCustom"
}
]
```
Save the file and restart Windows Terminal or open a new tab.
+28 -27
View File
@@ -11,33 +11,34 @@ OpenCode integrates with your Language Server Protocol (LSP) to help the LLM int
OpenCode comes with several built-in LSP servers for popular languages:
| LSP Server | Extensions | Requirements |
| ------------------ | ---------------------------------------------------- | ------------------------------------------------------------ |
| astro | .astro | Auto-installs for Astro projects |
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| csharp | .cs | `.NET SDK` installed |
| dart | .dart | `dart` command available |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| elixir-ls | .ex, .exs | `elixir` command available |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` installed |
| gleam | .gleam | `gleam` command available |
| gopls | .go | `go` command available |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| ocaml-lsp | .ml, .mli | `ocamllsp` command available |
| php intelephense | .php | Auto-installs for PHP projects |
| pyright | .py, .pyi | `pyright` dependency installed |
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| rust | .rs | `rust-analyzer` command available |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
| svelte | .svelte | Auto-installs for Svelte projects |
| terraform | .tf, .tfvars | Auto-installs from GitHub releases |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| vue | .vue | Auto-installs for Vue projects |
| yaml-ls | .yaml, .yml | Auto-installs Red Hat yaml-language-server |
| zls | .zig, .zon | `zig` command available |
| LSP Server | Extensions | Requirements |
| ------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------ |
| astro | .astro | Auto-installs for Astro projects |
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| csharp | .cs | `.NET SDK` installed |
| dart | .dart | `dart` command available |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| elixir-ls | .ex, .exs | `elixir` command available |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` installed |
| gleam | .gleam | `gleam` command available |
| gopls | .go | `go` command available |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| ocaml-lsp | .ml, .mli | `ocamllsp` command available |
| oxlint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue, .astro, .svelte | `oxlint` dependency in project |
| php intelephense | .php | Auto-installs for PHP projects |
| pyright | .py, .pyi | `pyright` dependency installed |
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| rust | .rs | `rust-analyzer` command available |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
| svelte | .svelte | Auto-installs for Svelte projects |
| terraform | .tf, .tfvars | Auto-installs from GitHub releases |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| vue | .vue | Auto-installs for Vue projects |
| yaml-ls | .yaml, .yml | Auto-installs Red Hat yaml-language-server |
| zls | .zig, .zon | `zig` command available |
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
-9
View File
@@ -3,12 +3,3 @@
import { $ } from "bun"
await $`bun run prettier --ignore-unknown --write .`
if (process.env["CI"] && (await $`git status --porcelain`.text())) {
await $`git config --local user.email "action@github.com"`
await $`git config --local user.name "GitHub Action"`
await $`git add -A`
await $`git commit -m "chore: format code"`
const branch = process.env["PUSH_BRANCH"]
await $`git push origin HEAD:${branch} --no-verify`
}
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bun
import { $ } from "bun"
await $`bun ./packages/sdk/js/script/build.ts`
await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode")
await $`./script/format.ts`
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
if (!Script.preview) {
await $`gh release edit ${process.env.OPENCODE_RELEASE_TAG} --draft=false`
}
await $`bun install`
await $`gh release download --pattern "opencode-linux-*64.tar.gz" --pattern "opencode-darwin-*64.zip" -D dist`
await import(`../packages/opencode/script/publish-registries.ts`)
+60 -27
View File
@@ -27,6 +27,20 @@ if (!Script.preview) {
const opencode = await createOpencode()
const session = await opencode.client.session.create()
console.log("generating changelog since " + previous)
const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
const raw = await opencode.client.session
.prompt({
path: {
@@ -35,29 +49,61 @@ if (!Script.preview) {
body: {
model: {
providerID: "opencode",
modelID: "claude-haiku-4-5",
modelID: "gemini-3-flash",
},
parts: [
{
type: "text",
text: `
Analyze these commits and generate a changelog of all notable user facing changes.
Analyze these commits and generate a changelog of all notable user facing changes, grouped by area.
Commits between ${previous} and HEAD:
${commits}
Each commit below includes:
- [author: username] showing the GitHub username of the commit author
- [areas: ...] showing which areas of the codebase were modified
- Do NOT make general statements about "improvements", be very specific about what was changed.
- Do NOT include any information about code changes if they do not affect the user facing changes.
- For commits that are already well-written and descriptive, avoid rewording them. Simply capitalize the first letter, fix any misspellings, and ensure proper English grammar.
- DO NOT read any other commits than the ones listed above (THIS IS IMPORTANT TO AVOID DUPLICATING THINGS IN OUR CHANGELOG)
- If a commit was made and then reverted do not include it in the changelog. If the commits only include a revert but not the original commit, then include the revert in the changelog.
Commits between ${previous} and HEAD:
${commits}
IMPORTANT: ONLY return a bulleted list of changes, do not include any other information. Do not include a preamble like "Based on my analysis..."
Group the changes into these categories based on the [areas: ...] tags (omit any category with no changes):
- **TUI**: Changes to "opencode" area (the terminal/CLI interface)
- **Desktop**: Changes to "desktop" or "tauri" areas (the desktop application)
- **SDK**: Changes to "sdk" or "plugin" areas (the SDK and plugin system)
- **Extensions**: Changes to "extensions/zed", "extensions/vscode", or "github" areas (editor extensions and GitHub Action)
- **Other**: Any user-facing changes that don't fit the above categories
<example>
- Added ability to @ mention agents
- Fixed a bug where the TUI would render improperly on some terminals
</example>
Excluded areas (omit these entirely unless they contain user-facing changes like refactors that may affect behavior):
- "nix", "infra", "script" - CI/build infrastructure
- "ui", "docs", "web", "console", "enterprise", "function", "util", "identity", "slack" - internal packages
Rules:
- Use the [areas: ...] tags to determine the correct category. If a commit touches multiple areas, put it in the most relevant user-facing category.
- ONLY include commits that have user-facing impact. Omit purely internal changes (CI, build scripts, internal tooling).
- However, DO include refactors that touch user-facing code - refactors can introduce bugs or change behavior.
- Do NOT make general statements about "improvements", be very specific about what was changed.
- For commits that are already well-written and descriptive, avoid rewording them. Simply capitalize the first letter, fix any misspellings, and ensure proper English grammar.
- DO NOT read any other commits than the ones listed above (THIS IS IMPORTANT TO AVOID DUPLICATING THINGS IN OUR CHANGELOG).
- If a commit was made and then reverted do not include it in the changelog. If the commits only include a revert but not the original commit, then include the revert in the changelog.
- Omit categories that have no changes.
- For community contributors: if the [author: username] is NOT in the team list, add (@username) at the end of the changelog entry. This is REQUIRED for all non-team contributors.
- The team members are: ${team.join(", ")}. Do NOT add @ mentions for team members.
IMPORTANT: ONLY return the grouped changelog, do not include any other information. Do not include a preamble like "Based on my analysis..." or "Here is the changelog..."
<example>
## TUI
- Added experimental support for the Ty language server (@OpeOginni)
- Added /fork slash command for keyboard-friendly session forking (@ariane-emory)
- Increased retry attempts for failed requests
- Fixed model validation before executing slash commands (@devxoul)
## Desktop
- Added shell mode support
- Fixed prompt history navigation and optimistic prompt duplication
- Disabled pinch-to-zoom on Linux (@Brendonovich)
## Extensions
- Added OIDC_BASE_URL support for custom GitHub App installations (@elithrar)
</example>
`,
},
],
@@ -74,19 +120,6 @@ if (!Script.preview) {
console.log("-----------------------------")
opencode.server.close()
// Get contributors
const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
const compare =
await $`gh api "/repos/sst/opencode/compare/v${previous}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
const contributors = new Map<string, string[]>()
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.167",
"version": "1.0.173",
"publisher": "sst-dev",
"repository": {
"type": "git",