Compare commits

...

58 Commits

Author SHA1 Message Date
Github Action
0d6a1b76e4 Update Nix flake.lock and hashes 2025-12-19 11:49:43 +00:00
Adam
826b8538e7 Merge branch 'dev' into brendan/update-solid-start 2025-12-19 05:46:22 -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 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 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
Matt Silverlock
8b298a233e github: add OIDC_BASE_URL for custom GitHub App installs (#5756) 2025-12-18 11:31:13 -06:00
Adam
6f43d03043 fix(desktop): checkbox render in safari fml 2025-12-18 11:16:33 -06:00
Adam
c868a4088d fix(desktop): rendering shell mode messages 2025-12-18 11:16:33 -06:00
Adam
83d8a88c90 fix(desktop): error styles 2025-12-18 11:16:33 -06:00
Adam
268f37f8c9 fix(desktop): prompt history nav, optimistic prompt dup 2025-12-18 11:16:33 -06:00
Adam
b0aaf04957 fix(desktop): session ordered by most recent 2025-12-18 11:16:32 -06:00
Adam
b7875256f3 feat(desktop): shell mode 2025-12-18 11:16:32 -06:00
Adam
7bc47fb904 chore: cleanup 2025-12-18 11:16:32 -06:00
GitHub Action
5cf8e54372 chore: format code 2025-12-18 16:39:21 +00:00
Ariane Emory
7437ccd6f4 feat(tui): fork slash command for keyboard-friendly session forking (resolves #5599) (#5610)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-18 10:38:19 -06:00
Jeon Suyeol
4bf882ba81 fix(command): validate model before executing slash command (#5740) 2025-12-18 10:35:40 -06:00
Frank
d5dcc55a47 Revert "add client header"
This reverts commit 2fb89161c8.
2025-12-18 11:21:22 -05:00
Github Action
35fab5f66d Update Nix flake.lock and hashes 2025-12-11 05:10:01 +00:00
Brendan Allan
d7a32846cf use version of start with fixed BASE_URL handling 2025-12-11 13:08:42 +08:00
Github Action
2bfacda9ba Update Nix flake.lock and hashes 2025-12-11 02:16:33 +00:00
Brendan Allan
3243612dbd bring back HttpHeader 2025-12-11 10:15:37 +08:00
Brendan Allan
d6af36a084 update solid start to 375155d 2025-12-11 10:15:03 +08:00
69 changed files with 1660 additions and 922 deletions

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

View File

@@ -55,7 +55,7 @@ jobs:
- 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

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)

129
bun.lock
View File

@@ -21,7 +21,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -49,7 +49,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -76,7 +76,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -100,7 +100,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -124,7 +124,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -154,6 +154,7 @@
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:",
"zod": "catalog:",
},
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
@@ -171,7 +172,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -200,7 +201,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -216,7 +217,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.167",
"version": "1.0.169",
"bin": {
"opencode": "./bin/opencode",
},
@@ -308,7 +309,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -328,7 +329,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.167",
"version": "1.0.169",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -339,7 +340,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -352,7 +353,7 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@solid-primitives/storage": "catalog:",
@@ -378,7 +379,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.167",
"version": "1.0.169",
"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.169",
"dependencies": {
"zod": "catalog:",
},
@@ -424,7 +425,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -478,7 +479,7 @@
"@solid-primitives/storage": "4.3.3",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@57aeb22",
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
@@ -1596,7 +1597,7 @@
"@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
"@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }],
"@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@57aeb22", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite-plugin-solid": "^2.11.9" }, "peerDependencies": { "vite": "^7" } }],
"@speed-highlight/core": ["@speed-highlight/core@1.2.12", "", {}, "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA=="],
@@ -1710,14 +1711,10 @@
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -1820,20 +1817,6 @@
"@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/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/pretty-format": ["@vitest/pretty-format@4.0.13", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-ooqfze8URWbI2ozOeLDMh8YZxWDpGXoeY3VOgcDnsUxN0jPyPWSUvjPQWqDGCBks+opWlN1E4oP1UYl3C/2EQA=="],
"@vitest/runner": ["@vitest/runner@4.0.13", "", { "dependencies": { "@vitest/utils": "4.0.13", "pathe": "^2.0.3" } }, "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A=="],
"@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/spy": ["@vitest/spy@4.0.13", "", {}, "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw=="],
"@vitest/utils": ["@vitest/utils@4.0.13", "", { "dependencies": { "@vitest/pretty-format": "4.0.13", "tinyrainbow": "^3.0.3" } }, "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA=="],
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
@@ -1900,8 +1883,6 @@
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"astro": ["astro@5.7.13", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.1", "@astrojs/telemetry": "3.2.1", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-cRGq2llKOhV3XMcYwQpfBIUcssN6HEK5CRbcMxAfd9OcFhvWE7KUy50zLioAZVVl3AqgUTJoNTlmZfD2eG0G1w=="],
@@ -2048,8 +2029,6 @@
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="],
"chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -2354,8 +2333,6 @@
"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=="],
"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=="],
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
@@ -3458,8 +3435,6 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
@@ -3522,8 +3497,6 @@
"sst-win32-x86": ["sst-win32-x86@3.17.23", "", { "os": "win32", "cpu": "none" }, "sha512-DIp3s54IpNAfdYjSRt6McvkbEPQDMxUu6RUeRAd2C+FcTJgTloon/ghAPQBaDgu2VoVgymjcJARO/XyfKcCLOQ=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
"stage-js": ["stage-js@1.0.0-alpha.17", "", {}, "sha512-AzlMO+t51v6cFvKZ+Oe9DJnL1OXEH5s9bEy6di5aOrUpcP7PCzI/wIeXF0u3zg0L89gwnceoKxrLId0ZpYnNXw=="],
@@ -3532,8 +3505,6 @@
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
@@ -3612,16 +3583,12 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
"titleize": ["titleize@4.0.0", "", {}, "sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@@ -3784,8 +3751,6 @@
"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=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
@@ -4154,8 +4119,6 @@
"@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
"@solidjs/start/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=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
@@ -4384,10 +4347,6 @@
"vite-plugin-icons-spritesheet/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"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=="],
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -4916,8 +4875,6 @@
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"vitest/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -5086,56 +5043,6 @@
"tw-to-css/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],

6
flake.lock generated
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": {

View File

@@ -26,6 +26,10 @@ inputs:
description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'"
required: false
oidc_base_url:
description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai"
required: false
runs:
using: "composite"
steps:
@@ -62,3 +66,4 @@ runs:
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
MENTIONS: ${{ inputs.mentions }}
OIDC_BASE_URL: ${{ inputs.oidc_base_url }}

View File

@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-g6XHWk9IoDoeXbvENs+U2fqk185xKMLb0BRopCbXaIk="
"nodeModules": "sha256-oT1WPPR1sHBhQcJaFL+mod5l3+V8O3uPKJUdrcfTst0="
}

View File

@@ -50,7 +50,7 @@
"vite": "7.1.4",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@57aeb22",
"solid-js": "1.9.10",
"vite-plugin-solid": "2.11.10"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.167",
"version": "1.0.169",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

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"
@@ -42,7 +42,7 @@ export default function Home() {
return (
<main data-page="opencode">
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
<Title>OpenCode | The open source AI coding agent</Title>
<Link rel="canonical" href={config.baseUrl} />
<Meta property="og:image" content="/social-share.png" />

View File

@@ -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"
@@ -30,7 +30,7 @@ export default function Home() {
const loggedin = createAsync(() => checkLoggedIn())
return (
<main data-page="zen">
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
<Meta property="og:image" content="/social-share-zen.png" />

View File

@@ -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,

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.169",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.167",
"version": "1.0.169",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.167",
"version": "1.0.169",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.167",
"version": "1.0.169",
"description": "",
"type": "module",
"exports": {
@@ -56,6 +56,7 @@
"solid-js": "catalog:",
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:"
"virtua": "catalog:",
"zod": "catalog:"
}
}

View File

@@ -39,9 +39,9 @@ const url =
export function App() {
return (
<ErrorBoundary fallback={ErrorPage}>
<MetaProvider>
<Font />
<MetaProvider>
<Font />
<ErrorBoundary fallback={ErrorPage}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
@@ -82,7 +82,7 @@ export function App() {
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</MetaProvider>
</ErrorBoundary>
</ErrorBoundary>
</MetaProvider>
)
}

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>

View File

@@ -21,6 +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 "@/utils/id"
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
@@ -99,6 +100,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
placeholder: number
dragging: boolean
imageAttachments: ImageAttachmentPart[]
mode: "normal" | "shell"
applyingHistory: boolean
}>({
popover: null,
historyIndex: -1,
@@ -106,6 +109,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
dragging: false,
imageAttachments: [],
mode: "normal",
applyingHistory: false,
})
const MAX_HISTORY = 100
@@ -133,10 +138,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
const length = position === "start" ? 0 : promptLength(p)
setStore("applyingHistory", true)
prompt.set(p, length)
requestAnimationFrame(() => {
editorRef.focus()
setCursorPosition(editorRef, length)
setStore("applyingHistory", false)
})
}
@@ -427,21 +434,42 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const rawParts = parseFromDOM()
const cursorPosition = getCursorPosition(editorRef)
const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("")
const trimmed = rawText.replace(/\u200B/g, "").trim()
const hasNonText = rawParts.some((part) => part.type !== "text")
const shouldReset = trimmed.length === 0 && !hasNonText
const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/)
const slashMatch = rawText.match(/^\/(\S*)$/)
if (shouldReset) {
setStore("popover", null)
if (store.historyIndex >= 0 && !store.applyingHistory) {
setStore("historyIndex", -1)
setStore("savedPrompt", null)
}
if (prompt.dirty()) {
prompt.set(DEFAULT_PROMPT, 0)
}
return
}
if (atMatch) {
onInput(atMatch[1])
setStore("popover", "file")
} else if (slashMatch) {
slashOnInput(slashMatch[1])
setStore("popover", "slash")
const shellMode = store.mode === "shell"
if (!shellMode) {
const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/)
const slashMatch = rawText.match(/^\/(\S*)$/)
if (atMatch) {
onInput(atMatch[1])
setStore("popover", "file")
} else if (slashMatch) {
slashOnInput(slashMatch[1])
setStore("popover", "slash")
} else {
setStore("popover", null)
}
} else {
setStore("popover", null)
}
if (store.historyIndex >= 0) {
if (store.historyIndex >= 0 && !store.applyingHistory) {
setStore("historyIndex", -1)
setStore("savedPrompt", null)
}
@@ -579,6 +607,29 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "!" && store.mode === "normal") {
const cursorPosition = getCursorPosition(editorRef)
if (cursorPosition === 0) {
setStore("mode", "shell")
setStore("popover", null)
event.preventDefault()
return
}
}
if (store.mode === "shell") {
const { collapsed, cursorPosition, textLength } = getCaretState()
if (event.key === "Escape") {
setStore("mode", "normal")
event.preventDefault()
return
}
if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) {
setStore("mode", "normal")
event.preventDefault()
return
}
}
if (store.popover && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
if (store.popover === "file") {
onKeyDown(event)
@@ -665,6 +716,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}`
: ""
return {
id: Identifier.ascending("part"),
type: "file" as const,
mime: "text/plain",
url: `file://${absolute}${query}`,
@@ -682,16 +734,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const imageAttachmentParts = store.imageAttachments.map((attachment) => ({
id: Identifier.ascending("part"),
type: "file" as const,
mime: attachment.mime,
url: attachment.dataUrl,
filename: attachment.filename,
}))
const isShellMode = store.mode === "shell"
tabs().setActive(undefined)
editorRef.innerHTML = ""
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
setStore("imageAttachments", [])
setStore("mode", "normal")
const model = {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
}
const agent = local.agent.current()!.name
if (isShellMode) {
sdk.client.session.shell({
sessionID: existing.id,
agent,
model,
command: text,
})
return
}
if (text.startsWith("/")) {
const [cmdName, ...args] = text.split(" ")
@@ -702,27 +773,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
sessionID: existing.id,
command: commandName,
arguments: args.join(" "),
agent: local.agent.current()!.name,
model: `${local.model.current()!.provider.id}/${local.model.current()!.id}`,
agent,
model: `${model.providerID}/${model.modelID}`,
})
return
}
}
const model = {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
const messageID = Identifier.ascending("message")
const textPart = {
id: Identifier.ascending("part"),
type: "text" as const,
text,
}
const agent = local.agent.current()!.name
const requestParts = [textPart, ...fileAttachmentParts, ...imageAttachmentParts]
const optimisticParts = requestParts.map((part) => ({
...part,
sessionID: existing.id,
messageID,
}))
sync.session.addOptimisticMessage({
sessionID: existing.id,
text,
parts: [
{ type: "text", text } as import("@opencode-ai/sdk/v2/client").Part,
...(fileAttachmentParts as import("@opencode-ai/sdk/v2/client").Part[]),
...(imageAttachmentParts as import("@opencode-ai/sdk/v2/client").Part[]),
],
messageID,
parts: optimisticParts,
agent,
model,
})
@@ -731,14 +805,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
sessionID: existing.id,
agent,
model,
parts: [
{
type: "text",
text,
},
...fileAttachmentParts,
...imageAttachmentParts,
],
messageID,
parts: requestParts,
})
}
@@ -879,34 +947,50 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
classList={{
"w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
"[&>[data-type=file]]:text-icon-info-active": true,
"font-mono!": store.mode === "shell",
}}
/>
<Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
Ask anything... "{PLACEHOLDERS[store.placeholder]}"
{store.mode === "shell"
? "Enter shell command..."
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
</div>
</Show>
</div>
<div class="relative p-3 flex items-center justify-between">
<div class="flex items-center justify-start gap-1">
<Select
options={local.agent.list().map((agent) => agent.name)}
current={local.agent.current().name}
onSelect={local.agent.set}
class="capitalize"
variant="ghost"
/>
<Button
as="div"
variant="ghost"
onClick={() =>
dialog.show(() => (providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />))
}
>
{local.model.current()?.name ?? "Select model"}
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
<Icon name="chevron-down" size="small" />
</Button>
<Switch>
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
<Icon name="console" size="small" class="text-icon-primary" />
<span class="text-12-regular text-text-primary">Shell</span>
<span class="text-12-regular text-text-weak">esc to exit</span>
</div>
</Match>
<Match when={store.mode === "normal"}>
<Select
options={local.agent.list().map((agent) => agent.name)}
current={local.agent.current().name}
onSelect={local.agent.set}
class="capitalize"
variant="ghost"
/>
<Button
as="div"
variant="ghost"
onClick={() =>
dialog.show(() =>
providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />,
)
}
>
{local.model.current()?.name ?? "Select model"}
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
<Icon name="chevron-down" size="small" />
</Button>
</Match>
</Switch>
</div>
<div class="flex items-center gap-1 absolute right-2 bottom-2">
<input
@@ -920,15 +1004,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()}

