mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-24 03:36:10 +00:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4100018ec8 | ||
|
|
f69c29b38c | ||
|
|
11d2eb3ec8 | ||
|
|
cb8be9ba12 | ||
|
|
15046658ba | ||
|
|
7255ce9a57 | ||
|
|
2b9cf5d388 | ||
|
|
a625d35f7b | ||
|
|
449c64928b | ||
|
|
35c88c3fc8 | ||
|
|
b5e09024d5 | ||
|
|
9a51765bd2 | ||
|
|
bb31c9b92c | ||
|
|
5117679148 | ||
|
|
014696db63 | ||
|
|
351172dd1b | ||
|
|
50f8938960 | ||
|
|
23bc29fde5 | ||
|
|
2a1565951f | ||
|
|
4db321512f | ||
|
|
ecaaceeecf | ||
|
|
249bbd599c | ||
|
|
2aeeca5243 | ||
|
|
c8f4c383cd | ||
|
|
25ee2435f6 |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -143,7 +143,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -157,7 +157,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -193,7 +193,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -220,7 +220,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -242,7 +242,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -266,7 +266,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -286,7 +286,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -380,7 +380,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"effect": "catalog:",
|
||||
@@ -434,7 +434,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -448,7 +448,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -460,7 +460,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -492,7 +492,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -508,7 +508,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -539,7 +539,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -558,7 +558,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -689,7 +689,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -765,7 +765,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -780,7 +780,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -795,7 +795,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -839,7 +839,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -852,7 +852,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -886,7 +886,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -905,7 +905,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -946,7 +946,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -973,7 +973,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1024,7 +1024,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1067,6 +1067,7 @@
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||
@@ -1074,10 +1075,8 @@
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
@@ -1113,7 +1112,7 @@
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tanstack/solid-virtual": "3.13.28",
|
||||
"@tanstack/solid-virtual": "3.13.32",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.13",
|
||||
@@ -1133,7 +1132,7 @@
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "17.0.1",
|
||||
"marked": "17.0.6",
|
||||
"marked-shiki": "1.2.1",
|
||||
"opentui-spinner": "0.0.7",
|
||||
"remeda": "2.26.0",
|
||||
@@ -2781,9 +2780,9 @@
|
||||
|
||||
"@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="],
|
||||
|
||||
"@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.28", "", { "dependencies": { "@tanstack/virtual-core": "3.17.0" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-kRuOEL5orH/rzGgxNgfgOttsgV6cgrUeupVtrHMITb5p0rZ3hnxhbu/lhKcR9+7x+EJdfUtJIb2CVC85mlw15g=="],
|
||||
"@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.32", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-yhX4A4Kgn+wyTg6Mmu8+zwoMTwjz4K1ucvLfRJ8f0rPGDDAIqSaf0v6oU0yT9+SvrjmUaZQ0VX7g4byexbhNng=="],
|
||||
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.0", "", {}, "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ=="],
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="],
|
||||
|
||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||
|
||||
@@ -4313,7 +4312,7 @@
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
|
||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
|
||||
|
||||
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
|
||||
|
||||
@@ -6051,6 +6050,8 @@
|
||||
|
||||
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-N4zM1zNufSg8DrDWOHWJYgVpn6vDghX/CJ0pym9ItxI=",
|
||||
"aarch64-linux": "sha256-Votrb6IbVt6OS5pcAlBd3L2btkZHa62Eu3mAFzKSlGM=",
|
||||
"aarch64-darwin": "sha256-Ofmy6plO4CFt/DoVdyt3Sr2rk6VJhas4zXq3DnvP/6A=",
|
||||
"x86_64-darwin": "sha256-LOeqfqlPbhp1c0Gq56fvKSzve7dvcCwlooTmDMFMznw="
|
||||
"x86_64-linux": "sha256-F1luclnqCPQk9yxfmeSYGaM/nScf28yBu9K3Fv+Xd24=",
|
||||
"aarch64-linux": "sha256-XW0XZnsCRkU3MFJH9TjMRYZHffzVy3cQyiNCkec2gl4=",
|
||||
"aarch64-darwin": "sha256-bf8kvORs3Fs2UYLp3PekF+AJR7NKOcHb+fIQA79RtMk=",
|
||||
"x86_64-darwin": "sha256-sBdQPkzd7JXNW6Lbi9JHiAsfHwdLwTKWY+uPeXAv2Nw="
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -43,7 +43,7 @@
|
||||
"@opentui/core": "0.4.3",
|
||||
"@opentui/keymap": "0.4.3",
|
||||
"@opentui/solid": "0.4.3",
|
||||
"@tanstack/solid-virtual": "3.13.28",
|
||||
"@tanstack/solid-virtual": "3.13.32",
|
||||
"@shikijs/stream": "4.2.0",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -70,7 +70,7 @@
|
||||
"hono-openapi": "1.1.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "17.0.1",
|
||||
"marked": "17.0.6",
|
||||
"marked-shiki": "1.2.1",
|
||||
"remend": "1.3.0",
|
||||
"@playwright/test": "1.59.1",
|
||||
@@ -152,10 +152,9 @@
|
||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch"
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { installSseTransport } from "../utils/sse-transport"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
const directory = "C:/OpenCode/RequestDocks"
|
||||
@@ -100,6 +101,67 @@ test("shows a pending permission dock", async ({ page }) => {
|
||||
expect(request.postDataJSON()).toEqual({ response: "once" })
|
||||
})
|
||||
|
||||
test("restores the draft caret before typing after a request dock closes", async ({ page }) => {
|
||||
const transport = await installSseTransport(page, {
|
||||
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
|
||||
retry: 20,
|
||||
})
|
||||
await mockServer(page, { questions: [] })
|
||||
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
|
||||
await transport.waitForConnection()
|
||||
await expectSessionTitle(page, title)
|
||||
|
||||
const editor = page.locator('[data-component="prompt-input"][contenteditable="true"]')
|
||||
const draft = "keep the caret at the end"
|
||||
await editor.fill(draft)
|
||||
await page.evaluate(() => new Promise<void>((resolve) => requestAnimationFrame(() => resolve())))
|
||||
for (let index = 0; index < 4; index++) await page.keyboard.press("ArrowLeft")
|
||||
const cursor = draft.length - 4
|
||||
await expect
|
||||
.poll(() =>
|
||||
editor.evaluate((element) => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection?.rangeCount || !element.contains(selection.anchorNode)) return -1
|
||||
const range = selection.getRangeAt(0).cloneRange()
|
||||
range.selectNodeContents(element)
|
||||
range.setEnd(selection.anchorNode!, selection.anchorOffset)
|
||||
return range.toString().length
|
||||
}),
|
||||
)
|
||||
.toBe(cursor)
|
||||
await transport.send({
|
||||
directory,
|
||||
payload: {
|
||||
type: "question.asked",
|
||||
properties: {
|
||||
id: "question-caret",
|
||||
sessionID,
|
||||
questions: [
|
||||
{
|
||||
header: "Continue",
|
||||
question: "Continue?",
|
||||
options: [{ label: "Yes", description: "Continue the session" }],
|
||||
},
|
||||
],
|
||||
tool: { messageID: "message-caret", callID: "call-caret" },
|
||||
},
|
||||
},
|
||||
})
|
||||
const question = page.locator('[data-component="dock-prompt"][data-kind="question"]')
|
||||
await expect(question).toBeVisible()
|
||||
await expect(editor).toHaveCount(0)
|
||||
|
||||
await transport.send({
|
||||
directory,
|
||||
payload: { type: "question.rejected", properties: { sessionID, requestID: "question-caret" } },
|
||||
})
|
||||
await expect(question).toHaveCount(0)
|
||||
await expect(editor).toBeVisible()
|
||||
await page.keyboard.press("x")
|
||||
|
||||
await expect(editor).toHaveText(`${draft.slice(0, cursor)}x${draft.slice(cursor)}`)
|
||||
})
|
||||
|
||||
async function mockServer(
|
||||
page: Page,
|
||||
requests: {
|
||||
|
||||
@@ -56,6 +56,11 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||
const path = url.pathname
|
||||
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
|
||||
if (path === "/global/health") return json(route, { healthy: true })
|
||||
if (path === "/api/session")
|
||||
return json(route, {
|
||||
data: config.sessions.map((session) => v2Session(session, config.directory)),
|
||||
cursor: {},
|
||||
})
|
||||
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
|
||||
if (path === "/permission")
|
||||
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
|
||||
@@ -132,6 +137,30 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||
})
|
||||
}
|
||||
|
||||
function v2Session(session: { id: string } & Record<string, unknown>, fallbackDirectory: string) {
|
||||
const time = session.time && typeof session.time === "object" ? session.time : {}
|
||||
return {
|
||||
id: session.id,
|
||||
parentID: session.parentID,
|
||||
projectID: session.projectID ?? "project",
|
||||
cost: session.cost ?? 0,
|
||||
tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: {
|
||||
created: "created" in time && typeof time.created === "number" ? time.created : 0,
|
||||
updated: "updated" in time && typeof time.updated === "number" ? time.updated : 0,
|
||||
...(session.time && typeof session.time === "object" && "archived" in session.time
|
||||
? { archived: session.time.archived }
|
||||
: {}),
|
||||
},
|
||||
title: session.title ?? session.id,
|
||||
location: {
|
||||
directory: typeof session.directory === "string" ? session.directory : fallbackDirectory,
|
||||
...(typeof session.workspaceID === "string" ? { workspaceID: session.workspaceID } : {}),
|
||||
},
|
||||
...(typeof session.path === "string" ? { subpath: session.path } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function json(route: Route, body: unknown, headers?: Record<string, string>, status = 200) {
|
||||
return route.fulfill({
|
||||
status,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
@@ -5,6 +5,8 @@ import { createStore } from "solid-js/store"
|
||||
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4"
|
||||
import homeImage from "@/assets/help/home.png"
|
||||
import tabsImage from "@/assets/help/tabs.png"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
const helpIcon = (
|
||||
@@ -65,7 +67,7 @@ export function TabsInfoPopup() {
|
||||
<Show when={!state.dismissed}>
|
||||
<div
|
||||
class="fixed bottom-14 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]"
|
||||
aria-label="Introducing Tabs. A faster, more intuitive way to work."
|
||||
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -107,7 +109,7 @@ export function TabsInfoPopup() {
|
||||
Introducing Tabs
|
||||
</p>
|
||||
<p class="w-full select-none text-[13px] font-[440] leading-[140%] tracking-[-0.04px] text-[#808080]">
|
||||
A faster, more intuitive way to work.
|
||||
Organize your work and active sessions with tabs
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
@@ -127,16 +129,32 @@ export function TabsInfoPopup() {
|
||||
icon={<IconV2 name="xmark-small" />}
|
||||
/>
|
||||
</div>
|
||||
<div class="relative flex w-full flex-col items-start gap-6 p-8">
|
||||
<div class="relative flex min-h-0 w-full flex-1 flex-col items-start gap-6 overflow-y-auto p-8">
|
||||
<p class="w-full shrink-0 self-stretch text-[21px] font-[610] leading-6 tracking-[-0.37px] tabular-nums text-v2-text-text-base">
|
||||
Introducing Tabs Navigation.
|
||||
</p>
|
||||
<p class="w-full flex-1 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base">
|
||||
We've introduced tabs as the primary navigation in OpenCode. Your most important session are now pinned at
|
||||
the top of your screen at all times. No more hunting through menus or losing your place mid-session. Switch
|
||||
contexts instantly, pick up exactly where you left off, and keep your focus where it belongs: on the
|
||||
sessions.
|
||||
Introducing Tabs
|
||||
</p>
|
||||
<div class="flex w-full flex-1 flex-col gap-4 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base">
|
||||
<p>OpenCode Desktop is now built around tabs.</p>
|
||||
<img src={tabsImage} alt="" class="aspect-video w-full rounded-[6px] object-cover" />
|
||||
<p>
|
||||
Start a new session in a tab, or open an existing session from any of your projects. Open a new tab when
|
||||
you're starting something new, and close it when you're done.
|
||||
</p>
|
||||
<p>
|
||||
Keeping a few tabs open makes it easier to organize your active sessions. Rename tabs to something
|
||||
memorable if you plan to keep them around.
|
||||
</p>
|
||||
<p>
|
||||
You'll find all your sessions and projects on the new Home screen. Selecting a session opens it in a tab.
|
||||
</p>
|
||||
<img src={homeImage} alt="" class="aspect-video w-full rounded-[6px] object-cover" />
|
||||
<p>When you reopen the app, your tabs are still open.</p>
|
||||
<p>
|
||||
The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using
|
||||
the previous layout , you can switch between layouts in Settings. Just keep in mind that the new layout
|
||||
will become permanent in a few weeks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -633,7 +633,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
||||
|
||||
const handleBlur = () => {
|
||||
savedCursor = currentCursor()
|
||||
const cursor = currentCursor()
|
||||
savedCursor = cursor
|
||||
if (cursor !== null && cursor !== prompt.cursor()) prompt.set(prompt.current(), cursor)
|
||||
closePopover()
|
||||
setComposing(false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[data-component="server-status-icon"] {
|
||||
color: var(--v2-icon-icon-muted);
|
||||
|
||||
[data-slot="server-status-left"],
|
||||
[data-slot="server-status-right"] {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
[data-slot="server-status-dot"] {
|
||||
fill: var(--server-status-color);
|
||||
}
|
||||
|
||||
&[data-state="reconnecting"] {
|
||||
--server-status-color: var(--v2-state-fg-warning);
|
||||
}
|
||||
|
||||
&[data-state="disconnected"] {
|
||||
--server-status-color: var(--v2-state-fg-danger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { ServerStatusIcon, type ServerStatusIconState } from "./server-status-icon"
|
||||
|
||||
export default {
|
||||
title: "Desktop V2/Server Status Icon",
|
||||
id: "desktop-v2-server-status-icon",
|
||||
component: ServerStatusIcon,
|
||||
tags: ["autodocs"],
|
||||
}
|
||||
|
||||
function Preview(props: { state: ServerStatusIconState; label: string }) {
|
||||
return (
|
||||
<div class="flex items-center gap-3">
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="!w-9 shrink-0"
|
||||
aria-label={props.label}
|
||||
icon={<ServerStatusIcon state={props.state} />}
|
||||
/>
|
||||
<span class="text-[13px] text-v2-text-text-muted">{props.label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Reconnecting = {
|
||||
render: () => <Preview state="reconnecting" label="Retrying automatically..." />,
|
||||
}
|
||||
|
||||
export const Disconnected = {
|
||||
render: () => <Preview state="disconnected" label="Server disconnected" />,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { resolveServerStatus } from "./server-status-icon"
|
||||
|
||||
describe("resolveServerStatus", () => {
|
||||
test("prioritizes a disconnected server over stream reconnection", () => {
|
||||
expect(resolveServerStatus(false, "reconnecting")).toBe("disconnected")
|
||||
})
|
||||
|
||||
test("shows reconnection while the server remains reachable", () => {
|
||||
expect(resolveServerStatus(true, "reconnecting")).toBe("reconnecting")
|
||||
})
|
||||
|
||||
test("stays hidden for healthy and initial connections", () => {
|
||||
expect(resolveServerStatus(true, "connected")).toBeUndefined()
|
||||
expect(resolveServerStatus(undefined, "connecting")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import "./server-status-icon.css"
|
||||
|
||||
export type ServerStatusIconState = "reconnecting" | "disconnected"
|
||||
export type ServerConnectionState = "connecting" | "connected" | "reconnecting"
|
||||
|
||||
export function resolveServerStatus(healthy: boolean | undefined, connection: ServerConnectionState) {
|
||||
if (healthy === false) return "disconnected" as const
|
||||
if (connection === "reconnecting") return "reconnecting" as const
|
||||
}
|
||||
|
||||
export function ServerStatusIcon(props: { state: ServerStatusIconState }) {
|
||||
return (
|
||||
<svg
|
||||
data-component="server-status-icon"
|
||||
data-state={props.state}
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M13.3141 6.00001V14.4572H2.68555V1.54285H9.5M13.3141 14.4572V9.54285H2.68555V14.4572"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path data-slot="server-status-left" d="M8.5 11.75H8V12.25H8.5Z" />
|
||||
<path data-slot="server-status-right" d="M11 11.75H10.5V12.25H11Z" />
|
||||
<path
|
||||
data-slot="server-status-dot"
|
||||
d="M13 5C14.3807 5 15.5 3.88071 15.5 2.5C15.5 1.11929 14.3807 0 13 0C11.6193 0 10.5 1.11929 10.5 2.5C10.5 3.88071 11.6193 5 13 5Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// @refresh reload
|
||||
import { AppIcon } from "@opencode-ai/ui/app-icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
@@ -16,7 +17,9 @@ import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useServer } from "@/context/server"
|
||||
import { ServerConnection, serverName, useServer } from "@/context/server"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
@@ -33,6 +36,8 @@ import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import { reviewTooltipKeybind } from "../command-tooltip-keybind"
|
||||
import { useTitlebarRightMount } from "../titlebar"
|
||||
import { useSettingsDialog } from "../settings-dialog"
|
||||
import { resolveServerStatus, ServerStatusIcon, type ServerStatusIconState } from "../server-status-icon"
|
||||
|
||||
const OPEN_APPS = [
|
||||
"vscode",
|
||||
@@ -144,6 +149,8 @@ export function SessionHeader() {
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const global = useGlobal()
|
||||
const serverSDK = useServerSDK()
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const { params, view } = useSessionLayout()
|
||||
@@ -165,6 +172,7 @@ export function SessionHeader() {
|
||||
const search = settings.visibility.search
|
||||
const status = settings.visibility.status
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
const openServerSettings = useSettingsDialog("servers")
|
||||
|
||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||
finder: true,
|
||||
@@ -234,9 +242,25 @@ export function SessionHeader() {
|
||||
const tint = createMemo(() =>
|
||||
messageAgentColor(params.id ? sync().data.message[params.id] : undefined, sync().data.agent),
|
||||
)
|
||||
const serverStatus = createMemo<ServerStatusIconState | undefined>(() => {
|
||||
const current = serverSDK()
|
||||
return resolveServerStatus(
|
||||
global.servers.health[ServerConnection.key(current.server)]?.healthy,
|
||||
current.connection.status,
|
||||
)
|
||||
})
|
||||
const serverStatusLabel = createMemo(() => {
|
||||
if (serverStatus() === "disconnected") {
|
||||
return language.t("app.server.status.disconnected", { server: serverName(serverSDK().server) })
|
||||
}
|
||||
return language.t("app.server.status.reconnecting", { server: serverName(serverSDK().server) })
|
||||
})
|
||||
const v2ActionsState = createMemo<SessionHeaderV2ActionsState>(() => ({
|
||||
statusVisible: status(),
|
||||
statusLabel: language.t("status.popover.trigger"),
|
||||
serverStatus: isDesktop() ? serverStatus() : undefined,
|
||||
serverStatusLabel: serverStatusLabel(),
|
||||
onServerStatusClick: openServerSettings,
|
||||
toolsVisible: isDesktop() && status(),
|
||||
statusLabel: language.t("status.popover.tools.trigger"),
|
||||
reviewLabel: language.t("command.review.toggle"),
|
||||
reviewKeybind: reviewTooltipKeybind(command),
|
||||
reviewVisible: isDesktop(),
|
||||
@@ -517,7 +541,10 @@ export function SessionHeader() {
|
||||
}
|
||||
|
||||
type SessionHeaderV2ActionsState = {
|
||||
statusVisible: boolean
|
||||
serverStatus?: ServerStatusIconState
|
||||
serverStatusLabel: string
|
||||
onServerStatusClick: () => void
|
||||
toolsVisible: boolean
|
||||
statusLabel: string
|
||||
reviewLabel: string
|
||||
reviewKeybind: string[]
|
||||
@@ -530,11 +557,26 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={props.state.statusVisible}>
|
||||
<Tooltip placement="bottom" value={props.state.statusLabel}>
|
||||
<div class="flex items-center gap-[6px]">
|
||||
<Show when={props.state.serverStatus}>
|
||||
{(status) => (
|
||||
<TooltipV2 placement="bottom" value={props.state.serverStatusLabel}>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="!w-9 shrink-0"
|
||||
onClick={props.state.onServerStatusClick}
|
||||
aria-label={props.state.serverStatusLabel}
|
||||
icon={<ServerStatusIcon state={status()} />}
|
||||
/>
|
||||
</TooltipV2>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.state.toolsVisible}>
|
||||
<TooltipV2 placement="bottom" value={props.state.statusLabel}>
|
||||
<StatusPopoverV2 />
|
||||
</Tooltip>
|
||||
</TooltipV2>
|
||||
</Show>
|
||||
<Show when={props.state.reviewVisible}>
|
||||
<TooltipV2
|
||||
|
||||
@@ -377,8 +377,8 @@ export const SettingsGeneralV2: Component<{
|
||||
</SettingsRowV2>
|
||||
|
||||
<SettingsRowV2
|
||||
title={language.t("settings.general.row.showStatus.title")}
|
||||
description={language.t("settings.general.row.showStatus.description")}
|
||||
title={language.t("settings.general.row.showTools.title")}
|
||||
description={language.t("settings.general.row.showTools.description")}
|
||||
>
|
||||
<div data-action="settings-show-status">
|
||||
<Switch
|
||||
|
||||
@@ -16,6 +16,7 @@ import { type ServerHealth } from "@/utils/server-health"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useMcpToggle } from "@/context/mcp"
|
||||
import { ToolsMenu } from "./tools-menu"
|
||||
|
||||
const pluginEmptyMessage = (value: string, file: string): JSXElement => {
|
||||
const parts = value.split(file)
|
||||
@@ -280,7 +281,14 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
const toggleMcp = useMcpToggle()
|
||||
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
|
||||
const mcpNames = createMemo(() => Object.keys(sync().data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
|
||||
const configuredMcpNames = createMemo(() =>
|
||||
Object.keys(sync().data.config.mcp ?? {}).sort((a, b) => a.localeCompare(b)),
|
||||
)
|
||||
const mcpStatus = (name: string) => sync().data.mcp?.[name]?.status
|
||||
const mcpError = (name: string) => {
|
||||
const status = sync().data.mcp?.[name]
|
||||
if (status?.status === "failed" || status?.status === "needs_client_registration") return status.error
|
||||
}
|
||||
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
||||
const lspItems = createMemo(() => sync().data.lsp ?? [])
|
||||
const lspCount = createMemo(() => lspItems().length)
|
||||
@@ -289,6 +297,42 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
)
|
||||
const pluginCount = createMemo(() => plugins().length)
|
||||
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
||||
if (settings.general.newLayoutDesigns()) {
|
||||
return (
|
||||
<ToolsMenu
|
||||
labels={{
|
||||
menu: language.t("status.popover.tools.trigger"),
|
||||
mcp: language.t("status.popover.tab.mcp"),
|
||||
lsp: language.t("status.popover.tab.lsp"),
|
||||
plugins: language.t("status.popover.tab.plugins"),
|
||||
mcpDescription: language.t("status.popover.tools.mcp.description"),
|
||||
lspDescription: language.t("status.popover.tools.lsp.description"),
|
||||
pluginsDescription: language.t("status.popover.tools.plugins.description"),
|
||||
disabled: language.t("mcp.status.disabled"),
|
||||
failed: language.t("mcp.status.failed"),
|
||||
reauthenticate: language.t("status.popover.tools.reauthenticate"),
|
||||
}}
|
||||
empty={{
|
||||
mcp: language.t("status.popover.tools.mcp.empty"),
|
||||
lsp: language.t("status.popover.tools.lsp.empty"),
|
||||
plugins: language.t("status.popover.tools.plugins.empty"),
|
||||
}}
|
||||
mcp={configuredMcpNames().map((name) => ({
|
||||
name,
|
||||
status: mcpStatus(name),
|
||||
error: mcpError(name),
|
||||
loading: !mcpStatus(name),
|
||||
pending: toggleMcp.isPending && toggleMcp.variables === name,
|
||||
onToggle: () => {
|
||||
if (toggleMcp.isPending) return
|
||||
toggleMcp.mutate(name)
|
||||
},
|
||||
}))}
|
||||
lsp={lspItems().map((item) => ({ name: item.name || item.id, status: item.status }))}
|
||||
plugins={plugins()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
|
||||
|
||||
describe("serverStatusDotClass", () => {
|
||||
test("uses the success token while the server and services are healthy", () => {
|
||||
expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: false })).toBe("bg-icon-success-base")
|
||||
})
|
||||
|
||||
test("uses the warning token for non-blocking issues while the server is online", () => {
|
||||
expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base")
|
||||
})
|
||||
|
||||
test("uses the critical token only after the server connection drops", () => {
|
||||
expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: false })).toBe("bg-icon-critical-base")
|
||||
expect(serverStatusDotClass({ ready: true, serverHealth: false, issue: true })).toBe("bg-icon-critical-base")
|
||||
})
|
||||
|
||||
test("stays neutral before status is ready", () => {
|
||||
expect(serverStatusDotClass({ ready: false, serverHealth: true, issue: false })).toBe("bg-border-weak-base")
|
||||
expect(serverStatusDotClass({ ready: false, serverHealth: undefined, issue: false })).toBe("bg-border-weak-base")
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasNonBlockingServiceIssue", () => {
|
||||
test("detects MCP failures that do not block chatting", () => {
|
||||
expect(hasNonBlockingServiceIssue({ mcp: ["failed"], lsp: [] })).toBe(true)
|
||||
expect(hasNonBlockingServiceIssue({ mcp: ["needs_auth"], lsp: [] })).toBe(true)
|
||||
expect(hasNonBlockingServiceIssue({ mcp: ["needs_client_registration"], lsp: [] })).toBe(true)
|
||||
expect(hasNonBlockingServiceIssue({ mcp: ["connected", "disabled"], lsp: [] })).toBe(false)
|
||||
})
|
||||
|
||||
test("detects LSP failures that do not block chatting", () => {
|
||||
expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["error"] })).toBe(true)
|
||||
expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { LspStatus, McpStatus } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
export function hasNonBlockingServiceIssue(input: {
|
||||
mcp: Array<McpStatus["status"]>
|
||||
lsp: Array<LspStatus["status"]>
|
||||
}) {
|
||||
return (
|
||||
input.mcp.some((status) => status !== "connected" && status !== "disabled") ||
|
||||
input.lsp.some((status) => status === "error")
|
||||
)
|
||||
}
|
||||
|
||||
export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) {
|
||||
if (input.serverHealth === false) return "bg-icon-critical-base"
|
||||
if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base"
|
||||
if (input.issue) return "bg-icon-warning-base"
|
||||
if (input.serverHealth === true) return "bg-icon-success-base"
|
||||
return "bg-border-weak-base"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @refresh reload
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -9,6 +9,8 @@ import { ServerConnection, useServer } from "@/context/server"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { ToolsMenuIcon } from "./tools-menu"
|
||||
import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
|
||||
|
||||
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
|
||||
@@ -19,16 +21,14 @@ export function StatusPopover() {
|
||||
const global = useGlobal()
|
||||
const sync = useSync()
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync().data.mcp_ready)
|
||||
const mcpIssue = createMemo(() => {
|
||||
const mcp = Object.values(sync().data.mcp ?? {})
|
||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
||||
const warn = mcp.some((item) => item.status === "needs_auth")
|
||||
if (failed) return "critical" as const
|
||||
if (warn) return "warning" as const
|
||||
})
|
||||
const serverHealthy = () => global.servers.health[server.key]?.healthy === true
|
||||
const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue())
|
||||
const serverHealth = () => global.servers.health[server.key]?.healthy
|
||||
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
||||
const issue = createMemo(() =>
|
||||
hasNonBlockingServiceIssue({
|
||||
mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
|
||||
lsp: (sync().data.lsp ?? []).map((item) => item.status),
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover
|
||||
@@ -47,13 +47,11 @@ export function StatusPopover() {
|
||||
<Icon name={shown() ? "status-active" : "status"} size="small" />
|
||||
</div>
|
||||
<div
|
||||
classList={{
|
||||
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": ready() && healthy(),
|
||||
"bg-icon-warning-base": ready() && serverHealthy() && mcpIssue() === "warning",
|
||||
"bg-icon-critical-base": serverHealthy() || (ready() && serverHealthy() && mcpIssue() === "critical"),
|
||||
"bg-border-weak-base": serverHealthy() || !ready(),
|
||||
}}
|
||||
class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({
|
||||
ready: ready(),
|
||||
serverHealth: serverHealth(),
|
||||
issue: issue(),
|
||||
})}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -87,22 +85,22 @@ function DirectoryStatusPopover() {
|
||||
const sync = useSync()
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy
|
||||
const ready = createMemo(() => serverHealth() === false || sync().data.mcp_ready)
|
||||
const mcpIssue = createMemo(() => {
|
||||
const mcp = Object.values(sync().data.mcp ?? {})
|
||||
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
||||
const warn = mcp.some((item) => item.status === "needs_auth")
|
||||
if (failed) return "critical" as const
|
||||
if (warn) return "warning" as const
|
||||
})
|
||||
const healthy = createMemo(() => serverHealth() === true && !mcpIssue())
|
||||
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
||||
const issue = createMemo(() =>
|
||||
hasNonBlockingServiceIssue({
|
||||
mcp: Object.keys(sync().data.config.mcp ?? {}).flatMap((name) => {
|
||||
const status = sync().data.mcp?.[name]
|
||||
return status ? [status.status] : []
|
||||
}),
|
||||
lsp: (sync().data.lsp ?? []).map((item) => item.status),
|
||||
}),
|
||||
)
|
||||
const state = createMemo<StatusPopoverState>(() => ({
|
||||
shown: shown(),
|
||||
ready: ready(),
|
||||
healthy: healthy(),
|
||||
serverHealth: serverHealth(),
|
||||
issue: mcpIssue(),
|
||||
label: language.t("status.popover.trigger"),
|
||||
issue: issue(),
|
||||
label: language.t("status.popover.tools.trigger"),
|
||||
onOpenChange: setShown,
|
||||
body: () => (
|
||||
<StatusPopoverBody shown={shown()}>
|
||||
@@ -123,8 +121,8 @@ function ServerStatusPopover() {
|
||||
const state = createMemo<StatusPopoverState>(() => ({
|
||||
shown: shown(),
|
||||
ready: serverHealth() !== undefined,
|
||||
healthy: serverHealth() === true,
|
||||
serverHealth: serverHealth(),
|
||||
issue: false,
|
||||
label: language.t("status.popover.trigger"),
|
||||
onOpenChange: setShown,
|
||||
body: () => (
|
||||
@@ -140,9 +138,8 @@ function ServerStatusPopover() {
|
||||
type StatusPopoverState = {
|
||||
shown: boolean
|
||||
ready: boolean
|
||||
healthy: boolean
|
||||
serverHealth: boolean | undefined
|
||||
issue?: "critical" | "warning"
|
||||
issue: boolean
|
||||
label: string
|
||||
onOpenChange: (value: boolean) => void
|
||||
body: () => JSX.Element
|
||||
@@ -161,16 +158,6 @@ function StatusPopoverBody(props: { shown: boolean; children: JSX.Element }) {
|
||||
}
|
||||
|
||||
function StatusPopoverView(props: { state: StatusPopoverState }) {
|
||||
const statusDotClass = () => ({
|
||||
"absolute rounded-full": true,
|
||||
"bg-icon-success-base": props.state.ready && props.state.healthy,
|
||||
"bg-icon-warning-base": props.state.ready && props.state.serverHealth === true && props.state.issue === "warning",
|
||||
"bg-icon-critical-base":
|
||||
props.state.serverHealth === false ||
|
||||
(props.state.ready && props.state.serverHealth === true && props.state.issue === "critical"),
|
||||
"bg-border-weak-base": props.state.serverHealth === undefined || !props.state.ready,
|
||||
})
|
||||
|
||||
const popoverProps = {
|
||||
class:
|
||||
"[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl",
|
||||
@@ -191,15 +178,7 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
|
||||
state: props.state.shown ? "pressed" : undefined,
|
||||
"aria-label": props.state.label,
|
||||
}}
|
||||
trigger={
|
||||
<div class="relative size-4">
|
||||
<IconV2 name={props.state.shown ? "status-active" : "status"} />
|
||||
<div
|
||||
classList={statusDotClass()}
|
||||
class="-top-1 -right-1 size-2 border border-[var(--v2-background-bg-deep)]"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
trigger={<ToolsMenuIcon warning={props.state.issue} />}
|
||||
{...popoverProps}
|
||||
>
|
||||
{props.state.body()}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { For, type JSXElement } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ToolsMenu, ToolsMenuIcon, type ToolsMenuProps, type ToolsMenuStatus, type ToolsMenuTab } from "./tools-menu"
|
||||
|
||||
export default {
|
||||
title: "Desktop V2/Tools Menu",
|
||||
id: "desktop-v2-tools-menu",
|
||||
component: ToolsMenu,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `The desktop V2 Tools menu shown in the titlebar. Stories cover every MCP status, both LSP states, populated plugin rows, and empty states for all tabs.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const labels = {
|
||||
menu: "Tools",
|
||||
mcp: "MCP",
|
||||
lsp: "LSP",
|
||||
plugins: "Plugins",
|
||||
mcpDescription: "Add MCP servers in opencode.json",
|
||||
lspDescription: "LSPs are auto-detected as you open files",
|
||||
pluginsDescription: "Add plugins in opencode.json",
|
||||
disabled: "Disabled",
|
||||
failed: "Failed",
|
||||
reauthenticate: "Reauthenticate",
|
||||
}
|
||||
|
||||
const empty = {
|
||||
mcp: "No MCPs configured yet",
|
||||
lsp: "No active language servers yet",
|
||||
plugins: "No plugins configured yet",
|
||||
}
|
||||
|
||||
const mcp = [
|
||||
{ name: "ui-docs", status: "needs_auth" },
|
||||
{ name: "browser-tools", status: "needs_client_registration" },
|
||||
{ name: "brave-search", status: "disabled" },
|
||||
{ name: "fastmail-mcp", status: "connected" },
|
||||
{ name: "puppeteer", status: "failed" },
|
||||
{ name: "figma", status: "connected" },
|
||||
{ name: "playwright", status: "failed" },
|
||||
] satisfies Array<{ name: string; status: ToolsMenuStatus }>
|
||||
|
||||
const lsp = [
|
||||
{ name: "typescript", status: "connected" },
|
||||
{ name: "eslint", status: "connected" },
|
||||
{ name: "rust-analyzer", status: "error" },
|
||||
] satisfies ToolsMenuProps["lsp"]
|
||||
|
||||
const plugins = ["opencode-devcontainers", "opencode-daytona", "~/.config/opencode/plugins/ping.ts"]
|
||||
|
||||
const populated = {
|
||||
labels,
|
||||
empty,
|
||||
mcp,
|
||||
lsp,
|
||||
plugins,
|
||||
} satisfies Omit<ToolsMenuProps, "defaultTab">
|
||||
|
||||
function InteractiveMenu() {
|
||||
const [state, setState] = createStore({ mcp: mcp.map((item) => ({ ...item, pending: false })) })
|
||||
return (
|
||||
<MenuPreview warning>
|
||||
<ToolsMenu
|
||||
{...populated}
|
||||
mcp={state.mcp.map((item, index) => ({
|
||||
...item,
|
||||
onToggle: () => {
|
||||
if (item.status === "needs_auth" || item.status === "needs_client_registration") return
|
||||
setState("mcp", index, "status", item.status === "connected" ? "disabled" : "connected")
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</MenuPreview>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuPreview(props: { warning?: boolean; children: JSXElement }) {
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex h-7 items-center gap-2 text-[11px] font-[530] uppercase tracking-[0.05px] text-v2-text-text-faint">
|
||||
<ToolsMenuIcon warning={props.warning} />
|
||||
{props.warning ? "warning" : "normal"}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Gallery(props: { empty?: boolean }) {
|
||||
const tabs: ToolsMenuTab[] = ["mcp", "lsp", "plugins"]
|
||||
return (
|
||||
<div class="flex flex-wrap items-start gap-6">
|
||||
<For each={tabs}>
|
||||
{(tab) => (
|
||||
<MenuPreview warning={!props.empty && tab !== "plugins"}>
|
||||
<ToolsMenu
|
||||
{...populated}
|
||||
defaultTab={tab}
|
||||
mcp={props.empty ? [] : populated.mcp}
|
||||
lsp={props.empty ? [] : populated.lsp}
|
||||
plugins={props.empty ? [] : populated.plugins}
|
||||
/>
|
||||
</MenuPreview>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Interactive = {
|
||||
render: () => <InteractiveMenu />,
|
||||
}
|
||||
|
||||
export const AllTabs = {
|
||||
name: "All populated tabs",
|
||||
render: () => <Gallery />,
|
||||
}
|
||||
|
||||
export const EmptyStates = {
|
||||
name: "All empty tabs",
|
||||
render: () => <Gallery empty />,
|
||||
}
|
||||
|
||||
export const McpPending = {
|
||||
name: "MCP pending toggle",
|
||||
render: () => (
|
||||
<MenuPreview warning>
|
||||
<ToolsMenu {...populated} mcp={populated.mcp.map((item) => ({ ...item, pending: item.name === "figma" }))} />
|
||||
</MenuPreview>
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||
import { SegmentedControlItemV2, SegmentedControlV2 } from "@opencode-ai/ui/v2/segmented-control-v2"
|
||||
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
|
||||
import { For, Index, Show, type JSXElement } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
export type ToolsMenuTab = "mcp" | "lsp" | "plugins"
|
||||
export type ToolsMenuStatus = "connected" | "failed" | "error" | "needs_auth" | "needs_client_registration" | "disabled"
|
||||
|
||||
export type ToolsMenuProps = {
|
||||
defaultTab?: ToolsMenuTab
|
||||
labels: {
|
||||
menu: string
|
||||
mcp: string
|
||||
lsp: string
|
||||
plugins: string
|
||||
mcpDescription: string
|
||||
lspDescription: string
|
||||
pluginsDescription: string
|
||||
disabled: string
|
||||
failed: string
|
||||
reauthenticate: string
|
||||
}
|
||||
empty: {
|
||||
mcp: JSXElement
|
||||
lsp: JSXElement
|
||||
plugins: JSXElement
|
||||
}
|
||||
mcp: Array<{
|
||||
name: string
|
||||
status?: ToolsMenuStatus
|
||||
error?: string
|
||||
loading?: boolean
|
||||
pending?: boolean
|
||||
onToggle?: () => void
|
||||
}>
|
||||
lsp: Array<{
|
||||
name: string
|
||||
status: "connected" | "error"
|
||||
}>
|
||||
plugins: string[]
|
||||
}
|
||||
|
||||
export function ToolsMenuIcon(props: { warning?: boolean }) {
|
||||
return (
|
||||
<div class="relative size-4 text-v2-icon-icon-muted">
|
||||
<Icon name={props.warning ? "tools-warning" : "tools"} />
|
||||
<Show when={props.warning}>
|
||||
<div class="absolute right-0 top-0 size-[5px] rounded-full bg-icon-warning-base" />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ToolsMenu(props: ToolsMenuProps) {
|
||||
const [state, setState] = createStore({ tab: props.defaultTab ?? ("mcp" as ToolsMenuTab) })
|
||||
|
||||
return (
|
||||
<div class="w-[360px] max-w-[calc(100vw-40px)] rounded-xl bg-v2-background-bg-base p-4 [font-family:var(--v2-font-family-sans)] shadow-[var(--v2-elevation-floating)]">
|
||||
<SegmentedControlV2
|
||||
value={state.tab}
|
||||
onChange={(value) => value && setState("tab", value as ToolsMenuTab)}
|
||||
class="segmented-control-v2--full-width"
|
||||
aria-label={props.labels.menu}
|
||||
>
|
||||
<SegmentedControlItemV2 value="mcp">{props.labels.mcp}</SegmentedControlItemV2>
|
||||
<SegmentedControlItemV2 value="lsp">{props.labels.lsp}</SegmentedControlItemV2>
|
||||
<SegmentedControlItemV2 value="plugins">{props.labels.plugins}</SegmentedControlItemV2>
|
||||
</SegmentedControlV2>
|
||||
|
||||
<Show when={state.tab === "mcp"}>
|
||||
<Show
|
||||
when={props.mcp.length > 0}
|
||||
fallback={<ToolsEmpty title={props.empty.mcp} description={props.labels.mcpDescription} />}
|
||||
>
|
||||
<ToolsList description={props.labels.mcpDescription}>
|
||||
<Index each={props.mcp}>
|
||||
{(item) => (
|
||||
<div class="flex h-8 items-center gap-2 px-2">
|
||||
<StatusDot status={item().status} />
|
||||
<span class="min-w-0 flex-1 truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base">
|
||||
{item().name}
|
||||
</span>
|
||||
<Show
|
||||
when={item().status === "needs_auth"}
|
||||
fallback={
|
||||
<>
|
||||
<Show when={item().status === "needs_client_registration"}>
|
||||
<span
|
||||
class="max-w-40 truncate text-[11px] font-[440] leading-4 tracking-[0.05px] text-v2-text-text-faint"
|
||||
title={item().error}
|
||||
>
|
||||
{item().error ?? props.labels.failed}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={item().status !== "needs_client_registration"}>
|
||||
<Show when={item().status === "disabled" || item().status === "failed"}>
|
||||
<span class="text-[11px] font-[440] capitalize leading-4 tracking-[0.05px] text-v2-text-text-faint">
|
||||
{item().status === "disabled" ? props.labels.disabled : props.labels.failed}
|
||||
</span>
|
||||
</Show>
|
||||
<Switch
|
||||
checked={item().status === "connected"}
|
||||
disabled={item().loading}
|
||||
hideLabel
|
||||
onChange={item().onToggle}
|
||||
readOnly={item().pending}
|
||||
>
|
||||
{item().name}
|
||||
</Switch>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ButtonV2 size="small" variant="outline" disabled={item().pending} onClick={item().onToggle}>
|
||||
{props.labels.reauthenticate}
|
||||
</ButtonV2>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Index>
|
||||
</ToolsList>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show when={state.tab === "lsp"}>
|
||||
<Show
|
||||
when={props.lsp.length > 0}
|
||||
fallback={<ToolsEmpty title={props.empty.lsp} description={props.labels.lspDescription} />}
|
||||
>
|
||||
<ToolsList description={props.labels.lspDescription}>
|
||||
<For each={props.lsp}>
|
||||
{(item) => (
|
||||
<div class="flex h-8 items-center gap-2 px-2">
|
||||
<StatusDot status={item.status} />
|
||||
<span class="min-w-0 flex-1 truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base">
|
||||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</ToolsList>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<Show when={state.tab === "plugins"}>
|
||||
<Show
|
||||
when={props.plugins.length > 0}
|
||||
fallback={<ToolsEmpty title={props.empty.plugins} description={props.labels.pluginsDescription} />}
|
||||
>
|
||||
<ToolsList description={props.labels.pluginsDescription}>
|
||||
<For each={props.plugins}>
|
||||
{(plugin) => (
|
||||
<div class="flex h-8 items-center gap-2 px-2">
|
||||
<StatusDot status="connected" />
|
||||
<span class="min-w-0 flex-1 truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base">
|
||||
{plugin}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</ToolsList>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolsList(props: { description: string; children: JSXElement }) {
|
||||
return (
|
||||
<div class="mt-2 flex flex-col">
|
||||
<div class="flex h-8 items-center px-2 text-[11px] font-[440] leading-4 tracking-[0.05px] text-v2-text-text-faint">
|
||||
{props.description}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolsEmpty(props: { title: JSXElement; description: string }) {
|
||||
return (
|
||||
<div class="mt-4 flex flex-col gap-2 text-center text-[13px] leading-4 tracking-[-0.04px]">
|
||||
<div class="h-4 font-[530] text-v2-text-text-muted">{props.title}</div>
|
||||
<div class="h-4 font-[440] text-v2-text-text-faint">{props.description}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusDot(props: { status?: ToolsMenuStatus }) {
|
||||
return (
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 shrink-0 rounded-full": true,
|
||||
"bg-v2-state-fg-success": props.status === "connected",
|
||||
"bg-v2-state-fg-danger": props.status === "failed" || props.status === "error",
|
||||
"bg-v2-state-fg-warning": props.status === "needs_auth" || props.status === "needs_client_registration",
|
||||
"bg-v2-border-border-base": props.status === "disabled",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -221,4 +221,56 @@ describe("createChildStoreManager", () => {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test("keeps non-bootstrapping children passive until a real directory access", () => {
|
||||
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
||||
const offset = querySingles.length
|
||||
const bootstraps: string[] = []
|
||||
|
||||
const dispose = createOwner((owner) => {
|
||||
manager = createChildStoreManager({
|
||||
owner,
|
||||
scope: ServerScope.local,
|
||||
persist,
|
||||
isBooting: () => false,
|
||||
isLoadingSessions: () => false,
|
||||
onBootstrap(directory) {
|
||||
bootstraps.push(directory)
|
||||
},
|
||||
onMcp() {},
|
||||
onDispose() {},
|
||||
translate: (key) => key,
|
||||
queryOptions: queryOptionsApi,
|
||||
global: { provider },
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
if (!manager) throw new Error("manager required")
|
||||
const [store] = manager.child("/project", { bootstrap: false })
|
||||
const queries = querySingles.slice(offset)
|
||||
|
||||
expect(queries).toHaveLength(6)
|
||||
expect(queries[0]?.().enabled).toBe(false)
|
||||
expect(queries[3]?.().enabled).toBe(false)
|
||||
expect(queries[4]?.().enabled).toBe(false)
|
||||
expect(queries[5]?.().enabled).toBe(false)
|
||||
expect(store.path.directory).toBe("/project")
|
||||
expect(store.provider_ready).toBe(false)
|
||||
expect(store.lsp_ready).toBe(false)
|
||||
expect(bootstraps).toEqual([])
|
||||
|
||||
manager.child("/project")
|
||||
expect(queries[0]?.().enabled).toBe(true)
|
||||
expect(queries[3]?.().enabled).toBe(true)
|
||||
expect(queries[4]?.().enabled).toBe(true)
|
||||
expect(queries[5]?.().enabled).toBe(true)
|
||||
expect(bootstraps).toEqual(["/project"])
|
||||
|
||||
manager.child("/project", { bootstrap: false })
|
||||
expect(queries[0]?.().enabled).toBe(true)
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,6 +45,8 @@ export function createChildStoreManager(input: {
|
||||
const disposers = new Map<string, () => void>()
|
||||
const mcpDirectories = new Set<string>()
|
||||
const mcpToggles = new Map<string, (enabled: boolean) => void>()
|
||||
const activeDirectories = new Set<string>()
|
||||
const activationToggles = new Map<string, (enabled: boolean) => void>()
|
||||
|
||||
const markKey = (key: DirectoryKey) => {
|
||||
if (!key) return
|
||||
@@ -118,6 +120,8 @@ export function createChildStoreManager(input: {
|
||||
lifecycle.delete(key)
|
||||
mcpDirectories.delete(key)
|
||||
mcpToggles.delete(key)
|
||||
activeDirectories.delete(key)
|
||||
activationToggles.delete(key)
|
||||
const dispose = disposers.get(key)
|
||||
if (dispose) {
|
||||
dispose()
|
||||
@@ -182,20 +186,27 @@ export function createChildStoreManager(input: {
|
||||
const initialMeta = meta[0].value
|
||||
const initialIcon = icon[0].value
|
||||
const [mcpEnabled, setMcpEnabled] = createSignal(false)
|
||||
const [instanceQueriesEnabled, setInstanceQueriesEnabled] = createSignal(false)
|
||||
|
||||
const pathQuery = useQuery(() => input.queryOptions.path(key))
|
||||
const pathQuery = useQuery(() => ({ ...input.queryOptions.path(key), enabled: instanceQueriesEnabled() }))
|
||||
const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
|
||||
const mcpResourceQuery = useQuery(() => ({ ...input.queryOptions.mcpResources(key), enabled: mcpEnabled() }))
|
||||
const lspQuery = useQuery(() => input.queryOptions.lsp(key))
|
||||
const providerQuery = useQuery(() => input.queryOptions.providers(key))
|
||||
const referenceQuery = useQuery(() => input.queryOptions.references(key))
|
||||
const lspQuery = useQuery(() => ({ ...input.queryOptions.lsp(key), enabled: instanceQueriesEnabled() }))
|
||||
const providerQuery = useQuery(() => ({
|
||||
...input.queryOptions.providers(key),
|
||||
enabled: instanceQueriesEnabled(),
|
||||
}))
|
||||
const referenceQuery = useQuery(() => ({
|
||||
...input.queryOptions.references(key),
|
||||
enabled: instanceQueriesEnabled(),
|
||||
}))
|
||||
|
||||
const child = createStore<State>({
|
||||
project: "",
|
||||
projectMeta: initialMeta,
|
||||
icon: initialIcon,
|
||||
get provider_ready() {
|
||||
return !providerQuery.isLoading
|
||||
return instanceQueriesEnabled() && !providerQuery.isLoading
|
||||
},
|
||||
get provider() {
|
||||
const EMPTY = { all: new Map(), connected: [], default: {} }
|
||||
@@ -236,7 +247,7 @@ export function createChildStoreManager(input: {
|
||||
return mcpResourceQuery.isLoading ? {} : (mcpResourceQuery.data ?? {})
|
||||
},
|
||||
get lsp_ready() {
|
||||
return !lspQuery.isLoading
|
||||
return instanceQueriesEnabled() && !lspQuery.isLoading
|
||||
},
|
||||
get lsp() {
|
||||
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
|
||||
@@ -250,6 +261,7 @@ export function createChildStoreManager(input: {
|
||||
children[key] = child
|
||||
disposers.set(key, dispose)
|
||||
mcpToggles.set(key, setMcpEnabled)
|
||||
activationToggles.set(key, setInstanceQueriesEnabled)
|
||||
|
||||
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
|
||||
if (!(init instanceof Promise)) return
|
||||
@@ -290,6 +302,7 @@ export function createChildStoreManager(input: {
|
||||
pinForOwner(key)
|
||||
if (options.mcp) enableMcp(directory, key, childStore)
|
||||
const shouldBootstrap = options.bootstrap ?? true
|
||||
if (shouldBootstrap) activate(key)
|
||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||
input.onBootstrap(directory)
|
||||
}
|
||||
@@ -301,6 +314,7 @@ export function createChildStoreManager(input: {
|
||||
const childStore = ensureChild(directory)
|
||||
if (options.mcp) enableMcp(directory, key, childStore)
|
||||
const shouldBootstrap = options.bootstrap ?? true
|
||||
if (shouldBootstrap) activate(key)
|
||||
if (shouldBootstrap && childStore[0].status === "loading") {
|
||||
input.onBootstrap(directory)
|
||||
}
|
||||
@@ -314,6 +328,16 @@ export function createChildStoreManager(input: {
|
||||
if (childStore[0].status !== "loading") input.onMcp(directory, childStore[1])
|
||||
}
|
||||
|
||||
// Passive Home/project metadata reads must not initialize the directory.
|
||||
// A real directory access enables these queries once for the store lifetime.
|
||||
// TODO(v2): After Home switches to v2.project.list and root-filtered,
|
||||
// updated-time v2.session.list, remove any Home-only passive child creation.
|
||||
function activate(key: DirectoryKey) {
|
||||
if (activeDirectories.has(key)) return
|
||||
activeDirectories.add(key)
|
||||
activationToggles.get(key)?.(true)
|
||||
}
|
||||
|
||||
function disableMcp(directory: string) {
|
||||
const key = directoryKey(directory)
|
||||
if (!mcpDirectories.delete(key)) return
|
||||
@@ -360,6 +384,7 @@ export function createChildStoreManager(input: {
|
||||
unpin,
|
||||
pinned,
|
||||
mcp: (directory: string) => mcpDirectories.has(directoryKey(directory)),
|
||||
active: (directory: string) => activeDirectories.has(directoryKey(directory)),
|
||||
disableMcp,
|
||||
disposeDirectory,
|
||||
runEviction,
|
||||
|
||||
@@ -243,6 +243,22 @@ describe("applyDirectoryEvent", () => {
|
||||
expect(store.session_status.ses_1).toBeUndefined()
|
||||
})
|
||||
|
||||
test("ignores an archived session absent from a passive directory store", () => {
|
||||
const [store, setStore] = createStore(baseState({ session: [], sessionTotal: 0 }))
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "session.updated", properties: { info: rootSession({ id: "missing", archived: 10 }) } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
directory: "/tmp",
|
||||
loadLsp() {},
|
||||
})
|
||||
|
||||
expect(store.session).toEqual([])
|
||||
expect(store.sessionTotal).toBe(0)
|
||||
})
|
||||
|
||||
test("cleans session caches when deleted and decrements only root totals", () => {
|
||||
const cases = [
|
||||
{ info: rootSession({ id: "ses_1" }), expectedTotal: 1 },
|
||||
|
||||
@@ -146,15 +146,14 @@ export function applyDirectoryEvent(input: {
|
||||
const info = (event.properties as { info: Session }).info
|
||||
const result = Binary.search(input.store.session, info.id, (s) => s.id)
|
||||
if (info.time.archived) {
|
||||
if (!result.found) break
|
||||
if (input.store.session[result.index]!.time.archived === info.time.archived) break
|
||||
if (result.found) {
|
||||
input.setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
input.setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo)
|
||||
if (info.parentID) break
|
||||
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
applyHomeSessionEvent,
|
||||
appendHomeSessionEvent,
|
||||
HOME_V2_SESSION_PAGE_LIMIT,
|
||||
loadHomeSessionIndex,
|
||||
homeSessionIndexSessions,
|
||||
homeSessionIndexRefresh,
|
||||
parseHomeSessionIndex,
|
||||
retainHomeSessions,
|
||||
} from "./home-session-index"
|
||||
|
||||
const session = (input: {
|
||||
id: string
|
||||
directory?: string
|
||||
parentID?: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}) => ({
|
||||
id: input.id,
|
||||
parentID: input.parentID,
|
||||
projectID: "project",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: input.updated ?? 1, archived: input.archived },
|
||||
title: input.id,
|
||||
location: { directory: input.directory ?? "/project" },
|
||||
})
|
||||
|
||||
describe("Home V2 session index", () => {
|
||||
test("loads the Home index with one global V2 request", async () => {
|
||||
const calls: unknown[] = []
|
||||
const result = await loadHomeSessionIndex(async (input) => {
|
||||
calls.push(input)
|
||||
return { data: { data: [session({ id: "root" })], cursor: {} } }
|
||||
})
|
||||
|
||||
expect(result.sessions).toHaveLength(1)
|
||||
expect(calls).toEqual([{ limit: HOME_V2_SESSION_PAGE_LIMIT, order: "desc" }])
|
||||
})
|
||||
|
||||
test("loads subsequent pages until the session index is complete", async () => {
|
||||
const calls: unknown[] = []
|
||||
const controller = new AbortController()
|
||||
const result = await loadHomeSessionIndex(
|
||||
async (input, options) => {
|
||||
calls.push({ input, signal: options.signal })
|
||||
if (!("cursor" in input)) {
|
||||
return {
|
||||
data: {
|
||||
data: Array.from({ length: HOME_V2_SESSION_PAGE_LIMIT }, (_, index) =>
|
||||
session({ id: `page-1-${index}` }),
|
||||
),
|
||||
cursor: { next: "next-page" },
|
||||
},
|
||||
}
|
||||
}
|
||||
return { data: { data: [session({ id: "page-2" })], cursor: {} } }
|
||||
},
|
||||
0,
|
||||
controller.signal,
|
||||
)
|
||||
|
||||
expect(result.sessions).toHaveLength(HOME_V2_SESSION_PAGE_LIMIT + 1)
|
||||
expect(calls).toEqual([
|
||||
{ input: { limit: HOME_V2_SESSION_PAGE_LIMIT, order: "desc" }, signal: controller.signal },
|
||||
{
|
||||
input: { limit: HOME_V2_SESSION_PAGE_LIMIT, order: "desc", cursor: "next-page" },
|
||||
signal: controller.signal,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("maps visible roots to Home session summaries", () => {
|
||||
const result = parseHomeSessionIndex([
|
||||
session({ id: "root", updated: 30 }),
|
||||
session({ id: "child", parentID: "root", updated: 40 }),
|
||||
session({ id: "archived", archived: 50, updated: 50 }),
|
||||
])
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "root",
|
||||
slug: "root",
|
||||
version: "",
|
||||
directory: "/project",
|
||||
projectID: "project",
|
||||
title: "root",
|
||||
time: { created: 1, updated: 30 },
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
test("preserves the per-directory Home retention limit", () => {
|
||||
const now = 10 * 60 * 60 * 1000
|
||||
const sessions = Array.from({ length: 80 }, (_, index) => ({
|
||||
...parseHomeSessionIndex([session({ id: `session-${index}`, updated: index + 1 })])[0],
|
||||
directory: index % 2 === 0 ? "/one" : "/two",
|
||||
}))
|
||||
|
||||
const retained = retainHomeSessions(sessions, 10, now)
|
||||
expect(retained.filter((item) => item.directory === "/one")).toHaveLength(10)
|
||||
expect(retained.filter((item) => item.directory === "/two")).toHaveLength(10)
|
||||
})
|
||||
|
||||
test("replays session events over the loaded index", () => {
|
||||
const initial = parseHomeSessionIndex([session({ id: "old" })])
|
||||
const created = { ...initial[0], id: "new", slug: "new", title: "new", time: { created: 2, updated: 2 } }
|
||||
|
||||
const afterCreate = applyHomeSessionEvent(initial, {
|
||||
type: "session.created",
|
||||
properties: { sessionID: created.id, info: created },
|
||||
})
|
||||
expect(
|
||||
applyHomeSessionEvent(afterCreate, {
|
||||
type: "session.deleted",
|
||||
properties: { sessionID: initial[0]!.id, info: initial[0]! },
|
||||
}),
|
||||
).toEqual([created])
|
||||
})
|
||||
|
||||
test("applies only events newer than the index baseline", () => {
|
||||
const initial = parseHomeSessionIndex([session({ id: "old" })])
|
||||
const stale = { ...initial[0], title: "stale" }
|
||||
const current = { ...initial[0], title: "current" }
|
||||
const first = appendHomeSessionEvent(undefined, {
|
||||
type: "session.updated",
|
||||
properties: { sessionID: stale.id, info: stale },
|
||||
})
|
||||
const events = appendHomeSessionEvent(first, {
|
||||
type: "session.updated",
|
||||
properties: { sessionID: current.id, info: current },
|
||||
})
|
||||
|
||||
expect(homeSessionIndexSessions({ sessions: initial, eventSequence: 1 }, events)[0]?.title).toBe("current")
|
||||
})
|
||||
|
||||
test("refetches after reconnect, disposal, and session moves", () => {
|
||||
expect(homeSessionIndexRefresh("server.connected", false)).toEqual({ connected: true, refetch: false })
|
||||
expect(homeSessionIndexRefresh("server.connected", true)).toEqual({ connected: true, refetch: true })
|
||||
expect(homeSessionIndexRefresh("global.disposed", true).refetch).toBe(true)
|
||||
expect(homeSessionIndexRefresh("session.next.moved", true).refetch).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,174 @@
|
||||
import type { Event, Session, SessionV2Info, V2SessionListResponse } from "@opencode-ai/sdk/v2/client"
|
||||
import type { QueryClient } from "@tanstack/solid-query"
|
||||
import { trimSessions } from "./session-trim"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
export const HOME_V2_SESSION_PAGE_LIMIT = 5_000
|
||||
|
||||
export type HomeSessionEvent = {
|
||||
type: "session.created" | "session.updated" | "session.deleted"
|
||||
properties: { sessionID: string; info: Session }
|
||||
}
|
||||
export type HomeSessionEvents = {
|
||||
sequence: number
|
||||
entries: Array<{ sequence: number; event: HomeSessionEvent }>
|
||||
}
|
||||
export type HomeSessionIndex = {
|
||||
sessions: Session[]
|
||||
eventSequence: number
|
||||
}
|
||||
|
||||
export const homeSessionIndexKey = (server: string) => ["home", "session-index", server] as const
|
||||
export const homeSessionEventsKey = (server: string) => ["home", "session-events", server] as const
|
||||
|
||||
type HomeSessionPage = { data?: V2SessionListResponse }
|
||||
|
||||
export async function loadHomeSessionIndex(
|
||||
list: (
|
||||
input: { limit: number; order: "desc"; cursor?: string },
|
||||
options: { signal?: AbortSignal },
|
||||
) => Promise<HomeSessionPage>,
|
||||
eventSequence = 0,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const data: SessionV2Info[] = []
|
||||
let cursor: string | undefined
|
||||
|
||||
for (;;) {
|
||||
const response = await list(
|
||||
{
|
||||
limit: HOME_V2_SESSION_PAGE_LIMIT,
|
||||
order: "desc",
|
||||
...(cursor ? { cursor } : {}),
|
||||
},
|
||||
{ signal },
|
||||
)
|
||||
const page = response.data!
|
||||
data.push(...page.data)
|
||||
if (page.data.length < HOME_V2_SESSION_PAGE_LIMIT || !page.cursor.next)
|
||||
return { sessions: parseHomeSessionIndex(data), eventSequence }
|
||||
cursor = page.cursor.next
|
||||
}
|
||||
}
|
||||
|
||||
export function appendHomeSessionEvent(current: HomeSessionEvents | undefined, event: HomeSessionEvent) {
|
||||
const sequence = (current?.sequence ?? 0) + 1
|
||||
return {
|
||||
sequence,
|
||||
entries: [...(current?.entries ?? []), { sequence, event }],
|
||||
}
|
||||
}
|
||||
|
||||
export function trimHomeSessionEvents(current: HomeSessionEvents | undefined, sequence: number): HomeSessionEvents {
|
||||
return {
|
||||
sequence: current?.sequence ?? sequence,
|
||||
entries: (current?.entries ?? []).filter((entry) => entry.sequence > sequence),
|
||||
}
|
||||
}
|
||||
|
||||
export function homeSessionIndexSessions(index: HomeSessionIndex | undefined, events: HomeSessionEvents | undefined) {
|
||||
if (!index) return []
|
||||
return (events?.entries ?? [])
|
||||
.filter((entry) => entry.sequence > index.eventSequence)
|
||||
.reduce((sessions, entry) => applyHomeSessionEvent(sessions, entry.event), index.sessions)
|
||||
}
|
||||
|
||||
export function homeSessionIndexRefresh(event: Event["type"], connected: boolean) {
|
||||
if (event === "server.connected") return { connected: true, refetch: connected }
|
||||
return {
|
||||
connected,
|
||||
refetch: event === "global.disposed" || event === "session.next.moved",
|
||||
}
|
||||
}
|
||||
|
||||
export function createHomeSessionIndexCache(queryClient: QueryClient, server: string) {
|
||||
const indexKey = homeSessionIndexKey(server)
|
||||
const eventsKey = homeSessionEventsKey(server)
|
||||
let connected = false
|
||||
|
||||
return {
|
||||
indexKey,
|
||||
eventsKey,
|
||||
eventSequence() {
|
||||
return queryClient.getQueryData<HomeSessionEvents>(eventsKey)?.sequence ?? 0
|
||||
},
|
||||
complete(sequence: number) {
|
||||
// Keep events received after the fetch began so its response cannot overwrite them.
|
||||
queryClient.setQueryData<HomeSessionEvents>(eventsKey, (current) => trimHomeSessionEvents(current, sequence))
|
||||
},
|
||||
sessions(index: HomeSessionIndex | undefined, events: HomeSessionEvents | undefined) {
|
||||
return homeSessionIndexSessions(index, events)
|
||||
},
|
||||
apply(event: HomeSessionEvent) {
|
||||
if (!queryClient.getQueryState(indexKey)) return
|
||||
const next = appendHomeSessionEvent(queryClient.getQueryData<HomeSessionEvents>(eventsKey), event)
|
||||
if (queryClient.isFetching({ queryKey: indexKey, exact: true }) > 0) {
|
||||
queryClient.setQueryData(eventsKey, next)
|
||||
return
|
||||
}
|
||||
|
||||
const index = queryClient.getQueryData<HomeSessionIndex>(indexKey)
|
||||
if (index) {
|
||||
queryClient.setQueryData<HomeSessionIndex>(indexKey, {
|
||||
sessions: homeSessionIndexSessions(index, next),
|
||||
eventSequence: next.sequence,
|
||||
})
|
||||
}
|
||||
queryClient.setQueryData<HomeSessionEvents>(eventsKey, { sequence: next.sequence, entries: [] })
|
||||
},
|
||||
refresh(event: Event["type"]) {
|
||||
const result = homeSessionIndexRefresh(event, connected)
|
||||
connected = result.connected
|
||||
if (!result.refetch) return
|
||||
void queryClient.refetchQueries({ queryKey: indexKey, exact: true, type: "active" })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(v2): This deliberately dumb full-table scan is necessary because the
|
||||
// current V2 API orders by creation time and cannot filter roots, archives, or
|
||||
// multiple directories. A bounded page could omit an old session updated today.
|
||||
// Once released, use client.v2.project.list() and client.v2.session.list({
|
||||
// parentID: null, order: "desc" }), then remove this adapter and its V1 fields.
|
||||
export function parseHomeSessionIndex(sessions: SessionV2Info[]): Session[] {
|
||||
return sessions.flatMap((item) => {
|
||||
if (item.parentID || item.time.archived !== undefined) return []
|
||||
return [toLegacySummary(item)]
|
||||
})
|
||||
}
|
||||
|
||||
export function retainHomeSessions(sessions: Session[], limit: number, now: number) {
|
||||
const grouped = Map.groupBy(sessions, (session) => pathKey(session.directory))
|
||||
return [...grouped.values()].flatMap((items) => trimSessions(items, { limit, permission: {}, now }))
|
||||
}
|
||||
|
||||
export function applyHomeSessionEvent(sessions: Session[], event: HomeSessionEvent) {
|
||||
const info = event.properties.info
|
||||
const index = sessions.findIndex((session) => session.id === info.id)
|
||||
if (event.type === "session.deleted" || info.parentID || info.time.archived !== undefined) {
|
||||
if (index === -1) return sessions
|
||||
return sessions.toSpliced(index, 1)
|
||||
}
|
||||
if (event.type !== "session.created" && event.type !== "session.updated") return sessions
|
||||
if (index === -1) return [...sessions, info]
|
||||
return sessions.with(index, info)
|
||||
}
|
||||
|
||||
function toLegacySummary(session: SessionV2Info): Session {
|
||||
return {
|
||||
id: session.id,
|
||||
slug: session.id,
|
||||
projectID: session.projectID,
|
||||
workspaceID: session.location.workspaceID,
|
||||
directory: session.location.directory,
|
||||
path: session.subpath,
|
||||
parentID: session.parentID,
|
||||
cost: session.cost,
|
||||
tokens: session.tokens,
|
||||
title: session.title,
|
||||
agent: session.agent,
|
||||
model: session.model,
|
||||
version: "",
|
||||
time: session.time,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { type Accessor, batch, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSdkForServer } from "@/utils/server"
|
||||
import { useLanguage } from "./language"
|
||||
import { usePlatform } from "./platform"
|
||||
@@ -142,6 +143,9 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
||||
let run: Promise<void> | undefined
|
||||
let started = false
|
||||
let generation = 0
|
||||
const [connection, setConnection] = createStore({
|
||||
status: "connecting" as "connecting" | "connected" | "reconnecting",
|
||||
})
|
||||
const HEARTBEAT_TIMEOUT_MS = 15_000
|
||||
let lastEventAt = Date.now()
|
||||
let heartbeat: ReturnType<typeof setTimeout> | undefined
|
||||
@@ -187,6 +191,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
||||
})
|
||||
},
|
||||
})
|
||||
setConnection("status", "connected")
|
||||
let yielded = Date.now()
|
||||
resetHeartbeat()
|
||||
for await (const event of events.stream) {
|
||||
@@ -218,6 +223,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
||||
}
|
||||
|
||||
if (abort.signal.aborted || !started || generation !== active) return
|
||||
setConnection("status", "reconnecting")
|
||||
await wait(RECONNECT_DELAY_MS)
|
||||
}
|
||||
})().finally(() => {
|
||||
@@ -269,6 +275,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
|
||||
listen: emitter.listen.bind(emitter),
|
||||
start,
|
||||
},
|
||||
connection,
|
||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||
return createSdkForServer({
|
||||
server: server.http,
|
||||
|
||||
@@ -43,6 +43,7 @@ import { useGlobal } from "./global"
|
||||
import { ServerConnection, useServer } from "./server"
|
||||
import { retry } from "@opencode-ai/core/util/retry"
|
||||
import type { ServerScope } from "@/utils/server-scope"
|
||||
import { createHomeSessionIndexCache } from "./global-sync/home-session-index"
|
||||
import { persisted } from "@/utils/persist"
|
||||
import { toggleMcp } from "./global-sync/mcp"
|
||||
import { createServerSession } from "./server-session"
|
||||
@@ -153,6 +154,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
})
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const homeSessions = createHomeSessionIndexCache(queryClient, ServerConnection.key(serverSDK.server))
|
||||
|
||||
let bootedAt = 0
|
||||
let bootingRoot = false
|
||||
@@ -376,6 +378,10 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
const recent = bootingRoot || Date.now() - bootedAt < 1500
|
||||
|
||||
session.apply(event)
|
||||
if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted") {
|
||||
homeSessions.apply(event)
|
||||
}
|
||||
homeSessions.refresh(event.type)
|
||||
|
||||
if (directory === "global") {
|
||||
applyGlobalEvent({
|
||||
@@ -390,6 +396,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
if (event.type === "server.connected" || event.type === "global.disposed") {
|
||||
if (recent) return
|
||||
for (const directory of Object.keys(children.children)) {
|
||||
if (!children.active(directory)) continue
|
||||
queue.push(directory)
|
||||
}
|
||||
}
|
||||
@@ -405,15 +412,19 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
directory,
|
||||
store,
|
||||
setStore,
|
||||
push: queue.push,
|
||||
push: (directory) => {
|
||||
if (children.active(directory)) queue.push(directory)
|
||||
},
|
||||
retainedLimit: sessionMeta.get(key)?.limit,
|
||||
sessionContent: false,
|
||||
permission: session.data.permission,
|
||||
vcsCache: children.vcsCache.get(key),
|
||||
loadLsp: () => {
|
||||
if (!children.active(key)) return
|
||||
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
|
||||
},
|
||||
loadReferences: () => {
|
||||
if (!children.active(key)) return
|
||||
void queryClient.fetchQuery(queryOptionsApi.references(key))
|
||||
},
|
||||
})
|
||||
@@ -486,6 +497,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
updateConfig: updateConfigMutation.mutateAsync,
|
||||
project: projectApi,
|
||||
session,
|
||||
homeSessions,
|
||||
mcp: {
|
||||
toggle: async (directory: string, name: string) => {
|
||||
const key = directoryKey(directory)
|
||||
|
||||
@@ -621,6 +621,14 @@ export const dict = {
|
||||
"session.header.open.menu": "خيارات الفتح",
|
||||
"session.header.open.copyPath": "نسخ المسار",
|
||||
"status.popover.trigger": "الحالة",
|
||||
"status.popover.tools.trigger": "الأدوات",
|
||||
"status.popover.tools.mcp.description": "أضف خوادم MCP في opencode.json",
|
||||
"status.popover.tools.lsp.description": "يتم اكتشاف خوادم LSP تلقائيًا عند فتح الملفات",
|
||||
"status.popover.tools.plugins.description": "أضف الإضافات في opencode.json",
|
||||
"status.popover.tools.mcp.empty": "لم يتم إعداد أي خوادم MCP بعد",
|
||||
"status.popover.tools.lsp.empty": "لا توجد خوادم لغة نشطة بعد",
|
||||
"status.popover.tools.plugins.empty": "لم يتم إعداد أي إضافات بعد",
|
||||
"status.popover.tools.reauthenticate": "إعادة المصادقة",
|
||||
"status.popover.ariaLabel": "إعدادات الخوادم",
|
||||
"status.popover.tab.servers": "الخوادم",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -722,6 +730,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "إظهار زر المحطة الطرفية في شريط عنوان سطح المكتب",
|
||||
"settings.general.row.showStatus.title": "حالة الخادم",
|
||||
"settings.general.row.showStatus.description": "إظهار زر حالة الخادم في شريط العنوان",
|
||||
"settings.general.row.showTools.title": "قائمة الأدوات",
|
||||
"settings.general.row.showTools.description": "إظهار وصول سريع إلى MCP وLSP والإضافات في شريط العنوان",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "التنقل السفلي",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"وضع شريط العنوان وعلامات تبويب الجلسات أسفل الشاشة على الأجهزة المحمولة",
|
||||
@@ -929,6 +939,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "تعذر الوصول إلى {{server}}",
|
||||
"app.server.retrying": "جاري إعادة المحاولة تلقائيًا...",
|
||||
"app.server.status.reconnecting": "جارٍ إعادة الاتصال بـ {{server}}...",
|
||||
"app.server.status.disconnected": "تعذر الاتصال بـ {{server}}",
|
||||
"app.server.otherServers": "خوادم أخرى",
|
||||
"dialog.server.add.usernamePlaceholder": "اسم المستخدم",
|
||||
"dialog.server.add.passwordPlaceholder": "كلمة المرور",
|
||||
|
||||
@@ -627,6 +627,14 @@ export const dict = {
|
||||
"session.header.open.menu": "Opções de abertura",
|
||||
"session.header.open.copyPath": "Copiar caminho",
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.tools.trigger": "Ferramentas",
|
||||
"status.popover.tools.mcp.description": "Adicione servidores MCP no opencode.json",
|
||||
"status.popover.tools.lsp.description": "Os LSPs são detectados automaticamente ao abrir arquivos",
|
||||
"status.popover.tools.plugins.description": "Adicione plugins no opencode.json",
|
||||
"status.popover.tools.mcp.empty": "Nenhum MCP configurado ainda",
|
||||
"status.popover.tools.lsp.empty": "Nenhum servidor de linguagem ativo ainda",
|
||||
"status.popover.tools.plugins.empty": "Nenhum plugin configurado ainda",
|
||||
"status.popover.tools.reauthenticate": "Autenticar novamente",
|
||||
"status.popover.ariaLabel": "Configurações de servidores",
|
||||
"status.popover.tab.servers": "Servidores",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -733,6 +741,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Mostrar o botão do terminal na barra de título do desktop",
|
||||
"settings.general.row.showStatus.title": "Status do servidor",
|
||||
"settings.general.row.showStatus.description": "Mostrar o botão de status do servidor na barra de título",
|
||||
"settings.general.row.showTools.title": "Menu de ferramentas",
|
||||
"settings.general.row.showTools.description": "Mostrar acesso rápido a MCPs, LSPs e plugins na barra de título",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Navegação inferior",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Posicionar a barra de título e as abas da sessão na parte inferior da tela em dispositivos móveis",
|
||||
@@ -943,6 +953,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Não foi possível conectar a {{server}}",
|
||||
"app.server.retrying": "Tentando novamente automaticamente...",
|
||||
"app.server.status.reconnecting": "Reconectando a {{server}}...",
|
||||
"app.server.status.disconnected": "Não foi possível conectar a {{server}}",
|
||||
"app.server.otherServers": "Outros servidores",
|
||||
"dialog.server.add.usernamePlaceholder": "nome de usuário",
|
||||
"dialog.server.add.passwordPlaceholder": "senha",
|
||||
|
||||
@@ -685,6 +685,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "Kopiraj putanju",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.tools.trigger": "Alati",
|
||||
"status.popover.tools.mcp.description": "Dodajte MCP servere u opencode.json",
|
||||
"status.popover.tools.lsp.description": "LSP serveri se automatski otkrivaju dok otvarate datoteke",
|
||||
"status.popover.tools.plugins.description": "Dodajte dodatke u opencode.json",
|
||||
"status.popover.tools.mcp.empty": "Još nema konfigurisanih MCP servera",
|
||||
"status.popover.tools.lsp.empty": "Još nema aktivnih jezičkih servera",
|
||||
"status.popover.tools.plugins.empty": "Još nema konfigurisanih dodataka",
|
||||
"status.popover.tools.reauthenticate": "Ponovo se autentificiraj",
|
||||
"status.popover.ariaLabel": "Konfiguracije servera",
|
||||
"status.popover.tab.servers": "Serveri",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -797,6 +805,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Prikaži dugme terminala u naslovnoj traci desktop aplikacije",
|
||||
"settings.general.row.showStatus.title": "Status servera",
|
||||
"settings.general.row.showStatus.description": "Prikaži dugme statusa servera u naslovnoj traci",
|
||||
"settings.general.row.showTools.title": "Meni alata",
|
||||
"settings.general.row.showTools.description": "Prikaži brzi pristup MCP, LSP i dodacima u naslovnoj traci",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Donja navigacija",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Postavi naslovnu traku i kartice sesije na dno ekrana na mobilnim uređajima",
|
||||
@@ -1019,6 +1029,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Nije moguće pristupiti {{server}}",
|
||||
"app.server.retrying": "Automatski ponovni pokušaj...",
|
||||
"app.server.status.reconnecting": "Ponovno povezivanje sa {{server}}...",
|
||||
"app.server.status.disconnected": "Nije moguće povezati se sa {{server}}",
|
||||
"app.server.otherServers": "Drugi serveri",
|
||||
"dialog.server.add.usernamePlaceholder": "korisničko ime",
|
||||
"dialog.server.add.passwordPlaceholder": "lozinka",
|
||||
|
||||
@@ -680,6 +680,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "Kopier sti",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.tools.trigger": "Værktøjer",
|
||||
"status.popover.tools.mcp.description": "Tilføj MCP-servere i opencode.json",
|
||||
"status.popover.tools.lsp.description": "LSP'er registreres automatisk, når du åbner filer",
|
||||
"status.popover.tools.plugins.description": "Tilføj plugins i opencode.json",
|
||||
"status.popover.tools.mcp.empty": "Ingen MCP'er konfigureret endnu",
|
||||
"status.popover.tools.lsp.empty": "Ingen aktive sprogservere endnu",
|
||||
"status.popover.tools.plugins.empty": "Ingen plugins konfigureret endnu",
|
||||
"status.popover.tools.reauthenticate": "Godkend igen",
|
||||
"status.popover.ariaLabel": "Serverkonfigurationer",
|
||||
"status.popover.tab.servers": "Servere",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -791,6 +799,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Vis terminalknappen i desktopappens titellinje",
|
||||
"settings.general.row.showStatus.title": "Serverstatus",
|
||||
"settings.general.row.showStatus.description": "Vis serverstatusknappen i titellinjen",
|
||||
"settings.general.row.showTools.title": "Værktøjsmenu",
|
||||
"settings.general.row.showTools.description": "Vis hurtig adgang til MCP'er, LSP'er og plugins i titellinjen",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Navigation nederst",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Placer titellinjen og sessionsfanerne nederst på skærmen på mobilenheder",
|
||||
@@ -1011,6 +1021,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Kunne ikke nå {{server}}",
|
||||
"app.server.retrying": "Prøver igen automatisk...",
|
||||
"app.server.status.reconnecting": "Opretter forbindelse til {{server}} igen...",
|
||||
"app.server.status.disconnected": "Kunne ikke oprette forbindelse til {{server}}",
|
||||
"app.server.otherServers": "Andre servere",
|
||||
"dialog.server.add.usernamePlaceholder": "brugernavn",
|
||||
"dialog.server.add.passwordPlaceholder": "adgangskode",
|
||||
|
||||
@@ -636,6 +636,14 @@ export const dict = {
|
||||
"session.header.open.menu": "Öffnen-Optionen",
|
||||
"session.header.open.copyPath": "Pfad kopieren",
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.tools.trigger": "Werkzeuge",
|
||||
"status.popover.tools.mcp.description": "MCP-Server in opencode.json hinzufügen",
|
||||
"status.popover.tools.lsp.description": "LSPs werden beim Öffnen von Dateien automatisch erkannt",
|
||||
"status.popover.tools.plugins.description": "Plugins in opencode.json hinzufügen",
|
||||
"status.popover.tools.mcp.empty": "Noch keine MCPs konfiguriert",
|
||||
"status.popover.tools.lsp.empty": "Noch keine aktiven Sprachserver",
|
||||
"status.popover.tools.plugins.empty": "Noch keine Plugins konfiguriert",
|
||||
"status.popover.tools.reauthenticate": "Erneut authentifizieren",
|
||||
"status.popover.ariaLabel": "Serverkonfigurationen",
|
||||
"status.popover.tab.servers": "Server",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -745,6 +753,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Terminal-Schaltfläche in der Desktop-Titelleiste anzeigen",
|
||||
"settings.general.row.showStatus.title": "Serverstatus",
|
||||
"settings.general.row.showStatus.description": "Serverstatus-Schaltfläche in der Titelleiste anzeigen",
|
||||
"settings.general.row.showTools.title": "Werkzeugmenü",
|
||||
"settings.general.row.showTools.description": "Schnellzugriff auf MCPs, LSPs und Plugins in der Titelleiste anzeigen",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Navigation unten",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Titelleiste und Sitzungs-Tabs auf Mobilgeräten am unteren Bildschirmrand platzieren",
|
||||
@@ -955,6 +965,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Konnte {{server}} nicht erreichen",
|
||||
"app.server.retrying": "Automatische erneute Verbindung...",
|
||||
"app.server.status.reconnecting": "Verbindung zu {{server}} wird wiederhergestellt...",
|
||||
"app.server.status.disconnected": "Verbindung zu {{server}} konnte nicht hergestellt werden",
|
||||
"app.server.otherServers": "Andere Server",
|
||||
"dialog.server.add.usernamePlaceholder": "Benutzername",
|
||||
"dialog.server.add.passwordPlaceholder": "Passwort",
|
||||
|
||||
@@ -332,6 +332,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Could not reach {{server}}",
|
||||
"app.server.retrying": "Retrying automatically...",
|
||||
"app.server.status.reconnecting": "Reconnecting to {{server}}...",
|
||||
"app.server.status.disconnected": "Couldn't connect to {{server}}",
|
||||
"app.server.otherServers": "Other servers",
|
||||
|
||||
"dialog.server.title": "Servers",
|
||||
@@ -728,6 +730,14 @@ export const dict = {
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Server configurations",
|
||||
"status.popover.tools.trigger": "Tools",
|
||||
"status.popover.tools.mcp.description": "Add MCP servers in opencode.json",
|
||||
"status.popover.tools.lsp.description": "LSPs are auto-detected as you open files",
|
||||
"status.popover.tools.plugins.description": "Add plugins in opencode.json",
|
||||
"status.popover.tools.mcp.empty": "No MCPs configured yet",
|
||||
"status.popover.tools.lsp.empty": "No active language servers yet",
|
||||
"status.popover.tools.plugins.empty": "No plugins configured yet",
|
||||
"status.popover.tools.reauthenticate": "Reauthenticate",
|
||||
"status.popover.tab.servers": "Servers",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
@@ -886,6 +896,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
|
||||
"settings.general.row.showStatus.title": "Server status",
|
||||
"settings.general.row.showStatus.description": "Show the server status button in the title bar",
|
||||
"settings.general.row.showTools.title": "Tools menu",
|
||||
"settings.general.row.showTools.description": "Show quick access to MCPs, LSPs, and plugins in the title bar",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Bottom navigation",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Place the title bar and session tabs at the bottom of the screen on mobile",
|
||||
|
||||
@@ -686,6 +686,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "Copiar ruta",
|
||||
|
||||
"status.popover.trigger": "Estado",
|
||||
"status.popover.tools.trigger": "Herramientas",
|
||||
"status.popover.tools.mcp.description": "Añade servidores MCP en opencode.json",
|
||||
"status.popover.tools.lsp.description": "Los LSP se detectan automáticamente al abrir archivos",
|
||||
"status.popover.tools.plugins.description": "Añade plugins en opencode.json",
|
||||
"status.popover.tools.mcp.empty": "Aún no hay MCP configurados",
|
||||
"status.popover.tools.lsp.empty": "Aún no hay servidores de lenguaje activos",
|
||||
"status.popover.tools.plugins.empty": "Aún no hay plugins configurados",
|
||||
"status.popover.tools.reauthenticate": "Volver a autenticar",
|
||||
"status.popover.ariaLabel": "Configuraciones del servidor",
|
||||
"status.popover.tab.servers": "Servidores",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -802,6 +810,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Mostrar el botón del terminal en la barra de título",
|
||||
"settings.general.row.showStatus.title": "Estado del servidor",
|
||||
"settings.general.row.showStatus.description": "Mostrar el botón de estado del servidor en la barra de título",
|
||||
"settings.general.row.showTools.title": "Menú de herramientas",
|
||||
"settings.general.row.showTools.description": "Mostrar acceso rápido a MCP, LSP y plugins en la barra de título",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Navegación inferior",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Colocar la barra de título y las pestañas de sesión en la parte inferior de la pantalla en dispositivos móviles",
|
||||
@@ -1027,6 +1037,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "No se pudo conectar con {{server}}",
|
||||
"app.server.retrying": "Reintentando automáticamente...",
|
||||
"app.server.status.reconnecting": "Reconectando con {{server}}...",
|
||||
"app.server.status.disconnected": "No se pudo conectar con {{server}}",
|
||||
"app.server.otherServers": "Otros servidores",
|
||||
"dialog.server.add.usernamePlaceholder": "usuario",
|
||||
"dialog.server.add.passwordPlaceholder": "contraseña",
|
||||
|
||||
@@ -632,6 +632,14 @@ export const dict = {
|
||||
"session.header.open.menu": "Options d'ouverture",
|
||||
"session.header.open.copyPath": "Copier le chemin",
|
||||
"status.popover.trigger": "Statut",
|
||||
"status.popover.tools.trigger": "Outils",
|
||||
"status.popover.tools.mcp.description": "Ajouter des serveurs MCP dans opencode.json",
|
||||
"status.popover.tools.lsp.description": "Les LSP sont détectés automatiquement à l’ouverture des fichiers",
|
||||
"status.popover.tools.plugins.description": "Ajouter des plugins dans opencode.json",
|
||||
"status.popover.tools.mcp.empty": "Aucun MCP configuré pour le moment",
|
||||
"status.popover.tools.lsp.empty": "Aucun serveur de langage actif pour le moment",
|
||||
"status.popover.tools.plugins.empty": "Aucun plugin configuré pour le moment",
|
||||
"status.popover.tools.reauthenticate": "S’authentifier à nouveau",
|
||||
"status.popover.ariaLabel": "Configurations des serveurs",
|
||||
"status.popover.tab.servers": "Serveurs",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -742,6 +750,9 @@ export const dict = {
|
||||
"Afficher le bouton du terminal dans la barre de titre de l'application desktop",
|
||||
"settings.general.row.showStatus.title": "État du serveur",
|
||||
"settings.general.row.showStatus.description": "Afficher le bouton d'état du serveur dans la barre de titre",
|
||||
"settings.general.row.showTools.title": "Menu Outils",
|
||||
"settings.general.row.showTools.description":
|
||||
"Afficher un accès rapide aux MCP, LSP et plugins dans la barre de titre",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Navigation en bas",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Placer la barre de titre et les onglets de session en bas de l'écran sur mobile",
|
||||
@@ -954,6 +965,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Impossible de joindre {{server}}",
|
||||
"app.server.retrying": "Nouvelle tentative automatique...",
|
||||
"app.server.status.reconnecting": "Reconnexion à {{server}}...",
|
||||
"app.server.status.disconnected": "Impossible de se connecter à {{server}}",
|
||||
"app.server.otherServers": "Autres serveurs",
|
||||
"dialog.server.add.usernamePlaceholder": "nom d'utilisateur",
|
||||
"dialog.server.add.passwordPlaceholder": "mot de passe",
|
||||
|
||||
@@ -623,6 +623,14 @@ export const dict = {
|
||||
"session.header.open.menu": "開くオプション",
|
||||
"session.header.open.copyPath": "パスをコピー",
|
||||
"status.popover.trigger": "ステータス",
|
||||
"status.popover.tools.trigger": "ツール",
|
||||
"status.popover.tools.mcp.description": "opencode.json に MCP サーバーを追加",
|
||||
"status.popover.tools.lsp.description": "ファイルを開くと LSP が自動検出されます",
|
||||
"status.popover.tools.plugins.description": "opencode.json にプラグインを追加",
|
||||
"status.popover.tools.mcp.empty": "MCP はまだ設定されていません",
|
||||
"status.popover.tools.lsp.empty": "アクティブな言語サーバーはまだありません",
|
||||
"status.popover.tools.plugins.empty": "プラグインはまだ設定されていません",
|
||||
"status.popover.tools.reauthenticate": "再認証",
|
||||
"status.popover.ariaLabel": "サーバー設定",
|
||||
"status.popover.tab.servers": "サーバー",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -728,6 +736,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "デスクトップのタイトルバーにターミナルボタンを表示します",
|
||||
"settings.general.row.showStatus.title": "サーバーステータス",
|
||||
"settings.general.row.showStatus.description": "タイトルバーにサーバーステータスボタンを表示します",
|
||||
"settings.general.row.showTools.title": "ツールメニュー",
|
||||
"settings.general.row.showTools.description": "タイトルバーに MCP、LSP、プラグインへのクイックアクセスを表示します",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "下部ナビゲーション",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"モバイルではタイトルバーとセッションタブを画面下部に配置します",
|
||||
@@ -936,6 +946,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "{{server}} に到達できませんでした",
|
||||
"app.server.retrying": "自動的に再試行中...",
|
||||
"app.server.status.reconnecting": "{{server}} に再接続しています...",
|
||||
"app.server.status.disconnected": "{{server}} に接続できませんでした",
|
||||
"app.server.otherServers": "その他のサーバー",
|
||||
"dialog.server.add.usernamePlaceholder": "ユーザー名",
|
||||
"dialog.server.add.passwordPlaceholder": "パスワード",
|
||||
|
||||
@@ -503,6 +503,14 @@ export const dict = {
|
||||
"session.header.open.menu": "열기 옵션",
|
||||
"session.header.open.copyPath": "경로 복사",
|
||||
"status.popover.trigger": "상태",
|
||||
"status.popover.tools.trigger": "도구",
|
||||
"status.popover.tools.mcp.description": "opencode.json에 MCP 서버 추가",
|
||||
"status.popover.tools.lsp.description": "파일을 열면 LSP가 자동으로 감지됩니다",
|
||||
"status.popover.tools.plugins.description": "opencode.json에 플러그인 추가",
|
||||
"status.popover.tools.mcp.empty": "아직 구성된 MCP가 없습니다",
|
||||
"status.popover.tools.lsp.empty": "아직 활성 언어 서버가 없습니다",
|
||||
"status.popover.tools.plugins.empty": "아직 구성된 플러그인이 없습니다",
|
||||
"status.popover.tools.reauthenticate": "다시 인증",
|
||||
"status.popover.ariaLabel": "서버 구성",
|
||||
"status.popover.tab.servers": "서버",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -782,6 +790,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "{{server}}에 연결할 수 없습니다",
|
||||
"app.server.retrying": "자동으로 재시도 중...",
|
||||
"app.server.status.reconnecting": "{{server}}에 다시 연결하는 중...",
|
||||
"app.server.status.disconnected": "{{server}}에 연결할 수 없습니다",
|
||||
"app.server.otherServers": "다른 서버",
|
||||
"dialog.server.add.usernamePlaceholder": "사용자 이름",
|
||||
"dialog.server.add.passwordPlaceholder": "비밀번호",
|
||||
@@ -995,6 +1005,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "데스크톱 제목 표시줄에 터미널 버튼 표시",
|
||||
"settings.general.row.showStatus.title": "서버 상태",
|
||||
"settings.general.row.showStatus.description": "제목 표시줄에 서버 상태 버튼 표시",
|
||||
"settings.general.row.showTools.title": "도구 메뉴",
|
||||
"settings.general.row.showTools.description": "제목 표시줄에 MCP, LSP 및 플러그인 빠른 접근 표시",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "하단 탐색",
|
||||
"settings.general.row.mobileTitlebarBottom.description": "모바일에서 제목 표시줄과 세션 탭을 화면 하단에 배치",
|
||||
"settings.general.row.showCustomAgents.title": "사용자 지정 에이전트",
|
||||
|
||||
@@ -567,6 +567,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "Kopier bane",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.tools.trigger": "Verktøy",
|
||||
"status.popover.tools.mcp.description": "Legg til MCP-servere i opencode.json",
|
||||
"status.popover.tools.lsp.description": "LSP-er oppdages automatisk når du åpner filer",
|
||||
"status.popover.tools.plugins.description": "Legg til plugins i opencode.json",
|
||||
"status.popover.tools.mcp.empty": "Ingen MCP-er konfigurert ennå",
|
||||
"status.popover.tools.lsp.empty": "Ingen aktive språkservere ennå",
|
||||
"status.popover.tools.plugins.empty": "Ingen plugins konfigurert ennå",
|
||||
"status.popover.tools.reauthenticate": "Autentiser på nytt",
|
||||
"status.popover.ariaLabel": "Serverkonfigurasjoner",
|
||||
"status.popover.tab.servers": "Servere",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -869,6 +877,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Kunne ikke nå {{server}}",
|
||||
"app.server.retrying": "Prøver på nytt automatisk...",
|
||||
"app.server.status.reconnecting": "Kobler til {{server}} på nytt...",
|
||||
"app.server.status.disconnected": "Kunne ikke koble til {{server}}",
|
||||
"app.server.otherServers": "Andre servere",
|
||||
"dialog.server.add.usernamePlaceholder": "brukernavn",
|
||||
"dialog.server.add.passwordPlaceholder": "passord",
|
||||
@@ -1090,6 +1100,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Vis terminalknappen på tittellinjen i skrivebordsappen",
|
||||
"settings.general.row.showStatus.title": "Serverstatus",
|
||||
"settings.general.row.showStatus.description": "Vis serverstatusknappen på tittellinjen",
|
||||
"settings.general.row.showTools.title": "Verktøymeny",
|
||||
"settings.general.row.showTools.description": "Vis hurtigtilgang til MCP-er, LSP-er og plugins på tittellinjen",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Navigasjon nederst",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Plasser tittellinjen og sesjonsfanene nederst på mobilskjermen",
|
||||
|
||||
@@ -627,6 +627,14 @@ export const dict = {
|
||||
"session.header.open.menu": "Opcje otwierania",
|
||||
"session.header.open.copyPath": "Kopiuj ścieżkę",
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.tools.trigger": "Narzędzia",
|
||||
"status.popover.tools.mcp.description": "Dodaj serwery MCP w opencode.json",
|
||||
"status.popover.tools.lsp.description": "LSP są wykrywane automatycznie podczas otwierania plików",
|
||||
"status.popover.tools.plugins.description": "Dodaj wtyczki w opencode.json",
|
||||
"status.popover.tools.mcp.empty": "Nie skonfigurowano jeszcze MCP",
|
||||
"status.popover.tools.lsp.empty": "Brak aktywnych serwerów językowych",
|
||||
"status.popover.tools.plugins.empty": "Nie skonfigurowano jeszcze wtyczek",
|
||||
"status.popover.tools.reauthenticate": "Uwierzytelnij ponownie",
|
||||
"status.popover.ariaLabel": "Konfiguracje serwerów",
|
||||
"status.popover.tab.servers": "Serwery",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -733,6 +741,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Pokaż przycisk terminala na pasku tytułu aplikacji komputerowej",
|
||||
"settings.general.row.showStatus.title": "Status serwera",
|
||||
"settings.general.row.showStatus.description": "Pokaż przycisk statusu serwera na pasku tytułu",
|
||||
"settings.general.row.showTools.title": "Menu narzędzi",
|
||||
"settings.general.row.showTools.description": "Pokaż szybki dostęp do MCP, LSP i wtyczek na pasku tytułu",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Dolna nawigacja",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Umieść pasek tytułu i karty sesji u dołu ekranu na urządzeniach mobilnych",
|
||||
@@ -942,6 +952,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Nie można połączyć z {{server}}",
|
||||
"app.server.retrying": "Ponawianie automatycznie...",
|
||||
"app.server.status.reconnecting": "Ponowne łączenie z {{server}}...",
|
||||
"app.server.status.disconnected": "Nie można połączyć z {{server}}",
|
||||
"app.server.otherServers": "Inne serwery",
|
||||
"dialog.server.add.usernamePlaceholder": "nazwa użytkownika",
|
||||
"dialog.server.add.passwordPlaceholder": "hasło",
|
||||
|
||||
@@ -683,6 +683,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "Копировать путь",
|
||||
|
||||
"status.popover.trigger": "Статус",
|
||||
"status.popover.tools.trigger": "Инструменты",
|
||||
"status.popover.tools.mcp.description": "Добавьте серверы MCP в opencode.json",
|
||||
"status.popover.tools.lsp.description": "LSP определяются автоматически при открытии файлов",
|
||||
"status.popover.tools.plugins.description": "Добавьте плагины в opencode.json",
|
||||
"status.popover.tools.mcp.empty": "MCP пока не настроены",
|
||||
"status.popover.tools.lsp.empty": "Активных языковых серверов пока нет",
|
||||
"status.popover.tools.plugins.empty": "Плагины пока не настроены",
|
||||
"status.popover.tools.reauthenticate": "Повторно авторизоваться",
|
||||
"status.popover.ariaLabel": "Настройки серверов",
|
||||
"status.popover.tab.servers": "Серверы",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -798,6 +806,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Показывать кнопку терминала в строке заголовка приложения",
|
||||
"settings.general.row.showStatus.title": "Состояние сервера",
|
||||
"settings.general.row.showStatus.description": "Показывать кнопку состояния сервера в строке заголовка",
|
||||
"settings.general.row.showTools.title": "Меню инструментов",
|
||||
"settings.general.row.showTools.description": "Показывать быстрый доступ к MCP, LSP и плагинам в строке заголовка",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Нижняя навигация",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"На мобильных устройствах размещать строку заголовка и вкладки сессии внизу экрана",
|
||||
@@ -1022,6 +1032,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Не удалось связаться с {{server}}",
|
||||
"app.server.retrying": "Автоматическая повторная попытка...",
|
||||
"app.server.status.reconnecting": "Повторное подключение к {{server}}...",
|
||||
"app.server.status.disconnected": "Не удалось подключиться к {{server}}",
|
||||
"app.server.otherServers": "Другие серверы",
|
||||
"dialog.server.add.usernamePlaceholder": "имя пользователя",
|
||||
"dialog.server.add.passwordPlaceholder": "пароль",
|
||||
|
||||
@@ -679,6 +679,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "คัดลอกเส้นทาง",
|
||||
|
||||
"status.popover.trigger": "สถานะ",
|
||||
"status.popover.tools.trigger": "เครื่องมือ",
|
||||
"status.popover.tools.mcp.description": "เพิ่มเซิร์ฟเวอร์ MCP ใน opencode.json",
|
||||
"status.popover.tools.lsp.description": "ระบบจะตรวจพบ LSP อัตโนมัติเมื่อคุณเปิดไฟล์",
|
||||
"status.popover.tools.plugins.description": "เพิ่มปลั๊กอินใน opencode.json",
|
||||
"status.popover.tools.mcp.empty": "ยังไม่ได้กำหนดค่า MCP",
|
||||
"status.popover.tools.lsp.empty": "ยังไม่มีเซิร์ฟเวอร์ภาษาที่ใช้งานอยู่",
|
||||
"status.popover.tools.plugins.empty": "ยังไม่ได้กำหนดค่าปลั๊กอิน",
|
||||
"status.popover.tools.reauthenticate": "ยืนยันตัวตนอีกครั้ง",
|
||||
"status.popover.ariaLabel": "การกำหนดค่าเซิร์ฟเวอร์",
|
||||
"status.popover.tab.servers": "เซิร์ฟเวอร์",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -789,6 +797,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "แสดงปุ่มเทอร์มินัลในแถบชื่อเรื่องเดสก์ท็อป",
|
||||
"settings.general.row.showStatus.title": "สถานะเซิร์ฟเวอร์",
|
||||
"settings.general.row.showStatus.description": "แสดงปุ่มสถานะเซิร์ฟเวอร์ในแถบชื่อเรื่อง",
|
||||
"settings.general.row.showTools.title": "เมนูเครื่องมือ",
|
||||
"settings.general.row.showTools.description": "แสดงทางลัดไปยัง MCP, LSP และปลั๊กอินในแถบชื่อเรื่อง",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "การนำทางด้านล่าง",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"วางแถบชื่อเรื่องและแท็บเซสชันไว้ด้านล่างของหน้าจอบนอุปกรณ์เคลื่อนที่",
|
||||
@@ -1006,6 +1016,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "ไม่สามารถติดต่อ {{server}}",
|
||||
"app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...",
|
||||
"app.server.status.reconnecting": "กำลังเชื่อมต่อกับ {{server}} อีกครั้ง...",
|
||||
"app.server.status.disconnected": "ไม่สามารถเชื่อมต่อกับ {{server}}",
|
||||
"app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ",
|
||||
"dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้",
|
||||
"dialog.server.add.passwordPlaceholder": "รหัสผ่าน",
|
||||
|
||||
@@ -689,6 +689,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "Yolu kopyala",
|
||||
|
||||
"status.popover.trigger": "Durum",
|
||||
"status.popover.tools.trigger": "Araçlar",
|
||||
"status.popover.tools.mcp.description": "opencode.json dosyasına MCP sunucuları ekleyin",
|
||||
"status.popover.tools.lsp.description": "Dosyaları açtığınızda LSP'ler otomatik olarak algılanır",
|
||||
"status.popover.tools.plugins.description": "opencode.json dosyasına eklentiler ekleyin",
|
||||
"status.popover.tools.mcp.empty": "Henüz yapılandırılmış MCP yok",
|
||||
"status.popover.tools.lsp.empty": "Henüz etkin dil sunucusu yok",
|
||||
"status.popover.tools.plugins.empty": "Henüz yapılandırılmış eklenti yok",
|
||||
"status.popover.tools.reauthenticate": "Yeniden kimlik doğrula",
|
||||
"status.popover.ariaLabel": "Sunucu yapılandırmaları",
|
||||
"status.popover.tab.servers": "Sunucular",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -803,6 +811,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Masaüstü başlık çubuğunda terminal düğmesini göster",
|
||||
"settings.general.row.showStatus.title": "Sunucu durumu",
|
||||
"settings.general.row.showStatus.description": "Başlık çubuğunda sunucu durumu düğmesini göster",
|
||||
"settings.general.row.showTools.title": "Araçlar menüsü",
|
||||
"settings.general.row.showTools.description": "Başlık çubuğunda MCP, LSP ve eklentilere hızlı erişim göster",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Alt gezinme",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Mobil cihazlarda başlık çubuğunu ve oturum sekmelerini ekranın altına yerleştir",
|
||||
@@ -1026,6 +1036,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "{{server}} sunucusuna ulaşılamadı",
|
||||
"app.server.retrying": "Otomatik olarak tekrar deneniyor...",
|
||||
"app.server.status.reconnecting": "{{server}} sunucusuna yeniden bağlanılıyor...",
|
||||
"app.server.status.disconnected": "{{server}} sunucusuna bağlanılamadı",
|
||||
"app.server.otherServers": "Diğer sunucular",
|
||||
"dialog.server.add.usernamePlaceholder": "kullanıcı adı",
|
||||
"dialog.server.add.passwordPlaceholder": "parola",
|
||||
|
||||
@@ -333,6 +333,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "Не вдалося досягти {{server}}",
|
||||
"app.server.retrying": "Автоматичне повторення...",
|
||||
"app.server.status.reconnecting": "Повторне підключення до {{server}}...",
|
||||
"app.server.status.disconnected": "Не вдалося підключитися до {{server}}",
|
||||
"app.server.otherServers": "Інші сервери",
|
||||
|
||||
"dialog.server.title": "Сервери",
|
||||
@@ -729,6 +731,14 @@ export const dict = {
|
||||
"session.header.open.app.sublimeText": "Sublime Text",
|
||||
|
||||
"status.popover.trigger": "Статус",
|
||||
"status.popover.tools.trigger": "Інструменти",
|
||||
"status.popover.tools.mcp.description": "Додайте сервери MCP в opencode.json",
|
||||
"status.popover.tools.lsp.description": "LSP визначаються автоматично під час відкриття файлів",
|
||||
"status.popover.tools.plugins.description": "Додайте плагіни в opencode.json",
|
||||
"status.popover.tools.mcp.empty": "MCP ще не налаштовано",
|
||||
"status.popover.tools.lsp.empty": "Активних мовних серверів ще немає",
|
||||
"status.popover.tools.plugins.empty": "Плагіни ще не налаштовано",
|
||||
"status.popover.tools.reauthenticate": "Повторно автентифікуватися",
|
||||
"status.popover.ariaLabel": "Конфігурації серверів",
|
||||
"status.popover.tab.servers": "Сервери",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -889,6 +899,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "Показувати кнопку термінала в заголовку робочого столу",
|
||||
"settings.general.row.showStatus.title": "Статус сервера",
|
||||
"settings.general.row.showStatus.description": "Показувати кнопку статусу сервера в заголовку",
|
||||
"settings.general.row.showTools.title": "Меню інструментів",
|
||||
"settings.general.row.showTools.description": "Показувати швидкий доступ до MCP, LSP і плагінів у заголовку",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "Нижня навігація",
|
||||
"settings.general.row.mobileTitlebarBottom.description":
|
||||
"Розмістити заголовок і вкладки сесії внизу екрана на мобільних пристроях",
|
||||
|
||||
@@ -675,6 +675,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "复制路径",
|
||||
|
||||
"status.popover.trigger": "状态",
|
||||
"status.popover.tools.trigger": "工具",
|
||||
"status.popover.tools.mcp.description": "在 opencode.json 中添加 MCP 服务器",
|
||||
"status.popover.tools.lsp.description": "打开文件时会自动检测 LSP",
|
||||
"status.popover.tools.plugins.description": "在 opencode.json 中添加插件",
|
||||
"status.popover.tools.mcp.empty": "尚未配置 MCP",
|
||||
"status.popover.tools.lsp.empty": "尚无活动的语言服务器",
|
||||
"status.popover.tools.plugins.empty": "尚未配置插件",
|
||||
"status.popover.tools.reauthenticate": "重新认证",
|
||||
"status.popover.ariaLabel": "服务器配置",
|
||||
"status.popover.tab.servers": "服务器",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -786,6 +794,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "在桌面版标题栏中显示终端按钮",
|
||||
"settings.general.row.showStatus.title": "服务器状态",
|
||||
"settings.general.row.showStatus.description": "在标题栏中显示服务器状态按钮",
|
||||
"settings.general.row.showTools.title": "工具菜单",
|
||||
"settings.general.row.showTools.description": "在标题栏中显示 MCP、LSP 和插件的快捷入口",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "底部导航",
|
||||
"settings.general.row.mobileTitlebarBottom.description": "在移动设备上将标题栏和会话标签页置于屏幕底部",
|
||||
"settings.general.row.showCustomAgents.title": "自定义智能体",
|
||||
@@ -999,6 +1009,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "无法连接到 {{server}}",
|
||||
"app.server.retrying": "正在自动重试...",
|
||||
"app.server.status.reconnecting": "正在重新连接到 {{server}}...",
|
||||
"app.server.status.disconnected": "无法连接到 {{server}}",
|
||||
"app.server.otherServers": "其他服务器",
|
||||
"dialog.server.add.usernamePlaceholder": "用户名",
|
||||
"dialog.server.add.passwordPlaceholder": "密码",
|
||||
|
||||
@@ -673,6 +673,14 @@ export const dict = {
|
||||
"session.header.open.copyPath": "複製路徑",
|
||||
|
||||
"status.popover.trigger": "狀態",
|
||||
"status.popover.tools.trigger": "工具",
|
||||
"status.popover.tools.mcp.description": "在 opencode.json 中新增 MCP 伺服器",
|
||||
"status.popover.tools.lsp.description": "開啟檔案時會自動偵測 LSP",
|
||||
"status.popover.tools.plugins.description": "在 opencode.json 中新增外掛程式",
|
||||
"status.popover.tools.mcp.empty": "尚未設定 MCP",
|
||||
"status.popover.tools.lsp.empty": "尚無作用中的語言伺服器",
|
||||
"status.popover.tools.plugins.empty": "尚未設定外掛程式",
|
||||
"status.popover.tools.reauthenticate": "重新驗證",
|
||||
"status.popover.ariaLabel": "伺服器設定",
|
||||
"status.popover.tab.servers": "伺服器",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
@@ -781,6 +789,8 @@ export const dict = {
|
||||
"settings.general.row.showTerminal.description": "在桌面標題列中顯示終端機按鈕",
|
||||
"settings.general.row.showStatus.title": "伺服器狀態",
|
||||
"settings.general.row.showStatus.description": "在標題列中顯示伺服器狀態按鈕",
|
||||
"settings.general.row.showTools.title": "工具選單",
|
||||
"settings.general.row.showTools.description": "在標題列中顯示 MCP、LSP 和外掛程式的快速入口",
|
||||
"settings.general.row.mobileTitlebarBottom.title": "底部導覽",
|
||||
"settings.general.row.mobileTitlebarBottom.description": "在行動裝置上將標題列和工作階段分頁置於畫面底部",
|
||||
"settings.general.row.showCustomAgents.title": "自訂代理程式",
|
||||
@@ -995,6 +1005,8 @@ export const dict = {
|
||||
|
||||
"app.server.unreachable": "無法連線至 {{server}}",
|
||||
"app.server.retrying": "正在自動重試...",
|
||||
"app.server.status.reconnecting": "正在重新連線至 {{server}}...",
|
||||
"app.server.status.disconnected": "無法連線至 {{server}}",
|
||||
"app.server.otherServers": "其他伺服器",
|
||||
"dialog.server.add.usernamePlaceholder": "使用者名稱",
|
||||
"dialog.server.add.passwordPlaceholder": "密碼",
|
||||
|
||||
@@ -40,7 +40,7 @@ import { DialogSelectServer, useServerManagementController } from "@/components/
|
||||
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
|
||||
import { ServerConnection, serverName, useServer } from "@/context/server"
|
||||
import { sessionHasOpenTab, useTabs } from "@/context/tabs"
|
||||
import { useServerSync, type ServerSync } from "@/context/server-sync"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import {
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
getProjectAvatarSource,
|
||||
homeProjectDirectories,
|
||||
projectForSession,
|
||||
sortedRootSessions,
|
||||
toggleHomeProjectSelection,
|
||||
} from "@/pages/layout/helpers"
|
||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||
@@ -69,6 +68,11 @@ import { archiveHomeSession } from "./home-session-archive"
|
||||
import { shouldOpenSessionInBackground } from "./home-session-open"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { fileManagerApp } from "@/utils/file-manager"
|
||||
import {
|
||||
loadHomeSessionIndex,
|
||||
retainHomeSessions,
|
||||
type HomeSessionEvents,
|
||||
} from "@/context/global-sync/home-session-index"
|
||||
|
||||
const HOME_SESSION_LIMIT = 64
|
||||
const HOME_SESSION_HEADER_STICKY_TOP = 12
|
||||
@@ -106,22 +110,24 @@ const HOME_SEARCH_RESULT_META =
|
||||
let pendingHomeNavigation: { server: ServerConnection.Key; href: string } | undefined
|
||||
|
||||
function buildHomeSessionRecords(input: {
|
||||
sync: Pick<ServerSync, "child">
|
||||
sessions: () => Session[]
|
||||
projectDirectories: () => string[]
|
||||
projects: () => LocalProject[]
|
||||
projectByID: () => Map<string, LocalProject>
|
||||
}) {
|
||||
return [
|
||||
...new Map(
|
||||
input
|
||||
.projectDirectories()
|
||||
.flatMap((directory) => sortedRootSessions(input.sync.child(directory, { bootstrap: false })[0], Date.now()))
|
||||
.map((session) => [`${pathKey(session.directory)}:${session.id}`, session] as const),
|
||||
).values(),
|
||||
]
|
||||
const directories = new Set(input.projectDirectories().map(pathKey))
|
||||
const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory)))
|
||||
return [...new Map(sessions.map((session) => [session.id, session] as const)).values()]
|
||||
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||
.flatMap((session) => {
|
||||
const project = projectForSession(session, input.projects(), input.projectByID())
|
||||
const directory = pathKey(session.directory)
|
||||
const project =
|
||||
input
|
||||
.projects()
|
||||
.find(
|
||||
(item) =>
|
||||
pathKey(item.worktree) === directory || item.sandboxes?.some((sandbox) => pathKey(sandbox) === directory),
|
||||
) ?? projectForSession(session, input.projects(), input.projectByID())
|
||||
if (!project) return []
|
||||
return {
|
||||
session,
|
||||
@@ -286,6 +292,7 @@ export function NewHome() {
|
||||
return global.ensureServerCtx(conn)
|
||||
})
|
||||
const focusedSync = () => focusedServerCtx()?.sync ?? sync()
|
||||
const homeSessions = () => focusedSync().homeSessions
|
||||
const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list())
|
||||
const recentlyClosed = createMemo(
|
||||
() => focusedServerCtx()?.projects.recentlyClosed() ?? layout.projects.recentlyClosed(),
|
||||
@@ -318,24 +325,47 @@ export function NewHome() {
|
||||
}
|
||||
return language.t("home.sessions.search.placeholder")
|
||||
})
|
||||
const sessionEventLoad = useQuery(() => ({
|
||||
queryKey: homeSessions().eventsKey,
|
||||
queryFn: async (): Promise<HomeSessionEvents> => ({ sequence: 0, entries: [] }),
|
||||
initialData: { sequence: 0, entries: [] } satisfies HomeSessionEvents,
|
||||
enabled: false,
|
||||
}))
|
||||
const sessionLoad = useQuery(() => ({
|
||||
queryKey: ["home", "sessions", selection().server, ...projectDirectories()] as const,
|
||||
queryFn: async () => {
|
||||
await Promise.all(
|
||||
projectDirectories().map((directory) =>
|
||||
focusedSync().project.loadSessions(directory, { limit: HOME_SESSION_LIMIT }),
|
||||
),
|
||||
queryKey: homeSessions().indexKey,
|
||||
enabled: !!focusedServerCtx(),
|
||||
queryFn: async ({ signal }) => {
|
||||
const ctx = focusedServerCtx()
|
||||
if (!ctx) return { sessions: [], eventSequence: 0 }
|
||||
const cache = homeSessions()
|
||||
const eventSequence = cache.eventSequence()
|
||||
const index = await loadHomeSessionIndex(
|
||||
(input, options) => ctx.sdk.client.v2.session.list(input, options),
|
||||
eventSequence,
|
||||
signal,
|
||||
)
|
||||
return null
|
||||
cache.complete(eventSequence)
|
||||
return index
|
||||
},
|
||||
retry: false,
|
||||
staleTime: 30_000,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
}))
|
||||
|
||||
const projectByID = createMemo(
|
||||
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
|
||||
)
|
||||
const indexedSessions = createMemo(() =>
|
||||
retainHomeSessions(
|
||||
homeSessions().sessions(sessionLoad.data, sessionEventLoad.data),
|
||||
HOME_SESSION_LIMIT,
|
||||
Date.now(),
|
||||
),
|
||||
)
|
||||
const allRecords = createMemo(() =>
|
||||
buildHomeSessionRecords({
|
||||
sync: focusedSync(),
|
||||
sessions: indexedSessions,
|
||||
projectDirectories,
|
||||
projects,
|
||||
projectByID,
|
||||
@@ -363,8 +393,7 @@ export function NewHome() {
|
||||
prefetched.add(key)
|
||||
createRoot((dispose) => {
|
||||
try {
|
||||
const directory = ctx.sync.ensureDirSyncContext(record.session.directory)
|
||||
void directory.session
|
||||
void ctx.sync.session
|
||||
.sync(record.session.id)
|
||||
.then(() => {
|
||||
return Promise.all(
|
||||
@@ -484,7 +513,13 @@ export function NewHome() {
|
||||
}
|
||||
|
||||
function openSession(session: Session, options?: OpenSessionOptions) {
|
||||
const project = projectForSession(session, projects(), projectByID())
|
||||
const directoryKey = pathKey(session.directory)
|
||||
const project =
|
||||
projects().find(
|
||||
(item) =>
|
||||
pathKey(item.worktree) === directoryKey ||
|
||||
item.sandboxes?.some((sandbox) => pathKey(sandbox) === directoryKey),
|
||||
) ?? projectForSession(session, projects(), projectByID())
|
||||
const conn = focusedServer()
|
||||
if (!conn) return
|
||||
const directory = project?.worktree ?? session.directory
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Show, createEffect, createMemo, createResource, createSignal, onCleanup, untrack } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useSearchParams } from "@solidjs/router"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import { NewSessionDesignView } from "@/components/session"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { StatusPopoverV2 } from "@/components/status-popover"
|
||||
import {
|
||||
PromptProjectAddButton,
|
||||
PromptProjectSelector,
|
||||
@@ -25,7 +22,6 @@ import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import { useComposerCommands } from "@/pages/session/use-composer-commands"
|
||||
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
||||
import { PromptWorkspaceSelector } from "@/components/prompt-workspace-selector"
|
||||
import { useTitlebarRightMount } from "@/components/titlebar"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useSettingsDialog } from "@/components/settings-dialog"
|
||||
@@ -86,7 +82,6 @@ export default function NewSessionPage() {
|
||||
])
|
||||
|
||||
const [store, setStore] = createStore<{ worktree?: string }>({})
|
||||
const rightMount = useTitlebarRightMount()
|
||||
|
||||
const showWorkspaceBar = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git")
|
||||
const newSessionWorktree = createMemo(() => {
|
||||
@@ -126,17 +121,6 @@ export default function NewSessionPage() {
|
||||
|
||||
return (
|
||||
<div class="relative size-full overflow-hidden flex flex-col">
|
||||
<Show when={rightMount()}>
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<Show when={settings.visibility.status()}>
|
||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||
<StatusPopoverV2 />
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
|
||||
<div class="@container relative flex flex-col min-h-0 h-full flex-1">
|
||||
<div class="flex-1 min-h-0 overflow-hidden rounded-[10px]">
|
||||
|
||||
@@ -58,6 +58,8 @@ import { useSync } from "@/context/sync"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { TerminalProvider, useTerminal } from "@/context/terminal"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { setCursorPosition } from "@/components/prompt-input/editor-dom"
|
||||
import { promptLength } from "@/components/prompt-input/history"
|
||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||
import {
|
||||
createPromptInputController,
|
||||
@@ -1054,7 +1056,10 @@ export default function Page() {
|
||||
|
||||
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
|
||||
if (composer.blocked() || isChildSession()) return
|
||||
inputRef?.focus()
|
||||
const input = inputRef
|
||||
if (!input) return
|
||||
input.focus()
|
||||
setCursorPosition(input, prompt.cursor() ?? promptLength(prompt.current()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual"
|
||||
import { createVirtualizer, defaultRangeExtractor, Virtualizer } from "@tanstack/solid-virtual"
|
||||
import { createRoot, createSignal } from "solid-js"
|
||||
import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items"
|
||||
|
||||
test("end anchoring survives consecutive resizes when the first scroll write is clamped", () => {
|
||||
const writes: { offset: number; adjustments?: number }[] = []
|
||||
const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>({
|
||||
count: 5,
|
||||
estimateSize: () => 50,
|
||||
initialOffset: 50,
|
||||
initialRect: { width: 400, height: 200 },
|
||||
anchorTo: "end",
|
||||
scrollEndThreshold: 1,
|
||||
getScrollElement: () => null,
|
||||
scrollToFn: (offset, options) => writes.push({ offset, adjustments: options.adjustments }),
|
||||
observeElementRect: () => {},
|
||||
observeElementOffset: () => {},
|
||||
})
|
||||
|
||||
virtualizer.getTotalSize()
|
||||
virtualizer.resizeItem(4, 120)
|
||||
expect(writes).toEqual([{ offset: 50, adjustments: 70 }])
|
||||
writes.length = 0
|
||||
|
||||
virtualizer.resizeItem(4, 200)
|
||||
expect(writes).toEqual([{ offset: 120, adjustments: 80 }])
|
||||
})
|
||||
|
||||
test("reactive count updates preserve measured row sizes", () => {
|
||||
createRoot((dispose) => {
|
||||
const [count, setCount] = createSignal(2)
|
||||
@@ -42,23 +66,26 @@ test("initial rect projects rows before a scroll element connects", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("logical scroll offset includes pending measurement adjustments", () => {
|
||||
createRoot((dispose) => {
|
||||
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
|
||||
count: 2,
|
||||
getScrollElement: () => null,
|
||||
estimateSize: () => 60,
|
||||
initialOffset: 100,
|
||||
initialRect: { width: 800, height: 60 },
|
||||
})
|
||||
|
||||
virtualizer.getTotalSize()
|
||||
virtualizer.resizeItem(0, 100)
|
||||
|
||||
expect(virtualizer.scrollOffset).toBe(100)
|
||||
expect(virtualizer.getLogicalScrollOffset()).toBe(140)
|
||||
dispose()
|
||||
test("clamps oversized offsets with scroll margin and padding changes", () => {
|
||||
const options = (paddingEnd: number) => ({
|
||||
count: 20,
|
||||
estimateSize: () => 60,
|
||||
initialOffset: Number.MAX_SAFE_INTEGER,
|
||||
initialRect: { width: 800, height: 600 },
|
||||
scrollMargin: 64,
|
||||
paddingEnd,
|
||||
overscan: 1,
|
||||
getScrollElement: () => null,
|
||||
scrollToFn: () => {},
|
||||
observeElementRect: () => {},
|
||||
observeElementOffset: () => {},
|
||||
})
|
||||
const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>(options(64))
|
||||
|
||||
expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
|
||||
|
||||
virtualizer.setOptions(options(600))
|
||||
expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([18, 19])
|
||||
})
|
||||
|
||||
test("stale pinned indexes do not produce missing virtual items after count shrinks", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -6,15 +6,12 @@ import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { createServer } from "http"
|
||||
import { OpenAIWebSocketPool } from "./ws-pool"
|
||||
import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
|
||||
import { isRecord } from "@/util/record"
|
||||
|
||||
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
const ISSUER = "https://auth.openai.com"
|
||||
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
||||
const OAUTH_PORT = 1455
|
||||
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
|
||||
const CODEX_COMPATIBILITY_VERSION = "0.144.0"
|
||||
const RESPONSES_LITE_MODEL = "gpt-5.6-luna"
|
||||
const ALLOWED_MODELS = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"])
|
||||
const DISALLOWED_MODELS = new Set(["gpt-5.5-pro"])
|
||||
|
||||
@@ -421,9 +418,7 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||
|
||||
const requestInit = {
|
||||
...init,
|
||||
body: parsed.pathname.endsWith("/responses")
|
||||
? prepareResponsesLiteRequest(init?.body, headers)
|
||||
: init?.body,
|
||||
body: init?.body,
|
||||
headers,
|
||||
}
|
||||
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
||||
@@ -568,61 +563,3 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function prepareResponsesLiteRequest(body: BodyInit | null | undefined, headers: Headers) {
|
||||
if (typeof body !== "string") return body
|
||||
const request: unknown = JSON.parse(body)
|
||||
if (!isRecord(request)) return body
|
||||
if (request.model !== RESPONSES_LITE_MODEL) return body
|
||||
if (!Array.isArray(request.input)) throw new Error("Responses Lite requires an input array")
|
||||
if (request.tools !== undefined && !Array.isArray(request.tools)) {
|
||||
throw new Error("Responses Lite requires a tools array")
|
||||
}
|
||||
if (request.instructions !== undefined && typeof request.instructions !== "string") {
|
||||
throw new Error("Responses Lite requires string instructions")
|
||||
}
|
||||
|
||||
const sessionID = headers.get("session-id")
|
||||
if (!sessionID) throw new Error("Responses Lite requires a session-id header")
|
||||
|
||||
function stripImageDetail(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(stripImageDetail)
|
||||
return
|
||||
}
|
||||
if (!isRecord(value)) return
|
||||
if (value.type === "input_image") delete value.detail
|
||||
Object.values(value).forEach(stripImageDetail)
|
||||
}
|
||||
|
||||
stripImageDetail(request.input)
|
||||
request.input = [
|
||||
{ type: "additional_tools", role: "developer", tools: request.tools ?? [] },
|
||||
...(request.instructions
|
||||
? [
|
||||
{
|
||||
type: "message",
|
||||
role: "developer",
|
||||
content: [{ type: "input_text", text: request.instructions }],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...request.input,
|
||||
]
|
||||
delete request.tools
|
||||
delete request.instructions
|
||||
request.tool_choice = "auto"
|
||||
request.parallel_tool_calls = false
|
||||
request.prompt_cache_key = sessionID
|
||||
request.reasoning = {
|
||||
...(isRecord(request.reasoning) ? request.reasoning : {}),
|
||||
context: "all_turns",
|
||||
}
|
||||
|
||||
headers.set("session-id", sessionID)
|
||||
headers.set("x-session-affinity", sessionID)
|
||||
headers.set("version", CODEX_COMPATIBILITY_VERSION)
|
||||
headers.set(OpenAIWebSocketPool.RESPONSES_LITE_HEADER, "true")
|
||||
headers.delete("content-length")
|
||||
return JSON.stringify(request)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { isRecord } from "@/util/record"
|
||||
import { OpenAIWebSocket } from "./ws"
|
||||
|
||||
export const TITLE_HEADER = "x-opencode-title"
|
||||
export const RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
|
||||
const RESPONSES_LITE_CLIENT_METADATA = "ws_request_header_x_openai_internal_codex_responses_lite"
|
||||
|
||||
export interface CreateWebSocketFetchOptions {
|
||||
httpFetch?: typeof globalThis.fetch
|
||||
@@ -100,16 +98,7 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
|
||||
})
|
||||
const response = OpenAIWebSocket.streamResponsesWebSocket({
|
||||
socket: entry.socket,
|
||||
body:
|
||||
internalHeaders[RESPONSES_LITE_HEADER] === "true"
|
||||
? {
|
||||
...body,
|
||||
client_metadata: {
|
||||
...(isRecord(body.client_metadata) ? body.client_metadata : {}),
|
||||
[RESPONSES_LITE_CLIENT_METADATA]: "true",
|
||||
},
|
||||
}
|
||||
: body,
|
||||
body,
|
||||
idleTimeout,
|
||||
signal: init?.signal ?? undefined,
|
||||
onFirstEvent: (error) => resolveFirstEvent(error ?? true),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("@opencode-ai/ui/fonts/Inter.ttf") format("truetype");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@opencode-ai/ui/styles/tailwind"
|
||||
import "@opencode-ai/session-ui/styles"
|
||||
import "@opencode-ai/ui/v2/styles/tailwind.css"
|
||||
import "./preview.css"
|
||||
|
||||
import { createEffect, onCleanup, onMount } from "solid-js"
|
||||
import addonA11y from "@storybook/addon-a11y"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DEFAULT_THEMES,
|
||||
addTheme,
|
||||
allThemes,
|
||||
generateSubtleSyntax,
|
||||
generateSyntax,
|
||||
generateSystem,
|
||||
hasTheme,
|
||||
@@ -268,6 +269,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
createEffect(() => renderer.setBackgroundColor(values().background))
|
||||
|
||||
const syntax = createSyntaxStyleMemo(() => generateSyntax(values()))
|
||||
const subtleSyntax = createSyntaxStyleMemo(() => generateSubtleSyntax(values()))
|
||||
|
||||
return {
|
||||
theme: new Proxy(values(), {
|
||||
@@ -282,6 +284,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
all: allThemes,
|
||||
has: hasTheme,
|
||||
syntax,
|
||||
subtleSyntax,
|
||||
mode: () => store.mode,
|
||||
locked: () => store.lock !== undefined,
|
||||
lock: () => pin(store.mode),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -96,3 +96,9 @@
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-component="popover-content"] {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Marked } from "marked"
|
||||
import { markedCodeSpanBoundary } from "./marked-code-span"
|
||||
|
||||
test("preserves code spans adjacent to tildes", async () => {
|
||||
const marked = new Marked(markedCodeSpanBoundary)
|
||||
|
||||
expect(await marked.parse("~`0.1576` to measurement-window-only `0.00092`")).toBe(
|
||||
"<p>~<code>0.1576</code> to measurement-window-only <code>0.00092</code></p>\n",
|
||||
)
|
||||
expect(await marked.parse("`before`~`after`")).toBe("<p><code>before</code>~<code>after</code></p>\n")
|
||||
expect(await marked.parse("~~`deleted code`~~")).toBe("<p><del><code>deleted code</code></del></p>\n")
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { MarkedExtension } from "marked"
|
||||
|
||||
// Keep adjacent tilde and backtick runs separate until markedjs/marked#4011 is released.
|
||||
export const markedCodeSpanBoundary = {
|
||||
tokenizer: {
|
||||
inlineText(src) {
|
||||
const match = /^(`+(?=~)|~+(?=`))/.exec(src)
|
||||
if (!match) return false
|
||||
return {
|
||||
type: "text",
|
||||
raw: match[0],
|
||||
text: match[0],
|
||||
escaped: this.lexer.state.inRawBlock,
|
||||
}
|
||||
},
|
||||
},
|
||||
} satisfies MarkedExtension
|
||||
@@ -3,6 +3,7 @@ import markedShiki from "marked-shiki"
|
||||
import katex from "katex"
|
||||
import { bundledLanguages, type BundledLanguage } from "shiki"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { markedCodeSpanBoundary } from "./marked-code-span"
|
||||
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
|
||||
|
||||
export const OpenCodeTheme = {
|
||||
@@ -521,6 +522,7 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
|
||||
name: "Marked",
|
||||
init: (props: { nativeParser?: NativeMarkdownParser }) => {
|
||||
const jsParser = marked.use(
|
||||
markedCodeSpanBoundary,
|
||||
{
|
||||
renderer: {
|
||||
link({ href, title, text }) {
|
||||
|
||||
@@ -33,6 +33,14 @@ const icons = {
|
||||
viewBox: "0 0 20 20",
|
||||
body: `<path d="M2 10V18H18V10M2 10V2H18V10M2 10H18M5 6H9M5 14H9" stroke="currentColor"/>`,
|
||||
},
|
||||
tools: {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M8 6.74934V9.24934M10.668 6.74934V9.24934M5.33398 6.74934V9.24934M1.33398 2.44418V13.5542H14.6673V2.44418H1.33398Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
},
|
||||
"tools-warning": {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M9.50073 2.44498H1.33398V13.555H14.6673V6.49997M8.00073 6.74934V9.24934M10.6687 6.74934V9.24934M5.33472 6.74934V9.24934" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
},
|
||||
"status-active": {
|
||||
viewBox: "0 0 20 20",
|
||||
body: `<path d="M18 2H2V10H18V2Z" fill="currentColor" fill-opacity="0.1"/><path d="M2 18H18V10H2V18Z" fill="currentColor" fill-opacity="0.1"/><path d="M2 10V18H18V10M2 10V2H18V10M2 10H18M5 6H9M5 14H9" stroke="currentColor"/>`,
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 440;
|
||||
font-size: 13px;
|
||||
line-height: 100%;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.04px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-variation-settings: "slnt" 0;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
|
||||
index 7e97823ea769398ccd9cf449b178c77675ed252c..d75183f11421af0e20e4e8a996af99c300ad936d 100644
|
||||
--- a/dist/cjs/index.cjs
|
||||
+++ b/dist/cjs/index.cjs
|
||||
@@ -39,7 +39,9 @@ function createVirtualizerBase(options) {
|
||||
(_a = options.onChange) == null ? void 0 : _a.call(options, instance2, sync);
|
||||
}
|
||||
}));
|
||||
- virtualizer.measure();
|
||||
+ virtualizer._willUpdate();
|
||||
+ setVirtualItems(store.reconcile(instance.getVirtualItems(), { key: "index" }));
|
||||
+ setTotalSize(instance.getTotalSize());
|
||||
});
|
||||
return virtualizer;
|
||||
}
|
||||
diff --git a/dist/esm/index.js b/dist/esm/index.js
|
||||
index 1d525463775fef3e8ece6ab191061ef9d0a36d73..14c680a2088c49a33959d8118cf32ee599ab83c2 100644
|
||||
--- a/dist/esm/index.js
|
||||
+++ b/dist/esm/index.js
|
||||
@@ -38,7 +38,9 @@ function createVirtualizerBase(options) {
|
||||
(_a = options.onChange) == null ? void 0 : _a.call(options, instance2, sync);
|
||||
}
|
||||
}));
|
||||
- virtualizer.measure();
|
||||
+ virtualizer._willUpdate();
|
||||
+ setVirtualItems(reconcile(instance.getVirtualItems(), { key: "index" }));
|
||||
+ setTotalSize(instance.getTotalSize());
|
||||
});
|
||||
return virtualizer;
|
||||
}
|
||||
diff --git a/src/index.tsx b/src/index.tsx
|
||||
index 69ac34fd70753b9bd00683c2540be7f62630f8f2..9f16672aa0f4a044aa2b35754d385d7d8031f743 100644
|
||||
--- a/src/index.tsx
|
||||
+++ b/src/index.tsx
|
||||
@@ -81,7 +81,9 @@ function createVirtualizerBase<
|
||||
},
|
||||
}),
|
||||
)
|
||||
- virtualizer.measure()
|
||||
+ virtualizer._willUpdate()
|
||||
+ setVirtualItems(reconcile(instance.getVirtualItems(), { key: 'index' }))
|
||||
+ setTotalSize(instance.getTotalSize())
|
||||
})
|
||||
|
||||
return virtualizer
|
||||
@@ -1,105 +0,0 @@
|
||||
diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
|
||||
index df75d0cf0347b62906e04e454d4f4ef062ed5c48..e5110715b049c6c8b992bd1375a7432df7c0182e 100644
|
||||
--- a/dist/cjs/index.cjs
|
||||
+++ b/dist/cjs/index.cjs
|
||||
@@ -526,6 +526,7 @@ class Virtualizer {
|
||||
this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset);
|
||||
return this.scrollOffset;
|
||||
};
|
||||
+ this.getLogicalScrollOffset = () => this.getScrollOffset() + this.scrollAdjustments;
|
||||
this.getFurthestMeasurement = (measurements, index) => {
|
||||
const furthestMeasurementsFound = /* @__PURE__ */ new Map();
|
||||
const furthestMeasurements = /* @__PURE__ */ new Map();
|
||||
@@ -715,10 +716,12 @@ class Virtualizer {
|
||||
this.options.lanes
|
||||
],
|
||||
(measurements, outerSize, scrollOffset, lanes) => {
|
||||
+ const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0);
|
||||
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
|
||||
return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({
|
||||
measurements,
|
||||
outerSize,
|
||||
- scrollOffset,
|
||||
+ scrollOffset: effectiveScrollOffset,
|
||||
lanes,
|
||||
// Pass the typed array so binary search + forward-walk can
|
||||
// read start/end directly from Float64Array, skipping the
|
||||
diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts
|
||||
index c61ee17752565253f795c7fc7d57e86237ecbb52..705bb7e3a121b040fb1a3e7890179eaa3e9b219e 100644
|
||||
--- a/dist/cjs/index.d.cts
|
||||
+++ b/dist/cjs/index.d.cts
|
||||
@@ -108,6 +108,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
|
||||
scrollRect: Rect | null;
|
||||
scrollOffset: number | null;
|
||||
scrollDirection: ScrollDirection | null;
|
||||
+ getLogicalScrollOffset: () => number;
|
||||
private scrollAdjustments;
|
||||
private _iosDeferredAdjustment;
|
||||
private _iosTouching;
|
||||
diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
|
||||
index b03abab604eb6578f6f56ff92c489259cfaf8f19..0495f372ea000dffc416c4f56809946f7ba73099 100644
|
||||
--- a/dist/esm/index.d.ts
|
||||
+++ b/dist/esm/index.d.ts
|
||||
@@ -108,6 +108,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
|
||||
scrollRect: Rect | null;
|
||||
scrollOffset: number | null;
|
||||
scrollDirection: ScrollDirection | null;
|
||||
+ getLogicalScrollOffset: () => number;
|
||||
private scrollAdjustments;
|
||||
private _iosDeferredAdjustment;
|
||||
private _iosTouching;
|
||||
diff --git a/dist/esm/index.js b/dist/esm/index.js
|
||||
index e384cf7541978a2782b9dca68146e869b16ac3f2..77af22006325377bf9ca0052a3554f881f9e75fe 100644
|
||||
--- a/dist/esm/index.js
|
||||
+++ b/dist/esm/index.js
|
||||
@@ -524,6 +524,7 @@ class Virtualizer {
|
||||
this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === "function" ? this.options.initialOffset() : this.options.initialOffset);
|
||||
return this.scrollOffset;
|
||||
};
|
||||
+ this.getLogicalScrollOffset = () => this.getScrollOffset() + this.scrollAdjustments;
|
||||
this.getFurthestMeasurement = (measurements, index) => {
|
||||
const furthestMeasurementsFound = /* @__PURE__ */ new Map();
|
||||
const furthestMeasurements = /* @__PURE__ */ new Map();
|
||||
@@ -713,10 +714,12 @@ class Virtualizer {
|
||||
this.options.lanes
|
||||
],
|
||||
(measurements, outerSize, scrollOffset, lanes) => {
|
||||
+ const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0);
|
||||
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
|
||||
return this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({
|
||||
measurements,
|
||||
outerSize,
|
||||
- scrollOffset,
|
||||
+ scrollOffset: effectiveScrollOffset,
|
||||
lanes,
|
||||
// Pass the typed array so binary search + forward-walk can
|
||||
// read start/end directly from Float64Array, skipping the
|
||||
diff --git a/src/index.ts b/src/index.ts
|
||||
index d35b3e0695a9c85b261bc1a4fbe23c0a60d5b204..c504e630a60a3a7791345622bb02754b1308fd86 100644
|
||||
--- a/src/index.ts
|
||||
+++ b/src/index.ts
|
||||
@@ -1047,6 +1047,8 @@ export class Virtualizer<
|
||||
return this.scrollOffset
|
||||
}
|
||||
|
||||
+ getLogicalScrollOffset = () => this.getScrollOffset() + this.scrollAdjustments
|
||||
+
|
||||
private getFurthestMeasurement = (
|
||||
measurements: Array<VirtualItem>,
|
||||
index: number,
|
||||
@@ -1334,12 +1336,14 @@ export class Virtualizer<
|
||||
this.options.lanes,
|
||||
],
|
||||
(measurements, outerSize, scrollOffset, lanes) => {
|
||||
+ const maxScrollOffset = Math.max(this.getTotalSize() - outerSize, 0)
|
||||
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset)
|
||||
return (this.range =
|
||||
measurements.length > 0 && outerSize > 0
|
||||
? calculateRange({
|
||||
measurements,
|
||||
outerSize,
|
||||
- scrollOffset,
|
||||
+ scrollOffset: effectiveScrollOffset,
|
||||
lanes,
|
||||
// Pass the typed array so binary search + forward-walk can
|
||||
// read start/end directly from Float64Array, skipping the
|
||||
@@ -0,0 +1,108 @@
|
||||
diff --git a/dist/cjs/index.cjs b/dist/cjs/index.cjs
|
||||
index 52ae6ca12f8d1c650ee7f1bd55573ee7d4f8b65f..830f646f5bd80f4e00f442291ad6e4c6c508630d 100644
|
||||
--- a/dist/cjs/index.cjs
|
||||
+++ b/dist/cjs/index.cjs
|
||||
@@ -716,17 +716,20 @@ class Virtualizer {
|
||||
this.getMeasurements(),
|
||||
this.getSize(),
|
||||
this.getScrollOffset(),
|
||||
- this.options.lanes
|
||||
+ this.options.lanes,
|
||||
+ this.options.paddingEnd
|
||||
],
|
||||
- (measurements, outerSize, scrollOffset, lanes) => {
|
||||
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
|
||||
if (measurements.length === 0 || outerSize === 0) {
|
||||
this.range = null;
|
||||
return null;
|
||||
}
|
||||
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
|
||||
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
|
||||
this.range = calculateRangeImpl(
|
||||
measurements,
|
||||
outerSize,
|
||||
- scrollOffset,
|
||||
+ effectiveScrollOffset,
|
||||
lanes,
|
||||
// Pass the typed array so binary search + forward-walk can read
|
||||
// start/end directly from Float64Array, skipping the Proxy traps.
|
||||
diff --git a/dist/cjs/index.d.cts b/dist/cjs/index.d.cts
|
||||
index c61ee17752565253f795c7fc7d57e86237ecbb52..430c622a18c69407df9f1ca82ee9440e9be287b7 100644
|
||||
--- a/dist/cjs/index.d.cts
|
||||
+++ b/dist/cjs/index.d.cts
|
||||
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
} | null;
|
||||
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
|
||||
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
|
||||
};
|
||||
getVirtualIndexes: {
|
||||
(): number[];
|
||||
diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts
|
||||
index b03abab604eb6578f6f56ff92c489259cfaf8f19..940310f9215ba5bf666c5b71e92dddbbbc85635d 100644
|
||||
--- a/dist/esm/index.d.ts
|
||||
+++ b/dist/esm/index.d.ts
|
||||
@@ -144,7 +144,7 @@ export declare class Virtualizer<TScrollElement extends Element | Window, TItemE
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
} | null;
|
||||
- updateDeps(newDeps: [VirtualItem[], number, number, number]): void;
|
||||
+ updateDeps(newDeps: [VirtualItem[], number, number, number, number]): void;
|
||||
};
|
||||
getVirtualIndexes: {
|
||||
(): number[];
|
||||
diff --git a/dist/esm/index.js b/dist/esm/index.js
|
||||
index 3032c0ca457582be3f47923cba1f7d92c848745c..6e52124048fb3bc7b7ee37e763354b33593a6285 100644
|
||||
--- a/dist/esm/index.js
|
||||
+++ b/dist/esm/index.js
|
||||
@@ -714,17 +714,20 @@ class Virtualizer {
|
||||
this.getMeasurements(),
|
||||
this.getSize(),
|
||||
this.getScrollOffset(),
|
||||
- this.options.lanes
|
||||
+ this.options.lanes,
|
||||
+ this.options.paddingEnd
|
||||
],
|
||||
- (measurements, outerSize, scrollOffset, lanes) => {
|
||||
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
|
||||
if (measurements.length === 0 || outerSize === 0) {
|
||||
this.range = null;
|
||||
return null;
|
||||
}
|
||||
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0);
|
||||
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset);
|
||||
this.range = calculateRangeImpl(
|
||||
measurements,
|
||||
outerSize,
|
||||
- scrollOffset,
|
||||
+ effectiveScrollOffset,
|
||||
lanes,
|
||||
// Pass the typed array so binary search + forward-walk can read
|
||||
// start/end directly from Float64Array, skipping the Proxy traps.
|
||||
diff --git a/src/index.ts b/src/index.ts
|
||||
index 7ad79aacd591c3a4f7855c91b2727a59a9579bca..201406779ee85d2f2e5b01a8ea284aa9a5f694e2 100644
|
||||
--- a/src/index.ts
|
||||
+++ b/src/index.ts
|
||||
@@ -1367,16 +1367,19 @@ export class Virtualizer<
|
||||
this.getSize(),
|
||||
this.getScrollOffset(),
|
||||
this.options.lanes,
|
||||
+ this.options.paddingEnd,
|
||||
],
|
||||
- (measurements, outerSize, scrollOffset, lanes) => {
|
||||
+ (measurements, outerSize, scrollOffset, lanes, _paddingEnd) => {
|
||||
if (measurements.length === 0 || outerSize === 0) {
|
||||
this.range = null
|
||||
return null
|
||||
}
|
||||
+ const maxScrollOffset = Math.max(this.options.scrollMargin + this.getTotalSize() - outerSize, 0)
|
||||
+ const effectiveScrollOffset = Math.min(Math.max(scrollOffset, 0), maxScrollOffset)
|
||||
this.range = calculateRangeImpl(
|
||||
measurements,
|
||||
outerSize,
|
||||
- scrollOffset,
|
||||
+ effectiveScrollOffset,
|
||||
lanes,
|
||||
// Pass the typed array so binary search + forward-walk can read
|
||||
// start/end directly from Float64Array, skipping the Proxy traps.
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.17.19",
|
||||
"version": "1.17.20",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user