mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-04 13:43:54 +00:00
Compare commits
12 Commits
production
...
actual-tui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d7e98dda4 | ||
|
|
303c365483 | ||
|
|
5169cbf18a | ||
|
|
06b2dc801b | ||
|
|
aa1a62ef50 | ||
|
|
674d1076bc | ||
|
|
cfffeef29b | ||
|
|
bc01b14343 | ||
|
|
984a1cae11 | ||
|
|
fa6479d3ac | ||
|
|
855a633461 | ||
|
|
dcc2ff0a93 |
31
.opencode/plugins/tui-smoke.tsx
Normal file
31
.opencode/plugins/tui-smoke.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import mytheme from "../themes/mytheme.json" with { type: "json" }
|
||||
|
||||
const slot = (label) => ({
|
||||
id: "workspace-smoke",
|
||||
slots: {
|
||||
home_hint() {
|
||||
return <text> [plugin:{label}]</text>
|
||||
},
|
||||
home_footer() {
|
||||
return <text> theme:workspace-plugin-smoke</text>
|
||||
},
|
||||
session_footer(_ctx, props) {
|
||||
return <text> session:{props.session_id.slice(0, 8)}</text>
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const themes = {
|
||||
"workspace-plugin-smoke": mytheme,
|
||||
}
|
||||
|
||||
const tui = async (input, options) => {
|
||||
if (options?.enabled === false) return
|
||||
const label = typeof options?.label === "string" ? options.label : "smoke"
|
||||
input.slots.register(slot(label))
|
||||
}
|
||||
|
||||
export default {
|
||||
themes,
|
||||
tui,
|
||||
}
|
||||
13
.opencode/tui.json
Normal file
13
.opencode/tui.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"theme": "workspace-plugin-smoke",
|
||||
"plugin": [
|
||||
[
|
||||
"./plugins/tui-smoke.tsx",
|
||||
{
|
||||
"enabled": true,
|
||||
"label": "workspace"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
55
bun.lock
55
bun.lock
@@ -304,8 +304,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||
"@opentui/core": "0.1.86",
|
||||
"@opentui/solid": "0.1.86",
|
||||
"@opentui/core": "0.0.0-20260304-eee67156",
|
||||
"@opentui/solid": "0.0.0-20260304-eee67156",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -382,11 +382,18 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "0.0.0-20260304-eee67156",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.87",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
],
|
||||
},
|
||||
"packages/script": {
|
||||
"name": "@opencode-ai/script",
|
||||
@@ -1345,21 +1352,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.86", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.86", "@opentui/core-darwin-x64": "0.1.86", "@opentui/core-linux-arm64": "0.1.86", "@opentui/core-linux-x64": "0.1.86", "@opentui/core-win32-arm64": "0.1.86", "@opentui/core-win32-x64": "0.1.86", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-3tRLbI9ADrQE1jEEn4x2aJexEOQZkv9Emk2BixMZqxfVhz2zr2SxtpimDAX0vmZK3+GnWAwBWxuaCAsxZpY4+w=="],
|
||||
"@opentui/core": ["@opentui/core@0.0.0-20260304-eee67156", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20260304-eee67156", "@opentui/core-darwin-x64": "0.0.0-20260304-eee67156", "@opentui/core-linux-arm64": "0.0.0-20260304-eee67156", "@opentui/core-linux-x64": "0.0.0-20260304-eee67156", "@opentui/core-win32-arm64": "0.0.0-20260304-eee67156", "@opentui/core-win32-x64": "0.0.0-20260304-eee67156", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-CWBnGrd9jp16xlaekDF85h15yQvBsNHtRSJUlmXrp6cQNq80fBcYJIKZfJtLD7Db4kcoimI068F06B23BPMzEQ=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.86", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Zp7q64+d+Dcx6YrH3mRcnHq8EOBnrfc1RvjgSWLhpXr49hY6LzuhqpfZM57aGErPYlR+ff8QM6e5FUkFnDfyjw=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20260304-eee67156", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gWIjWq4lLvd+yxTeGQ3NkHz76mRGsEOYCqSsZaj+7hUNlK09dDkp/GXMONH0HZTK8enHTRI7XGHuHMtaLC3fSw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.86", "", { "os": "darwin", "cpu": "x64" }, "sha512-NcxfjCJm1kLnTMVOpAPdRYNi8W8XdAXNa6N7i9khiVFrl2v5KRQfUjbrSOUYVxFJNc3jKFG6rsn3jEApvn92qA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20260304-eee67156", "", { "os": "darwin", "cpu": "x64" }, "sha512-vgiS74vRPRfssmjYhkFjgEN7po+Vh99dVm+Zq47N5FnVvkvy60D3Z4sBDZsJtnut2u544E2Po9IgA2jY1+w9Zw=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.86", "", { "os": "linux", "cpu": "arm64" }, "sha512-EDHAvqSOr8CXzbDvo1aE5blJ6wu1aSbR2LqoXtoeXHemr2T2W42D2TdIWewG6K+/BuRbzZnqt9wnYFBksLW6lw=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20260304-eee67156", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/ZfI8i6zJcMrHVIbFvNwTSPgrw1LgVZksQnY2qOsxML0nPGgH+kmCTwFHmdJdMVW3PFZHwUE/D6Dp9s/Lrbkg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.86", "", { "os": "linux", "cpu": "x64" }, "sha512-VBaBkVdQDxYV4WcKjb+jgyMS5PiVHepvfaoKWpz1Bq+J01xXW4XPcXyPGkgR1+2R93KzaugEnLscTW4mWtLHlQ=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20260304-eee67156", "", { "os": "linux", "cpu": "x64" }, "sha512-3I2y0Im6XYNgRGyV3vXrHxTL4I2AK2+c7GPootJM3OXWrKt6Si/KpW7lM7dydtQugVhkG52GZJC4EzqgTuSTjA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.86", "", { "os": "win32", "cpu": "arm64" }, "sha512-xKbT7sEKYKGwUPkoqmLfHjbJU+vwHPDwf/r/mIunL41JXQBB35CSZ3/QgIwpp2kkteu7oE1tdBdg15ogUU4OMg=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20260304-eee67156", "", { "os": "win32", "cpu": "arm64" }, "sha512-8DjoIk+vPIVudaa1+24UiEoO+efYiilCWxGWkU0RAaxp11AyvpfGEFzXxRL6WtV5Js0jGVCwYuY5V0BGgbeSNA=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.86", "", { "os": "win32", "cpu": "x64" }, "sha512-HRfgAUlcu71/MrtgfX4Gj7PsDtfXZiuC506Pkn1OnRN1Xomcu10BVRDweUa0/g8ldU9i9kLjMGGnpw6/NjaBFg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20260304-eee67156", "", { "os": "win32", "cpu": "x64" }, "sha512-Uawsa5RKILp1K9UhcO5q9ODauZnN1UVljYZzZ7GCT48R18Bwpp1cXvGiFfKt7d3zOfNpPKtvs081T8XYNzqKdQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.86", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.86", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pOZC9dlZIH+bpstVVZ2AvYukBnslZTKSl/y5H8FWcMTHGv/BzpGxXBxstL65E/IQASqPFbvFcs7yMRzdLhynmA=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.0.0-20260304-eee67156", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20260304-eee67156", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-h0qqJ78COKWkWphEPBMTScnz8AIaBkYFuN1lvncwxLwEP0cbxSwZrr/e/TZiG+Q9nBBp8OR/lqFS9BJZCPnJKQ=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -3471,7 +3478,7 @@
|
||||
|
||||
"pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="],
|
||||
|
||||
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
|
||||
|
||||
@@ -4931,6 +4938,10 @@
|
||||
|
||||
"openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core": ["@opentui/core@0.1.86", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.86", "@opentui/core-darwin-x64": "0.1.86", "@opentui/core-linux-arm64": "0.1.86", "@opentui/core-linux-x64": "0.1.86", "@opentui/core-win32-arm64": "0.1.86", "@opentui/core-win32-x64": "0.1.86", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-3tRLbI9ADrQE1jEEn4x2aJexEOQZkv9Emk2BixMZqxfVhz2zr2SxtpimDAX0vmZK3+GnWAwBWxuaCAsxZpY4+w=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.86", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.86", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pOZC9dlZIH+bpstVVZ2AvYukBnslZTKSl/y5H8FWcMTHGv/BzpGxXBxstL65E/IQASqPFbvFcs7yMRzdLhynmA=="],
|
||||
|
||||
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
@@ -5015,9 +5026,9 @@
|
||||
|
||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
|
||||
|
||||
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
@@ -5437,6 +5448,22 @@
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.86", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Zp7q64+d+Dcx6YrH3mRcnHq8EOBnrfc1RvjgSWLhpXr49hY6LzuhqpfZM57aGErPYlR+ff8QM6e5FUkFnDfyjw=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.86", "", { "os": "darwin", "cpu": "x64" }, "sha512-NcxfjCJm1kLnTMVOpAPdRYNi8W8XdAXNa6N7i9khiVFrl2v5KRQfUjbrSOUYVxFJNc3jKFG6rsn3jEApvn92qA=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.86", "", { "os": "linux", "cpu": "arm64" }, "sha512-EDHAvqSOr8CXzbDvo1aE5blJ6wu1aSbR2LqoXtoeXHemr2T2W42D2TdIWewG6K+/BuRbzZnqt9wnYFBksLW6lw=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.86", "", { "os": "linux", "cpu": "x64" }, "sha512-VBaBkVdQDxYV4WcKjb+jgyMS5PiVHepvfaoKWpz1Bq+J01xXW4XPcXyPGkgR1+2R93KzaugEnLscTW4mWtLHlQ=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.86", "", { "os": "win32", "cpu": "arm64" }, "sha512-xKbT7sEKYKGwUPkoqmLfHjbJU+vwHPDwf/r/mIunL41JXQBB35CSZ3/QgIwpp2kkteu7oE1tdBdg15ogUU4OMg=="],
|
||||
|
||||
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.86", "", { "os": "win32", "cpu": "x64" }, "sha512-HRfgAUlcu71/MrtgfX4Gj7PsDtfXZiuC506Pkn1OnRN1Xomcu10BVRDweUa0/g8ldU9i9kLjMGGnpw6/NjaBFg=="],
|
||||
|
||||
"opentui-spinner/@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=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
|
||||
|
||||
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||
|
||||
"readable-stream/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@@ -5817,6 +5844,8 @@
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||
|
||||
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||
|
||||
@@ -185,7 +185,9 @@ export function StatusPopover() {
|
||||
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
|
||||
const lspItems = createMemo(() => sync.data.lsp ?? [])
|
||||
const lspCount = createMemo(() => lspItems().length)
|
||||
const plugins = createMemo(() => sync.data.config.plugin ?? [])
|
||||
const plugins = createMemo(() =>
|
||||
(sync.data.config.plugin ?? []).map((item) => (typeof item === "string" ? item : item[0])),
|
||||
)
|
||||
const pluginCount = createMemo(() => plugins().length)
|
||||
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
|
||||
const overallHealthy = createMemo(() => {
|
||||
|
||||
@@ -89,8 +89,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.4",
|
||||
"@opentui/core": "0.1.86",
|
||||
"@opentui/solid": "0.1.86",
|
||||
"@opentui/core": "0.0.0-20260304-eee67156",
|
||||
"@opentui/solid": "0.0.0-20260304-eee67156",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { $ } from "bun"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import solidPlugin from "../node_modules/@opentui/solid/scripts/solid-plugin"
|
||||
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
@@ -59,6 +59,7 @@ console.log(`Loaded ${migrations.length} migrations`)
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
const plugin = createSolidTransformPlugin({ mode: "build" })
|
||||
|
||||
const allTargets: {
|
||||
os: string
|
||||
@@ -171,7 +172,7 @@ for (const item of targets) {
|
||||
await Bun.build({
|
||||
conditions: ["browser"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [solidPlugin],
|
||||
plugins: [plugin],
|
||||
sourcemap: "external",
|
||||
compile: {
|
||||
autoloadBunfig: false,
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||
import {
|
||||
createSlot,
|
||||
createSolidSlotRegistry,
|
||||
render,
|
||||
useKeyboard,
|
||||
useRenderer,
|
||||
useTerminalDimensions,
|
||||
type SolidPlugin,
|
||||
} from "@opentui/solid"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { Selection } from "@tui/util/selection"
|
||||
import { MouseButton, TextAttributes } from "@opentui/core"
|
||||
import { createCliRenderer, MouseButton, TextAttributes, type CliRendererConfig } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
|
||||
import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32"
|
||||
@@ -40,6 +48,9 @@ import { writeHeapSnapshot } from "v8"
|
||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||
import { TuiConfigProvider } from "./context/tui-config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import type { TuiSlotContext, TuiSlotMap, TuiSlots } from "@opencode-ai/plugin/tui"
|
||||
|
||||
type TuiSlot = <K extends keyof TuiSlotMap>(props: { name: K } & TuiSlotMap[K]) => unknown
|
||||
|
||||
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
// can't set raw mode if not a TTY
|
||||
@@ -103,6 +114,25 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
|
||||
import type { EventSource } from "./context/sdk"
|
||||
|
||||
function rendererConfig(_config: TuiConfig.Info): CliRendererConfig {
|
||||
return {
|
||||
targetFps: 60,
|
||||
gatherStats: false,
|
||||
exitOnCtrlC: false,
|
||||
useKittyKeyboard: {},
|
||||
autoFocus: false,
|
||||
openConsoleOnError: false,
|
||||
consoleOptions: {
|
||||
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
|
||||
onCopySelection: (text) => {
|
||||
Clipboard.copy(text).catch((error) => {
|
||||
console.error(`Failed to copy console selection to clipboard: ${error}`)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function tui(input: {
|
||||
url: string
|
||||
args: Args
|
||||
@@ -130,77 +160,89 @@ export function tui(input: {
|
||||
resolve()
|
||||
}
|
||||
|
||||
render(
|
||||
() => {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
|
||||
>
|
||||
<ArgsProvider {...input.args}>
|
||||
<ExitProvider onExit={onExit}>
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider>
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<SDKProvider
|
||||
url={input.url}
|
||||
directory={input.directory}
|
||||
fetch={input.fetch}
|
||||
headers={input.headers}
|
||||
events={input.events}
|
||||
>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
</TuiConfigProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
</KVProvider>
|
||||
</ExitProvider>
|
||||
</ArgsProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
const renderer = await createCliRenderer(rendererConfig(input.config))
|
||||
const registry = createSolidSlotRegistry<TuiSlotMap, TuiSlotContext>(
|
||||
renderer,
|
||||
{
|
||||
url: input.url,
|
||||
directory: input.directory,
|
||||
},
|
||||
{
|
||||
targetFps: 60,
|
||||
gatherStats: false,
|
||||
exitOnCtrlC: false,
|
||||
useKittyKeyboard: {},
|
||||
autoFocus: false,
|
||||
openConsoleOnError: false,
|
||||
consoleOptions: {
|
||||
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
|
||||
onCopySelection: (text) => {
|
||||
Clipboard.copy(text).catch((error) => {
|
||||
console.error(`Failed to copy console selection to clipboard: ${error}`)
|
||||
})
|
||||
},
|
||||
onPluginError(event) {
|
||||
console.error("[tui.slot] plugin error", {
|
||||
plugin: event.pluginId,
|
||||
slot: event.slot,
|
||||
phase: event.phase,
|
||||
source: event.source,
|
||||
message: event.error.message,
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
const Slot = createSlot<TuiSlotMap, TuiSlotContext>(registry)
|
||||
const slot: TuiSlot = (props) => Slot(props)
|
||||
const slots: TuiSlots = {
|
||||
register(plugin) {
|
||||
console.error("[tui.slot] register", plugin.id)
|
||||
return registry.register(plugin as SolidPlugin<TuiSlotMap, TuiSlotContext>)
|
||||
},
|
||||
}
|
||||
|
||||
await render(() => {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
|
||||
>
|
||||
<ArgsProvider {...input.args}>
|
||||
<ExitProvider onExit={onExit}>
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider>
|
||||
<TuiConfigProvider config={input.config}>
|
||||
<SDKProvider
|
||||
url={input.url}
|
||||
renderer={renderer}
|
||||
slots={slots}
|
||||
directory={input.directory}
|
||||
fetch={input.fetch}
|
||||
headers={input.headers}
|
||||
events={input.events}
|
||||
>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<App slot={slot} />
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProvider>
|
||||
</SDKProvider>
|
||||
</TuiConfigProvider>
|
||||
</RouteProvider>
|
||||
</ToastProvider>
|
||||
</KVProvider>
|
||||
</ExitProvider>
|
||||
</ArgsProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}, renderer)
|
||||
})
|
||||
}
|
||||
|
||||
function App() {
|
||||
function App(props: { slot: TuiSlot }) {
|
||||
const route = useRoute()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const renderer = useRenderer()
|
||||
@@ -751,10 +793,10 @@ function App() {
|
||||
>
|
||||
<Switch>
|
||||
<Match when={route.data.type === "home"}>
|
||||
<Home />
|
||||
<Home slot={props.slot} />
|
||||
</Match>
|
||||
<Match when={route.data.type === "session"}>
|
||||
<Session />
|
||||
<Session slot={props.slot} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
|
||||
@@ -16,7 +16,8 @@ export function DialogStatus() {
|
||||
|
||||
const plugins = createMemo(() => {
|
||||
const list = sync.data.config.plugin ?? []
|
||||
const result = list.map((value) => {
|
||||
const result = list.map((item) => {
|
||||
const value = typeof item === "string" ? item : item[0]
|
||||
if (value.startsWith("file://")) {
|
||||
const path = fileURLToPath(value)
|
||||
const parts = path.split("/")
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
|
||||
import type { CliRenderer } from "@opentui/core"
|
||||
import type { TuiSlots } from "@opencode-ai/plugin/tui"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
import { TuiPlugin } from "../plugin"
|
||||
|
||||
export type EventSource = {
|
||||
on: (handler: (event: Event) => void) => () => void
|
||||
@@ -11,6 +14,8 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: {
|
||||
url: string
|
||||
renderer: CliRenderer
|
||||
slots: TuiSlots
|
||||
directory?: string
|
||||
fetch?: typeof fetch
|
||||
headers?: RequestInit["headers"]
|
||||
@@ -29,6 +34,17 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}>()
|
||||
|
||||
TuiPlugin.init({
|
||||
client: sdk,
|
||||
event: emitter,
|
||||
url: props.url,
|
||||
directory: props.directory,
|
||||
renderer: props.renderer,
|
||||
slots: props.slots,
|
||||
}).catch((error) => {
|
||||
console.error("Failed to load TUI plugins", error)
|
||||
})
|
||||
|
||||
let queue: Event[] = []
|
||||
let timer: Timer | undefined
|
||||
let last = 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
|
||||
import path from "path"
|
||||
import { createEffect, createMemo, onMount } from "solid-js"
|
||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { Glob } from "../../../../util/glob"
|
||||
import aura from "./theme/aura.json" with { type: "json" }
|
||||
@@ -138,6 +138,44 @@ type ThemeJson = {
|
||||
}
|
||||
}
|
||||
|
||||
type ThemeRegistry = {
|
||||
themes: Record<string, ThemeJson>
|
||||
listeners: Set<(themes: Record<string, ThemeJson>) => void>
|
||||
}
|
||||
|
||||
const registry: ThemeRegistry = {
|
||||
themes: {},
|
||||
listeners: new Set(),
|
||||
}
|
||||
|
||||
export function registerThemes(themes: Record<string, unknown>) {
|
||||
const entries = Object.entries(themes).filter((entry): entry is [string, ThemeJson] => {
|
||||
const theme = entry[1]
|
||||
if (!theme || typeof theme !== "object") return false
|
||||
if (!("theme" in theme)) return false
|
||||
return true
|
||||
})
|
||||
if (entries.length === 0) return
|
||||
|
||||
for (const [name, theme] of entries) {
|
||||
registry.themes[name] = theme
|
||||
}
|
||||
|
||||
const payload = Object.fromEntries(entries)
|
||||
for (const handler of registry.listeners) {
|
||||
handler(payload)
|
||||
}
|
||||
}
|
||||
|
||||
function registeredThemes() {
|
||||
return registry.themes
|
||||
}
|
||||
|
||||
function onThemes(handler: (themes: Record<string, ThemeJson>) => void) {
|
||||
registry.listeners.add(handler)
|
||||
return () => registry.listeners.delete(handler)
|
||||
}
|
||||
|
||||
export const DEFAULT_THEMES: Record<string, ThemeJson> = {
|
||||
aura,
|
||||
ayu,
|
||||
@@ -296,6 +334,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
|
||||
function init() {
|
||||
resolveSystemTheme()
|
||||
mergeThemes(registeredThemes())
|
||||
getCustomThemes()
|
||||
.then((custom) => {
|
||||
setStore(
|
||||
@@ -315,6 +354,22 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
onMount(init)
|
||||
onCleanup(
|
||||
onThemes((themes) => {
|
||||
mergeThemes(themes)
|
||||
}),
|
||||
)
|
||||
|
||||
function mergeThemes(themes: Record<string, ThemeJson>) {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
for (const [name, theme] of Object.entries(themes)) {
|
||||
if (draft.themes[name]) continue
|
||||
draft.themes[name] = theme
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function resolveSystemTheme() {
|
||||
console.log("resolveSystemTheme")
|
||||
|
||||
114
packages/opencode/src/cli/cmd/tui/plugin.ts
Normal file
114
packages/opencode/src/cli/cmd/tui/plugin.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
type TuiPlugin as TuiPluginFn,
|
||||
type TuiPluginInput,
|
||||
type TuiPluginModule,
|
||||
type TuiSlotPlugin,
|
||||
} from "@opencode-ai/plugin/tui"
|
||||
import type { JSX } from "solid-js"
|
||||
import "@opentui/solid/preload"
|
||||
|
||||
import { Config } from "@/config/config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { Log } from "@/util/log"
|
||||
import { BunProc } from "@/bun"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { registerThemes } from "./context/theme"
|
||||
import { existsSync } from "fs"
|
||||
|
||||
export namespace TuiPlugin {
|
||||
const log = Log.create({ service: "tui.plugin" })
|
||||
let loaded: Promise<void> | undefined
|
||||
|
||||
export async function init(input: TuiPluginInput) {
|
||||
if (loaded) return loaded
|
||||
loaded = load(input)
|
||||
return loaded
|
||||
}
|
||||
|
||||
async function resolve(spec: string) {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
const lastAtIndex = spec.lastIndexOf("@")
|
||||
const pkg = lastAtIndex > 0 ? spec.substring(0, lastAtIndex) : spec
|
||||
const version = lastAtIndex > 0 ? spec.substring(lastAtIndex + 1) : "latest"
|
||||
return BunProc.install(pkg, version)
|
||||
}
|
||||
|
||||
function slot(entry: unknown) {
|
||||
if (!entry || typeof entry !== "object") return
|
||||
if ("id" in entry && typeof entry.id === "string" && "slots" in entry && typeof entry.slots === "object") {
|
||||
return entry as TuiSlotPlugin<JSX.Element>
|
||||
}
|
||||
if (!("slots" in entry)) return
|
||||
const value = entry.slots
|
||||
if (!value || typeof value !== "object") return
|
||||
if (!("id" in value) || typeof value.id !== "string") return
|
||||
if (!("slots" in value) || typeof value.slots !== "object") return
|
||||
return value as TuiSlotPlugin<JSX.Element>
|
||||
}
|
||||
|
||||
async function load(input: TuiPluginInput) {
|
||||
const base = input.directory ?? process.cwd()
|
||||
const dir = existsSync(base) ? base : process.cwd()
|
||||
if (dir !== base) {
|
||||
log.info("tui plugin directory not found, using local cwd", {
|
||||
requested: base,
|
||||
directory: dir,
|
||||
})
|
||||
}
|
||||
|
||||
await Instance.provide({
|
||||
directory: dir,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const plugins = config.plugin ?? []
|
||||
if (plugins.length) await TuiConfig.waitForDependencies()
|
||||
|
||||
for (const item of plugins) {
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
log.info("loading tui plugin", { path: spec })
|
||||
const target = await resolve(spec).catch((error) => {
|
||||
log.error("failed to resolve tui plugin", { path: spec, error })
|
||||
return
|
||||
})
|
||||
if (!target) continue
|
||||
|
||||
const mod = await import(target).catch((error) => {
|
||||
log.error("failed to load tui plugin", { path: spec, error })
|
||||
return
|
||||
})
|
||||
if (!mod) continue
|
||||
|
||||
const seen = new Set<unknown>()
|
||||
for (const entry of Object.values<TuiPluginModule>(mod)) {
|
||||
if (seen.has(entry)) continue
|
||||
seen.add(entry)
|
||||
|
||||
const themes = (() => {
|
||||
if (!entry || typeof entry !== "object") return
|
||||
if (!("themes" in entry)) return
|
||||
if (!entry.themes || typeof entry.themes !== "object") return
|
||||
return entry.themes as Record<string, unknown>
|
||||
})()
|
||||
if (themes) registerThemes(themes)
|
||||
|
||||
const plugin = slot(entry)
|
||||
if (plugin) {
|
||||
input.slots.register(plugin)
|
||||
}
|
||||
|
||||
const tui = (() => {
|
||||
if (!entry || typeof entry !== "object") return
|
||||
if (!("tui" in entry)) return
|
||||
if (typeof entry.tui !== "function") return
|
||||
return entry.tui as TuiPluginFn
|
||||
})()
|
||||
if (!tui) continue
|
||||
await tui(input, Config.pluginOptions(item))
|
||||
}
|
||||
}
|
||||
},
|
||||
}).catch((error) => {
|
||||
log.error("failed to load tui plugins", { directory: dir, error })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,14 @@ import { usePromptRef } from "../context/prompt"
|
||||
import { Installation } from "@/installation"
|
||||
import { useKV } from "../context/kv"
|
||||
import { useCommandDialog } from "../component/dialog-command"
|
||||
import type { TuiSlotMap } from "@opencode-ai/plugin/tui"
|
||||
|
||||
type Slot = <K extends "home_hint" | "home_footer">(props: { name: K } & TuiSlotMap[K]) => unknown
|
||||
|
||||
// TODO: what is the best way to do this?
|
||||
let once = false
|
||||
|
||||
export function Home() {
|
||||
export function Home(props: { slot: Slot }) {
|
||||
const sync = useSync()
|
||||
const kv = useKV()
|
||||
const { theme } = useTheme()
|
||||
@@ -56,8 +59,8 @@ export function Home() {
|
||||
])
|
||||
|
||||
const Hint = (
|
||||
<Show when={connectedMcpCount() > 0}>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<Show when={connectedMcpCount() > 0}>
|
||||
<text fg={theme.text}>
|
||||
<Switch>
|
||||
<Match when={mcpError()}>
|
||||
@@ -70,8 +73,9 @@ export function Home() {
|
||||
</Match>
|
||||
</Switch>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</Show>
|
||||
{props.slot({ name: "home_hint" }) as never}
|
||||
</box>
|
||||
)
|
||||
|
||||
let prompt: PromptRef
|
||||
@@ -136,6 +140,7 @@ export function Home() {
|
||||
</Show>
|
||||
</box>
|
||||
<box flexGrow={1} />
|
||||
{props.slot({ name: "home_footer" }) as never}
|
||||
<box flexShrink={0}>
|
||||
<text fg={theme.textMuted}>{Installation.VERSION}</text>
|
||||
</box>
|
||||
|
||||
@@ -70,7 +70,6 @@ import { Toast, useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv.tsx"
|
||||
import { Editor } from "../../util/editor"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { Footer } from "./footer.tsx"
|
||||
import { usePromptRef } from "../../context/prompt"
|
||||
import { useExit } from "../../context/exit"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
@@ -81,9 +80,12 @@ import { DialogExportOptions } from "../../ui/dialog-export-options"
|
||||
import { formatTranscript } from "../../util/transcript"
|
||||
import { UI } from "@/cli/ui.ts"
|
||||
import { useTuiConfig } from "../../context/tui-config"
|
||||
import type { TuiSlotMap } from "@opencode-ai/plugin/tui"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
type Slot = (props: { name: "session_footer"; session_id: TuiSlotMap["session_footer"]["session_id"] }) => unknown
|
||||
|
||||
class CustomSpeedScroll implements ScrollAcceleration {
|
||||
constructor(private speed: number) {}
|
||||
|
||||
@@ -113,7 +115,7 @@ function use() {
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function Session() {
|
||||
export function Session(props: { slot: Slot }) {
|
||||
const route = useRouteData("session")
|
||||
const { navigate } = useRoute()
|
||||
const sync = useSync()
|
||||
@@ -1172,6 +1174,7 @@ export function Session() {
|
||||
}}
|
||||
sessionID={route.sessionID}
|
||||
/>
|
||||
{props.slot({ name: "session_footer", session_id: route.sessionID }) as never}
|
||||
</box>
|
||||
</Show>
|
||||
<Toast />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { pathToFileURL, fileURLToPath } from "url"
|
||||
import { pathToFileURL } from "url"
|
||||
import { createRequire } from "module"
|
||||
import os from "os"
|
||||
import z from "zod"
|
||||
@@ -38,6 +38,11 @@ import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
export namespace Config {
|
||||
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
|
||||
const PluginOptions = z.record(z.string(), z.unknown())
|
||||
export const PluginSpec = z.union([z.string(), z.tuple([z.string(), PluginOptions])])
|
||||
|
||||
export type PluginOptions = z.infer<typeof PluginOptions>
|
||||
export type PluginSpec = z.infer<typeof PluginSpec>
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
@@ -449,7 +454,7 @@ export namespace Config {
|
||||
}
|
||||
|
||||
async function loadPlugin(dir: string) {
|
||||
const plugins: string[] = []
|
||||
const plugins: PluginSpec[] = []
|
||||
|
||||
for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
|
||||
cwd: dir,
|
||||
@@ -462,6 +467,32 @@ export namespace Config {
|
||||
return plugins
|
||||
}
|
||||
|
||||
export function pluginSpecifier(plugin: PluginSpec): string {
|
||||
return Array.isArray(plugin) ? plugin[0] : plugin
|
||||
}
|
||||
|
||||
export function pluginOptions(plugin: PluginSpec): PluginOptions | undefined {
|
||||
return Array.isArray(plugin) ? plugin[1] : undefined
|
||||
}
|
||||
|
||||
export function resolvePluginSpec(plugin: PluginSpec, configFilepath: string): PluginSpec {
|
||||
const spec = pluginSpecifier(plugin)
|
||||
try {
|
||||
const resolved = import.meta.resolve!(spec, configFilepath)
|
||||
if (Array.isArray(plugin)) return [resolved, plugin[1]]
|
||||
return resolved
|
||||
} catch {
|
||||
try {
|
||||
const require = createRequire(configFilepath)
|
||||
const resolved = pathToFileURL(require.resolve(spec)).href
|
||||
if (Array.isArray(plugin)) return [resolved, plugin[1]]
|
||||
return resolved
|
||||
} catch {
|
||||
return plugin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a canonical plugin name from a plugin specifier.
|
||||
* - For file:// URLs: extracts filename without extension
|
||||
@@ -472,15 +503,16 @@ export namespace Config {
|
||||
* getPluginName("oh-my-opencode@2.4.3") // "oh-my-opencode"
|
||||
* getPluginName("@scope/pkg@1.0.0") // "@scope/pkg"
|
||||
*/
|
||||
export function getPluginName(plugin: string): string {
|
||||
if (plugin.startsWith("file://")) {
|
||||
return path.parse(new URL(plugin).pathname).name
|
||||
export function getPluginName(plugin: PluginSpec): string {
|
||||
const spec = pluginSpecifier(plugin)
|
||||
if (spec.startsWith("file://")) {
|
||||
return path.parse(new URL(spec).pathname).name
|
||||
}
|
||||
const lastAt = plugin.lastIndexOf("@")
|
||||
const lastAt = spec.lastIndexOf("@")
|
||||
if (lastAt > 0) {
|
||||
return plugin.substring(0, lastAt)
|
||||
return spec.substring(0, lastAt)
|
||||
}
|
||||
return plugin
|
||||
return spec
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -494,14 +526,14 @@ export namespace Config {
|
||||
* Since plugins are added in low-to-high priority order,
|
||||
* we reverse, deduplicate (keeping first occurrence), then restore order.
|
||||
*/
|
||||
export function deduplicatePlugins(plugins: string[]): string[] {
|
||||
export function deduplicatePlugins(plugins: PluginSpec[]): PluginSpec[] {
|
||||
// seenNames: canonical plugin names for duplicate detection
|
||||
// e.g., "oh-my-opencode", "@scope/pkg"
|
||||
const seenNames = new Set<string>()
|
||||
|
||||
// uniqueSpecifiers: full plugin specifiers to return
|
||||
// e.g., "oh-my-opencode@2.4.3", "file:///path/to/plugin.js"
|
||||
const uniqueSpecifiers: string[] = []
|
||||
// e.g., "oh-my-opencode@2.4.3", ["file:///path/to/plugin.js", { ... }]
|
||||
const uniqueSpecifiers: PluginSpec[] = []
|
||||
|
||||
for (const specifier of plugins.toReversed()) {
|
||||
const name = getPluginName(specifier)
|
||||
@@ -997,7 +1029,7 @@ export namespace Config {
|
||||
ignore: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
plugin: z.string().array().optional(),
|
||||
plugin: PluginSpec.array().optional(),
|
||||
snapshot: z.boolean().optional(),
|
||||
share: z
|
||||
.enum(["manual", "auto", "disabled"])
|
||||
@@ -1245,19 +1277,7 @@ export namespace Config {
|
||||
const data = parsed.data
|
||||
if (data.plugin && isFile) {
|
||||
for (let i = 0; i < data.plugin.length; i++) {
|
||||
const plugin = data.plugin[i]
|
||||
try {
|
||||
data.plugin[i] = import.meta.resolve!(plugin, options.path)
|
||||
} catch (e) {
|
||||
try {
|
||||
// import.meta.resolve sometimes fails with newly created node_modules
|
||||
const require = createRequire(options.path)
|
||||
const resolvedPath = require.resolve(plugin)
|
||||
data.plugin[i] = pathToFileURL(resolvedPath).href
|
||||
} catch {
|
||||
// Ignore, plugin might be a generic string identifier like "mcp-server"
|
||||
}
|
||||
}
|
||||
data.plugin[i] = resolvePluginSpec(data.plugin[i], options.path)
|
||||
}
|
||||
}
|
||||
return data
|
||||
|
||||
@@ -29,6 +29,7 @@ export const TuiInfo = z
|
||||
$schema: z.string().optional(),
|
||||
theme: z.string().optional(),
|
||||
keybinds: KeybindOverride.optional(),
|
||||
plugin: Config.PluginSpec.array().optional(),
|
||||
})
|
||||
.extend(TuiOptions.shape)
|
||||
.strict()
|
||||
|
||||
@@ -18,7 +18,11 @@ export namespace TuiConfig {
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
function mergeInfo(target: Info, source: Info): Info {
|
||||
return mergeDeep(target, source)
|
||||
const merged = mergeDeep(target, source)
|
||||
if (target.plugin && source.plugin) {
|
||||
merged.plugin = [...target.plugin, ...source.plugin]
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
function customPath() {
|
||||
@@ -67,9 +71,23 @@ export namespace TuiConfig {
|
||||
}
|
||||
|
||||
result.keybinds = Config.Keybinds.parse(result.keybinds ?? {})
|
||||
result.plugin = Config.deduplicatePlugins(result.plugin ?? [])
|
||||
|
||||
const deps: Promise<void>[] = []
|
||||
for (const dir of unique(directories)) {
|
||||
if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
|
||||
deps.push(
|
||||
(async () => {
|
||||
const shouldInstall = await Config.needsInstall(dir)
|
||||
if (!shouldInstall) return
|
||||
await Config.installDependencies(dir)
|
||||
})(),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
config: result,
|
||||
deps,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,6 +95,11 @@ export namespace TuiConfig {
|
||||
return state().then((x) => x.config)
|
||||
}
|
||||
|
||||
export async function waitForDependencies() {
|
||||
const deps = await state().then((x) => x.deps)
|
||||
await Promise.all(deps)
|
||||
}
|
||||
|
||||
async function loadFile(filepath: string): Promise<Info> {
|
||||
const text = await ConfigPaths.readFile(filepath)
|
||||
if (!text) return {}
|
||||
@@ -87,13 +110,13 @@ export namespace TuiConfig {
|
||||
}
|
||||
|
||||
async function load(text: string, configFilepath: string): Promise<Info> {
|
||||
const data = await ConfigPaths.parseText(text, configFilepath, "empty")
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) return {}
|
||||
const raw = await ConfigPaths.parseText(text, configFilepath, "empty")
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}
|
||||
|
||||
// Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
|
||||
// (mirroring the old opencode.json shape) still get their settings applied.
|
||||
const normalized = (() => {
|
||||
const copy = { ...(data as Record<string, unknown>) }
|
||||
const copy = { ...(raw as Record<string, unknown>) }
|
||||
if (!("tui" in copy)) return copy
|
||||
if (!copy.tui || typeof copy.tui !== "object" || Array.isArray(copy.tui)) {
|
||||
delete copy.tui
|
||||
@@ -113,6 +136,13 @@ export namespace TuiConfig {
|
||||
return {}
|
||||
}
|
||||
|
||||
return parsed.data
|
||||
const data = parsed.data
|
||||
if (data.plugin) {
|
||||
for (let i = 0; i < data.plugin.length; i++) {
|
||||
data.plugin[i] = Config.resolvePluginSpec(data.plugin[i], configFilepath)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,48 +53,75 @@ export namespace Plugin {
|
||||
plugins = [...BUILTIN, ...plugins]
|
||||
}
|
||||
|
||||
for (let plugin of plugins) {
|
||||
// ignore old codex plugin since it is supported first party now
|
||||
if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue
|
||||
log.info("loading plugin", { path: plugin })
|
||||
if (!plugin.startsWith("file://")) {
|
||||
const lastAtIndex = plugin.lastIndexOf("@")
|
||||
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
|
||||
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
|
||||
plugin = await BunProc.install(pkg, version).catch((err) => {
|
||||
const cause = err instanceof Error ? err.cause : err
|
||||
const detail = cause instanceof Error ? cause.message : String(cause ?? err)
|
||||
log.error("failed to install plugin", { pkg, version, error: detail })
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to install plugin ${pkg}@${version}: ${detail}`,
|
||||
}).toObject(),
|
||||
})
|
||||
return ""
|
||||
async function resolve(spec: string) {
|
||||
if (spec.startsWith("file://")) return spec
|
||||
const lastAtIndex = spec.lastIndexOf("@")
|
||||
const pkg = lastAtIndex > 0 ? spec.substring(0, lastAtIndex) : spec
|
||||
const version = lastAtIndex > 0 ? spec.substring(lastAtIndex + 1) : "latest"
|
||||
const builtIn = BUILTIN.some((x) => x.startsWith(pkg + "@"))
|
||||
const installed = await BunProc.install(pkg, version).catch((err) => {
|
||||
const cause = err instanceof Error ? err.cause : err
|
||||
const detail = cause instanceof Error ? cause.message : String(cause ?? err)
|
||||
log.error("failed to install plugin", { pkg, version, error: detail })
|
||||
const label = builtIn ? "built-in plugin" : "plugin"
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to install ${label} ${pkg}@${version}: ${detail}`,
|
||||
}).toObject(),
|
||||
})
|
||||
if (!plugin) continue
|
||||
}
|
||||
return ""
|
||||
})
|
||||
if (!installed) return
|
||||
return installed
|
||||
}
|
||||
|
||||
for (const item of plugins) {
|
||||
const spec = Config.pluginSpecifier(item)
|
||||
// ignore old codex plugin since it is supported first party now
|
||||
if (spec.includes("opencode-openai-codex-auth") || spec.includes("opencode-copilot-auth")) continue
|
||||
log.info("loading plugin", { path: spec })
|
||||
const path = await resolve(spec)
|
||||
if (!path) continue
|
||||
const mod = await import(path).catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
log.error("failed to load plugin", { path: spec, error: message })
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to load plugin ${spec}: ${message}`,
|
||||
}).toObject(),
|
||||
})
|
||||
return
|
||||
})
|
||||
if (!mod) continue
|
||||
|
||||
// Prevent duplicate initialization when plugins export the same function
|
||||
// as both a named export and default export (e.g., `export const X` and `export default X`).
|
||||
// Object.entries(mod) would return both entries pointing to the same function reference.
|
||||
await import(plugin)
|
||||
.then(async (mod) => {
|
||||
const seen = new Set<PluginInstance>()
|
||||
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
||||
if (seen.has(fn)) continue
|
||||
seen.add(fn)
|
||||
hooks.push(await fn(input))
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const seen = new Set<unknown>()
|
||||
for (const entry of Object.values(mod)) {
|
||||
if (seen.has(entry)) continue
|
||||
seen.add(entry)
|
||||
const server = (() => {
|
||||
if (typeof entry === "function") return entry as PluginInstance
|
||||
if (!entry || typeof entry !== "object") return
|
||||
if (!("server" in entry)) return
|
||||
if (typeof entry.server !== "function") return
|
||||
return entry.server as PluginInstance
|
||||
})()
|
||||
if (!server) continue
|
||||
const init = await server(input, Config.pluginOptions(item)).catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
log.error("failed to load plugin", { path: plugin, error: message })
|
||||
log.error("failed to initialize plugin", { path: spec, error: message })
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to load plugin ${plugin}: ${message}`,
|
||||
message: `Failed to initialize plugin ${spec}: ${message}`,
|
||||
}).toObject(),
|
||||
})
|
||||
return
|
||||
})
|
||||
if (!init) continue
|
||||
hooks.push(init)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1682,7 +1682,7 @@ describe("deduplicatePlugins", () => {
|
||||
|
||||
const myPlugins = plugins.filter((p) => Config.getPluginName(p) === "my-plugin")
|
||||
expect(myPlugins.length).toBe(1)
|
||||
expect(myPlugins[0].startsWith("file://")).toBe(true)
|
||||
expect(Config.pluginSpecifier(myPlugins[0]).startsWith("file://")).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -508,3 +508,57 @@ test("gracefully falls back when tui.json has invalid JSON", async () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("supports tuple plugin specs with options in tui.json", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({
|
||||
plugin: [["acme-plugin@1.2.3", { enabled: true, label: "demo" }]],
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.plugin).toEqual([["acme-plugin@1.2.3", { enabled: true, label: "demo" }]])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("deduplicates tuple plugin specs by name with higher precedence winning", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(Global.Path.config, "tui.json"),
|
||||
JSON.stringify({
|
||||
plugin: [["acme-plugin@1.0.0", { source: "global" }]],
|
||||
}),
|
||||
)
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({
|
||||
plugin: [
|
||||
["acme-plugin@2.0.0", { source: "project" }],
|
||||
["second-plugin@3.0.0", { source: "project" }],
|
||||
],
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.plugin).toEqual([
|
||||
["acme-plugin@2.0.0", { source: "project" }],
|
||||
["second-plugin@3.0.0", { source: "project" }],
|
||||
])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./tool": "./src/tool.ts"
|
||||
"./tool": "./src/tool.ts",
|
||||
"./tui": "./src/tui.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -19,7 +20,16 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.87"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "0.0.0-20260304-eee67156",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
|
||||
@@ -9,9 +9,8 @@ import type {
|
||||
Message,
|
||||
Part,
|
||||
Auth,
|
||||
Config,
|
||||
Config as SDKConfig,
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
import type { BunShell } from "./shell"
|
||||
import { type ToolDefinition } from "./tool"
|
||||
|
||||
@@ -32,7 +31,13 @@ export type PluginInput = {
|
||||
$: BunShell
|
||||
}
|
||||
|
||||
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
||||
export type PluginOptions = Record<string, unknown>
|
||||
|
||||
export type Config = Omit<SDKConfig, "plugin"> & {
|
||||
plugin?: Array<string | [string, PluginOptions]>
|
||||
}
|
||||
|
||||
export type Plugin = (input: PluginInput, options?: PluginOptions) => Promise<Hooks>
|
||||
|
||||
export type AuthHook = {
|
||||
provider: string
|
||||
|
||||
86
packages/plugin/src/tui.ts
Normal file
86
packages/plugin/src/tui.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { createOpencodeClient as createOpencodeClientV2, Event as TuiEvent } from "@opencode-ai/sdk/v2"
|
||||
import type { CliRenderer } from "@opentui/core"
|
||||
import type { Plugin, PluginOptions } from "./index"
|
||||
|
||||
export type { CliRenderer } from "@opentui/core"
|
||||
|
||||
type HexColor = `#${string}`
|
||||
type RefName = string
|
||||
type Variant = {
|
||||
dark: HexColor | RefName | number
|
||||
light: HexColor | RefName | number
|
||||
}
|
||||
type ThemeColorValue = HexColor | RefName | number | Variant
|
||||
|
||||
export type ThemeJson = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
theme: Record<string, ThemeColorValue> & {
|
||||
selectedListItemText?: ThemeColorValue
|
||||
backgroundMenu?: ThemeColorValue
|
||||
thinkingOpacity?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type SlotMode = "append" | "replace" | "single_winner"
|
||||
|
||||
type SlotRenderer<Node, Props, Context extends object = object> = (ctx: Readonly<Context>, props: Props) => Node
|
||||
|
||||
type SlotPlugin<Node, Slots extends object, Context extends object = object> = {
|
||||
id: string
|
||||
order?: number
|
||||
setup?: (ctx: Readonly<Context>, renderer: CliRenderer) => void
|
||||
dispose?: () => void
|
||||
slots: {
|
||||
[K in keyof Slots]?: SlotRenderer<Node, Slots[K], Context>
|
||||
}
|
||||
}
|
||||
|
||||
export type TuiSlotMap = {
|
||||
home_hint: {}
|
||||
home_footer: {}
|
||||
session_footer: {
|
||||
session_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export type TuiSlotContext = {
|
||||
url: string
|
||||
directory?: string
|
||||
}
|
||||
|
||||
export type TuiSlotPlugin<Node = unknown> = SlotPlugin<Node, TuiSlotMap, TuiSlotContext>
|
||||
|
||||
export type TuiSlots = {
|
||||
register: (plugin: TuiSlotPlugin) => () => void
|
||||
}
|
||||
|
||||
export type TuiEventBus = {
|
||||
on: <Type extends TuiEvent["type"]>(
|
||||
type: Type,
|
||||
handler: (event: Extract<TuiEvent, { type: Type }>) => void,
|
||||
) => () => void
|
||||
}
|
||||
|
||||
export type TuiPluginInput<Renderer = CliRenderer> = {
|
||||
client: ReturnType<typeof createOpencodeClientV2>
|
||||
event: TuiEventBus
|
||||
url: string
|
||||
directory?: string
|
||||
renderer: Renderer
|
||||
slots: TuiSlots
|
||||
}
|
||||
|
||||
export type TuiPlugin<Renderer = CliRenderer> = (
|
||||
input: TuiPluginInput<Renderer>,
|
||||
options?: PluginOptions,
|
||||
) => Promise<void>
|
||||
|
||||
export type TuiPluginModule<Renderer = CliRenderer> =
|
||||
| TuiPlugin<Renderer>
|
||||
| {
|
||||
server?: Plugin
|
||||
tui?: TuiPlugin<Renderer>
|
||||
slots?: TuiSlotPlugin
|
||||
themes?: Record<string, ThemeJson>
|
||||
}
|
||||
@@ -1338,7 +1338,15 @@ export type Config = {
|
||||
watcher?: {
|
||||
ignore?: Array<string>
|
||||
}
|
||||
plugin?: Array<string>
|
||||
plugin?: Array<
|
||||
| string
|
||||
| [
|
||||
string,
|
||||
{
|
||||
[key: string]: unknown
|
||||
},
|
||||
]
|
||||
>
|
||||
snapshot?: boolean
|
||||
/**
|
||||
* Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing
|
||||
|
||||
Reference in New Issue
Block a user