mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-23 23:24:56 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a006d8700 | ||
|
|
3a0bf2f39f | ||
|
|
b556979634 | ||
|
|
691644eeeb | ||
|
|
4aebaaf067 | ||
|
|
77b3b46788 | ||
|
|
36dfe1646b | ||
|
|
6926dc26d1 | ||
|
|
eb74e4a6d2 | ||
|
|
85d8e143bf | ||
|
|
8e1b53b32c | ||
|
|
0a7dfc03ee | ||
|
|
4c27e7fc64 | ||
|
|
0f5626d2e4 | ||
|
|
5ea95451dd | ||
|
|
9239d877b9 | ||
|
|
fc68c24433 | ||
|
|
db9619dad6 | ||
|
|
84d9b38873 | ||
|
|
8035c3435b | ||
|
|
71e7603d71 | ||
|
|
40e49c5b49 | ||
|
|
afe9b97274 | ||
|
|
3b3549902d | ||
|
|
e9a9c75c1f | ||
|
|
2b171828b0 | ||
|
|
8dd817023a | ||
|
|
0d6c601365 | ||
|
|
5460bf9989 |
3
.github/VOUCHED.td
vendored
3
.github/VOUCHED.td
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -11,7 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
||||
2
.github/workflows/vouch-manage-by-issue.yml
vendored
2
.github/workflows/vouch-manage-by-issue.yml
vendored
@@ -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 }}
|
||||
|
||||
5
.opencode/command/changelog.md
Normal file
5
.opencode/command/changelog.md
Normal file
@@ -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
|
||||
31
bun.lock
31
bun.lock
@@ -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:",
|
||||
@@ -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:",
|
||||
@@ -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",
|
||||
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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, "")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -1043,7 +1043,7 @@ export const PromptInput: Component<PromptInputProps> = (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<PromptInputProps> = (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 = ""
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 }]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -267,14 +267,14 @@ export function SessionContextTab() {
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
class="@container h-full pb-10"
|
||||
class="@container h-full"
|
||||
viewportRef={(el) => {
|
||||
scroll = el
|
||||
restoreScroll()
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div class="px-6 pt-4 flex flex-col gap-10">
|
||||
<div class="px-6 pt-4 pb-10 flex flex-col gap-10">
|
||||
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
|
||||
<For each={stats}>
|
||||
{(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -157,34 +157,45 @@ const SessionHoverPreview = (props: {
|
||||
messageLabel: (message: Message) => string | undefined
|
||||
onMessageSelect: (message: Message) => void
|
||||
trigger: JSX.Element
|
||||
}): JSX.Element => (
|
||||
<HoverCard
|
||||
openDelay={1000}
|
||||
closeDelay={props.sidebarHovering() ? 600 : 0}
|
||||
placement="right-start"
|
||||
gutter={16}
|
||||
shift={-2}
|
||||
trigger={props.trigger}
|
||||
open={props.hoverSession() === props.session.id}
|
||||
onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)}
|
||||
>
|
||||
<Show
|
||||
when={props.hoverReady()}
|
||||
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
|
||||
}): JSX.Element => {
|
||||
let ref: HTMLDivElement | undefined
|
||||
|
||||
return (
|
||||
<HoverCard
|
||||
openDelay={1000}
|
||||
closeDelay={props.sidebarHovering() ? 600 : 0}
|
||||
placement="right-start"
|
||||
gutter={16}
|
||||
shift={-2}
|
||||
trigger={<div ref={ref}>{props.trigger}</div>}
|
||||
open={props.hoverSession() === props.session.id}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
props.setHoverSession(undefined)
|
||||
return
|
||||
}
|
||||
if (!ref?.matches(":hover")) return
|
||||
props.setHoverSession(props.session.id)
|
||||
}}
|
||||
>
|
||||
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
|
||||
<MessageNav
|
||||
messages={props.hoverMessages() ?? []}
|
||||
current={undefined}
|
||||
getLabel={props.messageLabel}
|
||||
onMessageSelect={props.onMessageSelect}
|
||||
size="normal"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</HoverCard>
|
||||
)
|
||||
<Show
|
||||
when={props.hoverReady()}
|
||||
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
|
||||
>
|
||||
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
|
||||
<MessageNav
|
||||
messages={props.hoverMessages() ?? []}
|
||||
current={undefined}
|
||||
getLabel={props.messageLabel}
|
||||
onMessageSelect={props.onMessageSelect}
|
||||
size="normal"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
const params = useParams()
|
||||
|
||||
@@ -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<Map<string, string[]>>
|
||||
workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
|
||||
workspaceChildren: (directory: string) => Map<string, string[]>
|
||||
setOpen: (value: boolean) => void
|
||||
ctx: ProjectSidebarContext
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}): 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: {
|
||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||
<Show when={preview() && !selected()} fallback={tile()}>
|
||||
<HoverCard
|
||||
open={!state.suppressHover && state.open && !state.menu}
|
||||
open={!state.suppressHover && hoverOpen() && !state.menu}
|
||||
openDelay={0}
|
||||
closeDelay={0}
|
||||
placement="right-start"
|
||||
@@ -366,7 +359,7 @@ export const SortableProject = (props: {
|
||||
onOpenChange={(value) => {
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -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: {
|
||||
<div
|
||||
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||
style={{
|
||||
width: slot.open ? "16px" : "0px",
|
||||
"margin-right": slot.open ? "8px" : "0px",
|
||||
width: working() ? "16px" : "0px",
|
||||
"margin-right": working() ? "8px" : "0px",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Show when={slot.show}>
|
||||
<Show when={workingStatus() !== "hidden"}>
|
||||
<div
|
||||
class="transition-opacity duration-200 ease-out"
|
||||
classList={{
|
||||
"opacity-0": slot.fade,
|
||||
}}
|
||||
classList={{ "opacity-0": workingStatus() === "hiding" }}
|
||||
>
|
||||
<Spinner class="size-4" style={{ color: tint() ?? "var(--icon-interactive-base)" }} />
|
||||
</div>
|
||||
@@ -912,7 +894,6 @@ export function MessageTimeline(props: {
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
role="log"
|
||||
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
|
||||
|
||||
@@ -438,12 +438,10 @@ export function SessionSidePanel(props: {
|
||||
size={layout.fileTree.width()}
|
||||
min={200}
|
||||
max={480}
|
||||
collapseThreshold={160}
|
||||
onResize={(width) => {
|
||||
props.size.touch()
|
||||
layout.fileTree.resize(width)
|
||||
}}
|
||||
onCollapse={layout.fileTree.close}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -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(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
||||
}),
|
||||
|
||||
44
packages/app/src/utils/prompt.test.ts
Normal file
44
packages/app/src/utils/prompt.test.ts
Normal file
@@ -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" },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string>) {
|
||||
treeKill(child.pid)
|
||||
}
|
||||
|
||||
return { events, child: { kill }, exit }
|
||||
return { events, child: { pid: child.pid, kill }, exit }
|
||||
}
|
||||
|
||||
function handleSqliteProgress(events: EventEmitter, line: string) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -101,6 +101,14 @@ export default {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "kotlin",
|
||||
wasm: "https://github.com/fwcd/tree-sitter-kotlin/releases/download/0.3.8/tree-sitter-kotlin.wasm",
|
||||
queries: {
|
||||
highlights: ["https://raw.githubusercontent.com/fwcd/tree-sitter-kotlin/0.3.8/queries/highlights.scm"],
|
||||
locals: ["https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/master/queries/kotlin/locals.scm"],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "ruby",
|
||||
wasm: "https://github.com/tree-sitter/tree-sitter-ruby/releases/download/v0.23.1/tree-sitter-ruby.wasm",
|
||||
@@ -158,6 +166,15 @@ export default {
|
||||
// },
|
||||
// },
|
||||
},
|
||||
{
|
||||
filetype: "hcl",
|
||||
wasm: "https://github.com/tree-sitter-grammars/tree-sitter-hcl/releases/download/v1.2.0/tree-sitter-hcl.wasm",
|
||||
queries: {
|
||||
highlights: [
|
||||
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/master/queries/hcl/highlights.scm",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "json",
|
||||
wasm: "https://github.com/tree-sitter/tree-sitter-json/releases/download/v0.24.8/tree-sitter-json.wasm",
|
||||
@@ -203,6 +220,16 @@ export default {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "lua",
|
||||
wasm: "https://github.com/tree-sitter-grammars/tree-sitter-lua/releases/download/v0.5.0/tree-sitter-lua.wasm",
|
||||
queries: {
|
||||
highlights: [
|
||||
"https://raw.githubusercontent.com/tree-sitter-grammars/tree-sitter-lua/v0.5.0/queries/highlights.scm",
|
||||
],
|
||||
locals: ["https://raw.githubusercontent.com/tree-sitter-grammars/tree-sitter-lua/v0.5.0/queries/locals.scm"],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "ocaml",
|
||||
wasm: "https://github.com/tree-sitter/tree-sitter-ocaml/releases/download/v0.24.2/tree-sitter-ocaml.wasm",
|
||||
@@ -236,6 +263,15 @@ export default {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "toml",
|
||||
wasm: "https://github.com/tree-sitter-grammars/tree-sitter-toml/releases/download/v0.7.0/tree-sitter-toml.wasm",
|
||||
queries: {
|
||||
highlights: [
|
||||
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/master/queries/toml/highlights.scm",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
filetype: "nix",
|
||||
// TODO: Replace with official tree-sitter-nix WASM when published
|
||||
|
||||
@@ -10,6 +10,26 @@ const openBrowser = (url: string) => Effect.promise(() => open(url).catch(() =>
|
||||
|
||||
const println = (msg: string) => Effect.sync(() => UI.println(msg))
|
||||
|
||||
const dim = (value: string) => UI.Style.TEXT_DIM + value + UI.Style.TEXT_NORMAL
|
||||
|
||||
const activeSuffix = (isActive: boolean) => (isActive ? dim(" (active)") : "")
|
||||
|
||||
export const formatAccountLabel = (account: { email: string; url: string }, isActive: boolean) =>
|
||||
`${account.email} ${dim(account.url)}${activeSuffix(isActive)}`
|
||||
|
||||
const formatOrgChoiceLabel = (account: { email: string }, org: { name: string }, isActive: boolean) =>
|
||||
`${org.name} (${account.email})${activeSuffix(isActive)}`
|
||||
|
||||
export const formatOrgLine = (
|
||||
account: { email: string; url: string },
|
||||
org: { id: string; name: string },
|
||||
isActive: boolean,
|
||||
) => {
|
||||
const dot = isActive ? UI.Style.TEXT_SUCCESS + "●" + UI.Style.TEXT_NORMAL : " "
|
||||
const name = isActive ? UI.Style.TEXT_HIGHLIGHT_BOLD + org.name + UI.Style.TEXT_NORMAL : org.name
|
||||
return ` ${dot} ${name} ${dim(account.email)} ${dim(account.url)} ${dim(org.id)}`
|
||||
}
|
||||
|
||||
const isActiveOrgChoice = (
|
||||
active: Option.Option<{ id: AccountID; active_org_id: OrgID | null }>,
|
||||
choice: { accountID: AccountID; orgID: OrgID },
|
||||
@@ -76,10 +96,9 @@ const logoutEffect = Effect.fn("logout")(function* (email?: string) {
|
||||
|
||||
const opts = accounts.map((a) => {
|
||||
const isActive = Option.isSome(activeID) && activeID.value === a.id
|
||||
const server = UI.Style.TEXT_DIM + a.url + UI.Style.TEXT_NORMAL
|
||||
return {
|
||||
value: a,
|
||||
label: isActive ? `${a.email} ${server}` + UI.Style.TEXT_DIM + " (active)" : `${a.email} ${server}`,
|
||||
label: formatAccountLabel(a, isActive),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -109,9 +128,7 @@ const switchEffect = Effect.fn("switch")(function* () {
|
||||
const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id })
|
||||
return {
|
||||
value: { orgID: org.id, accountID: group.account.id, label: org.name },
|
||||
label: isActive
|
||||
? `${org.name} (${group.account.email})` + UI.Style.TEXT_DIM + " (active)"
|
||||
: `${org.name} (${group.account.email})`,
|
||||
label: formatOrgChoiceLabel(group.account, org, isActive),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -139,15 +156,21 @@ const orgsEffect = Effect.fn("orgs")(function* () {
|
||||
for (const group of groups) {
|
||||
for (const org of group.orgs) {
|
||||
const isActive = isActiveOrgChoice(active, { accountID: group.account.id, orgID: org.id })
|
||||
const dot = isActive ? UI.Style.TEXT_SUCCESS + "●" + UI.Style.TEXT_NORMAL : " "
|
||||
const name = isActive ? UI.Style.TEXT_HIGHLIGHT_BOLD + org.name + UI.Style.TEXT_NORMAL : org.name
|
||||
const email = UI.Style.TEXT_DIM + group.account.email + UI.Style.TEXT_NORMAL
|
||||
const id = UI.Style.TEXT_DIM + org.id + UI.Style.TEXT_NORMAL
|
||||
yield* println(` ${dot} ${name} ${email} ${id}`)
|
||||
yield* println(formatOrgLine(group.account, org, isActive))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const openEffect = Effect.fn("open")(function* () {
|
||||
const service = yield* Account.Service
|
||||
const active = yield* service.active()
|
||||
if (Option.isNone(active)) return yield* println("No active account")
|
||||
|
||||
const url = active.value.url
|
||||
yield* openBrowser(url)
|
||||
yield* Prompt.outro("Opened " + url)
|
||||
})
|
||||
|
||||
export const LoginCommand = cmd({
|
||||
command: "login <url>",
|
||||
describe: false,
|
||||
@@ -195,6 +218,15 @@ export const OrgsCommand = cmd({
|
||||
},
|
||||
})
|
||||
|
||||
export const OpenCommand = cmd({
|
||||
command: "open",
|
||||
describe: false,
|
||||
async handler() {
|
||||
UI.empty()
|
||||
await Account.runPromise((_svc) => openEffect())
|
||||
},
|
||||
})
|
||||
|
||||
export const ConsoleCommand = cmd({
|
||||
command: "console",
|
||||
describe: false,
|
||||
@@ -216,6 +248,10 @@ export const ConsoleCommand = cmd({
|
||||
...OrgsCommand,
|
||||
describe: "list orgs",
|
||||
})
|
||||
.command({
|
||||
...OpenCommand,
|
||||
describe: "open active console account",
|
||||
})
|
||||
.demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!Flag.OPENCODE_EXPERIMENTAL_MARKDOWN}>
|
||||
|
||||
@@ -250,12 +250,16 @@ export namespace LLM {
|
||||
maxOutputTokens,
|
||||
abortSignal: input.abort,
|
||||
headers: {
|
||||
...(input.model.providerID.startsWith("opencode") && {
|
||||
"x-opencode-project": Instance.project.id,
|
||||
"x-opencode-session": input.sessionID,
|
||||
"x-opencode-request": input.user.id,
|
||||
"x-opencode-client": Flag.OPENCODE_CLIENT,
|
||||
}),
|
||||
...(input.model.providerID.startsWith("opencode")
|
||||
? {
|
||||
"x-opencode-project": Instance.project.id,
|
||||
"x-opencode-session": input.sessionID,
|
||||
"x-opencode-request": input.user.id,
|
||||
"x-opencode-client": Flag.OPENCODE_CLIENT,
|
||||
}
|
||||
: {
|
||||
"User-Agent": `opencode/${Installation.VERSION}`,
|
||||
}),
|
||||
...input.model.headers,
|
||||
...headers,
|
||||
},
|
||||
|
||||
26
packages/opencode/test/cli/account.test.ts
Normal file
26
packages/opencode/test/cli/account.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import stripAnsi from "strip-ansi"
|
||||
|
||||
import { formatAccountLabel, formatOrgLine } from "../../src/cli/cmd/account"
|
||||
|
||||
describe("console account display", () => {
|
||||
test("includes the account url in account labels", () => {
|
||||
expect(stripAnsi(formatAccountLabel({ email: "one@example.com", url: "https://one.example.com" }, false))).toBe(
|
||||
"one@example.com https://one.example.com",
|
||||
)
|
||||
})
|
||||
|
||||
test("includes the active marker in account labels", () => {
|
||||
expect(stripAnsi(formatAccountLabel({ email: "one@example.com", url: "https://one.example.com" }, true))).toBe(
|
||||
"one@example.com https://one.example.com (active)",
|
||||
)
|
||||
})
|
||||
|
||||
test("includes the account url in org rows", () => {
|
||||
expect(
|
||||
stripAnsi(
|
||||
formatOrgLine({ email: "one@example.com", url: "https://one.example.com" }, { id: "org-1", name: "One" }, true),
|
||||
),
|
||||
).toBe(" ● One one@example.com https://one.example.com org-1")
|
||||
})
|
||||
})
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -8,7 +8,7 @@ const dict: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,9 @@ export function BasicTool(props: BasicToolProps) {
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={!pending() && trigger().action}>{trigger().action}</Show>
|
||||
<Show when={!pending() && trigger().action}>
|
||||
<span data-slot="basic-tool-tool-action">{trigger().action}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Match>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function HoverCard(props: HoverCardProps) {
|
||||
|
||||
return (
|
||||
<Kobalte gutter={4} {...rest}>
|
||||
<Kobalte.Trigger as="div" data-slot="hover-card-trigger">
|
||||
<Kobalte.Trigger as="div" data-slot="hover-card-trigger" tabIndex={-1}>
|
||||
{local.trigger}
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal mount={local.mount}>
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
Reference in New Issue
Block a user