View File

@@ -148,6 +148,7 @@ export const Terminal = (props: TerminalProps) => {
<div
ref={container}
data-component="terminal"
data-prevent-autofocus
classList={{
...(local.classList ?? {}),
"size-full px-6 py-3 font-mono": true,

View File

@@ -72,6 +72,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: "",
@@ -107,7 +108,7 @@ function createGlobalSync() {
.slice()
.filter((s) => !s.time.archived)
.sort((a, b) => a.id.localeCompare(b.id))
// Include sessions up to the limit, plus any updated in the last hour
// Include up to the limit, plus any updated in the last 4 hours
const sessions = nonArchived.filter((s, i) => {
if (i < store.limit) return true
const updated = new Date(s.time.updated).getTime()
@@ -122,6 +123,7 @@ function createGlobalSync() {
}
async function bootstrapInstance(directory: string) {
if (!directory) return
const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,

View File

@@ -33,14 +33,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
addOptimisticMessage(input: {
sessionID: string
text: string
messageID: string
parts: Part[]
agent: string
model: { providerID: string; modelID: string }
}) {
const messageID = crypto.randomUUID()
const message: Message = {
id: messageID,
id: input.messageID,
sessionID: input.sessionID,
role: "user",
time: { created: Date.now() },
@@ -53,15 +52,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (!messages) {
draft.message[input.sessionID] = [message]
} else {
const result = Binary.search(messages, messageID, (m) => m.id)
const result = Binary.search(messages, input.messageID, (m) => m.id)
messages.splice(result.index, 0, message)
}
draft.part[messageID] = input.parts.map((part, i) => ({
...part,
id: `${messageID}-${i}`,
sessionID: input.sessionID,
messageID,
}))
draft.part[input.messageID] = input.parts.slice()
}),
)
},

View File

@@ -1,11 +1,6 @@
@import "@opencode-ai/ui/styles/tailwind";
:root {
html,
body {
touch-action: manipulation;
}
a {
cursor: default;
}

View File

@@ -60,9 +60,9 @@ interface ErrorPageProps {
export const ErrorPage: Component<ErrorPageProps> = (props) => {
const platform = usePlatform()
return (
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center">
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
<Logo class="h-8 w-auto text-text-strong" />
<Logo class="w-58.5 opacity-12 shrink-0" />
<div class="flex flex-col items-center gap-2 text-center">
<h1 class="text-lg font-medium text-text-strong">Something went wrong</h1>
<p class="text-sm text-text-weak">An error occurred while loading the application.</p>

View File

@@ -122,10 +122,18 @@ export default function Layout(props: ParentProps) {
}
}
function projectSessions(directory: string) {
if (!directory) return []
const sessions = globalSync
.child(directory)[0]
.session.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
return flattenSessions(sessions ?? [])
}
const currentSessions = createMemo(() => {
if (!params.dir) return []
const directory = base64Decode(params.dir)
return flattenSessions(globalSync.child(directory)[0].session ?? [])
return projectSessions(directory)
})
function navigateSessionByOffset(offset: number) {
@@ -162,7 +170,7 @@ export default function Layout(props: ParentProps) {
const nextProject = projects[nextProjectIndex]
if (!nextProject) return
const nextProjectSessions = flattenSessions(globalSync.child(nextProject.worktree)[0].session ?? [])
const nextProjectSessions = projectSessions(nextProject.worktree)
if (nextProjectSessions.length === 0) {
navigateToProject(nextProject.worktree)
return
@@ -350,7 +358,7 @@ export default function Layout(props: ParentProps) {
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
return (
<div class="relative size-5 shrink-0 rounded-sm overflow-hidden">
<div class="relative size-5 shrink-0 rounded-sm">
<Avatar
fallback={name()}
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
@@ -511,7 +519,9 @@ export default function Layout(props: ParentProps) {
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const [store, setProjectStore] = globalSync.child(props.project.worktree)
const sessions = createMemo(() => store.session ?? [])
const sessions = createMemo(() =>
store.session.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)),
)
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
const childSessionsByParent = createMemo(() => {
const map = new Map<string, Session[]>()
@@ -526,7 +536,7 @@ export default function Layout(props: ParentProps) {
})
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
const loadMoreSessions = async () => {
setProjectStore("limit", (limit) => limit + 10)
setProjectStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.project.worktree)
}
const [expanded, setExpanded] = createSignal(true)

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(() => [
{
@@ -327,11 +362,15 @@ export default function Page() {
])
const handleKeyDown = (event: KeyboardEvent) => {
if ((document.activeElement as HTMLElement)?.dataset?.component === "terminal") return
const activeElement = document.activeElement as HTMLElement | undefined
if (activeElement) {
const isProtected = activeElement.closest("[data-prevent-autofocus]")
const isInput = /^(INPUT|TEXTAREA|SELECT)$/.test(activeElement.tagName) || activeElement.isContentEditable
if (isProtected || isInput) return
}
if (dialog.active) return
const focused = document.activeElement === inputRef
if (focused) {
if (activeElement === inputRef) {
if (event.key === "Escape") inputRef?.blur()
return
}
@@ -598,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>
@@ -615,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",

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
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.167",
"version": "1.0.169",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.167"
version = "1.0.169"
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.169/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.169/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.169/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.169/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.169/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.167",
"version": "1.0.169",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.167",
"version": "1.0.169",
"name": "opencode",
"type": "module",
"private": true,

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
}
}
}

View File

@@ -395,6 +395,7 @@ export const GithubRunCommand = cmd({
const { providerID, modelID } = normalizeModel()
const runId = normalizeRunId()
const share = normalizeShare()
const oidcBaseUrl = normalizeOidcBaseUrl()
const { owner, repo } = context.repo
const payload = context.payload as IssueCommentEvent | PullRequestReviewCommentEvent
const issueEvent = isIssueCommentEvent(payload) ? payload : undefined
@@ -417,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) {
@@ -442,7 +444,7 @@ export const GithubRunCommand = cmd({
}
await assertPermissions()
await addReaction()
await addReaction(commentType)
// Setup opencode session
const repoData = await fetchRepo()
@@ -475,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 {
@@ -490,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
@@ -511,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) {
@@ -527,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);
@@ -572,6 +574,12 @@ export const GithubRunCommand = cmd({
throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
}
function normalizeOidcBaseUrl(): string {
const value = process.env["OIDC_BASE_URL"]
if (!value) return "https://api.opencode.ai"
return value.replace(/\/+$/, "")
}
function isIssueCommentEvent(
event: IssueCommentEvent | PullRequestReviewCommentEvent,
): event is IssueCommentEvent {
@@ -809,14 +817,14 @@ export const GithubRunCommand = cmd({
async function exchangeForAppToken(token: string) {
const response = token.startsWith("github_pat_")
? await fetch("https://api.opencode.ai/exchange_github_app_token_with_pat", {
? await fetch(`${oidcBaseUrl}/exchange_github_app_token_with_pat`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ owner, repo }),
})
: await fetch("https://api.opencode.ai/exchange_github_app_token", {
: await fetch(`${oidcBaseUrl}/exchange_github_app_token`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
@@ -970,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,
@@ -980,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,

View File

@@ -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"
@@ -270,6 +270,11 @@ export function Autocomplete(props: {
description: "jump to message",
onSelect: () => command.trigger("session.timeline"),
},
{
display: "/fork",
description: "fork from message",
onSelect: () => command.trigger("session.fork"),
},
{
display: "/thinking",
description: "toggle thinking visibility",
@@ -364,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,
@@ -390,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() {
@@ -492,6 +509,8 @@ export function Autocomplete(props: {
return 1
})
let scroll: ScrollBoxRenderable
return (
<box
visible={store.visible !== false}
@@ -503,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={
@@ -530,7 +554,7 @@ export function Autocomplete(props: {
</box>
)}
</For>
</box>
</scrollbox>
</box>
)
}

View File

@@ -0,0 +1,51 @@
import { createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util/locale"
import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import { useDialog } from "../../ui/dialog"
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
const sync = useSync()
const dialog = useDialog()
const sdk = useSDK()
const route = useRoute()
onMount(() => {
dialog.setSize("large")
})
const options = createMemo((): DialogSelectOption<string>[] => {
const messages = sync.data.message[props.sessionID] ?? []
const result = [] as DialogSelectOption<string>[]
for (const message of messages) {
if (message.role !== "user") continue
const part = (sync.data.part[message.id] ?? []).find(
(x) => x.type === "text" && !x.synthetic && !x.ignored,
) as TextPart
if (!part) continue
result.push({
title: part.text.replace(/\n/g, " "),
value: message.id,
footer: Locale.time(message.time.created),
onSelect: async (dialog) => {
const forked = await sdk.client.session.fork({
sessionID: props.sessionID,
messageID: message.id,
})
route.navigate({
sessionID: forked.data!.id,
type: "session",
})
dialog.clear()
},
})
}
result.reverse()
return result
})
return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork from message" options={options()} />
}

View File

@@ -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()
},
},
]}
/>
)
}

View File

@@ -24,7 +24,9 @@ export function DialogTimeline(props: {
const result = [] as DialogSelectOption<string>[]
for (const message of messages) {
if (message.role !== "user") continue
const part = (sync.data.part[message.id] ?? []).find((x) => x.type === "text" && !x.synthetic) as TextPart
const part = (sync.data.part[message.id] ?? []).find(
(x) => x.type === "text" && !x.synthetic && !x.ignored,
) as TextPart
if (!part) continue
result.push({
title: part.text.replace(/\n/g, " "),

View File

@@ -53,6 +53,7 @@ import { iife } from "@/util/iife"
import { DialogConfirm } from "@tui/ui/dialog-confirm"
import { DialogPrompt } from "@tui/ui/dialog-prompt"
import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
@@ -65,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)
@@ -295,6 +297,25 @@ export function Session() {
))
},
},
{
title: "Fork from message",
value: "session.fork",
keybind: "session_fork",
category: "Session",
onSelect: (dialog) => {
dialog.replace(() => (
<DialogForkFromTimeline
onMove={(messageID) => {
const child = scroll.getChildren().find((child) => {
return child.id === messageID
})
if (child) scroll.scrollBy(child.y - scroll.y - 1)
}}
sessionID={route.sessionID}
/>
))
},
},
{
title: "Compact session",
value: "session.compact",
@@ -1508,13 +1529,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>
@@ -1537,7 +1578,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>
)
},
})

View File

@@ -440,6 +440,8 @@ export namespace Config {
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
session_fork: z.string().optional().default("none").describe("Fork session from message"),
session_rename: z.string().optional().default("none").describe("Rename session"),
session_share: z.string().optional().default("none").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),

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()

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

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"],

View File

@@ -92,7 +92,6 @@ export namespace ModelsDev {
const result = await fetch("https://models.dev/api.json", {
headers: {
"User-Agent": Installation.USER_AGENT,
"x-opencode-client": Flag.OPENCODE_CLIENT,
},
signal: AbortSignal.timeout(10 * 1000),
}).catch((e) => {

View File

@@ -74,17 +74,12 @@ 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.field === "reasoning_content")
) {
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {

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(
{

View File

@@ -1333,6 +1333,20 @@ export namespace SessionPrompt {
if (input.model) return Provider.parseModel(input.model)
return await lastModel(input.sessionID)
})()
try {
await Provider.getModel(model.providerID, model.modelID)
} catch (e) {
if (Provider.ModelNotFoundError.isInstance(e)) {
const { providerID, modelID, suggestions } = e.data
const hint = suggestions?.length ? ` Did you mean: ${suggestions.join(", ")}?` : ""
Bus.publish(Session.Event.Error, {
sessionID: input.sessionID,
error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(),
})
}
throw e
}
const agent = await Agent.get(agentName)
const parts =

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"
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.167",
"version": "1.0.169",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.167",
"version": "1.0.169",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View File

@@ -842,6 +842,14 @@ export type KeybindsConfig = {
* Show session timeline
*/
session_timeline?: string
/**
* Fork session from message
*/
session_fork?: string
/**
* Rename session
*/
session_rename?: string
/**
* Share current session
*/

View File

@@ -7077,6 +7077,16 @@
"default": "<leader>g",
"type": "string"
},
"session_fork": {
"description": "Fork session from message",
"default": "none",
"type": "string"
},
"session_rename": {
"description": "Rename session",
"default": "none",
"type": "string"
},
"session_share": {
"description": "Share current session",
"default": "none",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.167",
"version": "1.0.169",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/tauri",
"private": true,
"version": "1.0.167",
"version": "1.0.169",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",

View File

@@ -89,6 +89,11 @@ const platform: Platform = {
createMenu()
// Stops mousewheel events from reaching Tauri's pinch-to-zoom handler
root?.addEventListener("mousewheel", (e) => {
e.stopPropagation()
})
render(() => {
return (
<PlatformProvider value={platform}>

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.167",
"version": "1.0.169",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",

View File

@@ -17,7 +17,7 @@ export function Checkbox(props: CheckboxProps) {
<Kobalte.Control data-slot="checkbox-checkbox-control">
<Kobalte.Indicator data-slot="checkbox-checkbox-indicator">
{local.icon || (
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 12 12" fill="none" width="10" height="10" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 7.17905L5.02703 8.85135L9 3.5"
stroke="currentColor"

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;
}
}

View File

@@ -152,9 +152,22 @@
align-items: flex-start;
justify-content: flex-start;
[data-component="markdown"] {
width: 100%;
min-width: 0;
pre {
margin: 0;
padding: 0;
background-color: transparent !important;
border: none !important;
}
}
pre {
margin: 0;
padding: 0;
background: none;
}
&[data-scrollable] {

View File

@@ -69,6 +69,7 @@ export interface MessagePartProps {
part: PartType
message: MessageType
hideDetails?: boolean
defaultOpen?: boolean
}
export type PartComponent = Component<MessagePartProps>
@@ -208,7 +209,13 @@ export function Part(props: MessagePartProps) {
const component = createMemo(() => PART_MAPPING[props.part.type])
return (
<Show when={component()}>
<Dynamic component={component()} part={props.part} message={props.message} hideDetails={props.hideDetails} />
<Dynamic
component={component()}
part={props.part}
message={props.message}
hideDetails={props.hideDetails}
defaultOpen={props.defaultOpen}
/>
</Show>
)
}
@@ -219,6 +226,7 @@ export interface ToolProps {
tool: string
output?: string
hideDetails?: boolean
defaultOpen?: boolean
}
export type ToolComponent = Component<ToolProps>
@@ -286,6 +294,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
metadata={metadata}
output={part.state.status === "completed" ? part.state.output : undefined}
hideDetails={props.hideDetails}
defaultOpen={props.defaultOpen}
/>
</Match>
</Switch>
@@ -326,6 +335,7 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool
{...props}
icon="glasses"
trigger={{
title: "Read",
@@ -340,7 +350,11 @@ ToolRegistry.register({
name: "list",
render(props) {
return (
<BasicTool icon="bullet-list" trigger={{ title: "List", subtitle: getDirectory(props.input.path || "/") }}>
<BasicTool
{...props}
icon="bullet-list"
trigger={{ title: "List", subtitle: getDirectory(props.input.path || "/") }}
>
<Show when={props.output}>
{(output) => (
<div data-component="tool-output" data-scrollable>
@@ -358,6 +372,7 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool
{...props}
icon="magnifying-glass-menu"
trigger={{
title: "Glob",
@@ -385,6 +400,7 @@ ToolRegistry.register({
if (props.input.include) args.push("include=" + props.input.include)
return (
<BasicTool
{...props}
icon="magnifying-glass-menu"
trigger={{
title: "Grep",
@@ -409,6 +425,7 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool
{...props}
icon="window-cursor"
trigger={{
title: "Webfetch",
@@ -438,6 +455,7 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool
{...props}
icon="task"
trigger={{
title: `${props.input.subagent_type || props.tool} Agent`,
@@ -462,6 +480,7 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool
{...props}
icon="console"
trigger={{
title: "Shell",
@@ -485,6 +504,7 @@ ToolRegistry.register({
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
return (
<BasicTool
{...props}
defaultOpen
icon="code-lines"
trigger={
@@ -534,6 +554,7 @@ ToolRegistry.register({
const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath))
return (
<BasicTool
{...props}
defaultOpen
icon="code-lines"
trigger={
@@ -575,6 +596,7 @@ ToolRegistry.register({
render(props) {
return (
<BasicTool
{...props}
defaultOpen
icon="checklist"
trigger={{

View File

@@ -3,11 +3,11 @@ 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"
import { Message } from "./message-part"
import { Message, Part } from "./message-part"
import { Markdown } from "./markdown"
import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header"
@@ -25,7 +25,8 @@ export function SessionTurn(
sessionID: string
messageID: string
stepsExpanded?: boolean
onStepsExpandedChange?: (expanded: boolean) => void
onStepsExpandedToggle?: () => void
onUserInteracted?: () => void
classes?: {
root?: string
content?: string
@@ -35,310 +36,295 @@ export function SessionTurn(
) {
const data = useData()
const diffComponent = useDiffComponent()
const messages = createMemo(() => (props.sessionID ? (data.store.message[props.sessionID] ?? []) : []))
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 message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
const lastUserMessage = createMemo(() => userMessages().at(-1)!)
const message = createMemo(() => userMessages().find((m) => m.id === props.messageID)!)
const status = createMemo(
() =>
data.store.session_status[props.sessionID] ?? {
type: "idle",
},
)
const working = createMemo(() => status()?.type !== "idle" && message()?.id === userMessages().at(-1)?.id)
const working = createMemo(() => status().type !== "idle" && message().id === lastUserMessage().id)
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 hasSteps = createMemo(() => assistantParts().some((p) => p?.type === "tool"))
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
})
function duration() {
const completed = lastAssistantMessage()?.time.completed
const from = DateTime.fromMillis(message().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"]
return interval.toDuration(unit).normalize().toHuman({
notation: "compact",
unitDisplay: "narrow",
compactDisplay: "short",
showZeros: false,
})
}
let scrollRef: HTMLDivElement | undefined
let lastScrollTop = 0
const [state, setState] = createStore({
const [store, setStore] = createStore({
contentRef: undefined as HTMLDivElement | undefined,
stickyTitleRef: undefined as HTMLDivElement | undefined,
stickyTriggerRef: undefined as HTMLDivElement | undefined,
lastScrollTop: 0,
autoScrolled: false,
userScrolled: false,
stickyHeaderHeight: 0,
retrySeconds: 0,
status: rawStatus(),
duration: duration(),
})
createEffect(() => {
const r = retry()
if (!r) {
setState("retrySeconds", 0)
setStore("retrySeconds", 0)
return
}
const updateSeconds = () => {
const next = r.next
if (next) setState("retrySeconds", Math.max(0, Math.round((next - Date.now()) / 1000)))
if (next) setStore("retrySeconds", Math.max(0, Math.round((next - Date.now()) / 1000)))
}
updateSeconds()
const timer = setInterval(updateSeconds, 1000)
onCleanup(() => clearInterval(timer))
})
function handleScroll() {
if (!scrollRef || state.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
if (scrolledUp && working()) {
setState("userScrolled", true)
if (!scrollRef || store.autoScrolled) return
const scrollTop = scrollRef.scrollTop
const reset = scrollTop <= 0 && store.lastScrollTop > 100 && working() && !store.userScrolled
if (reset) {
setStore("lastScrollTop", scrollTop)
requestAnimationFrame(scrollToBottom)
return
}
lastScrollTop = scrollTop
const scrolledUp = scrollTop < store.lastScrollTop - 10
if (scrolledUp && working()) {
setStore("userScrolled", true)
props.onUserInteracted?.()
}
setStore("lastScrollTop", scrollTop)
}
function handleInteraction() {
if (working()) {
setState("userScrolled", true)
setStore("userScrolled", true)
props.onUserInteracted?.()
}
}
function scrollToBottom() {
if (!scrollRef || state.userScrolled || !working()) return
setState("autoScrolled", true)
if (!scrollRef || store.userScrolled || !working()) return
setStore("autoScrolled", true)
requestAnimationFrame(() => {
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "smooth" })
requestAnimationFrame(() => {
lastScrollTop = scrollRef?.scrollTop ?? 0
setState("autoScrolled", false)
batch(() => {
setStore("lastScrollTop", scrollRef?.scrollTop ?? 0)
setStore("autoScrolled", false)
})
})
})
}
createResizeObserver(() => state.contentRef, scrollToBottom)
createResizeObserver(() => store.contentRef, scrollToBottom)
createEffect(() => {
if (!working()) {
setState("userScrolled", false)
}
if (!working()) setStore("userScrolled", false)
})
createResizeObserver(
() => state.stickyTitleRef,
() => store.stickyTitleRef,
({ height }) => {
const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
setState("stickyHeaderHeight", height + triggerHeight + 8)
const triggerHeight = store.stickyTriggerRef?.offsetHeight ?? 0
setStore("stickyHeaderHeight", height + triggerHeight + 8)
},
)
createResizeObserver(
() => state.stickyTriggerRef,
() => store.stickyTriggerRef,
({ height }) => {
const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
setState("stickyHeaderHeight", titleHeight + height + 8)
const titleHeight = store.stickyTitleRef?.offsetHeight ?? 0
setStore("stickyHeaderHeight", titleHeight + height + 8)
},
)
createEffect(() => {
const timer = setInterval(() => {
setStore("duration", duration())
}, 1000)
onCleanup(() => clearInterval(timer))
})
let lastStatusChange = Date.now()
let statusTimeout: number | undefined
createEffect(() => {
const newStatus = rawStatus()
if (newStatus === store.status || !newStatus) return
const timeSinceLastChange = Date.now() - lastStatusChange
if (timeSinceLastChange >= 2500) {
setStore("status", newStatus)
lastStatusChange = Date.now()
if (statusTimeout) {
clearTimeout(statusTimeout)
statusTimeout = undefined
}
} else {
if (statusTimeout) clearTimeout(statusTimeout)
statusTimeout = setTimeout(() => {
setStore("status", rawStatus())
lastStatusChange = Date.now()
statusTimeout = undefined
}, 2500 - timeSinceLastChange) as unknown as number
}
})
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}>
<Show when={message()}>
{(message) => {
const assistantMessages = createMemo(() => {
return messages()?.filter(
(m) => m.role === "assistant" && m.parentID == message().id,
) as AssistantMessage[]
})
const lastAssistantMessage = createMemo(() => assistantMessages()?.at(-1))
const assistantMessageParts = createMemo(() => assistantMessages()?.flatMap((m) => data.store.part[m.id]))
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const parts = createMemo(() => data.store.part[message().id])
const lastTextPart = createMemo(() =>
assistantMessageParts()
.filter((p) => p?.type === "text")
?.at(-1),
)
const summary = createMemo(() => message().summary?.body ?? lastTextPart()?.text)
const lastTextPartShown = createMemo(
() => !message().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0,
)
const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
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
})
function duration() {
const completed = lastAssistantMessage()?.time.completed
const from = DateTime.fromMillis(message()!.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"]
return interval.toDuration(unit).normalize().toHuman({
notation: "compact",
unitDisplay: "narrow",
compactDisplay: "short",
showZeros: false,
})
}
const [store, setStore] = createStore({
status: rawStatus(),
stepsExpanded: props.stepsExpanded ?? working(),
duration: duration(),
})
createEffect(() => {
if (props.stepsExpanded !== undefined) {
setStore("stepsExpanded", props.stepsExpanded)
}
})
createEffect(() => {
const timer = setInterval(() => {
setStore("duration", duration())
}, 1000)
onCleanup(() => clearInterval(timer))
})
let lastStatusChange = Date.now()
let statusTimeout: number | undefined
createEffect(() => {
const newStatus = rawStatus()
if (newStatus === store.status || !newStatus) return
const timeSinceLastChange = Date.now() - lastStatusChange
if (timeSinceLastChange >= 2500) {
setStore("status", newStatus)
lastStatusChange = Date.now()
if (statusTimeout) {
clearTimeout(statusTimeout)
statusTimeout = undefined
}
} else {
if (statusTimeout) clearTimeout(statusTimeout)
statusTimeout = setTimeout(() => {
setStore("status", rawStatus())
lastStatusChange = Date.now()
statusTimeout = undefined
}, 2500 - timeSinceLastChange) as unknown as number
}
})
createEffect((prev) => {
const isWorking = working()
if (!prev && isWorking) {
setStore("stepsExpanded", true)
props.onStepsExpandedChange?.(true)
}
if (prev && !isWorking && !state.userScrolled) {
setStore("stepsExpanded", false)
props.onStepsExpandedChange?.(false)
}
return isWorking
}, working())
return (
<div
ref={(el) => setState("contentRef", el)}
data-message={message().id}
data-slot="session-turn-message-container"
class={props.classes?.container}
style={{ "--sticky-header-height": `${state.stickyHeaderHeight}px` }}
>
{/* Title (sticky) */}
<div ref={(el) => setState("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}</h1>
</Match>
</Switch>
</div>
<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}</h1>
</Match>
</Switch>
</div>
</div>
{/* User Message */}
<div data-slot="session-turn-message-content">
<Message message={message()} parts={parts()} />
</div>
{/* Trigger (sticky) */}
<div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
</div>
{/* User Message */}
<div data-slot="session-turn-message-content">
<Message message={message()} 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={() => {
if (assistantMessages().length === 0) return
const next = !store.stepsExpanded
setStore("stepsExpanded", next)
props.onStepsExpandedChange?.(next)
}}
onClick={props.onStepsExpandedToggle ?? (() => {})}
>
<Show when={working()}>
<Spinner />
@@ -353,13 +339,13 @@ export function SessionTurn(
})()}
</span>
<span data-slot="session-turn-retry-seconds">
· retrying {state.retrySeconds > 0 ? `in ${state.retrySeconds}s ` : ""}
· 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>
<Match when={props.stepsExpanded}>Hide steps</Match>
<Match when={!props.stepsExpanded}>Show steps</Match>
</Switch>
<span>·</span>
<span>{store.duration}</span>
@@ -368,115 +354,115 @@ export function SessionTurn(
</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 (
<Switch>
<Match when={lastTextPartShown() && 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">
<h2 data-slot="session-turn-summary-title">
</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={message().summary?.diffs?.length}>Summary</Match>
<Match when={true}>Response</Match>
<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>
</h2>
<Show when={summary()}>
)
}}
</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) => (
<Markdown
data-slot="session-turn-markdown"
data-diffs={!!message().summary?.diffs?.length}
text={summary()}
/>
<>
<h2 data-slot="session-turn-summary-title">Summary</h2>
<Markdown data-slot="session-turn-markdown" data-diffs={hasDiffs()} text={summary()} />
</>
)}
</Show>
</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" />
</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>
</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>
</div>
)
}}
</Show>
<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>
</Match>
</Switch>
</div>
{props.children}
</div>
</div>

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.167",
"version": "1.0.169",
"private": true,
"type": "module",
"exports": {

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.167",
"version": "1.0.169",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View File

@@ -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 |
---

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.

238
script/changelog.ts Normal file
View File

@@ -0,0 +1,238 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { createOpencode } from "@opencode-ai/sdk"
const TEAM = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
const MODEL = "gemini-3-flash"
function getAreaFromPath(file: string): string {
if (file.startsWith("packages/")) {
const parts = file.replace("packages/", "").split("/")
if (parts[0] === "extensions" && parts[1]) return `extensions/${parts[1]}`
return parts[0] || "other"
}
if (file.startsWith("sdks/")) {
const name = file.replace("sdks/", "").split("/")[0] || "other"
return `extensions/${name}`
}
const rootDir = file.split("/")[0]
if (rootDir && !rootDir.includes(".")) return rootDir
return "other"
}
function buildPrompt(previous: string, commits: string): string {
return `
Analyze these commits and generate a changelog of all notable user facing changes, grouped by area.
Each commit below includes:
- [author: username] showing the GitHub username of the commit author
- [areas: ...] showing which areas of the codebase were modified
Commits between ${previous} and HEAD:
${commits}
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
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>
`
}
function parseChangelog(raw: string): string[] {
const lines: string[] = []
for (const line of raw.split("\n")) {
if (line.startsWith("## ")) {
if (lines.length > 0) lines.push("")
lines.push(line)
} else if (line.startsWith("- ")) {
lines.push(line)
}
}
return lines
}
function formatContributors(contributors: Map<string, string[]>): string[] {
if (contributors.size === 0) return []
const lines: string[] = []
lines.push("")
lines.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
for (const username of contributors.keys()) {
lines.push(`- @${username}`)
}
return lines
}
/**
* Generates a changelog for a release.
*
* Uses GitHub API for commit authors, git for file changes,
* and Gemini Flash via opencode SDK for changelog generation.
*
* @param previous - The previous version tag (e.g. "v1.0.167")
* @param current - The current ref (e.g. "HEAD" or "v1.0.168")
* @returns Formatted changelog string ready for GitHub release notes
*/
export async function generateChangelog(previous: string, current: string): Promise<string> {
// Fetch commit authors from GitHub API (hash -> login)
const compare =
await $`gh api "/repos/sst/opencode/compare/${previous}...${current}" --jq '.commits[] | {sha: .sha, login: .author.login, message: .commit.message}'`
.text()
.catch(() => "")
const authorByHash = new Map<string, string>()
const contributors = new Map<string, string[]>()
for (const line of compare.split("\n").filter(Boolean)) {
const { sha, login, message } = JSON.parse(line) as { sha: string; login: string | null; message: string }
if (login) authorByHash.set(sha, login)
const title = message.split("\n")[0] || ""
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
if (login && !TEAM.includes(login)) {
if (!contributors.has(login)) contributors.set(login, [])
contributors.get(login)?.push(title)
}
}
function findAuthor(shortHash: string): string | undefined {
for (const [sha, login] of authorByHash) {
if (sha.startsWith(shortHash)) return login
}
}
// Batch-fetch files for all commits (hash -> areas)
const diffLog = await $`git log ${previous}..${current} --name-only --format="%h"`.text()
const areasByHash = new Map<string, Set<string>>()
let currentHash: string | null = null
for (const rawLine of diffLog.split("\n")) {
const line = rawLine.trim()
if (!line) continue
if (/^[0-9a-f]{7,}$/i.test(line) && !line.includes("/")) {
currentHash = line
if (!areasByHash.has(currentHash)) areasByHash.set(currentHash, new Set())
continue
}
if (currentHash) {
areasByHash.get(currentHash)!.add(getAreaFromPath(line))
}
}
// Build commit lines with author and areas
const log = await $`git log ${previous}..${current} --oneline --format="%h %s"`.text()
const commitLines = log.split("\n").filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:|release:)/i))
const commitsWithMeta = commitLines
.map((line) => {
const hash = line.split(" ")[0]
if (!hash) return null
const author = findAuthor(hash)
const authorStr = author ? ` [author: ${author}]` : ""
const areas = areasByHash.get(hash)
const areaStr = areas && areas.size > 0 ? ` [areas: ${[...areas].join(", ")}]` : " [areas: other]"
return `${line}${authorStr}${areaStr}`
})
.filter(Boolean) as string[]
const commits = commitsWithMeta.join("\n")
if (!commits.trim()) {
console.error("No commits found to generate changelog")
}
// Generate changelog via LLM
// different port to not conflict with dev running opencode
let raw: string | undefined
try {
const opencode = await createOpencode({ port: 8192 })
try {
const session = await opencode.client.session.create()
if (!session.data?.id) {
console.error("Failed to create session:", session)
throw new Error("Failed to create session")
}
const response = await opencode.client.session.prompt({
path: { id: session.data.id },
body: {
model: { providerID: "opencode", modelID: MODEL },
parts: [{ type: "text", text: buildPrompt(previous, commits) }],
},
})
if (!response.data?.parts) {
console.error("Empty response from LLM:", response)
}
raw = response.data?.parts?.find((y) => y.type === "text")?.text
} finally {
opencode.server.close()
}
} catch (err) {
console.error("Failed to generate changelog via LLM:", err)
}
const notes = parseChangelog(raw ?? "")
notes.push(...formatContributors(contributors))
return notes.join("\n")
}
// Standalone runner for local testing
if (import.meta.main) {
const [previous, current] = process.argv.slice(2)
if (!previous || !current) {
console.error("Usage: bun script/changelog.ts <previous> <current>")
console.error("Example: bun script/changelog.ts v1.0.167 HEAD")
process.exit(1)
}
const changelog = await generateChangelog(previous, current)
console.log(changelog)
process.exit(0)
}

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
script/generate.ts Executable file
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`

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { createOpencode } from "@opencode-ai/sdk"
import { Script } from "@opencode-ai/script"
import { generateChangelog } from "./changelog"
const notes = [] as string[]
let notes = ""
console.log("=== publishing ===\n")
@@ -16,102 +16,11 @@ if (!Script.preview) {
})
.then((data: any) => data.version)
const log =
await $`git log v${previous}..HEAD --oneline --format="%h %s" -- packages/opencode packages/sdk packages/plugin packages/tauri packages/desktop`.text()
const commits = log
.split("\n")
.filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:)/i))
.join("\n")
const opencode = await createOpencode()
const session = await opencode.client.session.create()
console.log("generating changelog since " + previous)
const raw = await opencode.client.session
.prompt({
path: {
id: session.data!.id,
},
body: {
model: {
providerID: "opencode",
modelID: "claude-haiku-4-5",
},
parts: [
{
type: "text",
text: `
Analyze these commits and generate a changelog of all notable user facing changes.
Commits between ${previous} and HEAD:
${commits}
- 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.
IMPORTANT: ONLY return a bulleted list of changes, do not include any other information. Do not include a preamble like "Based on my analysis..."
<example>
- Added ability to @ mention agents
- Fixed a bug where the TUI would render improperly on some terminals
</example>
`,
},
],
},
})
.then((x) => x.data?.parts?.find((y) => y.type === "text")?.text)
for (const line of raw?.split("\n") ?? []) {
if (line.startsWith("- ")) {
notes.push(line)
}
}
notes = await generateChangelog(`v${previous}`, "HEAD")
console.log("---- Generated Changelog ----")
console.log(notes.join("\n"))
console.log(notes)
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[]>()
for (const line of compare.split("\n").filter(Boolean)) {
const { login, message } = JSON.parse(line) as { login: string | null; message: string }
const title = message.split("\n")[0] ?? ""
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
if (login && !team.includes(login)) {
if (!contributors.has(login)) contributors.set(login, [])
contributors.get(login)?.push(title)
}
}
if (contributors.size > 0) {
notes.push("")
notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
for (const [username, userCommits] of contributors) {
notes.push(`- @${username}:`)
for (const commit of userCommits) {
notes.push(` - ${commit}`)
}
}
}
}
const pkgjsons = await Array.fromAsync(
@@ -155,7 +64,7 @@ if (!Script.preview) {
await $`git cherry-pick HEAD..origin/dev`.nothrow()
await $`git push origin HEAD --tags --no-verify --force-with-lease`
await new Promise((resolve) => setTimeout(resolve, 5_000))
await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes ${notes.join("\n") || "No notable changes"} ./packages/opencode/dist/*.zip ./packages/opencode/dist/*.tar.gz`
await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes ${notes || "No notable changes"} ./packages/opencode/dist/*.zip ./packages/opencode/dist/*.tar.gz`
const release = await $`gh release view v${Script.version} --json id,tagName`.json()
if (process.env.GITHUB_OUTPUT) {
await Bun.write(process.env.GITHUB_OUTPUT, `releaseId=${release.id}\ntagName=${release.tagName}\n`)

View File

@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.167",
"version": "1.0.169",
"publisher": "sst-dev",
"repository": {
"type": "git",