mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 10:24:11 +00:00
Compare commits
58 Commits
github-v1.
...
brendan/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d6a1b76e4 | ||
|
|
826b8538e7 | ||
|
|
6a802c01cd | ||
|
|
14146428dd | ||
|
|
26d0280f70 | ||
|
|
3274a5813e | ||
|
|
382905602c | ||
|
|
8b5cea7899 | ||
|
|
100c31cbb1 | ||
|
|
0b286f1b84 | ||
|
|
2f6ca958fe | ||
|
|
5218e7a546 | ||
|
|
7f8e799392 | ||
|
|
289f4abaaa | ||
|
|
7ce898ce43 | ||
|
|
0dd716a75e | ||
|
|
87171467fa | ||
|
|
b99afdad91 | ||
|
|
4fd576f3af | ||
|
|
2f41d0bedd | ||
|
|
5f03290534 | ||
|
|
427157c683 | ||
|
|
a0ab3d98b7 | ||
|
|
c8de766913 | ||
|
|
d57b963141 | ||
|
|
0ebcaff927 | ||
|
|
15931fa170 | ||
|
|
af4087d7b5 | ||
|
|
323ea1040c | ||
|
|
1fe87b0233 | ||
|
|
8d11df1b3b | ||
|
|
ecc5050838 | ||
|
|
606cf3b6f2 | ||
|
|
67cfd7f06b | ||
|
|
ab9ac7c87a | ||
|
|
ee9f979613 | ||
|
|
228b6444f8 | ||
|
|
9998efdae2 | ||
|
|
9427f56e1a | ||
|
|
a6dd35d73d | ||
|
|
faeaafa5f5 | ||
|
|
8b298a233e | ||
|
|
6f43d03043 | ||
|
|
c868a4088d | ||
|
|
83d8a88c90 | ||
|
|
268f37f8c9 | ||
|
|
b0aaf04957 | ||
|
|
b7875256f3 | ||
|
|
7bc47fb904 | ||
|
|
5cf8e54372 | ||
|
|
7437ccd6f4 | ||
|
|
4bf882ba81 | ||
|
|
d5dcc55a47 | ||
|
|
35fab5f66d | ||
|
|
d7a32846cf | ||
|
|
2bfacda9ba | ||
|
|
3243612dbd | ||
|
|
d6af36a084 |
43
.github/workflows/generate.yml
vendored
43
.github/workflows/generate.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
129
bun.lock
@@ -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
6
flake.lock
generated
@@ -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": {
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-g6XHWk9IoDoeXbvENs+U2fqk185xKMLb0BRopCbXaIk="
|
||||
"nodeModules": "sha256-oT1WPPR1sHBhQcJaFL+mod5l3+V8O3uPKJUdrcfTst0="
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.167",
|
||||
"version": "1.0.169",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -112,6 +112,8 @@ export async function handler(
|
||||
headers.delete("content-length")
|
||||
headers.delete("x-opencode-request")
|
||||
headers.delete("x-opencode-session")
|
||||
headers.delete("x-opencode-project")
|
||||
headers.delete("x-opencode-client")
|
||||
return headers
|
||||
})(),
|
||||
body: reqBody,
|
||||
|
||||
@@ -1,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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,11 +24,6 @@ export function Header(props: {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
||||
const sessions = createMemo(() => store().session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => store().config.share !== "disabled")
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
@@ -45,101 +40,116 @@ export function Header(props: {
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
|
||||
<Show when={params.dir && layout.projects.list().length > 0}>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<Show when={layout.projects.list().length > 0 && params.dir}>
|
||||
{(directory) => {
|
||||
const currentDirectory = createMemo(() => base64Decode(directory()))
|
||||
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
||||
const sessions = createMemo(() => store().session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => store().config.share !== "disabled")
|
||||
return (
|
||||
<>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={props.navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={props.navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<Show when={currentSession()}>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">Ctrl `</span>
|
||||
<Show when={currentSession()}>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
<div class="flex items-center gap-4">
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">Ctrl `</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: currentDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
)
|
||||
return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: currentDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Show when={url()}>
|
||||
{(url) => <TextField value={url()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -21,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()}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
@import "@opencode-ai/ui/styles/tailwind";
|
||||
|
||||
:root {
|
||||
html,
|
||||
body {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
99
packages/desktop/src/utils/id.ts
Normal file
99
packages/desktop/src/utils/id.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import z from "zod"
|
||||
|
||||
const prefixes = {
|
||||
session: "ses",
|
||||
message: "msg",
|
||||
permission: "per",
|
||||
user: "usr",
|
||||
part: "prt",
|
||||
pty: "pty",
|
||||
} as const
|
||||
|
||||
const LENGTH = 26
|
||||
let lastTimestamp = 0
|
||||
let counter = 0
|
||||
|
||||
type Prefix = keyof typeof prefixes
|
||||
export namespace Identifier {
|
||||
export function schema(prefix: Prefix) {
|
||||
return z.string().startsWith(prefixes[prefix])
|
||||
}
|
||||
|
||||
export function ascending(prefix: Prefix, given?: string) {
|
||||
return generateID(prefix, false, given)
|
||||
}
|
||||
|
||||
export function descending(prefix: Prefix, given?: string) {
|
||||
return generateID(prefix, true, given)
|
||||
}
|
||||
}
|
||||
|
||||
function generateID(prefix: Prefix, descending: boolean, given?: string): string {
|
||||
if (!given) {
|
||||
return create(prefix, descending)
|
||||
}
|
||||
|
||||
if (!given.startsWith(prefixes[prefix])) {
|
||||
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||
}
|
||||
|
||||
return given
|
||||
}
|
||||
|
||||
function create(prefix: Prefix, descending: boolean, timestamp?: number): string {
|
||||
const currentTimestamp = timestamp ?? Date.now()
|
||||
|
||||
if (currentTimestamp !== lastTimestamp) {
|
||||
lastTimestamp = currentTimestamp
|
||||
counter = 0
|
||||
}
|
||||
|
||||
counter += 1
|
||||
|
||||
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
|
||||
|
||||
if (descending) {
|
||||
now = ~now
|
||||
}
|
||||
|
||||
const timeBytes = new Uint8Array(6)
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
||||
}
|
||||
|
||||
return prefixes[prefix] + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12)
|
||||
}
|
||||
|
||||
function bytesToHex(bytes: Uint8Array): string {
|
||||
let hex = ""
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
hex += bytes[i].toString(16).padStart(2, "0")
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
function randomBase62(length: number): string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
const bytes = getRandomBytes(length)
|
||||
let result = ""
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
result += chars[bytes[i] % 62]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getRandomBytes(length: number): Uint8Array {
|
||||
const bytes = new Uint8Array(length)
|
||||
const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : undefined
|
||||
|
||||
if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
|
||||
cryptoObj.getRandomValues(bytes)
|
||||
return bytes
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
bytes[i] = Math.floor(Math.random() * 256)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.167",
|
||||
"version": "1.0.169",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()} />
|
||||
}
|
||||
@@ -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()
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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, " "),
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/tauri",
|
||||
"private": true,
|
||||
"version": "1.0.167",
|
||||
"version": "1.0.169",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.167",
|
||||
"version": "1.0.169",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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)}‎</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)}‎</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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.167",
|
||||
"version": "1.0.169",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
238
script/changelog.ts
Normal 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)
|
||||
}
|
||||
@@ -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
9
script/generate.ts
Executable 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`
|
||||
@@ -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`)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user