Compare commits

..

109 Commits

Author SHA1 Message Date
opencode a3fbbece9a release: v1.0.141 2025-12-10 02:57:38 +00:00
Dax Raad e72c974c4c ci 2025-12-09 21:49:38 -05:00
Dax Raad a762da7cab ci 2025-12-09 21:49:05 -05:00
Dax Raad fa6c060324 ci 2025-12-09 21:44:13 -05:00
Dax Raad 8e33ac052b ci: publish with multiple tags instead of using dist-tag
npm dist-tag add command is broken, so publish package multiple times
with different tags directly instead
2025-12-09 21:39:05 -05:00
Dax Raad 0759696ec0 core: enable project discovery for experimental builds 2025-12-09 21:23:55 -05:00
Hosenur Rahaman 59dce63471 docs: Add portal project to ecosystem documentation (#5300)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 20:23:14 -06:00
Dax Raad 1ae28090e3 ci 2025-12-09 21:16:36 -05:00
Dax Raad 0decdf6a55 ci 2025-12-09 21:16:23 -05:00
Dax Raad 09b402a274 ci 2025-12-09 21:16:00 -05:00
Dax Raad 150baf3e96 ci 2025-12-09 21:10:58 -05:00
Dax Raad 78c51371af sync 2025-12-09 21:06:57 -05:00
Dax Raad 6dbcacf3ea ci 2025-12-09 21:04:06 -05:00
Dax Raad 4ecebc2c83 ci 2025-12-09 21:03:19 -05:00
Dax Raad 38a79fa449 ci 2025-12-09 21:02:04 -05:00
Dax Raad bafad6b8a8 ci 2025-12-09 21:01:48 -05:00
Dax Raad 5682dddd45 ci 2025-12-09 21:01:39 -05:00
Dax Raad a9aacdb94a ci 2025-12-09 20:59:31 -05:00
Dax Raad e7e32c946b ci 2025-12-09 20:58:01 -05:00
Dax Raad fc9bc26d86 ci 2025-12-09 20:56:13 -05:00
Dax Raad ee00b4e0ce ci 2025-12-09 20:54:56 -05:00
Dax Raad f82156f0b1 ci 2025-12-09 20:51:54 -05:00
Dax Raad 2ed6298584 ci 2025-12-09 20:51:25 -05:00
Dax Raad 52ef8dea3e ci 2025-12-09 20:50:46 -05:00
Adam ebe6015db0 fix: re-enable tauri typecheck 2025-12-09 19:34:07 -06:00
Dax Raad 56526114e4 ci 2025-12-09 20:26:46 -05:00
Dax Raad bb1c225027 ci 2025-12-09 20:24:31 -05:00
Dax Raad e5af0dde08 ci 2025-12-09 20:22:00 -05:00
Dax Raad 3cf17bc24f ci 2025-12-09 20:20:47 -05:00
Dax Raad 4aa1b8de0e ci 2025-12-09 20:19:43 -05:00
Dax Raad 73e9534d08 ci 2025-12-09 20:17:07 -05:00
Dax Raad cb188f907f ci 2025-12-09 19:53:50 -05:00
Dax Raad 63d9656ad8 ci 2025-12-09 19:46:59 -05:00
Dax Raad 3512d02e9e ci 2025-12-09 19:46:38 -05:00
Dax Raad 1efdceaf10 ci: combine sdk and format workflows into single generate workflow 2025-12-09 19:41:23 -05:00
GitHub Action 632a0fe009 chore: regen sdk 2025-12-10 00:33:05 +00:00
Dax Raad 6fb32cebec ci 2025-12-09 19:32:30 -05:00
GitHub Action 8b8b17d755 chore: format code 2025-12-10 00:28:05 +00:00
GitHub Action 2c27afaaf5 chore: regen sdk 2025-12-10 00:27:29 +00:00
Dax Raad 4bdc7c1426 ci fix 2025-12-09 19:26:54 -05:00
Github Action 3c1e6c2c8f Update Nix flake.lock and hashes 2025-12-09 23:56:13 +00:00
Aiden Cline b8f5809f95 ignore: rm chalk 2025-12-09 17:54:53 -06:00
Aiden Cline 552ee81455 tweak: add OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS 2025-12-09 17:28:34 -06:00
David Hill 9fdbe193cd fix: add spacer before queued badge 2025-12-09 22:56:44 +00:00
Aiden Cline df64612d54 better interleaved thinking support (#5298) 2025-12-09 16:32:12 -06:00
Adam 0aa3e6c270 wip(desktop): progress 2025-12-09 16:23:05 -06:00
Adam 44c17c1435 wip(desktop): progress 2025-12-09 16:23:05 -06:00
Dax Raad 132e772c26 core: fix project icon update handling to preserve existing icon properties 2025-12-09 16:55:26 -05:00
Adam 62cbed57cc wip(desktop): progress 2025-12-09 15:55:08 -06:00
Adam ebab7e176e wip(desktop): progress 2025-12-09 15:53:08 -06:00
Adam 9c93853e22 wip(desktop): progress 2025-12-09 15:46:23 -06:00
Aiden Cline 8a9c7a4ef3 add OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT 2025-12-09 15:44:56 -06:00
Adam 2dad56c9a2 wip(desktop): progress 2025-12-09 15:39:44 -06:00
Adam 41d78c1ecc wip(desktop): progress 2025-12-09 15:32:36 -06:00
ry2009 16c4b02b69 feat: add Biome LSP support (#5290) 2025-12-09 15:31:13 -06:00
Adam 35c04d9283 wip(desktop): progress 2025-12-09 15:24:11 -06:00
Adam 1fbd7a7f9a wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam d7563d1694 wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam b9fa7d9163 wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam f736751ab2 wip(desktop): progress 2025-12-09 15:21:45 -06:00
Aiden Cline dbcc779f0b ci: fix sdk workflow 2025-12-09 15:08:30 -06:00
Nick c33a90320c fix: resolve 'latest' to actual version when caching plugins (#5292) 2025-12-09 15:07:59 -06:00
Dax Raad 802b862aae format on write 2025-12-09 16:06:57 -05:00
GitHub Action b0cd171c1b chore: format code 2025-12-09 21:05:09 +00:00
GitHub Action 13755f4680 chore: regen sdk 2025-12-09 21:04:37 +00:00
Dax Raad b242659cc3 fix types 2025-12-09 16:03:58 -05:00
Dax Raad 5f6b2fdc6f fix tests 2025-12-09 16:03:33 -05:00
GitHub Action e34f18991e chore: format code 2025-12-09 20:53:06 +00:00
GitHub Action 209b0a06f7 chore: regen sdk 2025-12-09 20:52:31 +00:00
Dax Raad a2e460bc4b discover logic 2025-12-09 15:51:55 -05:00
GitHub Action fc9081afe4 chore: format code 2025-12-09 20:49:25 +00:00
GitHub Action 1a3f7c3d84 chore: regen sdk 2025-12-09 20:48:49 +00:00
Dax Raad 06aa1f49b8 sync 2025-12-09 15:48:22 -05:00
GitHub Action dfd67cd922 chore: format code 2025-12-09 20:44:09 +00:00
GitHub Action 70f7287ca1 chore: regen sdk 2025-12-09 20:43:35 +00:00
Dax Raad f1955b4d05 core: fix project event emission to include updated worktree data 2025-12-09 15:41:38 -05:00
Dax Raad c5e5627cbd test fixes 2025-12-09 15:41:38 -05:00
GitHub Action 93378526b9 chore: format code 2025-12-09 20:26:02 +00:00
GitHub Action abf176a335 chore: regen sdk 2025-12-09 20:25:21 +00:00
Aiden Cline 84a0868e66 fix: read when file is svg 2025-12-09 14:24:30 -06:00
GitHub Action 75a9c42789 chore: format code 2025-12-09 20:20:03 +00:00
GitHub Action 204fa54625 chore: regen sdk 2025-12-09 20:19:27 +00:00
Dax Raad 365584048f core: fix project creation to include updated timestamp 2025-12-09 15:18:55 -05:00
Dax Raad edffcc32cf core: make project updated timestamp optional to support legacy project data 2025-12-09 15:18:55 -05:00
GitHub Action 238f441bcb chore: format code 2025-12-09 20:17:37 +00:00
GitHub Action 0571a8302c chore: regen sdk 2025-12-09 20:16:59 +00:00
Aiden Cline 8c07382382 ci: fix sdk gen 2025-12-09 14:16:22 -06:00
GitHub Action fa32fbd187 chore: format code 2025-12-09 20:12:07 +00:00
GitHub Action 0fd2ecd0ba chore: regen sdk 2025-12-09 20:11:32 +00:00
Dax Raad 7439a40b00 core: fix project icon update to preserve existing icon properties 2025-12-09 15:11:00 -05:00
GitHub Action 2ad99713f3 chore: format code 2025-12-09 20:07:32 +00:00
GitHub Action 19ec970701 chore: regen sdk 2025-12-09 20:06:57 +00:00
Dax Raad b48caec218 core: add automatic project icon discovery from favicon/logo files 2025-12-09 15:06:24 -05:00
GitHub Action 380c34af53 chore: format code 2025-12-09 19:54:54 +00:00
GitHub Action 553d9013eb chore: regen sdk 2025-12-09 19:54:19 +00:00
Dax Raad 8bff3cdae8 fix ci 2025-12-09 14:53:47 -05:00
Dax Raad 0b40c3d37d rework project loading 2025-12-09 14:41:14 -05:00
Dax Raad 1e3bdcc71c rename bus 2025-12-09 14:32:09 -05:00
GitHub Action de577e17da chore: format code 2025-12-09 19:31:07 +00:00
GitHub Action 8a9e258ad7 chore: regen sdk 2025-12-09 19:30:32 +00:00
Adam 9a34965432 feat: add color to project 2025-12-09 13:29:59 -06:00
Adam c944d19c3b wip(desktop): progress 2025-12-09 13:24:37 -06:00
Dax Raad fb1b6c5e6b add project.name/icon 2025-12-09 13:57:18 -05:00
GitHub Action ad0c4c5d89 chore: format code 2025-12-09 18:37:14 +00:00
GitHub Action a54b663a39 chore: regen sdk 2025-12-09 18:36:38 +00:00
Adam ae4993f39a wip(desktop): progress 2025-12-09 12:36:06 -06:00
GitHub Action aa638cec48 chore: format code 2025-12-09 18:10:31 +00:00
GitHub Action 4db4a90559 chore: regen sdk 2025-12-09 18:07:43 +00:00
Aiden Cline e23a81097c core: add test to prevent MCP headers regression when OAuth is enabled
Custom headers configured for remote MCP servers were being silently
dropped when OAuth was enabled (the default). This test ensures headers
are always sent to MCP servers regardless of OAuth configuration.
2025-12-09 12:06:40 -06:00
99 changed files with 1503 additions and 10341 deletions
@@ -1,4 +1,4 @@
name: format
name: generate
on:
push:
@@ -8,14 +8,10 @@ on:
branches-ignore:
- production
workflow_dispatch:
workflow_run:
workflows: ["sdk"]
types:
- completed
jobs:
format:
generate:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.workflow_run.conclusion == 'success'
permissions:
contents: write
steps:
@@ -29,9 +25,14 @@ jobs:
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: run
- name: Generate SDK
run: |
./script/format.ts
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: Format
run: ./script/format.ts
env:
CI: true
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
+18 -18
View File
@@ -2,11 +2,15 @@ name: publish
run-name: "${{ format('release {0}', inputs.bump) }}"
on:
push:
branches:
- dev
- snapshot-*
workflow_dispatch:
inputs:
bump:
description: "Bump major, minor, or patch"
required: true
required: false
type: choice
options:
- major
@@ -20,6 +24,7 @@ on:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
id-token: write
contents: write
packages: write
@@ -34,20 +39,13 @@ jobs:
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: ./.github/actions/setup-bun
- name: Install makepkg
- name: Setup SSH for AUR
if: inputs.bump || inputs.version
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
@@ -56,12 +54,9 @@ jobs:
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install OpenCode
if: inputs.bump || inputs.version
run: curl -fsSL https://opencode.ai/install | bash
- name: Setup npm auth
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -69,19 +64,24 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Publish
run: |
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_CHANNEL: latest
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
publish-tauri:
if: inputs.bump || inputs.version
continue-on-error: true
strategy:
fail-fast: false
-39
View File
@@ -1,39 +0,0 @@
name: sdk
on:
push:
branches-ignore:
- production
pull_request:
branches-ignore:
- production
workflow_dispatch:
jobs:
format:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: run
run: |
bun ./packages/sdk/js/script/build.ts
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
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: regen sdk"
git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify
env:
CI: true
-38
View File
@@ -1,38 +0,0 @@
name: snapshot
on:
workflow_dispatch:
push:
branches:
- dev
- test-bedrock
- v0
- otui-diffs
- snapshot-*
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: ./.github/actions/setup-bun
- name: Publish
run: |
./script/publish.ts
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
+1
View File
@@ -1,5 +1,6 @@
---
description: git commit and push
model: opencode/glm-4.6
---
commit and push
+1 -3
View File
@@ -7,9 +7,7 @@
"instructions": ["STYLE_GUIDE.md"],
"provider": {
"opencode": {
"options": {
// "baseURL": "http://localhost:8080",
},
"options": {},
},
},
"mcp": {
+15 -15
View File
@@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -168,7 +168,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -196,7 +196,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -212,7 +212,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.138",
"version": "1.0.141",
"bin": {
"opencode": "./bin/opencode",
},
@@ -304,7 +304,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -324,7 +324,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.138",
"version": "1.0.141",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -335,7 +335,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -348,7 +348,7 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@tauri-apps/api": "^2",
@@ -370,7 +370,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -402,7 +402,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"zod": "catalog:",
},
@@ -413,7 +413,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1764947035,
"narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=",
"lastModified": 1765270179,
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a672be65651c80d3f592a89b3945466584a22069",
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
"type": "github"
},
"original": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.138",
"version": "1.0.141",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.138",
"version": "1.0.141",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.138",
"version": "1.0.141",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.138",
"version": "1.0.141",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.138",
"version": "1.0.141",
"description": "",
"type": "module",
"exports": {
@@ -1,5 +1,5 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
@@ -22,6 +22,8 @@ interface PromptInputProps {
ref?: (el: HTMLDivElement) => void
}
const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
export const PromptInput: Component<PromptInputProps> = (props) => {
const navigate = useNavigate()
const sdk = useSDK()
@@ -36,6 +38,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
popoverIsOpen: false,
})
const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length))
onMount(() => {
const interval = setInterval(() => {
setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length)
}, 5000)
onCleanup(() => clearInterval(interval))
})
createEffect(() => {
session.id
editorRef.focus()
@@ -68,7 +79,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const handleFileSelect = (path: string | undefined) => {
if (!path) return
addPart({ type: "file", path, content: "@" + getFilename(path), start: 0, end: 0 })
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
}
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
@@ -403,7 +414,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
<Show when={!session.prompt.dirty()}>
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
Plan and build anything
Ask anything... "{PLACEHOLDERS[placeholder()]}"
</div>
</Show>
</div>
+1 -1
View File
@@ -19,7 +19,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
sdk.global.event().then(async (events) => {
for await (const event of events.stream) {
// console.log("event", event)
emitter.emit(event.directory, event.payload)
emitter.emit(event.directory ?? "global", event.payload)
}
})
+57 -11
View File
@@ -18,11 +18,36 @@ import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
const PASTEL_COLORS = [
"#FCEAFD", // pastel pink
"#FFDFBA", // pastel peach
"#FFFFBA", // pastel yellow
"#BAFFC9", // pastel green
"#EAF6FD", // pastel blue
"#EFEAFD", // pastel lavender
"#FEC8D8", // pastel rose
"#D4F0F0", // pastel cyan
"#FDF0EA", // pastel coral
"#C1E1C1", // pastel mint
]
function randomPastelColor() {
return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
}
async function ensureProjectColor(project: Project, sdk: ReturnType<typeof useGlobalSDK>): Promise<Project> {
if (project.icon?.color) return project
const color = randomPastelColor()
const updated = { ...project, icon: { ...project.icon, color } }
sdk.client.project.update({ projectID: project.id, icon: { color } })
return updated
}
type State = {
ready: boolean
provider: Provider[]
agent: Agent[]
project: Project
project: string
config: Config
path: Path
session: Session[]
@@ -60,11 +85,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!children[directory]) {
setGlobalStore("children", directory, {
project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
project: "",
config: {},
path: { state: "", config: "", worktree: "", directory: "" },
ready: false,
@@ -88,9 +112,31 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const sdk = useGlobalSDK()
sdk.event.listen((e) => {
const directory = e.name
const [store, setStore] = child(directory)
const event = e.details
if (directory === "global") {
switch (event.type) {
case "project.updated": {
ensureProjectColor(event.properties, sdk).then((project) => {
const result = Binary.search(globalStore.projects, project.id, (s) => s.id)
if (result.found) {
setGlobalStore("projects", result.index, reconcile(project))
return
}
setGlobalStore(
"projects",
produce((draft) => {
draft.splice(result.index, 0, project)
}),
)
})
break
}
}
return
}
const [store, setStore] = child(directory)
switch (event.type) {
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
@@ -161,14 +207,14 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
})
Promise.all([
sdk.client.project.list().then((x) =>
sdk.client.project.list().then(async (x) => {
const filtered = x.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
const projects = await Promise.all(filtered.map((p) => ensureProjectColor(p, sdk)))
setGlobalStore(
"projects",
x
.data!.filter((x) => !x.worktree.includes("opencode-test") && x.vcs)
.sort((a, b) => b.time.created - a.time.created),
),
),
projects.sort((a, b) => a.id.localeCompare(b.id)),
)
}),
]).then(() => setGlobalStore("ready", true))
return {
+22 -11
View File
@@ -12,7 +12,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
projects: [] as { directory: string; expanded: boolean }[],
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
opened: false,
width: 280,
@@ -26,7 +26,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
{
name: "default-layout.v4",
name: "default-layout.v6",
},
)
@@ -43,32 +43,43 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
onMount(() => {
Promise.all(
store.projects.map(({ directory }) => {
return loadProjectSessions(directory)
store.projects.map(({ worktree }) => {
return loadProjectSessions(worktree)
}),
)
})
function enrich(project: { worktree: string; expanded: boolean }) {
const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
if (!metadata) return []
return [
{
...project,
...metadata,
},
]
}
return {
projects: {
list: createMemo(() => store.projects),
list: createMemo(() => store.projects.flatMap(enrich)),
open(directory: string) {
if (store.projects.find((x) => x.directory === directory)) return
if (store.projects.find((x) => x.worktree === directory)) return
loadProjectSessions(directory)
setStore("projects", (x) => [...x, { directory, expanded: true }])
setStore("projects", (x) => [...x, { worktree: directory, expanded: true }])
},
close(directory: string) {
setStore("projects", (x) => x.filter((x) => x.directory !== directory))
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
},
expand(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: true } : x)))
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
},
collapse(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
},
move(directory: string, toIndex: number) {
setStore("projects", (projects) => {
const fromIndex = projects.findIndex((x) => x.directory === directory)
const fromIndex = projects.findIndex((x) => x.worktree === directory)
if (fromIndex === -1 || fromIndex === toIndex) return projects
const result = [...projects]
const [item] = result.splice(fromIndex, 1)
+1 -4
View File
@@ -7,7 +7,6 @@ import { TextSelection } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { useSDK } from "./sdk"
export type LocalPTY = {
@@ -25,9 +24,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const sdk = useSDK()
const params = useParams()
const sync = useSync()
const name = createMemo(
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v2`,
)
const name = createMemo(() => `${params.dir}/session${params.id ? "/" + params.id : ""}.v3`)
const [store, setStore] = makePersisted(
createStore<{
+6 -1
View File
@@ -13,7 +13,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const [store, setStore] = globalSync.child(sdk.directory)
const load = {
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!)),
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
@@ -41,6 +41,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
get ready() {
return store.ready
},
get project() {
const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id)
if (match.found) return globalSync.data.projects[match.index]
return undefined
},
session: {
get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id)
+11 -4
View File
@@ -7,6 +7,7 @@ import { useNavigate } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
import { usePlatform } from "@/context/platform"
import { DateTime } from "luxon"
export default function Home() {
const sync = useGlobalSync()
@@ -47,8 +48,12 @@ export default function Home() {
</Button>
</Show>
</div>
<ol class="flex flex-col gap-2">
<For each={sync.data.projects.slice(0, 5)}>
<ul class="flex flex-col gap-2">
<For
each={sync.data.projects
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
.slice(0, 5)}
>
{(project) => (
<Button
size="large"
@@ -57,11 +62,13 @@ export default function Home() {
onClick={() => openProject(project.worktree)}
>
{project.worktree}
<div class="text-14-regular text-text-weak">10m ago</div>
<div class="text-14-regular text-text-weak">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</div>
</Button>
)}
</For>
</ol>
</ul>
</div>
</Match>
<Match when={true}>
+72 -39
View File
@@ -16,7 +16,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Session, Project } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore } from "solid-js/store"
import {
@@ -106,8 +106,8 @@ export default function Layout(props: ParentProps) {
const { draggable, droppable } = event
if (draggable && droppable) {
const projects = layout.projects.list()
const fromIndex = projects.findIndex((p) => p.directory === draggable.id.toString())
const toIndex = projects.findIndex((p) => p.directory === droppable.id.toString())
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== -1) {
layout.projects.move(draggable.id.toString(), toIndex)
}
@@ -140,27 +140,74 @@ export default function Layout(props: ParentProps) {
return <></>
}
const SortableProject = (props: { project: { directory: string; expanded: boolean } }): JSX.Element => {
const sortable = createSortable(props.project.directory)
const [projectStore] = globalSync.child(props.project.directory)
const slug = createMemo(() => base64Encode(props.project.directory))
const name = createMemo(() => getFilename(props.project.directory))
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
return (
<Switch>
<Match when={layout.sidebar.opened()}>
<Button
as={"div"}
variant="ghost"
data-active
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
>
<div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</div>
</Button>
</Match>
<Match when={true}>
<Button
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
data-selected={props.project.worktree === currentDirectory()}
onClick={() => navigateToProject(props.project.worktree)}
>
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
</Button>
</Match>
</Switch>
)
}
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
const sortable = createSortable(props.project.worktree)
const [projectStore] = globalSync.child(props.project.worktree)
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-0": sortable.isActiveDraggable }}>
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Switch>
<Match when={layout.sidebar.opened()}>
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
<Button
as={"div"}
variant="ghost"
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none"
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
>
<Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
background="var(--surface-info-base)"
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full group-hover/session:hidden"
/>
<Icon
@@ -176,7 +223,7 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={() => closeProject(props.project.directory)}>
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
<DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
@@ -238,18 +285,8 @@ export default function Layout(props: ParentProps) {
</Collapsible>
</Match>
<Match when={true}>
<Tooltip placement="right" value={props.project.directory}>
<Button
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none"
data-selected={props.project.directory === currentDirectory()}
onClick={() => navigateToProject(props.project.directory)}
>
<div class="size-6 shrink-0 inset-0">
<Avatar fallback={name()} background="var(--surface-info-base)" class="size-full" />
</div>
</Button>
<Tooltip placement="right" value={props.project.worktree}>
<ProjectVisual project={props.project} />
</Tooltip>
</Match>
</Switch>
@@ -258,16 +295,12 @@ export default function Layout(props: ParentProps) {
}
const ProjectDragOverlay = (): JSX.Element => {
const activeName = createMemo(() => {
if (!store.activeDraggable) return undefined
return getFilename(store.activeDraggable)
})
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeDraggable))
return (
<Show when={activeName()}>
{(name) => (
<div class="flex items-center gap-3 px-2 py-1 bg-background-stronger rounded-md border border-border-weak-base">
<Avatar fallback={name()} background="var(--surface-info-base)" class="size-6" />
<span class="text-14-medium text-text-strong">{name()}</span>
<Show when={project()}>
{(p) => (
<div class="bg-background-base rounded-md">
<ProjectVisual project={p()} />
</div>
)}
</Show>
@@ -294,7 +327,7 @@ export default function Layout(props: ParentProps) {
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.directory)}
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
@@ -386,7 +419,7 @@ export default function Layout(props: ParentProps) {
<Button
variant="ghost"
size="large"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@@ -422,7 +455,7 @@ export default function Layout(props: ParentProps) {
<DragDropSensors />
<ConstrainDragXAxis />
<div class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar">
<SortableProvider ids={layout.projects.list().map((p) => p.directory)}>
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
</SortableProvider>
</div>
@@ -435,7 +468,7 @@ export default function Layout(props: ParentProps) {
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="folder-add-left"
@@ -448,7 +481,7 @@ export default function Layout(props: ParentProps) {
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
disabled
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="settings-gear"
@@ -461,7 +494,7 @@ export default function Layout(props: ParentProps) {
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="bubble-5"
+102 -40
View File
@@ -28,7 +28,7 @@ import {
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { useSession } from "@/context/session"
import { useSession, type LocalPTY } from "@/context/session"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Terminal } from "@/components/terminal"
@@ -43,6 +43,7 @@ export default function Page() {
clickTimer: undefined as number | undefined,
fileSelectOpen: false,
activeDraggable: undefined as string | undefined,
activeTerminalDraggable: undefined as string | undefined,
})
let inputRef!: HTMLDivElement
@@ -178,6 +179,49 @@ export default function Page() {
setStore("activeDraggable", undefined)
}
const handleTerminalDragStart = (event: unknown) => {
const id = getDraggableId(event)
if (!id) return
setStore("activeTerminalDraggable", id)
}
const handleTerminalDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const terminals = session.terminal.all()
const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString())
const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString())
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
session.terminal.move(draggable.id.toString(), toIndex)
}
}
}
const handleTerminalDragEnd = () => {
setStore("activeTerminalDraggable", undefined)
}
const SortableTerminalTab = (props: { terminal: LocalPTY }): JSX.Element => {
const sortable = createSortable(props.terminal.id)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.terminal.id}
closeButton={
session.terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(props.terminal.id)} />
)
}
>
{props.terminal.title}
</Tabs.Trigger>
</div>
</div>
)
}
const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => {
return (
<div class="flex items-center gap-x-1.5">
@@ -401,15 +445,19 @@ export default function Page() {
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(sync.data.project.time.created).toRelative()}
</span>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
</Match>
</Switch>
@@ -614,40 +662,54 @@ export default function Page() {
onResize={layout.terminal.resize}
onCollapse={layout.terminal.close}
/>
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<DragDropProvider
onDragStart={handleTerminalDragStart}
onDragEnd={handleTerminalDragEnd}
onDragOver={handleTerminalDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<SortableProvider ids={session.terminal.all().map((t) => t.id)}>
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Trigger
value={terminal.id}
closeButton={
session.terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(terminal.id)} />
)
}
>
{terminal.title}
</Tabs.Trigger>
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>
</Tabs>
</Tabs>
<DragOverlay>
<Show when={store.activeTerminalDraggable}>
{(draggedId) => {
const terminal = createMemo(() => session.terminal.all().find((t) => t.id === draggedId()))
return (
<Show when={terminal()}>
{(t) => (
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
{t().title}
</div>
)}
</Show>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</Show>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.138",
"version": "1.0.141",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,4 +1,4 @@
import { FileDiff, Message, Model, Part, Session, SessionStatus } from "@opencode-ai/sdk"
import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
import { fn } from "@opencode-ai/util/fn"
import { iife } from "@opencode-ai/util/iife"
import { Identifier } from "@opencode-ai/util/identifier"
@@ -1,4 +1,4 @@
import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk"
import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/ui/context"
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.138"
version = "1.0.141"
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.138/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.141/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.138/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.141/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.138/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.141/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.138/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.141/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.138/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.141/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.138",
"version": "1.0.141",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+7 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.138",
"version": "1.0.141",
"name": "opencode",
"type": "module",
"private": true,
@@ -9,7 +9,12 @@
"test": "bun test",
"build": "./script/build.ts",
"dev": "bun run --conditions=browser ./src/index.ts",
"random": "echo 'Random script updated at $(date)'"
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
"lint": "echo 'Running lint checks...' && bun test --coverage",
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
},
"bin": {
"opencode": "./bin/opencode"
+15 -16
View File
@@ -35,26 +35,25 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
2,
),
)
for (const [name] of Object.entries(binaries)) {
try {
process.chdir(`./dist/${name}`)
if (process.platform !== "win32") {
await $`chmod 755 -R .`
}
await $`bun publish --access public --tag ${Script.channel}`
} finally {
process.chdir(dir)
}
}
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${Script.channel}`
const tags = [Script.channel]
if (!Script.preview) {
const major = Script.version.split(".")[0]
const majorTag = `latest-${major}`
for (const [name] of Object.entries(binaries)) {
await $`cd dist/${name} && npm dist-tag add ${name}@${Script.version} ${majorTag}`
tags[0] = `latest-${major}`
}
const tasks = Object.entries(binaries).map(async ([name]) => {
if (process.platform !== "win32") {
await $`chmod 755 -R .`.cwd(`./dist/${name}`)
}
await $`cd ./dist/${pkg.name} && npm dist-tag add ${pkg.name}-ai@${Script.version} ${majorTag}`
await $`bun pm pack`.cwd(`./dist/${name}`)
for (const tag of tags) {
await $`npm publish *.tgz --access public --tag ${tag}`.cwd(`./dist/${name}`)
}
})
await Promise.all(tasks)
for (const tag of tags) {
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${tag}`
}
if (!Script.preview) {
-1
View File
@@ -25,7 +25,6 @@ import { Provider } from "../provider/provider"
import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2"
import { Config } from "@/config/config"
import { MCP } from "@/mcp"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
+12 -1
View File
@@ -127,7 +127,18 @@ export namespace BunProc {
await runInstall()
parsed.dependencies[pkg] = version
// Resolve actual version from installed package when using "latest"
// This ensures subsequent starts use the cached version until explicitly updated
let resolvedVersion = version
if (version === "latest") {
const installedPkgJson = Bun.file(path.join(mod, "package.json"))
const installedPkg = await installedPkgJson.json().catch(() => null)
if (installedPkg?.version) {
resolvedVersion = installedPkg.version
}
}
parsed.dependencies[pkg] = resolvedVersion
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
return mod
}
+43
View File
@@ -0,0 +1,43 @@
import z from "zod"
import type { ZodType } from "zod"
import { Log } from "../util/log"
export namespace BusEvent {
const log = Log.create({ service: "event" })
export type Definition = ReturnType<typeof define>
const registry = new Map<string, Definition>()
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
const result = {
type,
properties,
}
registry.set(type, result)
return result
}
export function payloads() {
return z
.discriminatedUnion(
"type",
registry
.entries()
.map(([type, def]) => {
return z
.object({
type: z.literal(type),
properties: def.properties,
})
.meta({
ref: "Event" + "." + def.type,
})
})
.toArray() as any,
)
.meta({
ref: "Event",
})
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { EventEmitter } from "events"
export const GlobalBus = new EventEmitter<{
event: [
{
directory: string
directory?: string
payload: any
},
]
+5 -41
View File
@@ -1,7 +1,7 @@
import z from "zod"
import type { ZodType } from "zod"
import { Log } from "../util/log"
import { Instance } from "../project/instance"
import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global"
export namespace Bus {
@@ -9,10 +9,6 @@ export namespace Bus {
type Subscription = (event: any) => void
const disposedEventType = "server.instance.disposed"
export type EventDefinition = ReturnType<typeof event>
const registry = new Map<string, EventDefinition>()
const state = Instance.state(
() => {
const subscriptions = new Map<any, Subscription[]>()
@@ -36,46 +32,14 @@ export namespace Bus {
},
)
export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
const result = {
type,
properties,
}
registry.set(type, result)
return result
}
export const InstanceDisposed = event(
export const InstanceDisposed = BusEvent.define(
disposedEventType,
z.object({
directory: z.string(),
}),
)
export function payloads() {
return z
.discriminatedUnion(
"type",
registry
.entries()
.map(([type, def]) => {
return z
.object({
type: z.literal(type),
properties: def.properties,
})
.meta({
ref: "Event" + "." + def.type,
})
})
.toArray() as any,
)
.meta({
ref: "Event",
})
}
export async function publish<Definition extends EventDefinition>(
export async function publish<Definition extends BusEvent.Definition>(
def: Definition,
properties: z.output<Definition["properties"]>,
) {
@@ -100,14 +64,14 @@ export namespace Bus {
return Promise.all(pending)
}
export function subscribe<Definition extends EventDefinition>(
export function subscribe<Definition extends BusEvent.Definition>(
def: Definition,
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
) {
return raw(def.type, callback)
}
export function once<Definition extends EventDefinition>(
export function once<Definition extends BusEvent.Definition>(
def: Definition,
callback: (event: {
type: Definition["type"]
+4 -3
View File
@@ -1,9 +1,10 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
export const TuiEvent = {
PromptAppend: Bus.event("tui.prompt.append", z.object({ text: z.string() })),
CommandExecute: Bus.event(
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
CommandExecute: BusEvent.define(
"tui.command.execute",
z.object({
command: z.union([
@@ -27,7 +28,7 @@ export const TuiEvent = {
]),
}),
),
ToastShow: Bus.event(
ToastShow: BusEvent.define(
"tui.toast.show",
z.object({
title: z.string().optional(),
@@ -1057,6 +1057,7 @@ function UserMessage(props: {
</Show>
}
>
<span> </span>
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
</Show>
</text>
+3 -2
View File
@@ -1,14 +1,15 @@
import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event"
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
import { Identifier } from "../id/id"
import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_REVIEW from "./template/review.txt"
export namespace Command {
export const Event = {
Executed: Bus.event(
Executed: BusEvent.define(
"command.executed",
z.object({
name: z.string(),
+3 -2
View File
@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { $ } from "bun"
import type { BunFile } from "bun"
import { formatPatch, structuredPatch } from "diff"
@@ -111,7 +112,7 @@ export namespace File {
}
export const Event = {
Edited: Bus.event(
Edited: BusEvent.define(
"file.edited",
z.object({
file: z.string(),
+5 -3
View File
@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { FileIgnore } from "./ignore"
@@ -8,6 +9,7 @@ import { Config } from "../config/config"
import { createWrapper } from "@parcel/watcher/wrapper"
import { lazy } from "@/util/lazy"
import type ParcelWatcher from "@parcel/watcher"
import { $ } from "bun"
declare const OPENCODE_LIBC: string | undefined
@@ -15,7 +17,7 @@ export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" })
export const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"file.watcher.updated",
z.object({
file: z.string(),
@@ -65,7 +67,7 @@ export namespace FileWatcher {
}),
)
const vcsDir = Instance.project.vcsDir
const vcsDir = await $`git rev-parse --git-dir`.quiet().nothrow().cwd(Instance.worktree).text()
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
subs.push(
await watcher().subscribe(vcsDir, subscribe, {
+9 -2
View File
@@ -11,8 +11,6 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH =
process.env["OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH"]
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
@@ -20,9 +18,18 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
export const OPENCODE_ENABLE_EXA =
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
}
function number(key: string) {
const value = process.env[key]
if (!value) return undefined
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
}
}
+3 -2
View File
@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { spawn } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Log } from "../util/log"
import { Bus } from "../bus"
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
@@ -16,7 +17,7 @@ export namespace Ide {
const log = Log.create({ service: "ide" })
export const Event = {
Installed: Bus.event(
Installed: BusEvent.define(
"ide.installed",
z.object({
ide: z.string(),
+4 -3
View File
@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import path from "path"
import { $ } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { iife } from "@/util/iife"
@@ -17,13 +18,13 @@ export namespace Installation {
export type Method = Awaited<ReturnType<typeof method>>
export const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"installation.updated",
z.object({
version: z.string(),
}),
),
UpdateAvailable: Bus.event(
UpdateAvailable: BusEvent.define(
"installation.update-available",
z.object({
version: z.string(),
+3 -2
View File
@@ -1,9 +1,10 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import path from "path"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus"
import z from "zod"
import type { LSPServer } from "./server"
import { NamedError } from "@opencode-ai/util/error"
@@ -25,7 +26,7 @@ export namespace LSPClient {
)
export const Event = {
Diagnostics: Bus.event(
Diagnostics: BusEvent.define(
"lsp.client.diagnostics",
z.object({
serverID: z.string(),
+3 -2
View File
@@ -1,3 +1,5 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
@@ -6,13 +8,12 @@ import z from "zod"
import { Config } from "../config/config"
import { spawn } from "child_process"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
export namespace LSP {
const log = Log.create({ service: "lsp" })
export const Event = {
Updated: Bus.event("lsp.updated", z.object({})),
Updated: BusEvent.define("lsp.updated", z.object({})),
}
export const Range = z
+62
View File
@@ -209,6 +209,68 @@ export namespace LSPServer {
},
}
export const Biome: Info = {
id: "biome",
root: NearestRoot([
"biome.json",
"biome.jsonc",
"package-lock.json",
"bun.lockb",
"bun.lock",
"pnpm-lock.yaml",
"yarn.lock",
]),
extensions: [
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
".mts",
".cts",
".json",
".jsonc",
".vue",
".astro",
".svelte",
".css",
".graphql",
".gql",
".html",
],
async spawn(root) {
const localBin = path.join(root, "node_modules", ".bin", "biome")
let bin: string | undefined
if (await Bun.file(localBin).exists()) bin = localBin
if (!bin) {
const found = Bun.which("biome")
if (found) bin = found
}
let args = ["lsp-proxy", "--stdio"]
if (!bin) {
const resolved = await Bun.resolve("biome", root).catch(() => undefined)
if (!resolved) return
bin = BunProc.which()
args = ["x", "biome", "lsp-proxy", "--stdio"]
}
const proc = spawn(bin, args, {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
}
},
}
export const Gopls: Info = {
id: "gopls",
root: async (file) => {
+4 -3
View File
@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { Identifier } from "../id/id"
import { Plugin } from "../plugin"
@@ -38,8 +39,8 @@ export namespace Permission {
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event("permission.updated", Info),
Replied: Bus.event(
Updated: BusEvent.define("permission.updated", Info),
Replied: BusEvent.define(
"permission.replied",
z.object({
sessionID: z.string(),
+140 -57
View File
@@ -7,6 +7,10 @@ import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
import { Session } from "../session"
import { work } from "../util/queue"
import { fn } from "@opencode-ai/util/fn"
import { BusEvent } from "@/bus/bus-event"
import { iife } from "@/util/iife"
import { GlobalBus } from "@/bus/global"
export namespace Project {
const log = Log.create({ service: "project" })
@@ -14,11 +18,17 @@ export namespace Project {
.object({
id: z.string(),
worktree: z.string(),
vcsDir: z.string().optional(),
vcs: z.literal("git").optional(),
name: z.string().optional(),
icon: z
.object({
url: z.string().optional(),
color: z.string().optional(),
})
.optional(),
time: z.object({
created: z.number(),
updated: z.number().optional(),
updated: z.number(),
initialized: z.number().optional(),
}),
})
@@ -27,78 +37,123 @@ export namespace Project {
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: BusEvent.define("project.updated", Info),
}
export async function fromDirectory(directory: string) {
log.info("fromDirectory", { directory })
const matches = Filesystem.up({ targets: [".git"], start: directory })
const git = await matches.next().then((x) => x.value)
await matches.return()
if (!git) {
const project: Info = {
const { id, worktree, vcs } = await iife(async () => {
const matches = Filesystem.up({ targets: [".git"], start: directory })
const git = await matches.next().then((x) => x.value)
await matches.return()
if (git) {
let worktree = path.dirname(git)
let id = await Bun.file(path.join(git, "opencode"))
.text()
.then((x) => x.trim())
.catch(() => {})
if (!id) {
const roots = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
id = roots[0]
if (id) Bun.file(path.join(git, "opencode")).write(id)
}
if (!id)
return {
id: "global",
worktree,
vcs: "git",
}
worktree = await $`git rev-parse --show-toplevel`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) => path.resolve(worktree, x.trim()))
return { id, worktree, vcs: "git" }
}
return {
id: "global",
worktree: "/",
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
})
let existing = await Storage.read<Info>(["project", id]).catch(() => undefined)
if (!existing) {
existing = {
id,
worktree,
vcs: vcs as Info["vcs"],
time: {
created: Date.now(),
updated: Date.now(),
},
}
await Storage.write<Info>(["project", "global"], project)
return project
if (id !== "global") {
await migrateFromGlobal(id, worktree)
}
}
let worktree = path.dirname(git)
const timer = log.time("git.rev-parse")
let id = await Bun.file(path.join(git, "opencode"))
.text()
.then((x) => x.trim())
.catch(() => {})
if (!id) {
const roots = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
id = roots[0]
if (id) Bun.file(path.join(git, "opencode")).write(id)
}
timer.stop()
worktree = await $`git rev-parse --show-toplevel`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) => path.resolve(worktree, x.trim()))
const vcsDir = await $`git rev-parse --git-dir`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) => path.resolve(worktree, x.trim()))
const projectID = id || "global"
const existing = id ? await Storage.read<Info>(["project", id]).catch(() => undefined) : undefined
if (!existing && id) {
await migrateFromGlobal(projectID, worktree)
}
const project: Info = {
if (Flag.OPENCODE_EXPERIMENTAL) discover(existing)
const result: Info = {
...existing,
id: projectID,
worktree,
vcsDir,
vcs: "git",
vcs: vcs as Info["vcs"],
time: {
created: Date.now(),
...existing?.time,
...existing.time,
updated: Date.now(),
},
}
await Storage.write<Info>(["project", projectID], project)
return project
await Storage.write<Info>(["project", id], result)
GlobalBus.emit("event", {
payload: {
type: Event.Updated.type,
properties: result,
},
})
return result
}
export async function discover(input: Info) {
if (input.vcs !== "git") return
if (input.icon?.url) return
const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}")
const matches = await Array.fromAsync(
glob.scan({
cwd: input.worktree,
absolute: true,
onlyFiles: true,
followSymlinks: false,
dot: false,
}),
)
const shortest = matches.sort((a, b) => a.length - b.length)[0]
if (!shortest) return
const file = Bun.file(shortest)
const buffer = await file.arrayBuffer()
const base64 = Buffer.from(buffer).toString("base64")
const mime = file.type || "image/png"
const url = `data:${mime};base64,${base64}`
await update({
projectID: input.id,
icon: {
url,
},
})
return
}
async function migrateFromGlobal(newProjectID: string, worktree: string) {
@@ -135,4 +190,32 @@ export namespace Project {
const keys = await Storage.list(["project"])
return await Promise.all(keys.map((x) => Storage.read<Info>(x)))
}
export const update = fn(
z.object({
projectID: z.string(),
name: z.string().optional(),
icon: Info.shape.icon.optional(),
}),
async (input) => {
const result = await Storage.update<Info>(["project", input.projectID], (draft) => {
if (input.name !== undefined) draft.name = input.name
if (input.icon !== undefined) {
draft.icon = {
...draft.icon,
}
if (input.icon.url !== undefined) draft.icon.url = input.icon.url
if (input.icon.color !== undefined) draft.icon.color = input.icon.color
}
draft.time.updated = Date.now()
})
GlobalBus.emit("event", {
payload: {
type: Event.Updated.type,
properties: result,
},
})
return result
},
)
}
+5 -6
View File
@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { $ } from "bun"
import path from "path"
import z from "zod"
import { Log } from "@/util/log"
import { Bus } from "@/bus"
import { Instance } from "./instance"
import { FileWatcher } from "@/file/watcher"
@@ -10,7 +11,7 @@ const log = Log.create({ service: "vcs" })
export namespace Vcs {
export const Event = {
BranchUpdated: Bus.event(
BranchUpdated: BusEvent.define(
"vcs.branch.updated",
z.object({
branch: z.string().optional(),
@@ -39,16 +40,14 @@ export namespace Vcs {
const state = Instance.state(
async () => {
const vcsDir = Instance.project.vcsDir
if (Instance.project.vcs !== "git" || !vcsDir) {
if (Instance.project.vcs !== "git") {
return { branch: async () => undefined, unsubscribe: undefined }
}
let current = await currentBranch()
log.info("initialized", { branch: current })
const head = path.join(vcsDir, "HEAD")
const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, async (evt) => {
if (evt.properties.file !== head) return
if (evt.properties.file.endsWith("HEAD")) return
const next = await currentBranch()
if (next !== current) {
log.info("branch changed", { from: current, to: next })
+10
View File
@@ -17,6 +17,16 @@ export namespace ModelsDev {
reasoning: z.boolean(),
temperature: z.boolean(),
tool_call: z.boolean(),
interleaved: z
.union([
z.literal(true),
z
.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
})
.strict(),
])
.optional(),
cost: z
.object({
input: z.number(),
@@ -349,6 +349,12 @@ export namespace Provider {
video: z.boolean(),
pdf: z.boolean(),
}),
interleaved: z.union([
z.boolean(),
z.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
}),
]),
}),
cost: z.object({
input: z.number(),
@@ -450,6 +456,7 @@ export namespace Provider {
video: model.modalities?.output?.includes("video") ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? false,
},
interleaved: model.interleaved ?? false,
},
}
}
@@ -567,6 +574,7 @@ export namespace Provider {
video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved: model.interleaved ?? false,
},
cost: {
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
+17 -1
View File
@@ -273,7 +273,23 @@ export namespace ProviderTransform {
return options
}
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
export function providerOptions(model: Provider.Model, options: { [x: string]: any }, messages: ModelMessage[]) {
if (model.capabilities.interleaved && typeof model.capabilities.interleaved === "object") {
const cot = []
const assistantMessages = messages.filter((msg) => msg.role === "assistant")
for (const msg of assistantMessages) {
for (const part of msg.content) {
if (typeof part === "string") {
continue
}
if (part.type === "reasoning") {
cot.push(part)
}
}
}
options[model.capabilities.interleaved.field] = cot
}
switch (model.api.npm) {
case "@ai-sdk/openai":
case "@ai-sdk/azure":
+6 -5
View File
@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { type IPty } from "bun-pty"
import z from "zod"
import { Identifier } from "../id/id"
import { Log } from "../util/log"
import { Bus } from "../bus"
import type { WSContext } from "hono/ws"
import { Instance } from "../project/instance"
import { shell } from "@opencode-ai/util/shell"
@@ -73,10 +74,10 @@ export namespace Pty {
export type UpdateInput = z.infer<typeof UpdateInput>
export const Event = {
Created: Bus.event("pty.created", z.object({ info: Info })),
Updated: Bus.event("pty.updated", z.object({ info: Info })),
Exited: Bus.event("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
Deleted: Bus.event("pty.deleted", z.object({ id: Identifier.schema("pty") })),
Created: BusEvent.define("pty.created", z.object({ info: Info })),
Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
Exited: BusEvent.define("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
Deleted: BusEvent.define("pty.deleted", z.object({ id: Identifier.schema("pty") })),
}
interface ActiveSession {
+30 -1
View File
@@ -1,8 +1,10 @@
import { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi"
import { Instance } from "../project/instance"
import { Project } from "../project/project"
import z from "zod"
import { errors } from "./error"
export const ProjectRoute = new Hono()
.get(
@@ -48,3 +50,30 @@ export const ProjectRoute = new Hono()
return c.json(Instance.project)
},
)
.patch(
"/:projectID",
describeRoute({
summary: "Update project",
description: "Update project properties such as name, icon and color.",
operationId: "project.update",
responses: {
200: {
description: "Updated project information",
content: {
"application/json": {
schema: resolver(Project.Info),
},
},
},
...errors(400, 404),
},
}),
validator("param", z.object({ projectID: z.string() })),
validator("json", Project.update.schema.omit({ projectID: true })),
async (c) => {
const projectID = c.req.valid("param").projectID
const body = c.req.valid("json")
const project = await Project.update({ ...body, projectID })
return c.json(project)
},
)
+7 -6
View File
@@ -1,5 +1,7 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global"
import { Log } from "../util/log"
import { Bus } from "../bus"
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
import { Hono } from "hono"
import { cors } from "hono/cors"
@@ -8,7 +10,7 @@ import { proxy } from "hono/proxy"
import { Session } from "../session"
import z from "zod"
import { Provider } from "../provider/provider"
import { mapValues, pipe } from "remeda"
import { mapValues } from "remeda"
import { NamedError } from "@opencode-ai/util/error"
import { ModelsDev } from "../provider/models"
import { Ripgrep } from "../file/ripgrep"
@@ -41,7 +43,6 @@ import type { ContentfulStatusCode } from "hono/utils/http-status"
import { TuiEvent } from "@/cli/cmd/tui/event"
import { Snapshot } from "@/snapshot"
import { SessionSummary } from "@/session/summary"
import { GlobalBus } from "@/bus/global"
import { SessionStatus } from "@/session/status"
import { upgradeWebSocket, websocket } from "hono/bun"
import { errors } from "./error"
@@ -54,7 +55,7 @@ export namespace Server {
const log = Log.create({ service: "server" })
export const Event = {
Connected: Bus.event("server.connected", z.object({})),
Connected: BusEvent.define("server.connected", z.object({})),
}
const app = new Hono()
@@ -109,7 +110,7 @@ export namespace Server {
z
.object({
directory: z.string(),
payload: Bus.payloads(),
payload: BusEvent.payloads(),
})
.meta({
ref: "GlobalEvent",
@@ -2393,7 +2394,7 @@ export namespace Server {
description: "Event stream",
content: {
"text/event-stream": {
schema: resolver(Bus.payloads()),
schema: resolver(BusEvent.payloads()),
},
},
},
+4 -2
View File
@@ -1,3 +1,5 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { wrapLanguageModel, type ModelMessage } from "ai"
import { Session } from "."
import { Identifier } from "../id/id"
@@ -5,7 +7,6 @@ import { Instance } from "../project/instance"
import { Provider } from "../provider/provider"
import { MessageV2 } from "./message-v2"
import { SystemPrompt } from "./system"
import { Bus } from "../bus"
import z from "zod"
import { SessionPrompt } from "./prompt"
import { Flag } from "../flag/flag"
@@ -21,7 +22,7 @@ export namespace SessionCompaction {
const log = Log.create({ service: "session.compaction" })
export const Event = {
Compacted: Bus.event(
Compacted: BusEvent.define(
"session.compacted",
z.object({
sessionID: z.string(),
@@ -142,6 +143,7 @@ export namespace SessionCompaction {
providerOptions: ProviderTransform.providerOptions(
model,
pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
[],
),
headers: model.headers,
abortSignal: input.abort,
+7 -6
View File
@@ -1,7 +1,8 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Decimal } from "decimal.js"
import z from "zod"
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
import { Bus } from "../bus"
import { Config } from "../config/config"
import { Flag } from "../flag/flag"
import { Identifier } from "../id/id"
@@ -85,32 +86,32 @@ export namespace Session {
export type ShareInfo = z.output<typeof ShareInfo>
export const Event = {
Created: Bus.event(
Created: BusEvent.define(
"session.created",
z.object({
info: Info,
}),
),
Updated: Bus.event(
Updated: BusEvent.define(
"session.updated",
z.object({
info: Info,
}),
),
Deleted: Bus.event(
Deleted: BusEvent.define(
"session.deleted",
z.object({
info: Info,
}),
),
Diff: Bus.event(
Diff: BusEvent.define(
"session.diff",
z.object({
sessionID: z.string(),
diff: Snapshot.FileDiff.array(),
}),
),
Error: Bus.event(
Error: BusEvent.define(
"session.error",
z.object({
sessionID: z.string().optional(),
+6 -5
View File
@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { NamedError } from "@opencode-ai/util/error"
import { Message } from "./message"
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
@@ -375,27 +376,27 @@ export namespace MessageV2 {
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"message.updated",
z.object({
info: Info,
}),
),
Removed: Bus.event(
Removed: BusEvent.define(
"message.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
}),
),
PartUpdated: Bus.event(
PartUpdated: BusEvent.define(
"message.part.updated",
z.object({
part: Part,
delta: z.string().optional(),
}),
),
PartRemoved: Bus.event(
PartRemoved: BusEvent.define(
"message.part.removed",
z.object({
sessionID: z.string(),
+34 -33
View File
@@ -515,6 +515,37 @@ export namespace SessionPrompt {
})
}
const messages = [
...system.map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
...MessageV2.toModelMessage(
msgs.filter((m) => {
if (m.info.role !== "assistant" || m.info.error === undefined) {
return true
}
if (
MessageV2.AbortedError.isInstance(m.info.error) &&
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
) {
return true
}
return false
}),
),
...(isLastStep
? [
{
role: "assistant" as const,
content: MAX_STEPS,
},
]
: []),
]
const result = await processor.process({
onError(error) {
log.error("stream error", {
@@ -562,42 +593,12 @@ export namespace SessionPrompt {
OUTPUT_TOKEN_MAX,
),
abortSignal: abort,
providerOptions: ProviderTransform.providerOptions(model, params.options),
providerOptions: ProviderTransform.providerOptions(model, params.options, messages),
stopWhen: stepCountIs(1),
temperature: params.temperature,
topP: params.topP,
toolChoice: isLastStep ? "none" : undefined,
messages: [
...system.map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
...MessageV2.toModelMessage(
msgs.filter((m) => {
if (m.info.role !== "assistant" || m.info.error === undefined) {
return true
}
if (
MessageV2.AbortedError.isInstance(m.info.error) &&
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
) {
return true
}
return false
}),
),
...(isLastStep
? [
{
role: "assistant" as const,
content: MAX_STEPS,
},
]
: []),
],
messages,
tools: model.capabilities.toolcall === false ? undefined : tools,
model: wrapLanguageModel({
model: language,
@@ -1464,7 +1465,7 @@ export namespace SessionPrompt {
await generateText({
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
providerOptions: ProviderTransform.providerOptions(small, options),
providerOptions: ProviderTransform.providerOptions(small, options, []),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
+3 -2
View File
@@ -1,3 +1,4 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Instance } from "@/project/instance"
import z from "zod"
@@ -24,7 +25,7 @@ export namespace SessionStatus {
export type Info = z.infer<typeof Info>
export const Event = {
Status: Bus.event(
Status: BusEvent.define(
"session.status",
z.object({
sessionID: z.string(),
@@ -32,7 +33,7 @@ export namespace SessionStatus {
}),
),
// deprecated
Idle: Bus.event(
Idle: BusEvent.define(
"session.idle",
z.object({
sessionID: z.string(),
+2 -2
View File
@@ -91,7 +91,7 @@ export namespace SessionSummary {
if (textPart && !userMsg.summary?.title) {
const result = await generateText({
maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
providerOptions: ProviderTransform.providerOptions(small, options),
providerOptions: ProviderTransform.providerOptions(small, options, []),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
@@ -144,7 +144,7 @@ export namespace SessionSummary {
const result = await generateText({
model: language,
maxOutputTokens: 100,
providerOptions: ProviderTransform.providerOptions(small, options),
providerOptions: ProviderTransform.providerOptions(small, options, []),
messages: [
...SystemPrompt.summarize(small.providerID).map(
(x): ModelMessage => ({
+3 -2
View File
@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { Storage } from "../storage/storage"
export namespace Todo {
@@ -14,7 +15,7 @@ export namespace Todo {
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"todo.updated",
z.object({
sessionID: z.string(),
+2 -6
View File
@@ -16,12 +16,8 @@ import { Flag } from "@/flag/flag.ts"
import path from "path"
import { iife } from "@/util/iife"
const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
const MAX_OUTPUT_LENGTH = (() => {
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
})()
const DEFAULT_TIMEOUT = 2 * 60 * 1000
const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
const SIGKILL_TIMEOUT_MS = 200
export const log = Log.create({ service: "bash-tool" })
+1 -1
View File
@@ -93,7 +93,7 @@ export const ReadTool = Tool.define("read", {
throw new Error(`File not found: ${filepath}`)
}
const isImage = file.type.startsWith("image/")
const isImage = file.type.startsWith("image/") && file.type !== "image/svg+xml"
const isPdf = file.type === "application/pdf"
if (isImage || isPdf) {
const mime = file.type
+11 -5
View File
@@ -1,27 +1,33 @@
import { $ } from "bun"
import { realpathSync } from "fs"
import * as fs from "fs/promises"
import os from "os"
import path from "path"
// Strip null bytes from paths (defensive fix for CI environment issues)
function sanitizePath(p: string): string {
return p.replace(/\0/g, "")
}
type TmpDirOptions<T> = {
git?: boolean
init?: (dir: string) => Promise<T>
dispose?: (dir: string) => Promise<T>
}
export async function tmpdir<T>(options?: TmpDirOptions<T>) {
const dirpath = path.join(os.tmpdir(), "opencode-test-" + Math.random().toString(36).slice(2))
await $`mkdir -p ${dirpath}`.quiet()
const dirpath = sanitizePath(path.join(os.tmpdir(), "opencode-test-" + Math.random().toString(36).slice(2)))
await fs.mkdir(dirpath, { recursive: true })
if (options?.git) {
await $`git init`.cwd(dirpath).quiet()
await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
}
const extra = await options?.init?.(dirpath)
const realpath = sanitizePath(await fs.realpath(dirpath))
const result = {
[Symbol.asyncDispose]: async () => {
await options?.dispose?.(dirpath)
await $`rm -rf ${dirpath}`.quiet()
// await fs.rm(dirpath, { recursive: true, force: true })
},
path: realpathSync(dirpath),
path: realpath,
extra: extra as T,
}
return result
+153
View File
@@ -0,0 +1,153 @@
import { test, expect, mock, beforeEach } from "bun:test"
// Track what options were passed to each transport constructor
const transportCalls: Array<{
type: "streamable" | "sse"
url: string
options: { authProvider?: unknown; requestInit?: RequestInit }
}> = []
// Mock the transport constructors to capture their arguments
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: class MockStreamableHTTP {
constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
transportCalls.push({
type: "streamable",
url: url.toString(),
options: options ?? {},
})
}
async start() {
throw new Error("Mock transport cannot connect")
}
},
}))
mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
SSEClientTransport: class MockSSE {
constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
transportCalls.push({
type: "sse",
url: url.toString(),
options: options ?? {},
})
}
async start() {
throw new Error("Mock transport cannot connect")
}
},
}))
beforeEach(() => {
transportCalls.length = 0
})
// Import MCP after mocking
const { MCP } = await import("../../src/mcp/index")
const { Instance } = await import("../../src/project/instance")
const { tmpdir } = await import("../fixture/fixture")
test("headers are passed to transports when oauth is enabled (default)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
`${dir}/opencode.json`,
JSON.stringify({
$schema: "https://opencode.ai/config.json",
mcp: {
"test-server": {
type: "remote",
url: "https://example.com/mcp",
headers: {
Authorization: "Bearer test-token",
"X-Custom-Header": "custom-value",
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
// Trigger MCP initialization - it will fail to connect but we can check the transport options
await MCP.add("test-server", {
type: "remote",
url: "https://example.com/mcp",
headers: {
Authorization: "Bearer test-token",
"X-Custom-Header": "custom-value",
},
}).catch(() => {})
// Both transports should have been created with headers
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
for (const call of transportCalls) {
expect(call.options.requestInit).toBeDefined()
expect(call.options.requestInit?.headers).toEqual({
Authorization: "Bearer test-token",
"X-Custom-Header": "custom-value",
})
// OAuth should be enabled by default, so authProvider should exist
expect(call.options.authProvider).toBeDefined()
}
},
})
})
test("headers are passed to transports when oauth is explicitly disabled", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
transportCalls.length = 0
await MCP.add("test-server-no-oauth", {
type: "remote",
url: "https://example.com/mcp",
oauth: false,
headers: {
Authorization: "Bearer test-token",
},
}).catch(() => {})
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
for (const call of transportCalls) {
expect(call.options.requestInit).toBeDefined()
expect(call.options.requestInit?.headers).toEqual({
Authorization: "Bearer test-token",
})
// OAuth is disabled, so no authProvider
expect(call.options.authProvider).toBeUndefined()
}
},
})
})
test("no requestInit when headers are not provided", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
transportCalls.length = 0
await MCP.add("test-server-no-headers", {
type: "remote",
url: "https://example.com/mcp",
}).catch(() => {})
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
for (const call of transportCalls) {
// No headers means requestInit should be undefined
expect(call.options.requestInit).toBeUndefined()
}
},
})
})
+7 -5
View File
@@ -2,12 +2,14 @@
// xdg-basedir reads env vars at import time, so we must set these first
import os from "os"
import path from "path"
import fs from "fs/promises"
const testDataDir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
process.env["XDG_DATA_HOME"] = testDataDir
process.env["XDG_CACHE_HOME"] = path.join(testDataDir, "cache")
process.env["XDG_CONFIG_HOME"] = path.join(testDataDir, "config")
process.env["XDG_STATE_HOME"] = path.join(testDataDir, "state")
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
await fs.mkdir(dir, { recursive: true })
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
process.env["XDG_STATE_HOME"] = path.join(dir, "state")
// Clear provider env vars to ensure clean test state
delete process.env["ANTHROPIC_API_KEY"]
+31 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project/project"
import { Log } from "../../src/util/log"
import { Storage } from "../../src/storage/storage"
import { $ } from "bun"
import path from "path"
import { tmpdir } from "../fixture/fixture"
@@ -18,7 +19,6 @@ describe("Project.fromDirectory", () => {
expect(project.id).toBe("global")
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
expect(project.vcsDir).toBe(path.join(tmp.path, ".git"))
const opencodeFile = path.join(tmp.path, ".git", "opencode")
const fileExists = await Bun.file(opencodeFile).exists()
@@ -40,3 +40,33 @@ describe("Project.fromDirectory", () => {
expect(fileExists).toBe(true)
})
})
describe("Project.discover", () => {
test("should discover favicon.png in root", async () => {
await using tmp = await tmpdir({ git: true })
const project = await Project.fromDirectory(tmp.path)
const pngData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
await Bun.write(path.join(tmp.path, "favicon.png"), pngData)
await Project.discover(project)
const updated = await Storage.read<Project.Info>(["project", project.id])
expect(updated.icon).toBeDefined()
expect(updated.icon?.url).toStartWith("data:")
expect(updated.icon?.url).toContain("base64")
expect(updated.icon?.color).toBeUndefined()
})
test("should not discover non-image files", async () => {
await using tmp = await tmpdir({ git: true })
const project = await Project.fromDirectory(tmp.path)
await Bun.write(path.join(tmp.path, "favicon.txt"), "not an image")
await Project.discover(project)
const updated = await Storage.read<Project.Info>(["project", project.id])
expect(updated.icon).toBeUndefined()
})
})
@@ -130,6 +130,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.001,
@@ -184,6 +185,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.001,
@@ -236,6 +238,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.001,
@@ -281,6 +284,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.03,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.138",
"version": "1.0.141",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -17,5 +17,5 @@ for (const [key, value] of Object.entries(pkg.exports)) {
}
}
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
await $`bun publish --tag ${Script.channel} --access public`
await $`bun pm pack && npm publish *.tgz --tag ${Script.channel} --access public`
await Bun.write("package.json", JSON.stringify(original, null, 2))
+15 -4
View File
@@ -13,19 +13,30 @@ if (process.versions.bun !== expectedBunVersion) {
throw new Error(`This script requires bun@${expectedBunVersion}, but you are using bun@${process.versions.bun}`)
}
const CHANNEL = process.env["OPENCODE_CHANNEL"] ?? (await $`git branch --show-current`.text().then((x) => x.trim()))
const env = {
OPENCODE_CHANNEL: process.env["OPENCODE_CHANNEL"],
OPENCODE_BUMP: process.env["OPENCODE_BUMP"],
OPENCODE_VERSION: process.env["OPENCODE_VERSION"],
}
const CHANNEL = await (async () => {
if (env.OPENCODE_CHANNEL) return env.OPENCODE_CHANNEL
if (env.OPENCODE_BUMP) return "latest"
if (env.OPENCODE_VERSION && !env.OPENCODE_VERSION.startsWith("0.0.0-")) return "latest"
return await $`git branch --show-current`.text().then((x) => x.trim())
})()
const IS_PREVIEW = CHANNEL !== "latest"
const VERSION = await (async () => {
if (process.env["OPENCODE_VERSION"]) return process.env["OPENCODE_VERSION"]
if (env.OPENCODE_VERSION) return env.OPENCODE_VERSION
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest-1")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0)
const t = process.env["OPENCODE_BUMP"]?.toLowerCase()
const t = env.OPENCODE_BUMP?.toLowerCase()
if (t === "major") return `${major + 1}.0.0`
if (t === "minor") return `${major}.${minor + 1}.0`
return `${major}.${minor}.${patch + 1}`
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.138",
"version": "1.0.141",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1
View File
@@ -41,3 +41,4 @@ await $`bun prettier --write src/gen`
await $`bun prettier --write src/v2`
await $`rm -rf dist`
await $`bun tsc`
await $`rm openapi.json`
+2 -1
View File
@@ -19,5 +19,6 @@ for (const [key, value] of Object.entries(pkg.exports)) {
}
}
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
await $`bun publish --tag ${Script.channel} --access public`
await $`bun pm pack`
await $`npm publish *.tgz --tag ${Script.channel} --access public`
await Bun.write("package.json", JSON.stringify(original, null, 2))
+44
View File
@@ -51,6 +51,8 @@ import type {
PermissionRespondResponses,
ProjectCurrentResponses,
ProjectListResponses,
ProjectUpdateErrors,
ProjectUpdateResponses,
ProviderAuthResponses,
ProviderListResponses,
ProviderOauthAuthorizeErrors,
@@ -231,6 +233,48 @@ export class Project extends HeyApiClient {
...params,
})
}
/**
* Update project
*
* Update project properties such as name, icon and color.
*/
public update<ThrowOnError extends boolean = false>(
parameters: {
projectID: string
directory?: string
name?: string
icon?: {
url?: string
color?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "projectID" },
{ in: "query", key: "directory" },
{ in: "body", key: "name" },
{ in: "body", key: "icon" },
],
},
],
)
return (options?.client ?? this.client).patch<ProjectUpdateResponses, ProjectUpdateErrors, ThrowOnError>({
url: "/project/{projectID}",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Pty extends HeyApiClient {
+84 -20
View File
@@ -4,13 +4,6 @@ export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {})
}
export type EventServerInstanceDisposed = {
type: "server.instance.disposed"
properties: {
directory: string
}
}
export type EventInstallationUpdated = {
type: "installation.updated"
properties: {
@@ -25,6 +18,34 @@ export type EventInstallationUpdateAvailable = {
}
}
export type Project = {
id: string
worktree: string
vcs?: "git"
name?: string
icon?: {
url?: string
color?: string
}
time: {
created: number
updated: number
initialized?: number
}
}
export type EventProjectUpdated = {
type: "project.updated"
properties: Project
}
export type EventServerInstanceDisposed = {
type: "server.instance.disposed"
properties: {
directory: string
}
}
export type EventLspClientDiagnostics = {
type: "lsp.client.diagnostics"
properties: {
@@ -704,9 +725,10 @@ export type EventServerConnected = {
}
export type Event =
| EventServerInstanceDisposed
| EventInstallationUpdated
| EventInstallationUpdateAvailable
| EventProjectUpdated
| EventServerInstanceDisposed
| EventLspClientDiagnostics
| EventLspUpdated
| EventMessageUpdated
@@ -742,18 +764,6 @@ export type GlobalEvent = {
payload: Event
}
export type Project = {
id: string
worktree: string
vcsDir?: string
vcs?: "git"
time: {
created: number
updated?: number
initialized?: number
}
}
export type BadRequestError = {
data: unknown
errors: Array<{
@@ -1034,6 +1044,11 @@ export type ProviderConfig = {
reasoning?: boolean
temperature?: boolean
tool_call?: boolean
interleaved?:
| true
| {
field: "reasoning_content" | "reasoning_details"
}
cost?: {
input: number
output: number
@@ -1469,6 +1484,11 @@ export type Model = {
video: boolean
pdf: boolean
}
interleaved:
| boolean
| {
field: "reasoning_content" | "reasoning_details"
}
}
cost: {
input: number
@@ -1716,6 +1736,45 @@ export type ProjectCurrentResponses = {
export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses]
export type ProjectUpdateData = {
body?: {
name?: string
icon?: {
url?: string
color?: string
}
}
path: {
projectID: string
}
query?: {
directory?: string
}
url: "/project/{projectID}"
}
export type ProjectUpdateErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors]
export type ProjectUpdateResponses = {
/**
* Updated project information
*/
200: Project
}
export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses]
export type PtyListData = {
body?: never
path?: never
@@ -2977,6 +3036,11 @@ export type ProviderListResponses = {
reasoning: boolean
temperature: boolean
tool_call: boolean
interleaved?:
| true
| {
field: "reasoning_content" | "reasoning_details"
}
cost?: {
input: number
output: number
+226 -57
View File
@@ -102,6 +102,93 @@
]
}
},
"/project/{projectID}": {
"patch": {
"operationId": "project.update",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "path",
"name": "projectID",
"schema": {
"type": "string"
},
"required": true
}
],
"summary": "Update project",
"description": "Update project properties such as name, icon and color.",
"responses": {
"200": {
"description": "Updated project information",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotFoundError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"icon": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"color": {
"type": "string"
}
}
}
}
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.project.update({\n ...\n})"
}
]
}
},
"/pty": {
"get": {
"operationId": "pty.list",
@@ -2716,6 +2803,25 @@
"tool_call": {
"type": "boolean"
},
"interleaved": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "object",
"properties": {
"field": {
"type": "string",
"enum": ["reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
"additionalProperties": false
}
]
},
"cost": {
"type": "object",
"properties": {
@@ -4642,25 +4748,6 @@
},
"components": {
"schemas": {
"Event.server.instance.disposed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "server.instance.disposed"
},
"properties": {
"type": "object",
"properties": {
"directory": {
"type": "string"
}
},
"required": ["directory"]
}
},
"required": ["type", "properties"]
},
"Event.installation.updated": {
"type": "object",
"properties": {
@@ -4699,6 +4786,83 @@
},
"required": ["type", "properties"]
},
"Project": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"worktree": {
"type": "string"
},
"vcs": {
"type": "string",
"const": "git"
},
"name": {
"type": "string"
},
"icon": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"color": {
"type": "string"
}
}
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"updated": {
"type": "number"
},
"initialized": {
"type": "number"
}
},
"required": ["created", "updated"]
}
},
"required": ["id", "worktree", "time"]
},
"Event.project.updated": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "project.updated"
},
"properties": {
"$ref": "#/components/schemas/Project"
}
},
"required": ["type", "properties"]
},
"Event.server.instance.disposed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "server.instance.disposed"
},
"properties": {
"type": "object",
"properties": {
"directory": {
"type": "string"
}
},
"required": ["directory"]
}
},
"required": ["type", "properties"]
},
"Event.lsp.client.diagnostics": {
"type": "object",
"properties": {
@@ -6633,15 +6797,18 @@
},
"Event": {
"anyOf": [
{
"$ref": "#/components/schemas/Event.server.instance.disposed"
},
{
"$ref": "#/components/schemas/Event.installation.updated"
},
{
"$ref": "#/components/schemas/Event.installation.update-available"
},
{
"$ref": "#/components/schemas/Event.project.updated"
},
{
"$ref": "#/components/schemas/Event.server.instance.disposed"
},
{
"$ref": "#/components/schemas/Event.lsp.client.diagnostics"
},
@@ -6743,40 +6910,6 @@
},
"required": ["directory", "payload"]
},
"Project": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"worktree": {
"type": "string"
},
"vcsDir": {
"type": "string"
},
"vcs": {
"type": "string",
"const": "git"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"updated": {
"type": "number"
},
"initialized": {
"type": "number"
}
},
"required": ["created"]
}
},
"required": ["id", "worktree", "time"]
},
"BadRequestError": {
"type": "object",
"properties": {
@@ -7187,6 +7320,25 @@
"tool_call": {
"type": "boolean"
},
"interleaved": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "object",
"properties": {
"field": {
"type": "string",
"enum": ["reasoning_content", "reasoning_details"]
}
},
"required": ["field"],
"additionalProperties": false
}
]
},
"cost": {
"type": "object",
"properties": {
@@ -8196,9 +8348,26 @@
}
},
"required": ["text", "audio", "image", "video", "pdf"]
},
"interleaved": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "object",
"properties": {
"field": {
"type": "string",
"enum": ["reasoning_content", "reasoning_details"]
}
},
"required": ["field"]
}
]
}
},
"required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output"]
"required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output", "interleaved"]
},
"cost": {
"type": "object",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.138",
"version": "1.0.141",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+2 -1
View File
@@ -1,9 +1,10 @@
{
"name": "@opencode-ai/tauri",
"private": true,
"version": "1.0.138",
"version": "1.0.141",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
"predev": "bun ./scripts/predev.ts",
"dev": "vite",
"build": "bun run typecheck && vite build",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.138",
"version": "1.0.141",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",
+5
View File
@@ -13,6 +13,11 @@
color: oklch(from var(--avatar-bg) calc(l * 0.72) calc(c * 8) h);
}
[data-component="avatar"][data-has-image] {
background-color: transparent;
border: none;
}
[data-component="avatar"][data-size="small"] {
width: 1.25rem;
height: 1.25rem;
+7 -3
View File
@@ -2,27 +2,31 @@ import { type ComponentProps, splitProps, Show } from "solid-js"
export interface AvatarProps extends ComponentProps<"div"> {
fallback: string
src?: string
background?: string
size?: "small" | "normal" | "large"
}
export function Avatar(props: AvatarProps) {
const [split, rest] = splitProps(props, ["fallback", "background", "size", "class", "classList", "style"])
const [split, rest] = splitProps(props, ["fallback", "src", "background", "size", "class", "classList", "style"])
return (
<div
{...rest}
data-component="avatar"
data-size={split.size || "normal"}
data-has-image={split.src ? "" : undefined}
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
style={{
...(typeof split.style === "object" ? split.style : {}),
...(split.background ? { "--avatar-bg": split.background } : {}),
...(!split.src && split.background ? { "--avatar-bg": split.background } : {}),
}}
>
<Show when={split.fallback}>{split.fallback[0]}</Show>
<Show when={split.src} fallback={split.fallback?.[0]}>
{(src) => <img src={src()} draggable={false} class="size-full object-cover" />}
</Show>
</div>
)
}
+4 -1
View File
@@ -4,7 +4,7 @@
justify-content: center;
border-style: solid;
border-width: 1px;
border-radius: var(--radius-sm);
border-radius: var(--radius-md);
text-decoration: none;
user-select: none;
cursor: default;
@@ -57,6 +57,9 @@
&[data-selected="true"]:not(:disabled) {
background-color: var(--surface-raised-base-hover);
}
&[data-active="true"] {
background-color: var(--surface-raised-base-active);
}
}
&[data-variant="secondary"] {
+1 -1
View File
@@ -1,4 +1,4 @@
import { UserMessage } from "@opencode-ai/sdk"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
import { DiffChanges } from "./diff-changes"
import { Spinner } from "./spinner"
+1 -1
View File
@@ -7,7 +7,7 @@ import {
TextPart,
ToolPart,
UserMessage,
} from "@opencode-ai/sdk"
} from "@opencode-ai/sdk/v2"
import { useDiffComponent } from "../context/diff"
import { BasicTool } from "./basic-tool"
import { GenericTool } from "./basic-tool"
@@ -2,7 +2,7 @@ import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createS
import { Part } from "./message-part"
import { Spinner } from "./spinner"
import { useData } from "../context/data"
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk/v2"
export interface MessageProgressProps {
assistantMessages: () => AssistantMessageType[]
@@ -1,4 +1,4 @@
import { UserMessage } from "@opencode-ai/sdk"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { ComponentProps, Show, splitProps } from "solid-js"
import { MessageNav } from "./message-nav"
import "./session-message-rail.css"
@@ -8,7 +8,7 @@ import { useDiffComponent } from "../context/diff"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { For, Match, Show, Switch, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { type FileDiff } from "@opencode-ai/sdk"
import { type FileDiff } from "@opencode-ai/sdk/v2"
import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
import { Dynamic } from "solid-js/web"
import { checksum } from "@opencode-ai/util/encode"
+68 -24
View File
@@ -1,9 +1,9 @@
import { AssistantMessage } from "@opencode-ai/sdk"
import { AssistantMessage } from "@opencode-ai/sdk/v2"
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, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js"
import { createEffect, createMemo, createSignal, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
import { DiffChanges } from "./diff-changes"
import { Typewriter } from "./typewriter"
import { Message } from "./message-part"
@@ -17,6 +17,13 @@ import { MessageProgress } from "./message-progress"
import { Collapsible } from "./collapsible"
import { Dynamic } from "solid-js/web"
// Track animation state per message ID - persists across re-renders
// "empty" = first saw with no value (should animate when value arrives)
// "animating" = currently animating (keep returning true)
// "done" = already animated or first saw with value (never animate)
const titleAnimationState = new Map<string, "empty" | "animating" | "done">()
const summaryAnimationState = new Map<string, "empty" | "animating" | "done">()
export function SessionTurn(
props: ParentProps<{
sessionID: string
@@ -55,11 +62,37 @@ export function SessionTurn(
<div data-slot="session-turn-content" class={props.classes?.content}>
<Show when={message()}>
{(msg) => {
const titleKey = `app:seen:session:${props.sessionID}:${msg().id}:title`
const contentKey = `app:seen:session:${props.sessionID}:${msg().id}:content`
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
const [titled, setTitled] = createSignal(true)
const [faded, setFaded] = createSignal(true)
// Animation logic: only animate if we witness the value transition from empty to non-empty
// Track in module-level Maps keyed by message ID so it persists across re-renders
// Initialize animation state for current message (reactive - runs when msg().id changes)
createEffect(() => {
const id = msg().id
if (!titleAnimationState.has(id)) {
titleAnimationState.set(id, msg().summary?.title ? "done" : "empty")
}
if (!summaryAnimationState.has(id)) {
const assistantMsgs = messages()?.filter(
(m) => m.role === "assistant" && m.parentID == id,
) as AssistantMessage[]
const parts = assistantMsgs?.flatMap((m) => data.store.part[m.id])
const lastText = parts?.filter((p) => p?.type === "text")?.at(-1)
const summaryValue = msg().summary?.body ?? lastText?.text
summaryAnimationState.set(id, summaryValue ? "done" : "empty")
}
// When message changes or component unmounts, mark any "animating" states as "done"
onCleanup(() => {
if (titleAnimationState.get(id) === "animating") {
titleAnimationState.set(id, "done")
}
if (summaryAnimationState.get(id) === "animating") {
summaryAnimationState.set(id, "done")
}
})
})
const assistantMessages = createMemo(() => {
return messages()?.filter((m) => m.role === "assistant" && m.parentID == msg().id) as AssistantMessage[]
@@ -79,27 +112,38 @@ export function SessionTurn(
const summary = createMemo(() => msg().summary?.body ?? lastTextPart()?.text)
const lastTextPartShown = createMemo(() => !msg().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0)
// allowing time for the animations to finish
onMount(() => {
const titleSeen = sessionStorage.getItem(titleKey) === "true"
const contentSeen = sessionStorage.getItem(contentKey) === "true"
if (!titleSeen) {
setTitled(false)
const title = msg().summary?.title
if (title) setTimeout(() => setTitled(true), 10_000)
setTimeout(() => sessionStorage.setItem(titleKey, "true"), 1000)
// Should animate: state is "empty" AND value now exists, or state is "animating"
// Transition: empty -> animating -> done (done happens on cleanup)
const animateTitle = createMemo(() => {
const id = msg().id
const state = titleAnimationState.get(id)
const title = msg().summary?.title
if (state === "animating") {
return true
}
if (!contentSeen) {
setFaded(false)
setTimeout(() => sessionStorage.setItem(contentKey, "true"), 1000)
if (state === "empty" && title) {
titleAnimationState.set(id, "animating")
return true
}
return false
})
const animateSummary = createMemo(() => {
const id = msg().id
const state = summaryAnimationState.get(id)
const value = summary()
if (state === "animating") {
return true
}
if (state === "empty" && value) {
summaryAnimationState.set(id, "animating")
return true
}
return false
})
createEffect(() => {
const completed = !messageWorking()
setTimeout(() => setCompleted(completed), 1200)
const done = !messageWorking()
setTimeout(() => setCompleted(done), 1200)
})
return (
@@ -108,7 +152,7 @@ export function SessionTurn(
<div data-slot="session-turn-message-header">
<div data-slot="session-turn-message-title">
<Show
when={titled()}
when={!animateTitle()}
fallback={<Typewriter as="h1" text={msg().summary?.title} data-slot="session-turn-typewriter" />}
>
<h1>{msg().summary?.title}</h1>
@@ -133,7 +177,7 @@ export function SessionTurn(
<Markdown
data-slot="session-turn-markdown"
data-diffs={!!msg().summary?.diffs?.length}
data-fade={!msg().summary?.diffs?.length && !faded()}
data-fade={!msg().summary?.diffs?.length && animateSummary()}
text={summary()}
/>
)}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Message, Session, Part, FileDiff, SessionStatus } from "@opencode-ai/sdk"
import type { Message, Session, Part, FileDiff, SessionStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper"
import { PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
@@ -57,10 +57,6 @@
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
--radius-4xl: 2rem;
--shadow-xs: var(--shadow-xs);
--shadow-md: var(--shadow-md);
+1
View File
@@ -42,6 +42,7 @@
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--shadow-xs:
0 1px 2px -1px rgba(19, 16, 16, 0.04), 0 1px 2px 0 rgba(19, 16, 16, 0.06), 0 1px 3px 0 rgba(19, 16, 16, 0.08);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.0.138",
"version": "1.0.141",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Part } from "@opencode-ai/sdk/client"
import type { Part } from "@opencode-ai/sdk/v2/client"
export const sanitize = (text: string | undefined, remove?: RegExp) => (remove ? text?.replace(remove, "") : text) ?? ""
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.138",
"version": "1.0.141",
"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
| ------------------------------------------------------------- | ---------------------------------------------------------- |
| [kimaki](https://github.com/remorses/kimaki) | Discord bot to control OpenCode sessions, built on the SDK |
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim plugin for editor-aware prompts, built on the API |
| [portal](https://github.com/hosenur/portal) | Mobile-first web UI for OpenCode over Tailscale/VPN |
---
+1 -1
View File
@@ -9,7 +9,7 @@ const notes = [] as string[]
console.log("=== publishing ===\n")
if (!Script.preview) {
const previous = await fetch("https://registry.npmjs.org/opencode-ai/latest")
const previous = await fetch("https://registry.npmjs.org/opencode-ai/latest-1")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.138",
"version": "1.0.141",
"publisher": "sst-dev",
"repository": {
"type": "git",