diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 831f32edbf..8bce65ae92 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -10,6 +10,8 @@ adamdotdevin -agusbasari29 AI PR slop ariane-emory +-atharvau AI review spamming literally every PR +-danieljoshuanazareth -danieljoshuanazareth edemaine -florianleibert @@ -23,4 +25,3 @@ r44vc0rp rekram1-node -spider-yamet clawdbot/llm psychosis, spam pinging the team thdxr --danieljoshuanazareth diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml index 9604bf87f3..79687639df 100644 --- a/.github/workflows/vouch-manage-by-issue.yml +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -33,6 +33,6 @@ jobs: with: issue-id: ${{ github.event.issue.number }} comment-id: ${{ github.event.comment.id }} - roles: admin,maintain + roles: admin,maintain,write env: GITHUB_TOKEN: ${{ steps.committer.outputs.token }} diff --git a/.opencode/command/changelog.md b/.opencode/command/changelog.md new file mode 100644 index 0000000000..8a8e57d1bf --- /dev/null +++ b/.opencode/command/changelog.md @@ -0,0 +1,5 @@ +go through each PR merged since the last tag + +for each PR spawn a subagent to summarize what the PR was about. focus on user facing changes. if it was entirely internal or code related you can ignore it. also skip docs updates. each subagent should append its summary to UPCOMING_CHANGELOG.md + +once that is done, read UPCOMING_CHANGELOG.md and group it into sections for better readability. make sure all PR references are preserved diff --git a/bun.lock b/bun.lock index c44ad8de97..2a6a28b7d4 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -41,6 +41,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", + "@solid-primitives/timer": "1.4.4", "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", @@ -78,7 +79,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -112,7 +113,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -129,7 +130,7 @@ "devDependencies": { "@cloudflare/workers-types": "catalog:", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.0", + "@types/bun": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "drizzle-kit": "catalog:", @@ -139,7 +140,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -163,7 +164,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -187,7 +188,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -220,7 +221,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -251,7 +252,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -280,7 +281,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -296,7 +297,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.27", + "version": "1.3.0", "bin": { "opencode": "./bin/opencode", }, @@ -337,8 +338,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.88", - "@opentui/solid": "0.1.88", + "@opentui/core": "0.1.90", + "@opentui/solid": "0.1.90", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -420,7 +421,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -444,7 +445,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.27", + "version": "1.3.0", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -455,7 +456,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -490,7 +491,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -536,7 +537,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "zod": "catalog:", }, @@ -547,7 +548,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -611,7 +612,7 @@ "@tailwindcss/vite": "4.1.11", "@tsconfig/bun": "1.0.9", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.9", + "@types/bun": "1.3.11", "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@types/semver": "7.7.1", @@ -1447,21 +1448,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.88", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.88", "@opentui/core-darwin-x64": "0.1.88", "@opentui/core-linux-arm64": "0.1.88", "@opentui/core-linux-x64": "0.1.88", "@opentui/core-win32-arm64": "0.1.88", "@opentui/core-win32-x64": "0.1.88", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-eaDVZfAzZraddOIkgWSHMVkyaY0O20foYnPWKPQx1TY4t7G1oatIoan2zkytx67epW+4BZQ9vGib+61/uNM1MA=="], + "@opentui/core": ["@opentui/core@0.1.90", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.90", "@opentui/core-darwin-x64": "0.1.90", "@opentui/core-linux-arm64": "0.1.90", "@opentui/core-linux-x64": "0.1.90", "@opentui/core-win32-arm64": "0.1.90", "@opentui/core-win32-x64": "0.1.90", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Os2dviqWVETU3kaK36lbSvdcI93GAWhw0xb9ng/d0DWYuM9scRmAhLHiOayp61saWv/BR8OJXeuQYHvrp5rd6A=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.88", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oGRexWwZFeQJymOK5ORrLrwJUbPHMYaFa0EcLnlhvPnymm1xyMcRKm39ez0WSIdtiCCi/PmMHX95CfyyJB5VMA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.88", "", { "os": "darwin", "cpu": "x64" }, "sha512-ddnruYpXt7gXsAqZoQzNrHtZ50niYQfESVT3rhE5qgsz7zoWBdKe/RxLKcb6zQmHMZML6SjSh0NrMG86lsH4dQ=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.90", "", { "os": "darwin", "cpu": "x64" }, "sha512-vbDpUsnlZ+0CeVKyBBXE+l2+X1XoVncMxMOhXTiMtud2/Cwu+Vfs/g3LC/6Zv08yaytA+9g7Z8sdf0QCqFyQ4w=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.88", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfcU/Sw8re3aWWb9cQ4OXmVNp/pchu6lgDRqvfy0EKTpzd7CNIu6a0xm+rcUKiPO7BrTrwtumT5/jZWWgCdHlg=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.88", "", { "os": "linux", "cpu": "x64" }, "sha512-nyfilOYLu6XWRlPl1R0Y6WzdL+jVdIFnwShBWcZL+QC5HiJnQc6LKy5yX8uv0fVbY5xs1wBvlHVeUj1UwFQyFQ=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.88", "", { "os": "win32", "cpu": "arm64" }, "sha512-jv/dQwcku7YZ4lNnYjivVvjPwTfDfzGfcplUqHxmirnv1Q1pZL1qS5wH1PV6RhAKN779vHTvnYMD4OgHWzqVaA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.88", "", { "os": "win32", "cpu": "x64" }, "sha512-saGvsQqwL8H7B0VBCQ+szMCKh9WIfTebOR8cwPa2+DR+1FnrEG2I4kiikoj4hfYfRMX18A0A11vQxSh3vvy8Ig=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="], - "@opentui/solid": ["@opentui/solid@0.1.88", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.88", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-hAqMBk3u/MnUapOmRPdMZinXPOFC+5ccmW1rEQRf9HpShRlZfyg9/u+wUI5rUavyeNFtka92Mtjf/N4AKQpwuA=="], + "@opentui/solid": ["@opentui/solid@0.1.90", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.90", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-zEHDpJOTGS707ts5j4diqoWuFLSqV6yARKl1H0FJkwWOotu+rxCyksL+C0gX0jJUonAw2cjlZ2NNtZY8g78zkg=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -1889,6 +1890,8 @@ "@solid-primitives/storage": ["@solid-primitives/storage@4.3.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "@tauri-apps/plugin-store": "*", "solid-js": "^1.6.12" }, "optionalPeers": ["@tauri-apps/plugin-store"] }, "sha512-ACbNwMZ1s8VAvld6EUXkDkX/US3IhtlPLxg6+B2s9MwNUugwdd51I98LPEaHrdLpqPmyzqgoJe0TxEFlf3Dqrw=="], + "@solid-primitives/timer": ["@solid-primitives/timer@1.4.4", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ayjyb3+v1hyU92vuLUN0tVHq2mmTCPGxSDLGJMsDydRqx9ZfJIc9xj6cxK4XvdY3pif3ps2mIv52pjgToybEpQ=="], + "@solid-primitives/trigger": ["@solid-primitives/trigger@1.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Za2JebEiDyfamjmDwRaESYqBBYOlgYGzB8kHYH0QrkXyLf2qNADlKdGN+z3vWSLCTDcKxChS43Kssjuc0OZhng=="], "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], @@ -2057,7 +2060,7 @@ "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], @@ -2453,7 +2456,7 @@ "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], diff --git a/nix/hashes.json b/nix/hashes.json index 53b17622ef..a965ed58bd 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-u+uZX7mhtm5eywGybB7/MjBMG2xl4Ve9VG33AAFgNno=", - "aarch64-linux": "sha256-pc1Xhd2bkwNohGMtzRnEuS5ZN1qWhJncYhNVAXega1g=", - "aarch64-darwin": "sha256-A5qUpqgm9ZFvWVhn/WdiX4lVs4ihbAclJDvCFAmx5Wg=", - "x86_64-darwin": "sha256-ECLrMGE51AlYJ4JKDtziDKxhyK7WLt8R+8RVFdXH1WU=" + "x86_64-linux": "sha256-E5neEbBiwQDhIQ5QVhijpHCCP9hcxm319S9WrDKngSw=", + "aarch64-linux": "sha256-lnwaGSEirl9izskDooB/xQ0ZdirW0t3/S+OoOnfYaoQ=", + "aarch64-darwin": "sha256-RDxxW9NMlGMIdIxTsbOYVqxunflkILv2dA7JqjnJgm4=", + "x86_64-darwin": "sha256-1tvvktu2NRg6N6ASuKzqzcEmMrzH3/LFey0Vxr4E8zg=" } } diff --git a/package.json b/package.json index 7ce06896ad..915e2ef0ac 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "AI-powered development tool", "private": true, "type": "module", - "packageManager": "bun@1.3.10", + "packageManager": "bun@1.3.11", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev:desktop": "bun --cwd packages/desktop tauri dev", @@ -26,7 +26,7 @@ ], "catalog": { "@effect/platform-node": "4.0.0-beta.35", - "@types/bun": "1.3.9", + "@types/bun": "1.3.11", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", "ulid": "3.0.1", diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index aced0756c0..90af177ed1 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -175,9 +175,9 @@ export async function runTerminal(page: Page, input: { cmd: string; token: strin await expect.poll(() => terminalHas(page, { term, token: input.token }), { timeout }).toBe(true) } -export async function openPalette(page: Page) { +export async function openPalette(page: Page, key = "K") { await defocus(page) - await page.keyboard.press(`${modKey}+P`) + await page.keyboard.press(`${modKey}+${key}`) const dialog = page.getByRole("dialog") await expect(dialog).toBeVisible() diff --git a/packages/app/e2e/app/palette.spec.ts b/packages/app/e2e/app/palette.spec.ts index 3ccfd7a925..4c701fab27 100644 --- a/packages/app/e2e/app/palette.spec.ts +++ b/packages/app/e2e/app/palette.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from "../fixtures" -import { openPalette } from "../actions" +import { closeDialog, openPalette } from "../actions" test("search palette opens and closes", async ({ page, gotoSession }) => { await gotoSession() @@ -9,3 +9,12 @@ test("search palette opens and closes", async ({ page, gotoSession }) => { await page.keyboard.press("Escape") await expect(dialog).toHaveCount(0) }) + +test("search palette also opens with cmd+p", async ({ page, gotoSession }) => { + await gotoSession() + + const dialog = await openPalette(page, "P") + + await closeDialog(page, dialog) + await expect(dialog).toHaveCount(0) +}) diff --git a/packages/app/e2e/prompt/prompt-history.spec.ts b/packages/app/e2e/prompt/prompt-history.spec.ts index ec68998144..1c9c079550 100644 --- a/packages/app/e2e/prompt/prompt-history.spec.ts +++ b/packages/app/e2e/prompt/prompt-history.spec.ts @@ -108,7 +108,10 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page await page.keyboard.type(draft) await wait(page, draft) - await edge(page, "start") + // Clear the draft before navigating history (ArrowUp only works when prompt is empty) + await prompt.fill("") + await wait(page, "") + await page.keyboard.press("ArrowUp") await wait(page, second) @@ -119,7 +122,7 @@ test("prompt history restores unsent draft with arrow navigation", async ({ page await wait(page, second) await page.keyboard.press("ArrowDown") - await wait(page, draft) + await wait(page, "") }) }) diff --git a/packages/app/e2e/settings/settings-keybinds.spec.ts b/packages/app/e2e/settings/settings-keybinds.spec.ts index 5789dc0eb0..4fc50b68d7 100644 --- a/packages/app/e2e/settings/settings-keybinds.spec.ts +++ b/packages/app/e2e/settings/settings-keybinds.spec.ts @@ -241,7 +241,7 @@ test("changing file open keybind works", async ({ page, gotoSession }) => { await expect(keybindButton).toBeVisible() const initialKeybind = await keybindButton.textContent() - expect(initialKeybind).toContain("P") + expect(initialKeybind).toContain("K") await keybindButton.click() await expect(keybindButton).toHaveText(/press/i) diff --git a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts index d10fca0e49..1317d2bb68 100644 --- a/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts +++ b/packages/app/e2e/sidebar/sidebar-popover-actions.spec.ts @@ -1,6 +1,16 @@ import { test, expect } from "../fixtures" -import { cleanupSession, closeSidebar, hoverSessionItem } from "../actions" +import { + defocus, + cleanupSession, + cleanupTestProject, + closeSidebar, + createTestProject, + hoverSessionItem, + openSidebar, + waitSession, +} from "../actions" import { projectSwitchSelector } from "../selectors" +import { dirSlug } from "../utils" test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => { const stamp = Date.now() @@ -37,3 +47,72 @@ test("collapsed sidebar popover stays open when archiving a session", async ({ p await cleanupSession({ sdk, sessionID: two.id }) } }) + +test("open sidebar project popover stays closed after clicking avatar", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const slug = dirSlug(other) + + try { + await withProject( + async () => { + await openSidebar(page) + + const project = page.locator(projectSwitchSelector(slug)).first() + const card = page.locator('[data-component="hover-card-content"]') + + await expect(project).toBeVisible() + await project.hover() + await expect(card.getByText(/recent sessions/i)).toBeVisible() + + await page.mouse.down() + await expect(card).toHaveCount(0) + await page.mouse.up() + + await waitSession(page, { directory: other }) + await expect(card).toHaveCount(0) + }, + { extra: [other] }, + ) + } finally { + await cleanupTestProject(other) + } +}) + +test("open sidebar project switch activates on first tabbed enter", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const slug = dirSlug(other) + + try { + await withProject( + async () => { + await openSidebar(page) + await defocus(page) + + const project = page.locator(projectSwitchSelector(slug)).first() + + await expect(project).toBeVisible() + + let hit = false + for (let i = 0; i < 20; i++) { + hit = await project.evaluate((el) => { + return el.matches(":focus") || !!el.parentElement?.matches(":focus") + }) + if (hit) break + await page.keyboard.press("Tab") + } + + expect(hit).toBe(true) + + await page.keyboard.press("Enter") + await waitSession(page, { directory: other }) + }, + { extra: [other] }, + ) + } finally { + await cleanupTestProject(other) + } +}) diff --git a/packages/app/package.json b/packages/app/package.json index 3f4e2472f2..61265c28aa 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.27", + "version": "1.3.0", "description": "", "type": "module", "exports": { @@ -51,6 +51,7 @@ "@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/scroll": "2.1.3", "@solid-primitives/storage": "catalog:", + "@solid-primitives/timer": "1.4.4", "@solid-primitives/websocket": "1.3.1", "@solidjs/meta": "catalog:", "@solidjs/router": "catalog:", diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index f3d3e135de..34f83b13e2 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1043,7 +1043,7 @@ export const PromptInput: Component = (props) => { return true } - const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({ + const { addAttachments, removeAttachment, handlePaste } = createPromptAttachments({ editor: () => editorRef, isDialogActive: () => !!dialog.active, setDraggingType: (type) => setStore("draggingType", type), @@ -1388,11 +1388,7 @@ export const PromptInput: Component = (props) => { class="hidden" onChange={(e) => { const list = e.currentTarget.files - if (list) { - for (const file of Array.from(list)) { - void addAttachment(file) - } - } + if (list) void addAttachments(Array.from(list)) e.currentTarget.value = "" }} /> diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index eca508c6ce..fa9930f683 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -71,6 +71,18 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { const addAttachment = (file: File) => add(file) + const addAttachments = async (files: File[], toast = true) => { + let found = false + + for (const file of files) { + const ok = await add(file, false) + if (ok) found = true + } + + if (!found && files.length > 0 && toast) warn() + return found + } + const removeAttachment = (id: string) => { const current = prompt.current() const next = current.filter((part) => part.type !== "image" || part.id !== id) @@ -84,18 +96,14 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { event.preventDefault() event.stopPropagation() - const items = Array.from(clipboardData.items) - const fileItems = items.filter((item) => item.kind === "file") + const files = Array.from(clipboardData.items).flatMap((item) => { + if (item.kind !== "file") return [] + const file = item.getAsFile() + return file ? [file] : [] + }) - if (fileItems.length > 0) { - let found = false - for (const item of fileItems) { - const file = item.getAsFile() - if (!file) continue - const ok = await add(file, false) - if (ok) found = true - } - if (!found) warn() + if (files.length > 0) { + await addAttachments(files) return } @@ -169,12 +177,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { const dropped = event.dataTransfer?.files if (!dropped) return - let found = false - for (const file of Array.from(dropped)) { - const ok = await add(file, false) - if (ok) found = true - } - if (!found && dropped.length > 0) warn() + await addAttachments(Array.from(dropped)) } onMount(() => { @@ -191,6 +194,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { return { addAttachment, + addAttachments, removeAttachment, handlePaste, } diff --git a/packages/app/src/components/prompt-input/build-request-parts.test.ts b/packages/app/src/components/prompt-input/build-request-parts.test.ts index 4c2e2d8bec..ce09ae9217 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.test.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.test.ts @@ -49,6 +49,32 @@ describe("buildRequestParts", () => { expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true) }) + test("keeps multiple uploaded attachments in order", () => { + const result = buildRequestParts({ + prompt: [{ type: "text", content: "check these", start: 0, end: 11 }], + context: [], + images: [ + { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + { + type: "image", + id: "img_2", + filename: "b.pdf", + mime: "application/pdf", + dataUrl: "data:application/pdf;base64,BBB", + }, + ], + text: "check these", + messageID: "msg_multi", + sessionID: "ses_multi", + sessionDirectory: "/repo", + }) + + const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:")) + + expect(files).toHaveLength(2) + expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"]) + }) + test("deduplicates context files when prompt already includes same path", () => { const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }] diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts index 37b5ce1962..5e9c2c66ea 100644 --- a/packages/app/src/components/prompt-input/history.test.ts +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -126,7 +126,7 @@ describe("prompt-input history", () => { test("canNavigateHistoryAtCursor only allows prompt boundaries", () => { const value = "a\nb\nc" - expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true) + expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false) expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false) expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false) @@ -135,11 +135,14 @@ describe("prompt-input history", () => { expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false) expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true) - expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false) expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true) expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false) expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false) + expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true) expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true) expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true) diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts index de62653211..79e8abc0d9 100644 --- a/packages/app/src/components/prompt-input/history.ts +++ b/packages/app/src/components/prompt-input/history.ts @@ -27,7 +27,7 @@ export function canNavigateHistoryAtCursor(direction: "up" | "down", text: strin const atStart = position === 0 const atEnd = position === text.length if (inHistory) return atStart || atEnd - if (direction === "up") return position === 0 + if (direction === "up") return position === 0 && text.length === 0 return position === text.length } diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 9aa101bdb9..4d90930a0e 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -267,14 +267,14 @@ export function SessionContextTab() { return ( { scroll = el restoreScroll() }} onScroll={handleScroll} > -
+
{(stat) => [0])} value={stat.value()} />} diff --git a/packages/app/src/context/command-keybind.test.ts b/packages/app/src/context/command-keybind.test.ts index 4e38efd8da..d804195c40 100644 --- a/packages/app/src/context/command-keybind.test.ts +++ b/packages/app/src/context/command-keybind.test.ts @@ -40,4 +40,11 @@ describe("command keybind helpers", () => { expect(display.includes("Alt") || display.includes("⌥")).toBe(true) expect(formatKeybind("none")).toBe("") }) + + test("formatKeybind prefers the first combo", () => { + const display = formatKeybind("mod+k,mod+p") + + expect(display.includes("K") || display.includes("k")).toBe(true) + expect(display.includes("P") || display.includes("p")).toBe(false) + }) }) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 8efd9d3bc9..579b740d3a 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -276,7 +276,7 @@ export const dict = { "prompt.context.includeActiveFile": "Include active file", "prompt.context.removeActiveFile": "Remove active file from context", "prompt.context.removeFile": "Remove file from context", - "prompt.action.attachFile": "Add file", + "prompt.action.attachFile": "Add files", "prompt.attachment.remove": "Remove attachment", "prompt.action.send": "Send", "prompt.action.stop": "Stop", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 2adcd3b563..d8b0732580 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -211,13 +211,22 @@ export default function Layout(props: ParentProps) { onMount(() => { const stop = () => setState("sizing", false) + const blur = () => reset() + const hide = () => { + if (document.visibilityState !== "hidden") return + reset() + } window.addEventListener("pointerup", stop) window.addEventListener("pointercancel", stop) window.addEventListener("blur", stop) + window.addEventListener("blur", blur) + document.addEventListener("visibilitychange", hide) onCleanup(() => { window.removeEventListener("pointerup", stop) window.removeEventListener("pointercancel", stop) window.removeEventListener("blur", stop) + window.removeEventListener("blur", blur) + document.removeEventListener("visibilitychange", hide) }) }) @@ -237,6 +246,12 @@ export default function Layout(props: ParentProps) { navLeave.current = undefined } + const reset = () => { + disarm() + setState("hoverSession", undefined) + setHoverProject(undefined) + } + const arm = () => { if (layout.sidebar.opened()) return if (state.hoverProject === undefined) return @@ -305,8 +320,7 @@ export default function Layout(props: ParentProps) { const clearSidebarHoverState = () => { if (layout.sidebar.opened()) return - setState("hoverSession", undefined) - setHoverProject(undefined) + reset() } const navigateWithSidebarReset = (href: string) => { @@ -1975,6 +1989,10 @@ export default function Layout(props: ParentProps) { onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event), onProjectMouseLeave: (worktree) => aim.leave(worktree), onProjectFocus: (worktree) => aim.activate(worktree), + onHoverOpenChanged: (worktree, hoverOpen) => { + if (!hoverOpen && state.hoverProject && state.hoverProject !== worktree) return + setState("hoverProject", hoverOpen ? worktree : undefined) + }, navigateToProject, openSidebar: () => layout.sidebar.open(), closeProject, @@ -2368,14 +2386,12 @@ export default function Layout(props: ParentProps) { size={layout.sidebar.width()} min={244} max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64} - collapseThreshold={244} onResize={(w) => { setState("sizing", true) if (sizet !== undefined) clearTimeout(sizet) sizet = window.setTimeout(() => setState("sizing", false), 120) layout.sidebar.resize(w) }} - onCollapse={layout.sidebar.close} />
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index f8e16f3e12..a9627c5dbc 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -157,34 +157,45 @@ const SessionHoverPreview = (props: { messageLabel: (message: Message) => string | undefined onMessageSelect: (message: Message) => void trigger: JSX.Element -}): JSX.Element => ( - props.setHoverSession(open ? props.session.id : undefined)} - > - {props.language.t("session.messages.loading")}
} +}): JSX.Element => { + let ref: HTMLDivElement | undefined + + return ( + {props.trigger}
} + open={props.hoverSession() === props.session.id} + onOpenChange={(open) => { + if (!open) { + props.setHoverSession(undefined) + return + } + if (!ref?.matches(":hover")) return + props.setHoverSession(props.session.id) + }} > -
- -
- - -) + {props.language.t("session.messages.loading")}} + > +
+ +
+
+ + ) +} export const SessionItem = (props: SessionItemProps): JSX.Element => { const params = useParams() diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index 2528264561..aff0645dd8 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -23,6 +23,7 @@ export type ProjectSidebarContext = { onProjectMouseEnter: (worktree: string, event: MouseEvent) => void onProjectMouseLeave: (worktree: string) => void onProjectFocus: (worktree: string) => void + onHoverOpenChanged: (worktree: string, hovered: boolean) => void navigateToProject: (directory: string) => void openSidebar: () => void closeProject: (directory: string) => void @@ -109,8 +110,14 @@ const ProjectTile = (props: { "bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(), }} onPointerDown={(event) => { + if (event.button === 0 && !event.ctrlKey) { + props.setOpen(false) + props.setSuppressHover(true) + return + } if (!props.overlay()) return if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return + props.setOpen(false) props.setSuppressHover(true) event.preventDefault() }} @@ -130,12 +137,11 @@ const ProjectTile = (props: { props.onProjectFocus(props.project.worktree) }} onClick={() => { + props.setOpen(false) if (props.selected()) { - props.setSuppressHover(true) layout.sidebar.toggle() return } - props.setSuppressHover(false) props.navigateToProject(props.project.worktree) }} onBlur={() => props.setOpen(false)} @@ -192,7 +198,6 @@ const ProjectPreviewPanel = (props: { projectChildren: Accessor> workspaceSessions: (directory: string) => ReturnType workspaceChildren: (directory: string) => Map - setOpen: (value: boolean) => void ctx: ProjectSidebarContext language: ReturnType }): JSX.Element => ( @@ -259,7 +264,7 @@ const ProjectPreviewPanel = (props: { class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent" onClick={() => { props.ctx.openSidebar() - props.setOpen(false) + props.ctx.onHoverOpenChanged(props.project.worktree, false) if (props.selected()) return props.ctx.navigateToProject(props.project.worktree) }} @@ -284,28 +289,16 @@ export const SortableProject = (props: { const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) const [state, setState] = createStore({ - open: false, menu: false, suppressHover: false, }) + const isHoverProject = () => props.ctx.hoverProject() === props.project.worktree const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) - const active = createMemo( - () => state.menu || (preview() ? state.open : overlay() && props.ctx.hoverProject() === props.project.worktree), - ) + const active = createMemo(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject())) - createEffect(() => { - if (preview()) return - if (!state.open) return - setState("open", false) - }) - - createEffect(() => { - if (!selected()) return - if (!state.open) return - setState("open", false) - }) + const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu const label = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) @@ -346,7 +339,7 @@ export const SortableProject = (props: { workspacesEnabled={props.ctx.workspacesEnabled} closeProject={props.ctx.closeProject} setMenu={(value) => setState("menu", value)} - setOpen={(value) => setState("open", value)} + setOpen={(value) => props.ctx.onHoverOpenChanged(props.project.worktree, value)} setSuppressHover={(value) => setState("suppressHover", value)} language={language} /> @@ -357,7 +350,7 @@ export const SortableProject = (props: {
{ if (state.menu) return if (value && state.suppressHover) return - setState("open", value) + props.ctx.onHoverOpenChanged(props.project.worktree, value) if (value) props.ctx.setHoverSession(undefined) }} > @@ -381,7 +374,6 @@ export const SortableProject = (props: { projectChildren={projectChildren} workspaceSessions={workspaceSessions} workspaceChildren={workspaceChildren} - setOpen={(value) => setState("open", value)} ctx={props.ctx} language={language} /> diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 7ced21353f..fe61f16854 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,4 +1,4 @@ -import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js" +import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX, createSignal } from "solid-js" import { createStore, produce } from "solid-js/store" import { useNavigate } from "@solidjs/router" import { useMutation } from "@tanstack/solid-query" @@ -30,6 +30,7 @@ import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" import { messageAgentColor } from "@/utils/agent" import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" +import { makeTimer } from "@solid-primitives/timer" type MessageComment = { path: string @@ -250,38 +251,21 @@ export function MessageTimeline(props: { const working = createMemo(() => !!pending() || sessionStatus().type !== "idle") const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent)) - const [slot, setSlot] = createStore({ - open: false, - show: false, - fade: false, + const [timeoutDone, setTimeoutDone] = createSignal(true) + + const workingStatus = createMemo<"hidden" | "showing" | "hiding">((prev) => { + if (working()) return "showing" + if (prev === "showing" || !timeoutDone()) return "hiding" + return "hidden" }) - let f: number | undefined - const clear = () => { - if (f !== undefined) window.clearTimeout(f) - f = undefined - } + createEffect(() => { + if (workingStatus() !== "hiding") return + + setTimeoutDone(false) + makeTimer(() => setTimeoutDone(true), 260, setTimeout) + }) - onCleanup(clear) - createEffect( - on( - working, - (on, prev) => { - clear() - if (on) { - setSlot({ open: true, show: true, fade: false }) - return - } - if (prev) { - setSlot({ open: false, show: true, fade: true }) - f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260) - return - } - setSlot({ open: false, show: false, fade: false }) - }, - { defer: true }, - ), - ) const activeMessageID = createMemo(() => { const parentID = pending()?.parentID if (parentID) { @@ -676,17 +660,15 @@ export function MessageTimeline(props: {
-
{ props.size.touch() layout.fileTree.resize(width) }} - onCollapse={layout.fileTree.close} />
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 1a2e777f52..f17e3f7a1f 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -255,7 +255,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { id: "file.open", title: language.t("command.file.open"), description: language.t("palette.search.placeholder"), - keybind: "mod+p", + keybind: "mod+k,mod+p", slash: "open", onSelect: () => dialog.show(() => ), }), diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts new file mode 100644 index 0000000000..1ecaf02c97 --- /dev/null +++ b/packages/app/src/utils/prompt.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test" +import type { Part } from "@opencode-ai/sdk/v2" +import { extractPromptFromParts } from "./prompt" + +describe("extractPromptFromParts", () => { + test("restores multiple uploaded attachments", () => { + const parts = [ + { + id: "text_1", + type: "text", + text: "check these", + sessionID: "ses_1", + messageID: "msg_1", + }, + { + id: "file_1", + type: "file", + mime: "image/png", + url: "data:image/png;base64,AAA", + filename: "a.png", + sessionID: "ses_1", + messageID: "msg_1", + }, + { + id: "file_2", + type: "file", + mime: "application/pdf", + url: "data:application/pdf;base64,BBB", + filename: "b.pdf", + sessionID: "ses_1", + messageID: "msg_1", + }, + ] satisfies Part[] + + const result = extractPromptFromParts(parts) + + expect(result).toHaveLength(3) + expect(result[0]).toMatchObject({ type: "text", content: "check these" }) + expect(result.slice(1)).toMatchObject([ + { type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + { type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" }, + ]) + }) +}) diff --git a/packages/console/app/package.json b/packages/console/app/package.json index cb0f91a64f..b90d77f405 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 1a75caea2f..f2bb6ac745 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.27", + "version": "1.3.0", "private": true, "type": "module", "license": "MIT", @@ -42,7 +42,7 @@ "devDependencies": { "@cloudflare/workers-types": "catalog:", "@tsconfig/node22": "22.0.2", - "@types/bun": "1.3.0", + "@types/bun": "catalog:", "@types/node": "catalog:", "drizzle-kit": "catalog:", "mysql2": "3.14.4", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index fe327a5639..93e0ba71cb 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.27", + "version": "1.3.0", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 0525ffc21c..e0c677446c 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.27", + "version": "1.3.0", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/containers/bun-node/Dockerfile b/packages/containers/bun-node/Dockerfile index e6cad9c272..485375dd9f 100644 --- a/packages/containers/bun-node/Dockerfile +++ b/packages/containers/bun-node/Dockerfile @@ -4,7 +4,7 @@ FROM ${REGISTRY}/build/base:24.04 SHELL ["/bin/bash", "-lc"] ARG NODE_VERSION=24.4.0 -ARG BUN_VERSION=1.3.5 +ARG BUN_VERSION=1.3.11 ENV BUN_INSTALL=/opt/bun ENV PATH=/opt/bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index bcb80d9e69..b7872acc98 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop-electron/src/main/cli.ts b/packages/desktop-electron/src/main/cli.ts index fba301f36c..f2d918bd21 100644 --- a/packages/desktop-electron/src/main/cli.ts +++ b/packages/desktop-electron/src/main/cli.ts @@ -35,6 +35,7 @@ export type CommandEvent = export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" } export type CommandChild = { + pid: number | undefined kill: () => void } @@ -191,7 +192,7 @@ export function spawnCommand(args: string, extraEnv: Record) { treeKill(child.pid) } - return { events, child: { kill }, exit } + return { events, child: { pid: child.pid, kill }, exit } } function handleSqliteProgress(events: EventEmitter, line: string) { diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 484e4feb20..032343204c 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -81,6 +81,17 @@ function setupApp() { killSidecar() }) + app.on("will-quit", () => { + killSidecar() + }) + + for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, () => { + killSidecar() + app.exit(0) + }) + } + void app.whenReady().then(async () => { // migrate() app.setAsDefaultProtocolClient("opencode") @@ -234,8 +245,15 @@ registerIpcHandlers({ function killSidecar() { if (!sidecar) return + const pid = sidecar.pid sidecar.kill() sidecar = null + // tree-kill is async; also send process group signal as immediate fallback + if (pid && process.platform !== "win32") { + try { + process.kill(-pid, "SIGTERM") + } catch {} + } } function ensureLoopbackNoProxy() { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 73ec5278f6..c98e037be6 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index e61c75d0ec..724cc59493 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.27", + "version": "1.3.0", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 654e1dce75..9842aa49be 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.27" +version = "1.3.0" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.27/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.0/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index a20a61f74d..e8651be720 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.27", + "version": "1.3.0", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/git b/packages/opencode/git new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 824c3409c4..691724dd4c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.27", + "version": "1.3.0", "name": "opencode", "type": "module", "license": "MIT", @@ -101,8 +101,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.88", - "@opentui/solid": "0.1.88", + "@opentui/core": "0.1.90", + "@opentui/solid": "0.1.90", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8bb17ff133..dc052c4d2e 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -5,8 +5,8 @@ import { MouseButton, TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js" import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32" -import { Installation } from "@/installation" import { Flag } from "@/flag/flag" +import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" import { SDKProvider, useSDK } from "@tui/context/sdk" @@ -29,6 +29,7 @@ import { PromptHistoryProvider } from "./component/prompt/history" import { FrecencyProvider } from "./component/prompt/frecency" import { PromptStashProvider } from "./component/prompt/stash" import { DialogAlert } from "./ui/dialog-alert" +import { DialogConfirm } from "./ui/dialog-confirm" import { ToastProvider, useToast } from "./ui/toast" import { ExitProvider, useExit } from "./context/exit" import { Session as SessionApi } from "@/session" @@ -103,6 +104,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { } import type { EventSource } from "./context/sdk" +import { Installation } from "@/installation" export function tui(input: { url: string @@ -729,13 +731,51 @@ function App() { }) }) - sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => { + sdk.event.on("installation.update-available", async (evt) => { + const version = evt.properties.version + + const skipped = kv.get("skipped_version") + if (skipped && !semver.gt(version, skipped)) return + + const choice = await DialogConfirm.show( + dialog, + `Update Available`, + `A new release v${version} is available. Would you like to update now?`, + "skip", + ) + + if (choice === false) { + kv.set("skipped_version", version) + return + } + + if (choice !== true) return + toast.show({ variant: "info", - title: "Update Available", - message: `OpenCode v${evt.properties.version} is available. Run 'opencode upgrade' to update manually.`, - duration: 10000, + message: `Updating to v${version}...`, + duration: 30000, }) + + const result = await sdk.client.global.upgrade({ target: version }) + + if (result.error || !result.data?.success) { + toast.show({ + variant: "error", + title: "Update Failed", + message: "Update failed", + duration: 10000, + }) + return + } + + await DialogAlert.show( + dialog, + "Update Complete", + `Successfully updated to OpenCode v${result.data.version}. Please restart the application.`, + ) + + exit() }) return ( diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 2320c08ccc..d65fbf40ad 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -428,7 +428,7 @@ export function tint(base: RGBA, overlay: RGBA, alpha: number): RGBA { function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson { const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!) const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!) - const transparent = RGBA.fromInts(0, 0, 0, 0) + const transparent = RGBA.fromValues(bg.r, bg.g, bg.b, 0) const isDark = mode == "dark" const col = (i: number) => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 4682c50df1..0d9ddc746c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1465,6 +1465,8 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess streaming={true} content={props.part.text.trim()} conceal={ctx.conceal()} + fg={theme.markdownText} + bg={theme.background} /> diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index b86bd43251..ef75764a29 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -11,8 +11,11 @@ export type DialogConfirmProps = { message: string onConfirm?: () => void onCancel?: () => void + label?: string } +export type DialogConfirmResult = boolean | undefined + export function DialogConfirm(props: DialogConfirmProps) { const dialog = useDialog() const { theme } = useTheme() @@ -45,7 +48,7 @@ export function DialogConfirm(props: DialogConfirmProps) { {props.message} - + {(key) => ( - {Locale.titlecase(key)} + {Locale.titlecase(key === "cancel" ? (props.label ?? key) : key)} )} @@ -68,8 +71,8 @@ export function DialogConfirm(props: DialogConfirmProps) { ) } -DialogConfirm.show = (dialog: DialogContext, title: string, message: string) => { - return new Promise((resolve) => { +DialogConfirm.show = (dialog: DialogContext, title: string, message: string, label?: string) => { + return new Promise((resolve) => { dialog.replace( () => ( message={message} onConfirm={() => resolve(true)} onCancel={() => resolve(false)} + label={label} /> ), - () => resolve(false), + () => resolve(undefined), ) }) } diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index 2d46ae39fa..e40750a2ec 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -8,12 +8,18 @@ export async function upgrade() { const method = await Installation.method() const latest = await Installation.latest(method).catch(() => {}) if (!latest) return - if (Installation.VERSION === latest) return - if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) { + if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) { + await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) return } - if (config.autoupdate === "notify") { + + if (Installation.VERSION === latest) return + if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return + + const kind = Installation.getReleaseType(Installation.VERSION, latest) + + if (config.autoupdate === "notify" || kind !== "patch") { await Bus.publish(Installation.Event.UpdateAvailable, { version: latest }) return } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 05f04c85ce..0c55187b9d 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -18,6 +18,7 @@ export namespace Flag { export declare const OPENCODE_CONFIG_DIR: string | undefined export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE") + export const OPENCODE_ALWAYS_NOTIFY_UPDATE = truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE") export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE") export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE") export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"] diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 1e4e45f2cd..3551c861e4 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -15,11 +15,15 @@ declare global { const OPENCODE_CHANNEL: string } +import semver from "semver" + export namespace Installation { const log = Log.create({ service: "installation" }) export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" + export type ReleaseType = "patch" | "minor" | "major" + export const Event = { Updated: BusEvent.define( "installation.updated", @@ -35,6 +39,17 @@ export namespace Installation { ), } + export function getReleaseType(current: string, latest: string): ReleaseType { + const currMajor = semver.major(current) + const currMinor = semver.minor(current) + const newMajor = semver.major(latest) + const newMinor = semver.minor(latest) + + if (newMajor > currMajor) return "major" + if (newMinor > currMinor) return "minor" + return "patch" + } + export const Info = z .object({ version: z.string(), diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index 4a6a3ebc7e..4dd30db2af 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -1,7 +1,8 @@ import { Hono } from "hono" -import { describeRoute, resolver, validator } from "hono-openapi" +import { describeRoute, validator, resolver } from "hono-openapi" import { streamSSE } from "hono/streaming" import z from "zod" +import { Bus } from "../../bus" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { AsyncQueue } from "@/util/queue" @@ -195,5 +196,62 @@ export const GlobalRoutes = lazy(() => }) return c.json(true) }, + ) + .post( + "/upgrade", + describeRoute({ + summary: "Upgrade opencode", + description: "Upgrade opencode to the specified version or latest if not specified.", + operationId: "global.upgrade", + responses: { + 200: { + description: "Upgrade result", + content: { + "application/json": { + schema: resolver( + z.union([ + z.object({ + success: z.literal(true), + version: z.string(), + }), + z.object({ + success: z.literal(false), + error: z.string(), + }), + ]), + ), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + target: z.string().optional(), + }), + ), + async (c) => { + const method = await Installation.method() + if (method === "unknown") { + return c.json({ success: false, error: "Unknown installation method" }, 400) + } + const target = c.req.valid("json").target || (await Installation.latest(method)) + const result = await Installation.upgrade(method, target) + .then(() => ({ success: true as const, version: target })) + .catch((e) => ({ success: false as const, error: e instanceof Error ? e.message : String(e) })) + if (result.success) { + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Installation.Event.Updated.type, + properties: { version: target }, + }, + }) + return c.json(result) + } + return c.json(result, 500) + }, ), ) diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts index 8cbd478cba..6658634e54 100644 --- a/packages/opencode/test/file/watcher.test.ts +++ b/packages/opencode/test/file/watcher.test.ts @@ -177,13 +177,17 @@ describeWatcher("FileWatcher", () => { await withWatcher(tmp.path, Effect.void) // Now write a file — no watcher should be listening - await Effect.runPromise( - noUpdate( - tmp.path, - (e) => e.file === file, - Effect.promise(() => fs.writeFile(file, "gone")), - ), - ) + await Instance.provide({ + directory: tmp.path, + fn: () => + Effect.runPromise( + noUpdate( + tmp.path, + (e) => e.file === file, + Effect.promise(() => fs.writeFile(file, "gone")), + ), + ), + }) }) test("ignores .git/index changes", async () => { diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 5dedb464c4..6907f2a332 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 8ae3dae9c3..bcc035d4bb 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b6821322e2..7a4f4e40cf 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -46,6 +46,8 @@ import type { GlobalDisposeResponses, GlobalEventResponses, GlobalHealthResponses, + GlobalUpgradeErrors, + GlobalUpgradeResponses, InstanceDisposeResponses, LspStatusResponses, McpAddErrors, @@ -303,6 +305,30 @@ export class Global extends HeyApiClient { }) } + /** + * Upgrade opencode + * + * Upgrade opencode to the specified version or latest if not specified. + */ + public upgrade( + parameters?: { + target?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "target" }] }]) + return (options?.client ?? this.client).post({ + url: "/global/upgrade", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + private _config?: Config get config(): Config { return (this._config ??= new Config({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index f7aab687e6..86a0c7e425 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2030,6 +2030,41 @@ export type GlobalDisposeResponses = { export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] +export type GlobalUpgradeData = { + body?: { + target?: string + } + path?: never + query?: never + url: "/global/upgrade" +} + +export type GlobalUpgradeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalUpgradeError = GlobalUpgradeErrors[keyof GlobalUpgradeErrors] + +export type GlobalUpgradeResponses = { + /** + * Upgrade result + */ + 200: + | { + success: true + version: string + } + | { + success: false + error: string + } +} + +export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses] + export type AuthRemoveData = { body?: never path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 9f3a69c54c..a66ef63647 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -158,6 +158,82 @@ ] } }, + "/global/upgrade": { + "post": { + "operationId": "global.upgrade", + "summary": "Upgrade opencode", + "description": "Upgrade opencode to the specified version or latest if not specified.", + "responses": { + "200": { + "description": "Upgrade result", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true + }, + "version": { + "type": "string" + } + }, + "required": ["success", "version"] + }, + { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": false + }, + "error": { + "type": "string" + } + }, + "required": ["success", "error"] + } + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "target": { + "type": "string" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.global.upgrade({\n ...\n})" + } + ] + } + }, "/auth/{providerID}": { "put": { "operationId": "auth.set", diff --git a/packages/slack/package.json b/packages/slack/package.json index a29ffc4000..732b452280 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/storybook/.storybook/mocks/app/context/language.ts b/packages/storybook/.storybook/mocks/app/context/language.ts index 8744655422..f75240b9c4 100644 --- a/packages/storybook/.storybook/mocks/app/context/language.ts +++ b/packages/storybook/.storybook/mocks/app/context/language.ts @@ -8,7 +8,7 @@ const dict: Record = { "prompt.placeholder.shell": "Run a shell command...", "prompt.placeholder.summarizeComment": "Summarize this comment", "prompt.placeholder.summarizeComments": "Summarize these comments", - "prompt.action.attachFile": "Attach file", + "prompt.action.attachFile": "Attach files", "prompt.action.send": "Send", "prompt.action.stop": "Stop", "prompt.attachment.remove": "Remove attachment", diff --git a/packages/ui/package.json b/packages/ui/package.json index 3f42296889..bb907a4a64 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.27", + "version": "1.3.0", "type": "module", "license": "MIT", "exports": { diff --git a/packages/ui/src/components/basic-tool.css b/packages/ui/src/components/basic-tool.css index 1dbfce26ec..f52a5e5762 100644 --- a/packages/ui/src/components/basic-tool.css +++ b/packages/ui/src/components/basic-tool.css @@ -8,7 +8,10 @@ justify-content: flex-start; [data-slot="basic-tool-tool-trigger-content"] { + flex: 0 1 auto; width: auto; + max-width: calc(100% - 24px); + min-width: 0; display: flex; align-items: center; align-self: stretch; @@ -51,12 +54,16 @@ [data-slot="basic-tool-tool-info"] { flex: 0 1 auto; min-width: 0; + max-width: 100%; font-size: 14px; } [data-slot="basic-tool-tool-info-structured"] { + flex: 0 1 auto; width: auto; - display: flex; + max-width: 100%; + min-width: 0; + display: inline-flex; align-items: center; gap: 8px; justify-content: flex-start; @@ -151,4 +158,10 @@ letter-spacing: var(--letter-spacing-normal); color: var(--text-base); } + + [data-slot="basic-tool-tool-action"] { + display: inline-flex; + align-items: center; + flex-shrink: 0; + } } diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 0b2c1e1ce4..a02fe941b1 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -174,7 +174,9 @@ export function BasicTool(props: BasicToolProps) { - {trigger().action} + + {trigger().action} + )} diff --git a/packages/ui/src/components/hover-card.tsx b/packages/ui/src/components/hover-card.tsx index 210fd54160..8330375aa3 100644 --- a/packages/ui/src/components/hover-card.tsx +++ b/packages/ui/src/components/hover-card.tsx @@ -13,7 +13,7 @@ export function HoverCard(props: HoverCardProps) { return ( - + {local.trigger} diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 8031bf2631..aa685392a9 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -424,7 +424,7 @@ display: flex; align-items: center; justify-content: space-between; - gap: 8px; + gap: 12px; width: 100%; [data-slot="message-part-title-area"] { @@ -436,10 +436,11 @@ } [data-slot="message-part-title"] { - flex-shrink: 0; + flex: 1 1 auto; display: flex; align-items: center; gap: 8px; + min-width: 0; font-family: var(--font-family-sans); font-size: 14px; font-style: normal; @@ -466,12 +467,17 @@ } [data-slot="message-part-title-text"] { + flex-shrink: 0; text-transform: capitalize; color: var(--text-strong); } [data-slot="message-part-title-filename"] { /* No text-transform - preserve original filename casing */ + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-weight: var(--font-weight-regular); } @@ -501,6 +507,7 @@ gap: 16px; align-items: center; justify-content: flex-end; + flex-shrink: 0; } } @@ -1183,6 +1190,7 @@ display: flex; flex-grow: 1; min-width: 0; + overflow: hidden; } [data-slot="apply-patch-directory"] { @@ -1196,7 +1204,11 @@ [data-slot="apply-patch-filename"] { color: var(--text-strong); - flex-shrink: 0; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } [data-slot="apply-patch-trigger-actions"] { diff --git a/packages/util/package.json b/packages/util/package.json index b48b755f3c..0f6a3c31ff 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.27", + "version": "1.3.0", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 6a2e48f7d3..051ae6402d 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.27", + "version": "1.3.0", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 2adf05f419..ee9307de1a 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.27", + "version": "1.3.0", "publisher": "sst-dev", "repository": { "type": "git",