Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e86c9b791 | ||
|
|
94658c31c5 | ||
|
|
9fd672a1cb | ||
|
|
10523c4372 | ||
|
|
d1cd7d0344 | ||
|
|
06ac1be226 | ||
|
|
05489bc843 | ||
|
|
3f02eecf22 | ||
|
|
f5ca78ed7b | ||
|
|
894cbaa51e | ||
|
|
8b70b89fde | ||
|
|
f9dbc586dc | ||
|
|
ffeef63ca1 | ||
|
|
4da58294d9 | ||
|
|
fa2e88f49b | ||
|
|
28e765ef0a | ||
|
|
bfbcb5f200 | ||
|
|
89492b3002 | ||
|
|
2663415d47 | ||
|
|
51be67cc14 | ||
|
|
92a1943771 | ||
|
|
1e15fc273a | ||
|
|
104a895a71 | ||
|
|
f98e730405 | ||
|
|
b12bef05d3 | ||
|
|
2f1d001cc5 | ||
|
|
65d0b3ed6d | ||
|
|
22a34d7958 | ||
|
|
cb4401ec92 | ||
|
|
febf467b03 | ||
|
|
d55a2fd56c | ||
|
|
40f577e5e7 | ||
|
|
9e49870118 | ||
|
|
fe38e3ab02 | ||
|
|
0170577743 | ||
|
|
7de6ea5922 | ||
|
|
2fe7d13e69 | ||
|
|
1bc3c98ae7 | ||
|
|
55787f2caa | ||
|
|
7df61a74a0 | ||
|
|
4f23110880 | ||
|
|
041353f4ff | ||
|
|
c72f8b17c6 | ||
|
|
eb304f4115 | ||
|
|
5565f14ef5 | ||
|
|
10a4455c6f | ||
|
|
5ded6d6ad7 | ||
|
|
849a38c30c | ||
|
|
68050ab802 | ||
|
|
91d01fd4cc | ||
|
|
9beb0f8512 | ||
|
|
d4cb47eadc | ||
|
|
261ff416a9 | ||
|
|
d0a70cb217 | ||
|
|
20fc56d020 | ||
|
|
a57ae3ec93 | ||
|
|
30f9fa12d9 | ||
|
|
d473d4ffc8 | ||
|
|
af50596529 | ||
|
|
3823d8d50e | ||
|
|
7a926b32ce | ||
|
|
a5ede68241 | ||
|
|
60dc38050d | ||
|
|
31d0caee38 | ||
|
|
2a7ab45605 | ||
|
|
019054dd1e | ||
|
|
a018a15f32 | ||
|
|
e630d680dd | ||
|
|
9e392f25a6 | ||
|
|
2cc4e6ad7c | ||
|
|
70d8d1ab1e | ||
|
|
342aa27e03 | ||
|
|
e1aed0cd01 |
@@ -5,6 +5,8 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version-file: package.json
|
||||||
|
|
||||||
- name: Cache ~/.bun
|
- name: Cache ~/.bun
|
||||||
id: cache-bun
|
id: cache-bun
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
const description = issue.body || '';
|
const description = issue.body || '';
|
||||||
|
|
||||||
// Check for version patterns like v1.0.x or 1.0.x
|
// Check for version patterns like v1.0.x or 1.0.x
|
||||||
const versionPattern = /\b[v]?1\.0\.[x\d]\b/i;
|
const versionPattern = /[v]?1\.0\./i;
|
||||||
|
|
||||||
if (versionPattern.test(title) || versionPattern.test(description)) {
|
if (versionPattern.test(title) || versionPattern.test(description)) {
|
||||||
await github.rest.issues.addLabels({
|
await github.rest.issues.addLabels({
|
||||||
@@ -30,11 +30,4 @@ jobs:
|
|||||||
issue_number: issue.number,
|
issue_number: issue.number,
|
||||||
labels: ['opentui']
|
labels: ['opentui']
|
||||||
});
|
});
|
||||||
|
|
||||||
await github.rest.issues.addAssignees({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
assignees: ['thdxr']
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
with:
|
with:
|
||||||
model: opencode/kimi-k2
|
model: opencode/glm-4.6
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
*~
|
||||||
openapi.json
|
openapi.json
|
||||||
playground
|
playground
|
||||||
tmp
|
tmp
|
||||||
|
|||||||
@@ -19,5 +19,5 @@ For anything in the packages/app use the ignore: prefix.
|
|||||||
prefer to explain WHY something was done from an end user perspective instead of
|
prefer to explain WHY something was done from an end user perspective instead of
|
||||||
WHAT was done.
|
WHAT was done.
|
||||||
|
|
||||||
do not do generic messages like "improvied agent experience" be very specific
|
do not do generic messages like "improved agent experience" be very specific
|
||||||
about what user facing changes were made
|
about what user facing changes were made
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
## Tool Calling
|
## Tool Calling
|
||||||
|
|
||||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environnement:
|
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
|
||||||
|
|
||||||
json
|
json
|
||||||
{
|
{
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
|||||||
|
|
||||||
## Developing OpenCode
|
## Developing OpenCode
|
||||||
|
|
||||||
- Requirements: Bun 1.3+, Go 1.24.x.
|
- Requirements: Bun 1.3+
|
||||||
- Install dependencies and start the dev server from the repo root:
|
- Install dependencies and start the dev server from the repo root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -36,11 +36,11 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
|||||||
|
|
||||||
- Core pieces:
|
- Core pieces:
|
||||||
- `packages/opencode`: OpenCode core business logic & server.
|
- `packages/opencode`: OpenCode core business logic & server.
|
||||||
- `packages/tui`: The TUI code, written in Go (will be removed soon in favor of [opentui](https://github.com/sst/opentui))
|
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> After touching `packages/opencode/src/server/server.ts`, the OpenCode team must regenerate the Stainless SDK before any client updates merge.
|
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
||||||
|
|
||||||
## Pull Request Expectations
|
## Pull Request Expectations
|
||||||
|
|
||||||
|
|||||||
@@ -126,3 +126,5 @@
|
|||||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||||
|
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||||
|
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -184,8 +184,8 @@
|
|||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opentui/core": "0.0.0-20251031-fc297165",
|
"@opentui/core": "0.1.32",
|
||||||
"@opentui/solid": "0.0.0-20251031-fc297165",
|
"@opentui/solid": "0.1.32",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
"hono-openapi": "1.0.7",
|
"hono-openapi": "1.1.1",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.81.0",
|
"@hey-api/openapi-ts": "0.81.0",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -317,7 +317,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -961,21 +961,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.0.0-20251031-fc297165", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251031-fc297165", "@opentui/core-darwin-x64": "0.0.0-20251031-fc297165", "@opentui/core-linux-arm64": "0.0.0-20251031-fc297165", "@opentui/core-linux-x64": "0.0.0-20251031-fc297165", "@opentui/core-win32-arm64": "0.0.0-20251031-fc297165", "@opentui/core-win32-x64": "0.0.0-20251031-fc297165", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-xtUF/uJF04d1wl4f7vsRNsDN8P9uK9Mcx1SAcm79wAN90VPNB4j2G0s7qlt8SD4zB0iWPjXICqJidjRzrQ3QVg=="],
|
"@opentui/core": ["@opentui/core@0.1.32", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.32", "@opentui/core-darwin-x64": "0.1.32", "@opentui/core-linux-arm64": "0.1.32", "@opentui/core-linux-x64": "0.1.32", "@opentui/core-win32-arm64": "0.1.32", "@opentui/core-win32-x64": "0.1.32", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-6Ms1Gybyvl3Rt4k8OdA2c/5YlhobICcXjF5mn4k7tWujFnrBTm441G8k02pdIUffy7fD7dsouq12gfAVmSBmvA=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251031-fc297165", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SD5AiofTfOT+JBx7tcBcd6BdD9sc+RPkHbhIJeqkw5V/GJ4OjyUW3m2kyR9iTs1nLMbKD5o9gyVXpLig4KmFiQ=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.32", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5yvhJEsXnZGS/q2jIfz97eA4wHOJyF/zTvJL6ykvqjlXwW+bOQ8S7WcpBShR2gf+49Exak3cO+XB16yMWmCyEw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251031-fc297165", "", { "os": "darwin", "cpu": "x64" }, "sha512-uhzxSvmfeK7vv8uNdhl8Mn2yMnjOVqdjZTOIV2aI8H9SCp8cmnzuLA8FXFO+BW6kgxsg6LbVdp4d4jDCgwtKLQ=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.32", "", { "os": "darwin", "cpu": "x64" }, "sha512-DKEA3kYvFuj5C4i1N1ck+VEqYH1iCc1O958iGrc5r++jGcP0osKKEA5qSWbSlEy+iflr7Oydr350Aqyyt7J/pA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251031-fc297165", "", { "os": "linux", "cpu": "arm64" }, "sha512-qGjjk/QTrAyqwzPC+6NhqiQZ31k3GxufbtccF8Yqan0GLuA6GrKcU72IcPwVA5t/6VIXaLkJZyFfub7CoO1D/g=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.32", "", { "os": "linux", "cpu": "arm64" }, "sha512-02rgp+Rq21hg3MhQIo8guvHNGuerJgdGSWqPsUs7HwsHLL1yD8ndMxdZxOvyHNsEjzrzklQqPishHZ97QbAVYQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251031-fc297165", "", { "os": "linux", "cpu": "x64" }, "sha512-gre61Sxc9yX8lrqGNXz5fyE7xJHfkgDi8smGPE2OVP8HmXh0Rn1tXMzFywweEs9MELP3kdQ0VhimYJWkp8FyWg=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.32", "", { "os": "linux", "cpu": "x64" }, "sha512-+ek+EYyJKC9xxVeqD14XlBYaaN7Xm45PGv7pviuqZMwJn+G6v6JxwfUNASOr/KT0N1BZdcDGp9EGToYGXwjsQg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251031-fc297165", "", { "os": "win32", "cpu": "arm64" }, "sha512-44jsq/Ea+jIjZDXyt0w23/DkvwniQFPRB1tocGp6VrOHyHKa0IPHAQ+iuM0felbnmdMUFYyTyh1iOfAcuZyaaA=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.32", "", { "os": "win32", "cpu": "arm64" }, "sha512-k9vt+jBrrWAKOWmOC02G4S8V+1iftsq6a+8+Lt9Vc6GdXNTWIKCooB5FPAoLaQeb9TXKjK1WJFgibEXo3Q9XXQ=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251031-fc297165", "", { "os": "win32", "cpu": "x64" }, "sha512-L20tCPrLFMCuX4lC2JTcixiCGFNM5RTHQwKLRcxcsSdKBr6a/7ztOG2a/2RNWkrrlbwTrUREVXH4Ivk3EOuStw=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.32", "", { "os": "win32", "cpu": "x64" }, "sha512-aLUYKZVMyyyN2A0d5ETbt4ktTb6GCp4PXYHijhOXy7QPvg779H8fy7dpGUHZYEJdwAdGrIvi/y6SKn9FmuXisA=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.0.0-20251031-fc297165", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251031-fc297165", "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-9u7ULKztDG1SvvU/wNCTFL7JYNPkG+pevcEU3JA7M2uUTIWrvKf/rD13lxtfVe7/yfxcY69SMRlaJGWpfxud5w=="],
|
"@opentui/solid": ["@opentui/solid@0.1.32", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.32", "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-JYQ7DpC1oF1jCO4I27mxYGz8M5AmBzsm7xbcG3VANA9cpCY/Tp3YXxanw2Gx1G/xSTY6QMBPhaTEuiFdIQ6FRw=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -2221,7 +2221,7 @@
|
|||||||
|
|
||||||
"hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
|
"hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="],
|
||||||
|
|
||||||
"hono-openapi": ["hono-openapi@1.0.7", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.1", "@standard-community/standard-openapi": "^0.2.4", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-rMn+nn4/HMisyi549L3zT7WCmVvmpiKsyt790GcGfqvJf9mJfhq6txw09l0IhSBxpJpA0pXVKxFijcsnGfshUA=="],
|
"hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
|
||||||
|
|
||||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||||
|
|
||||||
|
|||||||
+45
-15
@@ -168,7 +168,9 @@ try {
|
|||||||
const summary = await summarize(response)
|
const summary = await summarize(response)
|
||||||
await pushToLocalBranch(summary)
|
await pushToLocalBranch(summary)
|
||||||
}
|
}
|
||||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
|
const hasShared = prData.comments.nodes.some((c) =>
|
||||||
|
c.body.includes(`${useShareUrl()}/s/${shareId}`),
|
||||||
|
)
|
||||||
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
||||||
}
|
}
|
||||||
// Fork PR
|
// Fork PR
|
||||||
@@ -180,7 +182,9 @@ try {
|
|||||||
const summary = await summarize(response)
|
const summary = await summarize(response)
|
||||||
await pushToForkBranch(summary, prData)
|
await pushToForkBranch(summary, prData)
|
||||||
}
|
}
|
||||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
|
const hasShared = prData.comments.nodes.some((c) =>
|
||||||
|
c.body.includes(`${useShareUrl()}/s/${shareId}`),
|
||||||
|
)
|
||||||
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,7 +365,9 @@ async function getAccessToken() {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const responseJson = (await response.json()) as { error?: string }
|
const responseJson = (await response.json()) as { error?: string }
|
||||||
throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`)
|
throw new Error(
|
||||||
|
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseJson = (await response.json()) as { token: string }
|
const responseJson = (await response.json()) as { token: string }
|
||||||
@@ -402,8 +408,12 @@ async function getUserPrompt() {
|
|||||||
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
|
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
|
||||||
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
|
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
|
||||||
// ie. 
|
// ie. 
|
||||||
const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi)
|
const mdMatches = prompt.matchAll(
|
||||||
const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi)
|
/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi,
|
||||||
|
)
|
||||||
|
const tagMatches = prompt.matchAll(
|
||||||
|
/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi,
|
||||||
|
)
|
||||||
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
|
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
|
||||||
console.log("Images", JSON.stringify(matches, null, 2))
|
console.log("Images", JSON.stringify(matches, null, 2))
|
||||||
|
|
||||||
@@ -430,7 +440,8 @@ async function getUserPrompt() {
|
|||||||
|
|
||||||
// Replace img tag with file path, ie. @image.png
|
// Replace img tag with file path, ie. @image.png
|
||||||
const replacement = `@${filename}`
|
const replacement = `@${filename}`
|
||||||
prompt = prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
|
prompt =
|
||||||
|
prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
|
||||||
offset += replacement.length - tag.length
|
offset += replacement.length - tag.length
|
||||||
|
|
||||||
const contentType = res.headers.get("content-type")
|
const contentType = res.headers.get("content-type")
|
||||||
@@ -498,7 +509,12 @@ async function subscribeSessionEvents() {
|
|||||||
? JSON.stringify(part.state.input)
|
? JSON.stringify(part.state.input)
|
||||||
: "Unknown"
|
: "Unknown"
|
||||||
console.log()
|
console.log()
|
||||||
console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title)
|
console.log(
|
||||||
|
color + `|`,
|
||||||
|
"\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`,
|
||||||
|
"",
|
||||||
|
"\x1b[0m" + title,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
@@ -710,7 +726,8 @@ async function assertPermissions() {
|
|||||||
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
|
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
if (!["admin", "write"].includes(permission))
|
||||||
|
throw new Error(`User ${actor} does not have write permissions`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateComment(body: string) {
|
async function updateComment(body: string) {
|
||||||
@@ -730,12 +747,13 @@ async function updateComment(body: string) {
|
|||||||
async function createPR(base: string, branch: string, title: string, body: string) {
|
async function createPR(base: string, branch: string, title: string, body: string) {
|
||||||
console.log("Creating pull request...")
|
console.log("Creating pull request...")
|
||||||
const { repo } = useContext()
|
const { repo } = useContext()
|
||||||
|
const truncatedTitle = title.length > 256 ? title.slice(0, 253) + "..." : title
|
||||||
const pr = await octoRest.rest.pulls.create({
|
const pr = await octoRest.rest.pulls.create({
|
||||||
owner: repo.owner,
|
owner: repo.owner,
|
||||||
repo: repo.repo,
|
repo: repo.repo,
|
||||||
head: branch,
|
head: branch,
|
||||||
base,
|
base,
|
||||||
title,
|
title: truncatedTitle,
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
return pr.data.number
|
return pr.data.number
|
||||||
@@ -753,7 +771,9 @@ function footer(opts?: { image?: boolean }) {
|
|||||||
|
|
||||||
return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
|
return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
|
||||||
})()
|
})()
|
||||||
const shareUrl = shareId ? `[opencode session](${useShareUrl()}/s/${shareId}) | ` : ""
|
const shareUrl = shareId
|
||||||
|
? `[opencode session](${useShareUrl()}/s/${shareId}) | `
|
||||||
|
: ""
|
||||||
return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})`
|
return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,9 +956,13 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
|
|||||||
})
|
})
|
||||||
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
||||||
|
|
||||||
const files = (pr.files.nodes || []).map((f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
|
const files = (pr.files.nodes || []).map(
|
||||||
|
(f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`,
|
||||||
|
)
|
||||||
const reviewData = (pr.reviews.nodes || []).map((r) => {
|
const reviewData = (pr.reviews.nodes || []).map((r) => {
|
||||||
const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`)
|
const comments = (r.comments.nodes || []).map(
|
||||||
|
(c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`,
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
`- ${r.author.login} at ${r.submittedAt}:`,
|
`- ${r.author.login} at ${r.submittedAt}:`,
|
||||||
` - Review body: ${r.body}`,
|
` - Review body: ${r.body}`,
|
||||||
@@ -960,9 +984,15 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
|
|||||||
`Deletions: ${pr.deletions}`,
|
`Deletions: ${pr.deletions}`,
|
||||||
`Total Commits: ${pr.commits.totalCount}`,
|
`Total Commits: ${pr.commits.totalCount}`,
|
||||||
`Changed Files: ${pr.files.nodes.length} files`,
|
`Changed Files: ${pr.files.nodes.length} files`,
|
||||||
...(comments.length > 0 ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] : []),
|
...(comments.length > 0
|
||||||
...(files.length > 0 ? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"] : []),
|
? ["<pull_request_comments>", ...comments, "</pull_request_comments>"]
|
||||||
...(reviewData.length > 0 ? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"] : []),
|
: []),
|
||||||
|
...(files.length > 0
|
||||||
|
? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"]
|
||||||
|
: []),
|
||||||
|
...(reviewData.length > 0
|
||||||
|
? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"]
|
||||||
|
: []),
|
||||||
"</pull_request>",
|
"</pull_request>",
|
||||||
].join("\n")
|
].join("\n")
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.0",
|
"packageManager": "bun@1.3.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
||||||
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||||
"start": "vinxi start",
|
"start": "vinxi start",
|
||||||
"version": "1.0.2"
|
"version": "1.0.13"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,34 +1,64 @@
|
|||||||
import { For, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||||
import { Part } from "@opencode-ai/ui"
|
import { Markdown, Part } from "@opencode-ai/ui"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import type { AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
|
import type { AssistantMessage as AssistantMessageType, Part as PartType, ToolPart } from "@opencode-ai/sdk"
|
||||||
|
import { Spinner } from "./spinner"
|
||||||
|
|
||||||
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[] }) {
|
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const items = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
|
const parts = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
|
||||||
|
const done = createMemo(() => props.done ?? false)
|
||||||
|
const currentTask = createMemo(
|
||||||
|
() =>
|
||||||
|
parts().findLast(
|
||||||
|
(p) =>
|
||||||
|
p &&
|
||||||
|
p.type === "tool" &&
|
||||||
|
p.tool === "task" &&
|
||||||
|
p.state &&
|
||||||
|
"metadata" in p.state &&
|
||||||
|
p.state.metadata &&
|
||||||
|
p.state.metadata.sessionId &&
|
||||||
|
p.state.status === "running",
|
||||||
|
) as ToolPart,
|
||||||
|
)
|
||||||
|
|
||||||
const finishedItems = createMemo(() => [
|
const resolvedParts = createMemo(() => {
|
||||||
"",
|
let resolved = parts()
|
||||||
"",
|
const task = currentTask()
|
||||||
"Loading...",
|
if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
|
||||||
...items().filter(
|
const messages = sync.data.message[task.state.metadata.sessionId as string]?.filter((m) => m.role === "assistant")
|
||||||
(p) =>
|
resolved = messages?.flatMap((m) => sync.data.part[m.id]) ?? parts()
|
||||||
p?.type === "text" ||
|
}
|
||||||
(p?.type === "reasoning" && p.time?.end) ||
|
return resolved
|
||||||
(p?.type === "tool" && p.state.status === "completed"),
|
})
|
||||||
),
|
const currentText = createMemo(
|
||||||
"",
|
() =>
|
||||||
|
resolvedParts().findLast((p) => p?.type === "text")?.text ||
|
||||||
|
resolvedParts().findLast((p) => p?.type === "reasoning")?.text,
|
||||||
|
)
|
||||||
|
const eligibleItems = createMemo(() => {
|
||||||
|
return resolvedParts().filter((p) => p?.type === "tool" && p.state.status === "completed")
|
||||||
|
})
|
||||||
|
const finishedItems = createMemo<(JSXElement | PartType)[]>(() => [
|
||||||
|
<div class="h-8 w-full" />,
|
||||||
|
<div class="h-8 w-full" />,
|
||||||
|
<div class="flex items-center gap-x-5 pl-3 text-text-base">
|
||||||
|
<Spinner /> <span class="text-12-medium">Thinking...</span>
|
||||||
|
</div>,
|
||||||
|
...eligibleItems(),
|
||||||
|
...(done() ? [<div class="h-8 w-full" />, <div class="h-8 w-full" />, <div class="h-8 w-full" />] : []),
|
||||||
])
|
])
|
||||||
|
|
||||||
const MINIMUM_DELAY = 400
|
const delay = createMemo(() => (done() ? 220 : 400))
|
||||||
const [visibleCount, setVisibleCount] = createSignal(1)
|
const [visibleCount, setVisibleCount] = createSignal(eligibleItems().length)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const total = finishedItems().length
|
const total = finishedItems().length
|
||||||
if (total > visibleCount()) {
|
if (total > visibleCount()) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setVisibleCount((prev) => prev + 1)
|
setVisibleCount((prev) => prev + 1)
|
||||||
}, MINIMUM_DELAY)
|
}, delay())
|
||||||
onCleanup(() => clearTimeout(timer))
|
onCleanup(() => clearTimeout(timer))
|
||||||
} else if (total < visibleCount()) {
|
} else if (total < visibleCount()) {
|
||||||
setVisibleCount(total)
|
setVisibleCount(total)
|
||||||
@@ -42,41 +72,57 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="flex flex-col gap-3">
|
||||||
class="h-30 overflow-hidden pointer-events-none
|
|
||||||
mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
|
|
||||||
mask-b-from-90% mask-b-from-background-base mask-b-to-transparent"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-col items-start self-stretch gap-2 py-8
|
class="h-30 overflow-hidden pointer-events-none pb-1
|
||||||
transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
|
||||||
style={{ transform: `translateY(${translateY()})` }}
|
mask-b-from-95% mask-b-from-background-base mask-b-to-transparent"
|
||||||
>
|
>
|
||||||
<For each={finishedItems()}>
|
<div
|
||||||
{(part) => {
|
class="w-full flex flex-col items-start self-stretch gap-2 py-8
|
||||||
if (typeof part === "string") return <div class="h-8 flex items-center w-full">{part}</div>
|
transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID))
|
style={{ transform: `translateY(${translateY()})` }}
|
||||||
return (
|
>
|
||||||
<div class="h-8 flex items-center w-full">
|
<For each={finishedItems()}>
|
||||||
<Switch>
|
{(part) => {
|
||||||
<Match when={part.type === "text" && part}>
|
if (part && typeof part === "object" && "type" in part) {
|
||||||
{(p) => (
|
const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID))
|
||||||
<div
|
return (
|
||||||
textContent={p().text}
|
<div class="h-8 flex items-center w-full">
|
||||||
class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
|
<Switch>
|
||||||
/>
|
<Match when={part.type === "text" && part}>
|
||||||
)}
|
{(p) => (
|
||||||
</Match>
|
<div
|
||||||
<Match when={part.type === "reasoning" && part}>
|
textContent={p().text}
|
||||||
{(p) => <Part message={message()!} part={p()} />}
|
class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
|
||||||
</Match>
|
/>
|
||||||
<Match when={part.type === "tool" && part}>{(p) => <Part message={message()!} part={p()} />}</Match>
|
)}
|
||||||
</Switch>
|
</Match>
|
||||||
</div>
|
<Match when={part.type === "reasoning" && part}>
|
||||||
)
|
{(p) => <Part message={message()!} part={p()} />}
|
||||||
}}
|
</Match>
|
||||||
</For>
|
<Match when={part.type === "tool" && part}>
|
||||||
|
{(p) => <Part message={message()!} part={p()} />}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div class="h-8 flex items-center w-full">{part}</div>
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={currentText()}>
|
||||||
|
{(text) => (
|
||||||
|
<div
|
||||||
|
class="max-h-36 flex flex-col justify-end overflow-hidden py-3
|
||||||
|
mask-alpha mask-t-from-80% mask-t-from-background-base mask-t-to-transparent"
|
||||||
|
>
|
||||||
|
<Markdown text={text()} class="w-full shrink-0 overflow-visible" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
classList={{
|
classList={{
|
||||||
"bg-surface-raised-stronger-non-alpha border border-border-strong-base": true,
|
"bg-surface-raised-stronger-non-alpha border border-border-strong-base": true,
|
||||||
"rounded-2xl overflow-clip focus-within:shadow-xs-border-selected": true,
|
"rounded-2xl overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true,
|
||||||
[props.class ?? ""]: !!props.class,
|
[props.class ?? ""]: !!props.class,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -162,10 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
|
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
|
||||||
|
|
||||||
|
const cycle = (direction: 1 | -1) => {
|
||||||
|
const recentList = recent()
|
||||||
|
const current = currentModel()
|
||||||
|
if (!current) return
|
||||||
|
|
||||||
|
const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
|
||||||
|
if (index === -1) return
|
||||||
|
|
||||||
|
let next = index + direction
|
||||||
|
if (next < 0) next = recentList.length - 1
|
||||||
|
if (next >= recentList.length) next = 0
|
||||||
|
|
||||||
|
const val = recentList[next]
|
||||||
|
if (!val) return
|
||||||
|
|
||||||
|
model.set({
|
||||||
|
providerID: val.provider.id,
|
||||||
|
modelID: val.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
current: currentModel,
|
current: currentModel,
|
||||||
recent,
|
recent,
|
||||||
list,
|
list,
|
||||||
|
cycle,
|
||||||
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("model", agent.current().name, model ?? fallbackModel())
|
setStore("model", agent.current().name, model ?? fallbackModel())
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
Accordion,
|
Accordion,
|
||||||
Diff,
|
Diff,
|
||||||
Collapsible,
|
Collapsible,
|
||||||
Part,
|
|
||||||
DiffChanges,
|
DiffChanges,
|
||||||
ProgressCircle,
|
ProgressCircle,
|
||||||
Message,
|
Message,
|
||||||
@@ -548,7 +547,79 @@ export default function Page() {
|
|||||||
<For each={local.session.userMessages()}>
|
<For each={local.session.userMessages()}>
|
||||||
{(message) => {
|
{(message) => {
|
||||||
const diffs = createMemo(() => message.summary?.diffs ?? [])
|
const diffs = createMemo(() => message.summary?.diffs ?? [])
|
||||||
const working = createMemo(() => !message.summary?.title)
|
const working = createMemo(() => !message.summary?.body)
|
||||||
|
const assistantMessages = createMemo(() => {
|
||||||
|
return sync.data.message[activeSession().id]?.filter(
|
||||||
|
(m) => m.role === "assistant" && m.parentID == message.id,
|
||||||
|
) as AssistantMessageType[]
|
||||||
|
})
|
||||||
|
const parts = createMemo(() =>
|
||||||
|
assistantMessages().flatMap((m) => sync.data.part[m.id]),
|
||||||
|
)
|
||||||
|
const lastPart = createMemo(() => parts().slice(-1)?.at(0))
|
||||||
|
const rawStatus = createMemo(() => {
|
||||||
|
const defaultStatus = "Working..."
|
||||||
|
const last = lastPart()
|
||||||
|
if (!last) return defaultStatus
|
||||||
|
|
||||||
|
if (last.type === "tool") {
|
||||||
|
switch (last.tool) {
|
||||||
|
case "task":
|
||||||
|
return "Delegating work..."
|
||||||
|
case "todowrite":
|
||||||
|
case "todoread":
|
||||||
|
return "Planning next steps..."
|
||||||
|
case "read":
|
||||||
|
return "Gathering context..."
|
||||||
|
case "list":
|
||||||
|
case "grep":
|
||||||
|
case "glob":
|
||||||
|
return "Searching the codebase..."
|
||||||
|
case "webfetch":
|
||||||
|
return "Searching the web..."
|
||||||
|
case "edit":
|
||||||
|
case "write":
|
||||||
|
return "Making edits..."
|
||||||
|
case "bash":
|
||||||
|
return "Running commands..."
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (last.type === "reasoning") {
|
||||||
|
return "Thinking..."
|
||||||
|
} else if (last.type === "text") {
|
||||||
|
return "Gathering thoughts..."
|
||||||
|
}
|
||||||
|
return defaultStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const [status, setStatus] = createSignal(rawStatus())
|
||||||
|
let lastStatusChange = Date.now()
|
||||||
|
let statusTimeout: number | undefined
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const newStatus = rawStatus()
|
||||||
|
if (newStatus === status()) return
|
||||||
|
|
||||||
|
const timeSinceLastChange = Date.now() - lastStatusChange
|
||||||
|
|
||||||
|
if (timeSinceLastChange >= 1000) {
|
||||||
|
setStatus(newStatus)
|
||||||
|
lastStatusChange = Date.now()
|
||||||
|
if (statusTimeout) {
|
||||||
|
clearTimeout(statusTimeout)
|
||||||
|
statusTimeout = undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (statusTimeout) clearTimeout(statusTimeout)
|
||||||
|
statusTimeout = setTimeout(() => {
|
||||||
|
setStatus(rawStatus())
|
||||||
|
lastStatusChange = Date.now()
|
||||||
|
statusTimeout = undefined
|
||||||
|
}, 1000 - timeSinceLastChange) as unknown as number
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li class="group/li flex items-center self-stretch">
|
<li class="group/li flex items-center self-stretch">
|
||||||
<button
|
<button
|
||||||
@@ -570,7 +641,10 @@ export default function Page() {
|
|||||||
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
|
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{message.summary?.title ?? local.session.getMessageText(message)}
|
<Switch>
|
||||||
|
<Match when={working()}>{status()}</Match>
|
||||||
|
<Match when={true}>{message.summary?.title}</Match>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -583,7 +657,8 @@ export default function Page() {
|
|||||||
<For each={local.session.userMessages()}>
|
<For each={local.session.userMessages()}>
|
||||||
{(message) => {
|
{(message) => {
|
||||||
const isActive = createMemo(() => local.session.activeMessage()?.id === message.id)
|
const isActive = createMemo(() => local.session.activeMessage()?.id === message.id)
|
||||||
const [initialized, setInitialized] = createSignal(!!message.summary?.title)
|
const [titled, setTitled] = createSignal(!!message.summary?.title)
|
||||||
|
const [completed, setCompleted] = createSignal(!!message.summary?.body)
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const parts = createMemo(() => sync.data.part[message.id])
|
const parts = createMemo(() => sync.data.part[message.id])
|
||||||
const title = createMemo(() => message.summary?.title)
|
const title = createMemo(() => message.summary?.title)
|
||||||
@@ -597,11 +672,18 @@ export default function Page() {
|
|||||||
const hasToolPart = createMemo(() =>
|
const hasToolPart = createMemo(() =>
|
||||||
assistantMessages()
|
assistantMessages()
|
||||||
?.flatMap((m) => sync.data.part[m.id])
|
?.flatMap((m) => sync.data.part[m.id])
|
||||||
.some((p) => p.type === "tool"),
|
.some((p) => p?.type === "tool"),
|
||||||
)
|
)
|
||||||
const working = createMemo(() => !summary())
|
const working = createMemo(() => !summary())
|
||||||
|
|
||||||
|
// allowing time for the animations to finish
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setTimeout(() => setInitialized(!!title()), 10_000)
|
title()
|
||||||
|
setTimeout(() => setTitled(!!title()), 10_000)
|
||||||
|
})
|
||||||
|
createEffect(() => {
|
||||||
|
summary()
|
||||||
|
setTimeout(() => setCompleted(!!summary()), 1200)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -612,9 +694,18 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
|
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
|
||||||
<div class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
|
<div class="w-full text-14-medium text-text-strong">
|
||||||
<Show when={initialized()} fallback={<Typewriter as="h1" text={title()} />}>
|
<Show
|
||||||
<h1>{title()}</h1>
|
when={titled()}
|
||||||
|
fallback={
|
||||||
|
<Typewriter
|
||||||
|
as="h1"
|
||||||
|
text={title()}
|
||||||
|
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">{title()}</h1>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -622,7 +713,7 @@ export default function Page() {
|
|||||||
<Message message={message} parts={parts()} />
|
<Message message={message} parts={parts()} />
|
||||||
</div>
|
</div>
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<Show when={!working()}>
|
<Show when={completed()}>
|
||||||
<div class="w-full flex flex-col gap-6 items-start self-stretch">
|
<div class="w-full flex flex-col gap-6 items-start self-stretch">
|
||||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
<div class="flex flex-col items-start gap-1 self-stretch">
|
||||||
<h2 class="text-12-medium text-text-weak">
|
<h2 class="text-12-medium text-text-weak">
|
||||||
@@ -631,7 +722,14 @@ export default function Page() {
|
|||||||
<Match when={true}>Response</Match>
|
<Match when={true}>Response</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</h2>
|
</h2>
|
||||||
<Show when={summary()}>{(summary) => <Markdown text={summary()} />}</Show>
|
<Show when={summary()}>
|
||||||
|
{(summary) => (
|
||||||
|
<Markdown
|
||||||
|
classList={{ "[&>*]:fade-up-text": !diffs().length }}
|
||||||
|
text={summary()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Accordion class="w-full" multiple>
|
<Accordion class="w-full" multiple>
|
||||||
<For each={diffs()}>
|
<For each={diffs()}>
|
||||||
@@ -684,17 +782,17 @@ export default function Page() {
|
|||||||
{/* Response */}
|
{/* Response */}
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={working()}>
|
<Match when={!completed()}>
|
||||||
<MessageProgress assistantMessages={assistantMessages} />
|
<MessageProgress assistantMessages={assistantMessages} done={!working()} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!working() && hasToolPart()}>
|
<Match when={completed() && hasToolPart()}>
|
||||||
<Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
|
<Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
|
||||||
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
|
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
|
||||||
<div class="flex items-center gap-1 self-stretch">
|
<div class="flex items-center gap-1 self-stretch">
|
||||||
<div class="text-12-medium">
|
<div class="text-12-medium">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={expanded()}>Hide steps</Match>
|
<Match when={expanded()}>Hide details</Match>
|
||||||
<Match when={!expanded()}>Show steps</Match>
|
<Match when={!expanded()}>Show details</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<Collapsible.Arrow />
|
<Collapsible.Arrow />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -54,8 +54,8 @@
|
|||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opentui/core": "0.0.0-20251031-fc297165",
|
"@opentui/core": "0.1.32",
|
||||||
"@opentui/solid": "0.0.0-20251031-fc297165",
|
"@opentui/solid": "0.1.32",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
"hono-openapi": "1.0.7",
|
"hono-openapi": "1.1.1",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import { Storage } from "@/storage/storage"
|
|||||||
import { Command } from "@/command"
|
import { Command } from "@/command"
|
||||||
import { Agent as Agents } from "@/agent/agent"
|
import { Agent as Agents } from "@/agent/agent"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Session } from "@/session"
|
|
||||||
import { Identifier } from "@/id/id"
|
|
||||||
import { SessionCompaction } from "@/session/compaction"
|
import { SessionCompaction } from "@/session/compaction"
|
||||||
import type { Config } from "@/config/config"
|
import type { Config } from "@/config/config"
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
@@ -89,7 +87,11 @@ export namespace ACP {
|
|||||||
})
|
})
|
||||||
if (!res) return
|
if (!res) return
|
||||||
if (res.outcome.outcome !== "selected") {
|
if (res.outcome.outcome !== "selected") {
|
||||||
Permission.respond({ sessionID: permission.sessionID, permissionID: permission.id, response: "reject" })
|
Permission.respond({
|
||||||
|
sessionID: permission.sessionID,
|
||||||
|
permissionID: permission.id,
|
||||||
|
response: "reject",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Permission.respond({
|
Permission.respond({
|
||||||
@@ -111,9 +113,11 @@ export namespace ACP {
|
|||||||
const acpSession = this.sessionManager.get(part.sessionID)
|
const acpSession = this.sessionManager.get(part.sessionID)
|
||||||
if (!acpSession) return
|
if (!acpSession) return
|
||||||
|
|
||||||
const message = await Storage.read<MessageV2.Info>(["message", part.sessionID, part.messageID]).catch(
|
const message = await Storage.read<MessageV2.Info>([
|
||||||
() => undefined,
|
"message",
|
||||||
)
|
part.sessionID,
|
||||||
|
part.messageID,
|
||||||
|
]).catch(() => undefined)
|
||||||
if (!message || message.role !== "assistant") return
|
if (!message || message.role !== "assistant") return
|
||||||
|
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
@@ -192,7 +196,9 @@ export namespace ACP {
|
|||||||
sessionUpdate: "plan",
|
sessionUpdate: "plan",
|
||||||
entries: parsedTodos.data.map((todo) => {
|
entries: parsedTodos.data.map((todo) => {
|
||||||
const status: PlanEntry["status"] =
|
const status: PlanEntry["status"] =
|
||||||
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
|
todo.status === "cancelled"
|
||||||
|
? "completed"
|
||||||
|
: (todo.status as PlanEntry["status"])
|
||||||
return {
|
return {
|
||||||
priority: "medium",
|
priority: "medium",
|
||||||
status,
|
status,
|
||||||
@@ -375,11 +381,6 @@ export namespace ACP {
|
|||||||
description: command.description ?? "",
|
description: command.description ?? "",
|
||||||
}))
|
}))
|
||||||
const names = new Set(availableCommands.map((c) => c.name))
|
const names = new Set(availableCommands.map((c) => c.name))
|
||||||
if (!names.has("init"))
|
|
||||||
availableCommands.push({
|
|
||||||
name: "init",
|
|
||||||
description: "create/update a AGENTS.md",
|
|
||||||
})
|
|
||||||
if (!names.has("compact"))
|
if (!names.has("compact"))
|
||||||
availableCommands.push({
|
availableCommands.push({
|
||||||
name: "compact",
|
name: "compact",
|
||||||
@@ -404,7 +405,8 @@ export namespace ACP {
|
|||||||
description: agent.description,
|
description: agent.description,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
|
const currentModeId =
|
||||||
|
availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
|
||||||
|
|
||||||
const mcpServers: Record<string, Config.Mcp> = {}
|
const mcpServers: Record<string, Config.Mcp> = {}
|
||||||
for (const server of params.mcpServers) {
|
for (const server of params.mcpServers) {
|
||||||
@@ -585,14 +587,6 @@ export namespace ACP {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (cmd.name) {
|
switch (cmd.name) {
|
||||||
case "init":
|
|
||||||
await Session.initialize({
|
|
||||||
sessionID,
|
|
||||||
messageID: Identifier.ascending("message"),
|
|
||||||
providerID: model.providerID,
|
|
||||||
modelID: model.modelID,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case "compact":
|
case "compact":
|
||||||
await SessionCompaction.run({
|
await SessionCompaction.run({
|
||||||
sessionID,
|
sessionID,
|
||||||
@@ -665,7 +659,9 @@ export namespace ACP {
|
|||||||
|
|
||||||
function parseUri(
|
function parseUri(
|
||||||
uri: string,
|
uri: string,
|
||||||
): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
|
):
|
||||||
|
| { type: "file"; url: string; filename: string; mime: string }
|
||||||
|
| { type: "text"; text: string } {
|
||||||
try {
|
try {
|
||||||
if (uri.startsWith("file://")) {
|
if (uri.startsWith("file://")) {
|
||||||
const path = uri.slice(7)
|
const path = uri.slice(7)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const ServeCommand = cmd({
|
|||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
.option("hostname", {
|
.option("hostname", {
|
||||||
alias: ["h"],
|
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "hostname to listen on",
|
describe: "hostname to listen on",
|
||||||
default: "127.0.0.1",
|
default: "127.0.0.1",
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ export const TuiCommand = cmd({
|
|||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
.option("hostname", {
|
.option("hostname", {
|
||||||
alias: ["h"],
|
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "hostname to listen on",
|
describe: "hostname to listen on",
|
||||||
default: "127.0.0.1",
|
default: "127.0.0.1",
|
||||||
|
|||||||
@@ -23,21 +23,81 @@ import { Session } from "@tui/routes/session"
|
|||||||
import { PromptHistoryProvider } from "./component/prompt/history"
|
import { PromptHistoryProvider } from "./component/prompt/history"
|
||||||
import { DialogAlert } from "./ui/dialog-alert"
|
import { DialogAlert } from "./ui/dialog-alert"
|
||||||
import { ToastProvider, useToast } from "./ui/toast"
|
import { ToastProvider, useToast } from "./ui/toast"
|
||||||
import { ExitProvider } from "./context/exit"
|
import { ExitProvider, useExit } from "./context/exit"
|
||||||
import type { SessionRoute } from "./context/route"
|
import type { SessionRoute } from "./context/route"
|
||||||
import { Session as SessionApi } from "@/session"
|
import { Session as SessionApi } from "@/session"
|
||||||
import { TuiEvent } from "./event"
|
import { TuiEvent } from "./event"
|
||||||
import { KVProvider, useKV } from "./context/kv"
|
import { KVProvider, useKV } from "./context/kv"
|
||||||
|
|
||||||
|
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let timeout: NodeJS.Timeout
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
process.stdin.setRawMode(false)
|
||||||
|
process.stdin.removeListener("data", handler)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = (data: Buffer) => {
|
||||||
|
const str = data.toString()
|
||||||
|
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
|
||||||
|
if (match) {
|
||||||
|
cleanup()
|
||||||
|
const color = match[1]
|
||||||
|
// Parse RGB values from color string
|
||||||
|
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0
|
||||||
|
|
||||||
|
if (color.startsWith("rgb:")) {
|
||||||
|
const parts = color.substring(4).split("/")
|
||||||
|
r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
} else if (color.startsWith("#")) {
|
||||||
|
r = parseInt(color.substring(1, 3), 16)
|
||||||
|
g = parseInt(color.substring(3, 5), 16)
|
||||||
|
b = parseInt(color.substring(5, 7), 16)
|
||||||
|
} else if (color.startsWith("rgb(")) {
|
||||||
|
const parts = color.substring(4, color.length - 1).split(",")
|
||||||
|
r = parseInt(parts[0])
|
||||||
|
g = parseInt(parts[1])
|
||||||
|
b = parseInt(parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate luminance using relative luminance formula
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||||
|
|
||||||
|
// Determine if dark or light based on luminance threshold
|
||||||
|
resolve(luminance > 0.5 ? "light" : "dark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true)
|
||||||
|
process.stdin.on("data", handler)
|
||||||
|
process.stdout.write("\x1b]11;?\x07")
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
cleanup()
|
||||||
|
resolve("dark")
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function tui(input: {
|
export function tui(input: {
|
||||||
url: string
|
url: string
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
model?: string
|
model?: string
|
||||||
agent?: string
|
agent?: string
|
||||||
|
prompt?: string
|
||||||
onExit?: () => Promise<void>
|
onExit?: () => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
// promise to prevent immediate exit
|
// promise to prevent immediate exit
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>(async (resolve) => {
|
||||||
|
const mode = await getTerminalBackgroundColor()
|
||||||
|
|
||||||
const routeData: Route | undefined = input.sessionID
|
const routeData: Route | undefined = input.sessionID
|
||||||
? {
|
? {
|
||||||
type: "session",
|
type: "session",
|
||||||
@@ -53,15 +113,23 @@ export function tui(input: {
|
|||||||
render(
|
render(
|
||||||
() => {
|
() => {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary fallback={<text>Something went wrong</text>}>
|
<ErrorBoundary
|
||||||
|
fallback={(error, reset) => (
|
||||||
|
<ErrorComponent error={error} reset={reset} onExit={onExit} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
<ExitProvider onExit={onExit}>
|
<ExitProvider onExit={onExit}>
|
||||||
<KVProvider>
|
<KVProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<RouteProvider data={routeData}>
|
<RouteProvider data={routeData}>
|
||||||
<SDKProvider url={input.url}>
|
<SDKProvider url={input.url}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider mode={mode}>
|
||||||
<LocalProvider initialModel={input.model} initialAgent={input.agent}>
|
<LocalProvider
|
||||||
|
initialModel={input.model}
|
||||||
|
initialAgent={input.agent}
|
||||||
|
initialPrompt={input.prompt}
|
||||||
|
>
|
||||||
<KeybindProvider>
|
<KeybindProvider>
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<CommandProvider>
|
<CommandProvider>
|
||||||
@@ -86,6 +154,7 @@ export function tui(input: {
|
|||||||
targetFps: 60,
|
targetFps: 60,
|
||||||
gatherStats: false,
|
gatherStats: false,
|
||||||
exitOnCtrlC: false,
|
exitOnCtrlC: false,
|
||||||
|
useKittyKeyboard: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -104,7 +173,8 @@ function App() {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const [sessionExists, setSessionExists] = createSignal(false)
|
const [sessionExists, setSessionExists] = createSignal(false)
|
||||||
const { theme } = useTheme()
|
const { theme, mode, setMode } = useTheme()
|
||||||
|
const exit = useExit()
|
||||||
|
|
||||||
useKeyboard(async (evt) => {
|
useKeyboard(async (evt) => {
|
||||||
if (evt.meta && evt.name === "t") {
|
if (evt.meta && evt.name === "t") {
|
||||||
@@ -168,6 +238,24 @@ function App() {
|
|||||||
dialog.replace(() => <DialogModel />)
|
dialog.replace(() => <DialogModel />)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Model cycle",
|
||||||
|
value: "model.cycle_recent",
|
||||||
|
keybind: "model_cycle_recent",
|
||||||
|
category: "Agent",
|
||||||
|
onSelect: () => {
|
||||||
|
local.model.cycle(1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Model cycle reverse",
|
||||||
|
value: "model.cycle_recent_reverse",
|
||||||
|
keybind: "model_cycle_recent_reverse",
|
||||||
|
category: "Agent",
|
||||||
|
onSelect: () => {
|
||||||
|
local.model.cycle(-1)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Switch agent",
|
title: "Switch agent",
|
||||||
value: "agent.list",
|
value: "agent.list",
|
||||||
@@ -214,6 +302,14 @@ function App() {
|
|||||||
},
|
},
|
||||||
category: "System",
|
category: "System",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
|
||||||
|
value: "theme.switch_mode",
|
||||||
|
onSelect: () => {
|
||||||
|
setMode(mode() === "dark" ? "light" : "dark")
|
||||||
|
},
|
||||||
|
category: "System",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Help",
|
title: "Help",
|
||||||
value: "help.show",
|
value: "help.show",
|
||||||
@@ -222,11 +318,17 @@ function App() {
|
|||||||
},
|
},
|
||||||
category: "System",
|
category: "System",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Exit the app",
|
||||||
|
value: "app.exit",
|
||||||
|
onSelect: exit,
|
||||||
|
category: "System",
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const providerID = local.model.current().providerID
|
const providerID = local.model.current().providerID
|
||||||
if (providerID === "openrouter" && !kv.data.openrouter_warning) {
|
if (providerID === "openrouter" && !kv.get("openrouter_warning", false)) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
DialogAlert.show(
|
DialogAlert.show(
|
||||||
dialog,
|
dialog,
|
||||||
@@ -274,8 +376,9 @@ function App() {
|
|||||||
/* @ts-expect-error */
|
/* @ts-expect-error */
|
||||||
renderer.writeOut(finalOsc52)
|
renderer.writeOut(finalOsc52)
|
||||||
await Clipboard.copy(text)
|
await Clipboard.copy(text)
|
||||||
|
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||||
|
.catch(toast.error)
|
||||||
renderer.clearSelection()
|
renderer.clearSelection()
|
||||||
toast.show({ message: "Copied to clipboard", variant: "info" })
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -304,7 +407,9 @@ function App() {
|
|||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
>
|
>
|
||||||
<text fg={theme.textMuted}>open</text>
|
<text fg={theme.textMuted}>open</text>
|
||||||
<text attributes={TextAttributes.BOLD}>code </text>
|
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||||
|
code{" "}
|
||||||
|
</text>
|
||||||
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
||||||
</box>
|
</box>
|
||||||
<box paddingLeft={1} paddingRight={1}>
|
<box paddingLeft={1} paddingRight={1}>
|
||||||
@@ -319,7 +424,7 @@ function App() {
|
|||||||
<text
|
<text
|
||||||
bg={local.agent.color(local.agent.current().name)}
|
bg={local.agent.color(local.agent.current().name)}
|
||||||
fg={theme.background}
|
fg={theme.background}
|
||||||
wrapMode="none"
|
wrapMode={undefined}
|
||||||
>
|
>
|
||||||
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
||||||
<span> AGENT </span>
|
<span> AGENT </span>
|
||||||
@@ -329,3 +434,57 @@ function App() {
|
|||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise<void> }) {
|
||||||
|
const term = useTerminalDimensions()
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (evt.ctrl && evt.name === "c") {
|
||||||
|
props.onExit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const [copied, setCopied] = createSignal(false)
|
||||||
|
|
||||||
|
const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
|
||||||
|
|
||||||
|
if (props.error.message) {
|
||||||
|
issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.error.stack) {
|
||||||
|
issueURL.searchParams.set(
|
||||||
|
"description",
|
||||||
|
"```\n" + props.error.stack.substring(0, 6000 - issueURL.toString().length) + "...\n```",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyIssueURL = () => {
|
||||||
|
Clipboard.copy(issueURL.toString()).then(() => {
|
||||||
|
setCopied(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" gap={1}>
|
||||||
|
<box flexDirection="row" gap={1} alignItems="center">
|
||||||
|
<text attributes={TextAttributes.BOLD}>Please report an issue.</text>
|
||||||
|
<box onMouseUp={copyIssueURL} backgroundColor="#565f89" padding={1}>
|
||||||
|
<text attributes={TextAttributes.BOLD}>Copy issue URL (exception info pre-filled)</text>
|
||||||
|
</box>
|
||||||
|
{copied() && <text>Successfully copied</text>}
|
||||||
|
</box>
|
||||||
|
<box flexDirection="row" gap={2} alignItems="center">
|
||||||
|
<text>A fatal error occurred!</text>
|
||||||
|
<box onMouseUp={props.reset} backgroundColor="#565f89" padding={1}>
|
||||||
|
<text>Reset TUI</text>
|
||||||
|
</box>
|
||||||
|
<box onMouseUp={props.onExit} backgroundColor="#565f89" padding={1}>
|
||||||
|
<text>Exit</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<scrollbox height={Math.floor(term().height * 0.7)}>
|
||||||
|
<text>{props.error.stack}</text>
|
||||||
|
</scrollbox>
|
||||||
|
<text>{props.error.message}</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ function init() {
|
|||||||
return registrations().flatMap((x) => x())
|
return registrations().flatMap((x) => x())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let keybinds = true
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
|
if (!keybinds) return
|
||||||
for (const option of options()) {
|
for (const option of options()) {
|
||||||
if (option.keybind && keybind.match(option.keybind, evt)) {
|
if (option.keybind && keybind.match(option.keybind, evt)) {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
@@ -39,14 +41,20 @@ function init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
trigger(name: string) {
|
trigger(name: string, source?: "prompt") {
|
||||||
for (const option of options()) {
|
for (const option of options()) {
|
||||||
if (option.value === name) {
|
if (option.value === name) {
|
||||||
option.onSelect?.(dialog)
|
option.onSelect?.(dialog, source)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
keybinds(enabled: boolean) {
|
||||||
|
keybinds = enabled
|
||||||
|
},
|
||||||
|
show() {
|
||||||
|
dialog.replace(() => <DialogCommand options={options()} />)
|
||||||
|
},
|
||||||
register(cb: () => CommandOption[]) {
|
register(cb: () => CommandOption[]) {
|
||||||
const results = createMemo(cb)
|
const results = createMemo(cb)
|
||||||
setRegistrations((arr) => [results, ...arr])
|
setRegistrations((arr) => [results, ...arr])
|
||||||
@@ -75,7 +83,7 @@ export function CommandProvider(props: ParentProps) {
|
|||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
|
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
if (keybind.match("command_list", evt)) {
|
if (keybind.match("command_list", evt) && dialog.stack.length === 0) {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
dialog.replace(() => <DialogCommand options={value.options} />)
|
dialog.replace(() => <DialogCommand options={value.options} />)
|
||||||
return
|
return
|
||||||
@@ -90,7 +98,10 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
|||||||
return (
|
return (
|
||||||
<DialogSelect
|
<DialogSelect
|
||||||
title="Commands"
|
title="Commands"
|
||||||
options={props.options.map((x) => ({ ...x, footer: x.keybind ? keybind.print(x.keybind) : undefined }))}
|
options={props.options.map((x) => ({
|
||||||
|
...x,
|
||||||
|
footer: x.keybind ? keybind.print(x.keybind) : undefined,
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Locale } from "@/util/locale"
|
|||||||
import { Keybind } from "@/util/keybind"
|
import { Keybind } from "@/util/keybind"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
|
import { DialogSessionRename } from "./dialog-session-rename"
|
||||||
|
|
||||||
export function DialogSessionList() {
|
export function DialogSessionList() {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -59,7 +60,7 @@ export function DialogSessionList() {
|
|||||||
}}
|
}}
|
||||||
keybind={[
|
keybind={[
|
||||||
{
|
{
|
||||||
keybind: Keybind.parse("delete")[0],
|
keybind: Keybind.parse("ctrl+d")[0],
|
||||||
title: "delete",
|
title: "delete",
|
||||||
onTrigger: async (option) => {
|
onTrigger: async (option) => {
|
||||||
if (toDelete() === option.value) {
|
if (toDelete() === option.value) {
|
||||||
@@ -74,6 +75,13 @@ export function DialogSessionList() {
|
|||||||
setToDelete(option.value)
|
setToDelete(option.value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
keybind: Keybind.parse("ctrl+r")[0],
|
||||||
|
title: "rename",
|
||||||
|
onTrigger: async (option) => {
|
||||||
|
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { DialogPrompt } from "@tui/ui/dialog-prompt"
|
||||||
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
|
import { useSync } from "@tui/context/sync"
|
||||||
|
import { createMemo } from "solid-js"
|
||||||
|
import { useSDK } from "../context/sdk"
|
||||||
|
|
||||||
|
interface DialogSessionRenameProps {
|
||||||
|
session: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogSessionRename(props: DialogSessionRenameProps) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const sync = useSync()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const session = createMemo(() => sync.session.get(props.session))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogPrompt
|
||||||
|
title="Rename Session"
|
||||||
|
value={session()?.title}
|
||||||
|
onConfirm={(value) => {
|
||||||
|
sdk.client.session.update({
|
||||||
|
path: {
|
||||||
|
id: props.session,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
title: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
dialog.clear()
|
||||||
|
}}
|
||||||
|
onCancel={() => dialog.clear()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { For, Match, Switch, Show } from "solid-js"
|
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
||||||
|
|
||||||
export type DialogStatusProps = {}
|
export type DialogStatusProps = {}
|
||||||
|
|
||||||
@@ -9,15 +9,19 @@ export function DialogStatus() {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD}>Status</text>
|
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||||
|
Status
|
||||||
|
</text>
|
||||||
<text fg={theme.textMuted}>esc</text>
|
<text fg={theme.textMuted}>esc</text>
|
||||||
</box>
|
</box>
|
||||||
<Show when={Object.keys(sync.data.mcp).length > 0}>
|
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
|
||||||
<box>
|
<box>
|
||||||
<text>{Object.keys(sync.data.mcp).length} MCP Servers</text>
|
<text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
|
||||||
<For each={Object.entries(sync.data.mcp)}>
|
<For each={Object.entries(sync.data.mcp)}>
|
||||||
{([key, item]) => (
|
{([key, item]) => (
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
@@ -33,7 +37,7 @@ export function DialogStatus() {
|
|||||||
>
|
>
|
||||||
•
|
•
|
||||||
</text>
|
</text>
|
||||||
<text wrapMode="word">
|
<text fg={theme.text} wrapMode="word">
|
||||||
<b>{key}</b>{" "}
|
<b>{key}</b>{" "}
|
||||||
<span style={{ fg: theme.textMuted }}>
|
<span style={{ fg: theme.textMuted }}>
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -50,7 +54,7 @@ export function DialogStatus() {
|
|||||||
</Show>
|
</Show>
|
||||||
{sync.data.lsp.length > 0 && (
|
{sync.data.lsp.length > 0 && (
|
||||||
<box>
|
<box>
|
||||||
<text>{sync.data.lsp.length} LSP Servers</text>
|
<text fg={theme.text}>{sync.data.lsp.length} LSP Servers</text>
|
||||||
<For each={sync.data.lsp}>
|
<For each={sync.data.lsp}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
@@ -65,7 +69,7 @@ export function DialogStatus() {
|
|||||||
>
|
>
|
||||||
•
|
•
|
||||||
</text>
|
</text>
|
||||||
<text wrapMode="word">
|
<text fg={theme.text} wrapMode="word">
|
||||||
<b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
|
<b>{item.id}</b> <span style={{ fg: theme.textMuted }}>{item.root}</span>
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -73,6 +77,31 @@ export function DialogStatus() {
|
|||||||
</For>
|
</For>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
|
<Show
|
||||||
|
when={enabledFormatters().length > 0}
|
||||||
|
fallback={<text fg={theme.text}>No Formatters</text>}
|
||||||
|
>
|
||||||
|
<box>
|
||||||
|
<text fg={theme.text}>{enabledFormatters().length} Formatters</text>
|
||||||
|
<For each={enabledFormatters()}>
|
||||||
|
{(item) => (
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<text
|
||||||
|
flexShrink={0}
|
||||||
|
style={{
|
||||||
|
fg: theme.success,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</text>
|
||||||
|
<text wrapMode="word" fg={theme.text}>
|
||||||
|
<b>{item.name}</b>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ export function DialogThemeList() {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
let confirmed = false
|
let confirmed = false
|
||||||
let ref: DialogSelectRef<keyof typeof THEMES>
|
let ref: DialogSelectRef<keyof typeof THEMES>
|
||||||
const initial = theme.selectedTheme
|
const initial = theme.selected
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// highlight the first theme in the list when we open it for UX
|
// highlight the first theme in the list when we open it for UX
|
||||||
theme.setSelectedTheme(Object.keys(THEMES)[0] as keyof typeof THEMES)
|
theme.set(Object.keys(THEMES)[0] as keyof typeof THEMES)
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
// if we close the dialog without confirming, reset back to the initial theme
|
// if we close the dialog without confirming, reset back to the initial theme
|
||||||
if (!confirmed) theme.setSelectedTheme(initial)
|
if (!confirmed) theme.set(initial)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,10 +29,10 @@ export function DialogThemeList() {
|
|||||||
title="Themes"
|
title="Themes"
|
||||||
options={options}
|
options={options}
|
||||||
onMove={(opt) => {
|
onMove={(opt) => {
|
||||||
theme.setSelectedTheme(opt.value)
|
theme.set(opt.value)
|
||||||
}}
|
}}
|
||||||
onSelect={(opt) => {
|
onSelect={(opt) => {
|
||||||
theme.setSelectedTheme(opt.value)
|
theme.set(opt.value)
|
||||||
confirmed = true
|
confirmed = true
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
}}
|
}}
|
||||||
@@ -40,12 +41,12 @@ export function DialogThemeList() {
|
|||||||
}}
|
}}
|
||||||
onFilter={(query) => {
|
onFilter={(query) => {
|
||||||
if (query.length === 0) {
|
if (query.length === 0) {
|
||||||
theme.setSelectedTheme(initial)
|
theme.set(initial)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = ref.filtered[0]
|
const first = ref.filtered[0]
|
||||||
if (first) theme.setSelectedTheme(first.value)
|
if (first) theme.set(first.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type AutocompleteRef = {
|
|||||||
|
|
||||||
export type AutocompleteOption = {
|
export type AutocompleteOption = {
|
||||||
display: string
|
display: string
|
||||||
|
aliases?: string[]
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
description?: string
|
description?: string
|
||||||
onSelect?: () => void
|
onSelect?: () => void
|
||||||
@@ -207,6 +208,7 @@ export function Autocomplete(props: {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: "/compact",
|
display: "/compact",
|
||||||
|
aliases: ["/summarize"],
|
||||||
description: "compact the session",
|
description: "compact the session",
|
||||||
onSelect: () => command.trigger("session.compact"),
|
onSelect: () => command.trigger("session.compact"),
|
||||||
},
|
},
|
||||||
@@ -222,11 +224,22 @@ export function Autocomplete(props: {
|
|||||||
description: "unshare a session",
|
description: "unshare a session",
|
||||||
onSelect: () => command.trigger("session.unshare"),
|
onSelect: () => command.trigger("session.unshare"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
display: "/rename",
|
||||||
|
description: "rename session",
|
||||||
|
onSelect: () => command.trigger("session.rename"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: "/timeline",
|
||||||
|
description: "jump to message",
|
||||||
|
onSelect: () => command.trigger("session.timeline"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
results.push(
|
results.push(
|
||||||
{
|
{
|
||||||
display: "/new",
|
display: "/new",
|
||||||
|
aliases: ["/clear"],
|
||||||
description: "create a new session",
|
description: "create a new session",
|
||||||
onSelect: () => command.trigger("session.new"),
|
onSelect: () => command.trigger("session.new"),
|
||||||
},
|
},
|
||||||
@@ -242,6 +255,7 @@ export function Autocomplete(props: {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: "/session",
|
display: "/session",
|
||||||
|
aliases: ["/resume", "/continue"],
|
||||||
description: "list sessions",
|
description: "list sessions",
|
||||||
onSelect: () => command.trigger("session.list"),
|
onSelect: () => command.trigger("session.list"),
|
||||||
},
|
},
|
||||||
@@ -250,11 +264,32 @@ export function Autocomplete(props: {
|
|||||||
description: "show status",
|
description: "show status",
|
||||||
onSelect: () => command.trigger("opencode.status"),
|
onSelect: () => command.trigger("opencode.status"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
display: "/theme",
|
||||||
|
description: "toggle theme",
|
||||||
|
onSelect: () => command.trigger("theme.switch"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: "/editor",
|
||||||
|
description: "open editor",
|
||||||
|
onSelect: () => command.trigger("prompt.editor", "prompt"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
display: "/help",
|
display: "/help",
|
||||||
description: "show help",
|
description: "show help",
|
||||||
onSelect: () => command.trigger("help.show"),
|
onSelect: () => command.trigger("help.show"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
display: "/commands",
|
||||||
|
description: "show all commands",
|
||||||
|
onSelect: () => command.show(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: "/exit",
|
||||||
|
aliases: ["/quit", "/q"],
|
||||||
|
description: "exit the app",
|
||||||
|
onSelect: () => command.trigger("app.exit"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
||||||
if (!max) return results
|
if (!max) return results
|
||||||
@@ -273,7 +308,7 @@ export function Autocomplete(props: {
|
|||||||
const currentFilter = filter()
|
const currentFilter = filter()
|
||||||
if (!currentFilter) return mixed.slice(0, 10)
|
if (!currentFilter) return mixed.slice(0, 10)
|
||||||
const result = fuzzysort.go(currentFilter, mixed, {
|
const result = fuzzysort.go(currentFilter, mixed, {
|
||||||
keys: ["display", "description"],
|
keys: ["display", "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
})
|
})
|
||||||
return result.map((arr) => arr.obj)
|
return result.map((arr) => arr.obj)
|
||||||
@@ -301,6 +336,7 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show(mode: "@" | "/") {
|
function show(mode: "@" | "/") {
|
||||||
|
command.keybinds(false)
|
||||||
setStore({
|
setStore({
|
||||||
visible: mode,
|
visible: mode,
|
||||||
index: props.input().visualCursor.offset,
|
index: props.input().visualCursor.offset,
|
||||||
@@ -318,6 +354,7 @@ export function Autocomplete(props: {
|
|||||||
const cursor = props.input().logicalCursor
|
const cursor = props.input().logicalCursor
|
||||||
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
||||||
}
|
}
|
||||||
|
command.keybinds(true)
|
||||||
setStore("visible", false)
|
setStore("visible", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
dim,
|
dim,
|
||||||
fg,
|
fg,
|
||||||
} from "@opentui/core"
|
} from "@opentui/core"
|
||||||
import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js"
|
import { createEffect, createMemo, Match, Switch, type JSX, onMount, batch } from "solid-js"
|
||||||
import { useLocal } from "@tui/context/local"
|
import { useLocal } from "@tui/context/local"
|
||||||
import { SyntaxTheme, useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
import { SplitBorder } from "@tui/component/border"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
@@ -60,7 +60,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const history = usePromptHistory()
|
const history = usePromptHistory()
|
||||||
const command = useCommandDialog()
|
const command = useCommandDialog()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const { theme } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
|
|
||||||
const textareaKeybindings = createMemo(() => {
|
const textareaKeybindings = createMemo(() => {
|
||||||
const newlineBindings = keybind.all.input_newline || []
|
const newlineBindings = keybind.all.input_newline || []
|
||||||
@@ -86,9 +86,9 @@ export function Prompt(props: PromptProps) {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileStyleId = SyntaxTheme.getStyleId("extmark.file")!
|
const fileStyleId = syntax().getStyleId("extmark.file")!
|
||||||
const agentStyleId = SyntaxTheme.getStyleId("extmark.agent")!
|
const agentStyleId = syntax().getStyleId("extmark.agent")!
|
||||||
const pasteStyleId = SyntaxTheme.getStyleId("extmark.paste")!
|
const pasteStyleId = syntax().getStyleId("extmark.paste")!
|
||||||
let promptPartTypeId: number
|
let promptPartTypeId: number
|
||||||
|
|
||||||
command.register(() => {
|
command.register(() => {
|
||||||
@@ -98,14 +98,9 @@ export function Prompt(props: PromptProps) {
|
|||||||
category: "Session",
|
category: "Session",
|
||||||
keybind: "editor_open",
|
keybind: "editor_open",
|
||||||
value: "prompt.editor",
|
value: "prompt.editor",
|
||||||
onSelect: async (dialog) => {
|
onSelect: async (dialog, trigger) => {
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
const value = input.plainText
|
const value = trigger === "prompt" ? "" : input.plainText
|
||||||
input.clear()
|
|
||||||
setStore("prompt", {
|
|
||||||
input: "",
|
|
||||||
parts: [],
|
|
||||||
})
|
|
||||||
const content = await Editor.open({ value, renderer })
|
const content = await Editor.open({ value, renderer })
|
||||||
if (content) {
|
if (content) {
|
||||||
input.setText(content, { history: false })
|
input.setText(content, { history: false })
|
||||||
@@ -194,6 +189,16 @@ export function Prompt(props: PromptProps) {
|
|||||||
input.focus()
|
input.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
local.setInitialPrompt.listen((initialPrompt) => {
|
||||||
|
batch(() => {
|
||||||
|
setStore("prompt", {
|
||||||
|
input: initialPrompt,
|
||||||
|
parts: [],
|
||||||
|
})
|
||||||
|
input.insertText(initialPrompt)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
promptPartTypeId = input.extmarks.registerType("prompt-part")
|
promptPartTypeId = input.extmarks.registerType("prompt-part")
|
||||||
})
|
})
|
||||||
@@ -530,6 +535,17 @@ export function Prompt(props: PromptProps) {
|
|||||||
setStore("extmarkToPartIndex", new Map())
|
setStore("extmarkToPartIndex", new Map())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (keybind.match("input_forward_delete", e) && store.prompt.input !== "") {
|
||||||
|
const cursorOffset = input.cursorOffset
|
||||||
|
if (cursorOffset < input.plainText.length) {
|
||||||
|
const text = input.plainText
|
||||||
|
const newText = text.slice(0, cursorOffset) + text.slice(cursorOffset + 1)
|
||||||
|
input.setText(newText)
|
||||||
|
input.cursorOffset = cursorOffset
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (keybind.match("app_exit", e)) {
|
if (keybind.match("app_exit", e)) {
|
||||||
await exit()
|
await exit()
|
||||||
return
|
return
|
||||||
@@ -552,10 +568,11 @@ export function Prompt(props: PromptProps) {
|
|||||||
if (store.mode === "normal") autocomplete.onKeyDown(e)
|
if (store.mode === "normal") autocomplete.onKeyDown(e)
|
||||||
if (!autocomplete.visible) {
|
if (!autocomplete.visible) {
|
||||||
if (
|
if (
|
||||||
(e.name === "up" && input.cursorOffset === 0) ||
|
(keybind.match("history_previous", e) && input.cursorOffset === 0) ||
|
||||||
(e.name === "down" && input.cursorOffset === input.plainText.length)
|
(keybind.match("history_next", e) &&
|
||||||
|
input.cursorOffset === input.plainText.length)
|
||||||
) {
|
) {
|
||||||
const direction = e.name === "up" ? -1 : 1
|
const direction = keybind.match("history_previous", e) ? -1 : 1
|
||||||
const item = history.move(direction, input.plainText)
|
const item = history.move(direction, input.plainText)
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
@@ -599,14 +616,16 @@ export function Prompt(props: PromptProps) {
|
|||||||
|
|
||||||
// trim ' from the beginning and end of the pasted content. just
|
// trim ' from the beginning and end of the pasted content. just
|
||||||
// ' and nothing else
|
// ' and nothing else
|
||||||
const filepath = pastedContent.replace(/^'+|'+$/g, "")
|
const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
|
||||||
|
console.log(pastedContent, filepath)
|
||||||
try {
|
try {
|
||||||
const file = Bun.file(filepath)
|
const file = Bun.file(filepath)
|
||||||
if (file.type.startsWith("image/")) {
|
if (file.type.startsWith("image/")) {
|
||||||
|
event.preventDefault()
|
||||||
const content = await file
|
const content = await file
|
||||||
.arrayBuffer()
|
.arrayBuffer()
|
||||||
.then((buffer) => Buffer.from(buffer).toString("base64"))
|
.then((buffer) => Buffer.from(buffer).toString("base64"))
|
||||||
.catch(() => {})
|
.catch(console.error)
|
||||||
if (content) {
|
if (content) {
|
||||||
await pasteImage({
|
await pasteImage({
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
@@ -663,7 +682,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
||||||
focusedBackgroundColor={theme.backgroundElement}
|
focusedBackgroundColor={theme.backgroundElement}
|
||||||
cursorColor={theme.primary}
|
cursorColor={theme.primary}
|
||||||
syntaxStyle={SyntaxTheme}
|
syntaxStyle={syntax()}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box
|
<box
|
||||||
@@ -674,7 +693,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
></box>
|
></box>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text flexShrink={0} wrapMode="none">
|
<text flexShrink={0} wrapMode="none" fg={theme.text}>
|
||||||
<span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "}
|
<span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "}
|
||||||
<span style={{ bold: true }}>{local.model.parsed().model}</span>
|
<span style={{ bold: true }}>{local.model.parsed().model}</span>
|
||||||
</text>
|
</text>
|
||||||
@@ -684,14 +703,14 @@ export function Prompt(props: PromptProps) {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={status() === "working"}>
|
<Match when={status() === "working"}>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
esc <span style={{ fg: theme.textMuted }}>interrupt</span>
|
esc <span style={{ fg: theme.textMuted }}>interrupt</span>
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.hint}>{props.hint!}</Match>
|
<Match when={props.hint}>{props.hint!}</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
ctrl+p <span style={{ fg: theme.textMuted }}>commands</span>
|
ctrl+p <span style={{ fg: theme.textMuted }}>commands</span>
|
||||||
</text>
|
</text>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import { createSignal } from "solid-js"
|
import { createSignal, type Setter } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -8,10 +8,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
|||||||
name: "KV",
|
name: "KV",
|
||||||
init: () => {
|
init: () => {
|
||||||
const [ready, setReady] = createSignal(false)
|
const [ready, setReady] = createSignal(false)
|
||||||
const [kvStore, setKvStore] = createStore({
|
const [kvStore, setKvStore] = createStore<Record<string, any>>()
|
||||||
openrouter_warning: false,
|
|
||||||
theme: "opencode",
|
|
||||||
})
|
|
||||||
const file = Bun.file(path.join(Global.Path.state, "kv.json"))
|
const file = Bun.file(path.join(Global.Path.state, "kv.json"))
|
||||||
|
|
||||||
file
|
file
|
||||||
@@ -24,22 +21,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
|
|||||||
setReady(true)
|
setReady(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
get data() {
|
|
||||||
return kvStore
|
|
||||||
},
|
|
||||||
get ready() {
|
get ready() {
|
||||||
return ready()
|
return ready()
|
||||||
},
|
},
|
||||||
|
signal<T>(name: string, defaultValue: T) {
|
||||||
|
if (!kvStore[name]) setKvStore(name, defaultValue)
|
||||||
|
return [
|
||||||
|
function () {
|
||||||
|
return result.get(name)
|
||||||
|
},
|
||||||
|
function setter(next: Setter<T>) {
|
||||||
|
result.set(name, next)
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
},
|
||||||
|
get(key: string, defaultValue?: any) {
|
||||||
|
return kvStore[key] ?? defaultValue
|
||||||
|
},
|
||||||
set(key: string, value: any) {
|
set(key: string, value: any) {
|
||||||
setKvStore(key as any, value)
|
setKvStore(key, value)
|
||||||
Bun.write(
|
Bun.write(file, JSON.stringify(kvStore, null, 2))
|
||||||
file,
|
|
||||||
JSON.stringify({
|
|
||||||
[key]: value,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { Global } from "@/global"
|
|||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
|
import { createEventBus } from "@solid-primitives/event-bus"
|
||||||
|
|
||||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
name: "Local",
|
name: "Local",
|
||||||
init: (props: { initialModel?: string; initialAgent?: string }) => {
|
init: (props: { initialModel?: string; initialAgent?: string; initialPrompt?: string }) => {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
@@ -147,15 +148,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
setModelStore("ready", true)
|
setModelStore("ready", true)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
Bun.write(
|
|
||||||
file,
|
|
||||||
JSON.stringify({
|
|
||||||
recent: modelStore.recent,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const fallbackModel = createMemo(() => {
|
const fallbackModel = createMemo(() => {
|
||||||
if (sync.data.config.model) {
|
if (sync.data.config.model) {
|
||||||
const [providerID, modelID] = sync.data.config.model.split("/")
|
const [providerID, modelID] = sync.data.config.model.split("/")
|
||||||
@@ -206,6 +198,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
model: model.name ?? value.modelID,
|
model: model.name ?? value.modelID,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
cycle(direction: 1 | -1) {
|
||||||
|
const current = currentModel()
|
||||||
|
if (!current) return
|
||||||
|
const recent = modelStore.recent
|
||||||
|
const index = recent.findIndex(
|
||||||
|
(x) => x.providerID === current.providerID && x.modelID === current.modelID,
|
||||||
|
)
|
||||||
|
if (index === -1) return
|
||||||
|
let next = index + direction
|
||||||
|
if (next < 0) next = recent.length - 1
|
||||||
|
if (next >= recent.length) next = 0
|
||||||
|
const val = recent[next]
|
||||||
|
if (!val) return
|
||||||
|
setModelStore("model", agent.current().name, { ...val })
|
||||||
|
},
|
||||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
if (!isModelValid(model)) {
|
if (!isModelValid(model)) {
|
||||||
@@ -216,21 +223,36 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setModelStore("model", agent.current().name, model)
|
setModelStore("model", agent.current().name, model)
|
||||||
if (options?.recent) {
|
if (options?.recent) {
|
||||||
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||||
if (uniq.length > 5) uniq.pop()
|
if (uniq.length > 5) uniq.pop()
|
||||||
setModelStore("recent", uniq)
|
setModelStore("recent", uniq)
|
||||||
|
Bun.write(
|
||||||
|
file,
|
||||||
|
JSON.stringify({
|
||||||
|
recent: modelStore.recent,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const setInitialPrompt = createEventBus<string>()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (props.initialPrompt)
|
||||||
|
setInitialPrompt.emit(props.initialPrompt)
|
||||||
|
})
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
model,
|
model,
|
||||||
agent,
|
agent,
|
||||||
|
get setInitialPrompt() {
|
||||||
|
return setInitialPrompt
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
Permission,
|
Permission,
|
||||||
LspStatus,
|
LspStatus,
|
||||||
McpStatus,
|
McpStatus,
|
||||||
|
FormatterStatus,
|
||||||
} from "@opencode-ai/sdk"
|
} from "@opencode-ai/sdk"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
@@ -42,6 +43,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
mcp: {
|
mcp: {
|
||||||
[key: string]: McpStatus
|
[key: string]: McpStatus
|
||||||
}
|
}
|
||||||
|
formatter: FormatterStatus[]
|
||||||
}>({
|
}>({
|
||||||
config: {},
|
config: {},
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -55,6 +57,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
part: {},
|
part: {},
|
||||||
lsp: [],
|
lsp: [],
|
||||||
mcp: {},
|
mcp: {},
|
||||||
|
formatter: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
@@ -220,6 +223,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
|
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
|
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
|
||||||
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
|
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||||
|
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ export function Home() {
|
|||||||
const Hint = (
|
const Hint = (
|
||||||
<Show when={Object.keys(sync.data.mcp).length > 0}>
|
<Show when={Object.keys(sync.data.mcp).length > 0}>
|
||||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={mcpError()}>
|
<Match when={mcpError()}>
|
||||||
<span style={{ fg: theme.error }}>•</span> mcp errors{" "}
|
<span style={{ fg: theme.error }}>•</span> mcp errors{" "}
|
||||||
@@ -76,7 +76,7 @@ function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
return (
|
return (
|
||||||
<box flexDirection="row" justifyContent="space-between" width="100%">
|
<box flexDirection="row" justifyContent="space-between" width="100%">
|
||||||
<text>{props.children}</text>
|
<text fg={theme.text}>{props.children}</text>
|
||||||
<text fg={theme.primary}>{keybind.print(props.keybind)}</text>
|
<text fg={theme.primary}>{keybind.print(props.keybind)}</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function Header() {
|
|||||||
borderColor={theme.backgroundElement}
|
borderColor={theme.backgroundElement}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<span style={{ bold: true, fg: theme.accent }}>#</span>{" "}
|
<span style={{ bold: true, fg: theme.accent }}>#</span>{" "}
|
||||||
<span style={{ bold: true }}>{session().title}</span>
|
<span style={{ bold: true }}>{session().title}</span>
|
||||||
</text>
|
</text>
|
||||||
@@ -64,7 +64,7 @@ export function Header() {
|
|||||||
</text>
|
</text>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<text wrapMode="word">
|
<text fg={theme.text} wrapMode="word">
|
||||||
/share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
|
/share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
|
||||||
</text>
|
</text>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { useRouteData } from "@tui/context/route"
|
import { useRoute, useRouteData } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
import { SplitBorder } from "@tui/component/border"
|
||||||
import { SyntaxTheme, useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
|
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
|
||||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||||
import type {
|
import type {
|
||||||
@@ -62,7 +62,10 @@ import { DialogTimeline } from "./dialog-timeline"
|
|||||||
import { Sidebar } from "./sidebar"
|
import { Sidebar } from "./sidebar"
|
||||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||||
import parsers from "../../../../../../parsers-config.ts"
|
import parsers from "../../../../../../parsers-config.ts"
|
||||||
import { Toast } from "../../ui/toast"
|
import { Clipboard } from "../../util/clipboard"
|
||||||
|
import { Toast, useToast } from "../../ui/toast"
|
||||||
|
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||||
|
import { useKV } from "../../context/kv.tsx"
|
||||||
|
|
||||||
addDefaultParsers(parsers.parsers)
|
addDefaultParsers(parsers.parsers)
|
||||||
|
|
||||||
@@ -79,7 +82,9 @@ function use() {
|
|||||||
|
|
||||||
export function Session() {
|
export function Session() {
|
||||||
const route = useRouteData("session")
|
const route = useRouteData("session")
|
||||||
|
const { navigate } = useRoute()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const kv = useKV()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const session = createMemo(() => sync.session.get(route.sessionID)!)
|
const session = createMemo(() => sync.session.get(route.sessionID)!)
|
||||||
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
|
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
|
||||||
@@ -90,7 +95,7 @@ export function Session() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">("auto")
|
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
|
||||||
const [conceal, setConceal] = createSignal(true)
|
const [conceal, setConceal] = createSignal(true)
|
||||||
|
|
||||||
const wide = createMemo(() => dimensions().width > 120)
|
const wide = createMemo(() => dimensions().width > 120)
|
||||||
@@ -99,6 +104,8 @@ export function Session() {
|
|||||||
|
|
||||||
createEffect(() => sync.session.sync(route.sessionID))
|
createEffect(() => sync.session.sync(route.sessionID))
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
let scroll: ScrollBoxRenderable
|
let scroll: ScrollBoxRenderable
|
||||||
@@ -150,6 +157,23 @@ export function Session() {
|
|||||||
|
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
|
|
||||||
|
function moveChild(direction: number) {
|
||||||
|
const parentID = session()?.parentID ?? session()?.id
|
||||||
|
let children = sync.data.session
|
||||||
|
.filter((x) => x.parentID === parentID || x.id === parentID)
|
||||||
|
.toSorted((b, a) => a.id.localeCompare(b.id))
|
||||||
|
if (children.length === 1) return
|
||||||
|
let next = children.findIndex((x) => x.id === session()?.id) + direction
|
||||||
|
if (next >= children.length) next = 0
|
||||||
|
if (next < 0) next = children.length - 1
|
||||||
|
if (children[next]) {
|
||||||
|
navigate({
|
||||||
|
type: "session",
|
||||||
|
sessionID: children[next].id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const command = useCommandDialog()
|
const command = useCommandDialog()
|
||||||
command.register(() => [
|
command.register(() => [
|
||||||
{
|
{
|
||||||
@@ -195,12 +219,20 @@ export function Session() {
|
|||||||
keybind: "session_share",
|
keybind: "session_share",
|
||||||
disabled: !!session()?.share?.url,
|
disabled: !!session()?.share?.url,
|
||||||
category: "Session",
|
category: "Session",
|
||||||
onSelect: (dialog) => {
|
onSelect: async (dialog) => {
|
||||||
sdk.client.session.share({
|
await sdk.client.session
|
||||||
path: {
|
.share({
|
||||||
id: route.sessionID,
|
path: {
|
||||||
},
|
id: route.sessionID,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
.then((res) =>
|
||||||
|
Clipboard.copy(res.data!.share!.url).catch(() =>
|
||||||
|
toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
|
||||||
|
.catch(() => toast.show({ message: "Failed to share session", variant: "error" }))
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -240,7 +272,9 @@ export function Session() {
|
|||||||
prompt.set(
|
prompt.set(
|
||||||
parts.reduce(
|
parts.reduce(
|
||||||
(agg, part) => {
|
(agg, part) => {
|
||||||
if (part.type === "text") agg.input += part.text
|
if (part.type === "text") {
|
||||||
|
if (!part.synthetic) agg.input += part.text
|
||||||
|
}
|
||||||
if (part.type === "file") agg.parts.push(part)
|
if (part.type === "file") agg.parts.push(part)
|
||||||
return agg
|
return agg
|
||||||
},
|
},
|
||||||
@@ -291,6 +325,8 @@ export function Session() {
|
|||||||
if (prev === "show") return "hide"
|
if (prev === "show") return "hide"
|
||||||
return "show"
|
return "show"
|
||||||
})
|
})
|
||||||
|
if (sidebar() === "show") kv.set("sidebar", "auto")
|
||||||
|
if (sidebar() === "hide") kv.set("sidebar", "hide")
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -370,6 +406,37 @@ export function Session() {
|
|||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Rename session",
|
||||||
|
value: "session.rename",
|
||||||
|
keybind: "session_rename",
|
||||||
|
category: "Session",
|
||||||
|
onSelect: (dialog) => {
|
||||||
|
dialog.replace(() => <DialogSessionRename session={route.sessionID} />)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Next child session",
|
||||||
|
value: "session.child.next",
|
||||||
|
keybind: "session_child_cycle",
|
||||||
|
category: "Session",
|
||||||
|
disabled: true,
|
||||||
|
onSelect: (dialog) => {
|
||||||
|
moveChild(1)
|
||||||
|
dialog.clear()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Previous child session",
|
||||||
|
value: "session.child.previous",
|
||||||
|
keybind: "session_child_cycle_reverse",
|
||||||
|
category: "Session",
|
||||||
|
disabled: true,
|
||||||
|
onSelect: (dialog) => {
|
||||||
|
moveChild(-1)
|
||||||
|
dialog.clear()
|
||||||
|
},
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const revert = createMemo(() => {
|
const revert = createMemo(() => {
|
||||||
@@ -431,6 +498,34 @@ export function Session() {
|
|||||||
>
|
>
|
||||||
<box flexGrow={1} gap={1}>
|
<box flexGrow={1} gap={1}>
|
||||||
<Show when={session()}>
|
<Show when={session()}>
|
||||||
|
<Show when={session().parentID}>
|
||||||
|
<box
|
||||||
|
backgroundColor={theme.backgroundPanel}
|
||||||
|
justifyContent="space-between"
|
||||||
|
flexDirection="row"
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
flexShrink={0}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
Previous{" "}
|
||||||
|
<span style={{ fg: theme.textMuted }}>
|
||||||
|
{keybind.print("session_child_cycle_reverse")}
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
<b>Viewing subagent session</b>
|
||||||
|
</text>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
<span style={{ fg: theme.textMuted }}>
|
||||||
|
{keybind.print("session_child_cycle")}
|
||||||
|
</span>{" "}
|
||||||
|
Next
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
<Show when={!sidebarVisible()}>
|
<Show when={!sidebarVisible()}>
|
||||||
<Header />
|
<Header />
|
||||||
</Show>
|
</Show>
|
||||||
@@ -614,7 +709,7 @@ function UserMessage(props: {
|
|||||||
borderColor={color()}
|
borderColor={color()}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<text>{text()?.text}</text>
|
<text fg={theme.text}>{text()?.text}</text>
|
||||||
<Show when={files().length}>
|
<Show when={files().length}>
|
||||||
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
|
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
|
||||||
<For each={files()}>
|
<For each={files()}>
|
||||||
@@ -625,7 +720,7 @@ function UserMessage(props: {
|
|||||||
return theme.secondary
|
return theme.secondary
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<span style={{ bg: bg(), fg: theme.background }}>
|
<span style={{ bg: bg(), fg: theme.background }}>
|
||||||
{" "}
|
{" "}
|
||||||
{MIME_BADGE[file.mime] ?? file.mime}{" "}
|
{MIME_BADGE[file.mime] ?? file.mime}{" "}
|
||||||
@@ -640,7 +735,7 @@ function UserMessage(props: {
|
|||||||
</For>
|
</For>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
{sync.data.config.username ?? "You"}{" "}
|
{sync.data.config.username ?? "You"}{" "}
|
||||||
<Show
|
<Show
|
||||||
when={queued()}
|
when={queued()}
|
||||||
@@ -755,7 +850,7 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
|
|||||||
paddingLeft={2}
|
paddingLeft={2}
|
||||||
backgroundColor={theme.backgroundPanel}
|
backgroundColor={theme.backgroundPanel}
|
||||||
>
|
>
|
||||||
<text>{props.part.text.trim()}</text>
|
<text fg={theme.text}>{props.part.text.trim()}</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -764,13 +859,14 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
|
|||||||
|
|
||||||
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
|
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
|
||||||
const ctx = use()
|
const ctx = use()
|
||||||
|
const { syntax } = useTheme()
|
||||||
return (
|
return (
|
||||||
<Show when={props.part.text.trim()}>
|
<Show when={props.part.text.trim()}>
|
||||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
|
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||||
<code
|
<code
|
||||||
filetype="markdown"
|
filetype="markdown"
|
||||||
drawUnstyledText={false}
|
drawUnstyledText={false}
|
||||||
syntaxStyle={SyntaxTheme}
|
syntaxStyle={syntax()}
|
||||||
content={props.part.text.trim()}
|
content={props.part.text.trim()}
|
||||||
conceal={ctx.conceal()}
|
conceal={ctx.conceal()}
|
||||||
/>
|
/>
|
||||||
@@ -893,16 +989,14 @@ function GenericTool(props: ToolProps<any>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ToolRegistration<T extends Tool.Info = any> = {
|
||||||
|
name: string
|
||||||
|
container: "inline" | "block"
|
||||||
|
render?: Component<ToolProps<T>>
|
||||||
|
}
|
||||||
const ToolRegistry = (() => {
|
const ToolRegistry = (() => {
|
||||||
const state: Record<
|
const state: Record<string, ToolRegistration> = {}
|
||||||
string,
|
function register<T extends Tool.Info>(input: ToolRegistration<T>) {
|
||||||
{ name: string; container: "inline" | "block"; render?: Component<ToolProps<any>> }
|
|
||||||
> = {}
|
|
||||||
function register<T extends Tool.Info>(input: {
|
|
||||||
name: string
|
|
||||||
container: "inline" | "block"
|
|
||||||
render?: Component<ToolProps<T>>
|
|
||||||
}) {
|
|
||||||
state[input.name] = input
|
state[input.name] = input
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
@@ -970,7 +1064,7 @@ ToolRegistry.register<typeof WriteTool>({
|
|||||||
name: "write",
|
name: "write",
|
||||||
container: "block",
|
container: "block",
|
||||||
render(props) {
|
render(props) {
|
||||||
const { theme } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
const lines = createMemo(() => {
|
const lines = createMemo(() => {
|
||||||
return props.input.content?.split("\n") ?? []
|
return props.input.content?.split("\n") ?? []
|
||||||
})
|
})
|
||||||
@@ -1001,7 +1095,7 @@ ToolRegistry.register<typeof WriteTool>({
|
|||||||
<box paddingLeft={1} flexGrow={1}>
|
<box paddingLeft={1} flexGrow={1}>
|
||||||
<code
|
<code
|
||||||
filetype={filetype(props.input.filePath!)}
|
filetype={filetype(props.input.filePath!)}
|
||||||
syntaxStyle={SyntaxTheme}
|
syntaxStyle={syntax()}
|
||||||
content={code()}
|
content={code()}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
@@ -1066,6 +1160,8 @@ ToolRegistry.register<typeof TaskTool>({
|
|||||||
container: "block",
|
container: "block",
|
||||||
render(props) {
|
render(props) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const keybind = useKeybind()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolTitle icon="%" fallback="Delegating..." when={props.input.description}>
|
<ToolTitle icon="%" fallback="Delegating..." when={props.input.description}>
|
||||||
@@ -1082,6 +1178,10 @@ ToolRegistry.register<typeof TaskTool>({
|
|||||||
</For>
|
</For>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
{keybind.print("session_child_cycle")}, {keybind.print("session_child_cycle_reverse")}
|
||||||
|
<span style={{ fg: theme.textMuted }}> to navigate between subagent sessions</span>
|
||||||
|
</text>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -1104,6 +1204,7 @@ ToolRegistry.register<typeof EditTool>({
|
|||||||
container: "block",
|
container: "block",
|
||||||
render(props) {
|
render(props) {
|
||||||
const ctx = use()
|
const ctx = use()
|
||||||
|
const { theme, syntax } = useTheme()
|
||||||
|
|
||||||
const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked"))
|
const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked"))
|
||||||
|
|
||||||
@@ -1183,21 +1284,21 @@ ToolRegistry.register<typeof EditTool>({
|
|||||||
</ToolTitle>
|
</ToolTitle>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.permission["diff"]}>
|
<Match when={props.permission["diff"]}>
|
||||||
<text>{props.permission["diff"]?.trim()}</text>
|
<text fg={theme.text}>{props.permission["diff"]?.trim()}</text>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={diff() && style() === "split"}>
|
<Match when={diff() && style() === "split"}>
|
||||||
<box paddingLeft={1} flexDirection="row" gap={2}>
|
<box paddingLeft={1} flexDirection="row" gap={2}>
|
||||||
<box flexGrow={1} flexBasis={0}>
|
<box flexGrow={1} flexBasis={0}>
|
||||||
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={diff()!.oldContent} />
|
<code filetype={ft()} syntaxStyle={syntax()} content={diff()!.oldContent} />
|
||||||
</box>
|
</box>
|
||||||
<box flexGrow={1} flexBasis={0}>
|
<box flexGrow={1} flexBasis={0}>
|
||||||
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={diff()!.newContent} />
|
<code filetype={ft()} syntaxStyle={syntax()} content={diff()!.newContent} />
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={code()}>
|
<Match when={code()}>
|
||||||
<box paddingLeft={1}>
|
<box paddingLeft={1}>
|
||||||
<code filetype={ft()} syntaxStyle={SyntaxTheme} content={code()} />
|
<code filetype={ft()} syntaxStyle={syntax()} content={code()} />
|
||||||
</box>
|
</box>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -1210,6 +1311,7 @@ ToolRegistry.register<typeof PatchTool>({
|
|||||||
name: "patch",
|
name: "patch",
|
||||||
container: "block",
|
container: "block",
|
||||||
render(props) {
|
render(props) {
|
||||||
|
const { theme } = useTheme()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolTitle icon="%" fallback="Preparing patch..." when={true}>
|
<ToolTitle icon="%" fallback="Preparing patch..." when={true}>
|
||||||
@@ -1217,7 +1319,7 @@ ToolRegistry.register<typeof PatchTool>({
|
|||||||
</ToolTitle>
|
</ToolTitle>
|
||||||
<Show when={props.output}>
|
<Show when={props.output}>
|
||||||
<box>
|
<box>
|
||||||
<text>{props.output?.trim()}</text>
|
<text fg={theme.text}>{props.output?.trim()}</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function Sidebar(props: { sessionID: string }) {
|
|||||||
<Show when={session()}>
|
<Show when={session()}>
|
||||||
<box flexShrink={0} gap={1} width={40}>
|
<box flexShrink={0} gap={1} width={40}>
|
||||||
<box>
|
<box>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<b>{session().title}</b>
|
<b>{session().title}</b>
|
||||||
</text>
|
</text>
|
||||||
<Show when={session().share?.url}>
|
<Show when={session().share?.url}>
|
||||||
@@ -50,7 +50,7 @@ export function Sidebar(props: { sessionID: string }) {
|
|||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
<box>
|
<box>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<b>Context</b>
|
<b>Context</b>
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
|
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
|
||||||
@@ -59,7 +59,7 @@ export function Sidebar(props: { sessionID: string }) {
|
|||||||
</box>
|
</box>
|
||||||
<Show when={Object.keys(sync.data.mcp).length > 0}>
|
<Show when={Object.keys(sync.data.mcp).length > 0}>
|
||||||
<box>
|
<box>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<b>MCP</b>
|
<b>MCP</b>
|
||||||
</text>
|
</text>
|
||||||
<For each={Object.entries(sync.data.mcp)}>
|
<For each={Object.entries(sync.data.mcp)}>
|
||||||
@@ -77,7 +77,7 @@ export function Sidebar(props: { sessionID: string }) {
|
|||||||
>
|
>
|
||||||
•
|
•
|
||||||
</text>
|
</text>
|
||||||
<text wrapMode="word">
|
<text fg={theme.text} wrapMode="word">
|
||||||
{key}{" "}
|
{key}{" "}
|
||||||
<span style={{ fg: theme.textMuted }}>
|
<span style={{ fg: theme.textMuted }}>
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -96,7 +96,7 @@ export function Sidebar(props: { sessionID: string }) {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={sync.data.lsp.length > 0}>
|
<Show when={sync.data.lsp.length > 0}>
|
||||||
<box>
|
<box>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<b>LSP</b>
|
<b>LSP</b>
|
||||||
</text>
|
</text>
|
||||||
<For each={sync.data.lsp}>
|
<For each={sync.data.lsp}>
|
||||||
@@ -123,7 +123,7 @@ export function Sidebar(props: { sessionID: string }) {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={session().summary?.diffs}>
|
<Show when={session().summary?.diffs}>
|
||||||
<box>
|
<box>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<b>Modified Files</b>
|
<b>Modified Files</b>
|
||||||
</text>
|
</text>
|
||||||
<For each={session().summary?.diffs || []}>
|
<For each={session().summary?.diffs || []}>
|
||||||
@@ -155,7 +155,7 @@ export function Sidebar(props: { sessionID: string }) {
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={todo().length > 0}>
|
<Show when={todo().length > 0}>
|
||||||
<box>
|
<box>
|
||||||
<text>
|
<text fg={theme.text}>
|
||||||
<b>Todo</b>
|
<b>Todo</b>
|
||||||
</text>
|
</text>
|
||||||
<For each={todo()}>
|
<For each={todo()}>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const TuiSpawnCommand = cmd({
|
|||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
.option("hostname", {
|
.option("hostname", {
|
||||||
alias: ["h"],
|
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "hostname to listen on",
|
describe: "hostname to listen on",
|
||||||
default: "127.0.0.1",
|
default: "127.0.0.1",
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ export const TuiThreadCommand = cmd({
|
|||||||
describe: "session id to continue",
|
describe: "session id to continue",
|
||||||
type: "string",
|
type: "string",
|
||||||
})
|
})
|
||||||
|
.option("prompt", {
|
||||||
|
alias: ["p"],
|
||||||
|
type: "string",
|
||||||
|
describe: "prompt to use",
|
||||||
|
})
|
||||||
.option("agent", {
|
.option("agent", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "agent to use",
|
describe: "agent to use",
|
||||||
@@ -42,7 +47,6 @@ export const TuiThreadCommand = cmd({
|
|||||||
default: 0,
|
default: 0,
|
||||||
})
|
})
|
||||||
.option("hostname", {
|
.option("hostname", {
|
||||||
alias: ["h"],
|
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "hostname to listen on",
|
describe: "hostname to listen on",
|
||||||
default: "127.0.0.1",
|
default: "127.0.0.1",
|
||||||
@@ -96,6 +100,7 @@ export const TuiThreadCommand = cmd({
|
|||||||
sessionID,
|
sessionID,
|
||||||
model: args.model,
|
model: args.model,
|
||||||
agent: args.agent,
|
agent: args.agent,
|
||||||
|
prompt: args.prompt,
|
||||||
onExit: async () => {
|
onExit: async () => {
|
||||||
await client.call("shutdown", undefined)
|
await client.call("shutdown", undefined)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
|
import { useDialog, type DialogContext } from "./dialog"
|
||||||
|
import { onMount } from "solid-js"
|
||||||
|
|
||||||
|
export type DialogPromptProps = {
|
||||||
|
title: string
|
||||||
|
value?: string
|
||||||
|
onConfirm?: (value: string) => void
|
||||||
|
onCancel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogPrompt(props: DialogPromptProps) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
let textarea: TextareaRenderable
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
dialog.setSize("large")
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.focus()
|
||||||
|
}, 1)
|
||||||
|
textarea.gotoLineEnd()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
||||||
|
<text fg={theme.textMuted}>esc</text>
|
||||||
|
</box>
|
||||||
|
<box>
|
||||||
|
<textarea
|
||||||
|
onSubmit={() => {
|
||||||
|
props.onConfirm?.(textarea.plainText)
|
||||||
|
dialog.clear()
|
||||||
|
}}
|
||||||
|
keyBindings={[{ name: "return", action: "submit" }]}
|
||||||
|
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||||
|
initialValue={props.value}
|
||||||
|
placeholder="Enter text"
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
<box paddingBottom={1}>
|
||||||
|
<text fg={theme.textMuted}>Press enter to confirm, esc to cancel</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogPrompt.show = (dialog: DialogContext, title: string, value?: string) => {
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
dialog.replace(
|
||||||
|
() => (
|
||||||
|
<DialogPrompt
|
||||||
|
title={title}
|
||||||
|
value={value}
|
||||||
|
onConfirm={(value) => resolve(value)}
|
||||||
|
onCancel={() => resolve(null)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
() => resolve(null),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ export interface DialogSelectOption<T = any> {
|
|||||||
category?: string
|
category?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
bg?: RGBA
|
bg?: RGBA
|
||||||
onSelect?: (ctx: DialogContext) => void
|
onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DialogSelectRef<T> = {
|
export type DialogSelectRef<T> = {
|
||||||
@@ -59,7 +59,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
props.options,
|
props.options,
|
||||||
filter((x) => x.disabled !== true),
|
filter((x) => x.disabled !== true),
|
||||||
take(props.limit ?? Infinity),
|
take(props.limit ?? Infinity),
|
||||||
(x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
|
(x) =>
|
||||||
|
!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -122,20 +123,26 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
|
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
if (evt.name === "up") move(-1)
|
if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
|
||||||
if (evt.name === "down") move(1)
|
if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
|
||||||
if (evt.name === "pageup") move(-10)
|
if (evt.name === "pageup") move(-10)
|
||||||
if (evt.name === "pagedown") move(10)
|
if (evt.name === "pagedown") move(10)
|
||||||
if (evt.name === "return") {
|
if (evt.name === "return") {
|
||||||
const option = selected()
|
const option = selected()
|
||||||
if (option.onSelect) option.onSelect(dialog)
|
if (option) {
|
||||||
props.onSelect?.(option)
|
evt.preventDefault()
|
||||||
|
if (option.onSelect) option.onSelect(dialog)
|
||||||
|
props.onSelect?.(option)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of props.keybind ?? []) {
|
for (const item of props.keybind ?? []) {
|
||||||
if (Keybind.match(item.keybind, keybind.parse(evt))) {
|
if (Keybind.match(item.keybind, keybind.parse(evt))) {
|
||||||
const s = selected()
|
const s = selected()
|
||||||
if (s) item.onTrigger(s)
|
if (s) {
|
||||||
|
evt.preventDefault()
|
||||||
|
item.onTrigger(s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -155,7 +162,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<box paddingLeft={3} paddingRight={2}>
|
<box paddingLeft={3} paddingRight={2}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||||
|
{props.title}
|
||||||
|
</text>
|
||||||
<text fg={theme.textMuted}>esc</text>
|
<text fg={theme.textMuted}>esc</text>
|
||||||
</box>
|
</box>
|
||||||
<box paddingTop={1} paddingBottom={1}>
|
<box paddingTop={1} paddingBottom={1}>
|
||||||
@@ -166,12 +175,13 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
props.onFilter?.(e)
|
props.onFilter?.(e)
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {}}
|
||||||
focusedBackgroundColor={theme.backgroundPanel}
|
focusedBackgroundColor={theme.backgroundPanel}
|
||||||
cursorColor={theme.primary}
|
cursorColor={theme.primary}
|
||||||
focusedTextColor={theme.textMuted}
|
focusedTextColor={theme.textMuted}
|
||||||
ref={(r) => {
|
ref={(r) => {
|
||||||
input = r
|
input = r
|
||||||
input.focus()
|
setTimeout(() => input.focus(), 1)
|
||||||
}}
|
}}
|
||||||
placeholder="Enter search term"
|
placeholder="Enter search term"
|
||||||
/>
|
/>
|
||||||
@@ -206,11 +216,15 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
props.onSelect?.(option)
|
props.onSelect?.(option)
|
||||||
}}
|
}}
|
||||||
onMouseOver={() => {
|
onMouseOver={() => {
|
||||||
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
|
const index = filtered().findIndex((x) =>
|
||||||
|
isDeepEqual(x.value, option.value),
|
||||||
|
)
|
||||||
if (index === -1) return
|
if (index === -1) return
|
||||||
moveTo(index)
|
moveTo(index)
|
||||||
}}
|
}}
|
||||||
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
|
backgroundColor={
|
||||||
|
active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)
|
||||||
|
}
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
gap={1}
|
gap={1}
|
||||||
@@ -218,7 +232,9 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
<Option
|
<Option
|
||||||
title={option.title}
|
title={option.title}
|
||||||
footer={option.footer}
|
footer={option.footer}
|
||||||
description={option.description !== category ? option.description : undefined}
|
description={
|
||||||
|
option.description !== category ? option.description : undefined
|
||||||
|
}
|
||||||
active={active()}
|
active={active()}
|
||||||
current={isDeepEqual(option.value, props.current)}
|
current={isDeepEqual(option.value, props.current)}
|
||||||
/>
|
/>
|
||||||
@@ -230,11 +246,13 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
<box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1}>
|
<box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1} gap={1}>
|
||||||
<For each={props.keybind ?? []}>
|
<For each={props.keybind ?? []}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<text>
|
<text>
|
||||||
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>{Keybind.toString(item.keybind)}</span>
|
<span style={{ fg: theme.text, attributes: TextAttributes.BOLD }}>
|
||||||
|
{Keybind.toString(item.keybind)}
|
||||||
|
</span>
|
||||||
<span style={{ fg: theme.textMuted }}> {item.title}</span>
|
<span style={{ fg: theme.textMuted }}> {item.title}</span>
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
@@ -263,7 +281,10 @@ function Option(props: {
|
|||||||
wrapMode="none"
|
wrapMode="none"
|
||||||
>
|
>
|
||||||
{Locale.truncate(props.title, 62)}
|
{Locale.truncate(props.title, 62)}
|
||||||
<span style={{ fg: props.active ? theme.background : theme.textMuted }}> {props.description}</span>
|
<span style={{ fg: props.active ? theme.background : theme.textMuted }}>
|
||||||
|
{" "}
|
||||||
|
{props.description}
|
||||||
|
</span>
|
||||||
</text>
|
</text>
|
||||||
<Show when={props.footer}>
|
<Show when={props.footer}>
|
||||||
<box flexShrink={0}>
|
<box flexShrink={0}>
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { batch, createContext, createEffect, Show, useContext, type JSX, type ParentProps } from "solid-js"
|
import {
|
||||||
|
batch,
|
||||||
|
createContext,
|
||||||
|
createEffect,
|
||||||
|
Show,
|
||||||
|
useContext,
|
||||||
|
type JSX,
|
||||||
|
type ParentProps,
|
||||||
|
} from "solid-js"
|
||||||
import { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { Renderable, RGBA } from "@opentui/core"
|
import { Renderable, RGBA } from "@opentui/core"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createEventBus } from "@solid-primitives/event-bus"
|
import { createEventBus } from "@solid-primitives/event-bus"
|
||||||
|
|
||||||
const Border = {
|
|
||||||
topLeft: "┃",
|
|
||||||
topRight: "┃",
|
|
||||||
bottomLeft: "┃",
|
|
||||||
bottomRight: "┃",
|
|
||||||
horizontal: "",
|
|
||||||
vertical: "┃",
|
|
||||||
topT: "+",
|
|
||||||
bottomT: "+",
|
|
||||||
leftT: "+",
|
|
||||||
rightT: "+",
|
|
||||||
cross: "+",
|
|
||||||
}
|
|
||||||
export function Dialog(
|
export function Dialog(
|
||||||
props: ParentProps<{
|
props: ParentProps<{
|
||||||
size?: "medium" | "large"
|
size?: "medium" | "large"
|
||||||
@@ -45,11 +40,9 @@ export function Dialog(
|
|||||||
onMouseUp={async (e) => {
|
onMouseUp={async (e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
customBorderChars={Border}
|
|
||||||
width={props.size === "large" ? 80 : 60}
|
width={props.size === "large" ? 80 : 60}
|
||||||
maxWidth={dimensions().width - 2}
|
maxWidth={dimensions().width - 2}
|
||||||
backgroundColor={theme.backgroundPanel}
|
backgroundColor={theme.backgroundPanel}
|
||||||
borderColor={theme.border}
|
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
@@ -138,7 +131,7 @@ function init() {
|
|||||||
},
|
},
|
||||||
get allClosedEvent() {
|
get allClosedEvent() {
|
||||||
return allClosedEvent
|
return allClosedEvent
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function init() {
|
|||||||
|
|
||||||
let timeoutHandle: NodeJS.Timeout | null = null
|
let timeoutHandle: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
return {
|
const toast = {
|
||||||
show(options: ToastOptions) {
|
show(options: ToastOptions) {
|
||||||
const parsedOptions = TuiEvent.ToastShow.properties.parse(options)
|
const parsedOptions = TuiEvent.ToastShow.properties.parse(options)
|
||||||
const { duration, ...currentToast } = parsedOptions
|
const { duration, ...currentToast } = parsedOptions
|
||||||
@@ -59,10 +59,22 @@ function init() {
|
|||||||
setStore("currentToast", null)
|
setStore("currentToast", null)
|
||||||
}, duration).unref()
|
}, duration).unref()
|
||||||
},
|
},
|
||||||
|
error: (err: any) => {
|
||||||
|
if (err instanceof Error)
|
||||||
|
return toast.show({
|
||||||
|
variant: "error",
|
||||||
|
message: err.message,
|
||||||
|
})
|
||||||
|
toast.show({
|
||||||
|
variant: "error",
|
||||||
|
message: "An unknown error has occurred",
|
||||||
|
})
|
||||||
|
},
|
||||||
get currentToast(): ToastOptions | null {
|
get currentToast(): ToastOptions | null {
|
||||||
return store.currentToast
|
return store.currentToast
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
return toast
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToastContext = ReturnType<typeof init>
|
export type ToastContext = ReturnType<typeof init>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export namespace Clipboard {
|
|||||||
const getCopyMethod = lazy(() => {
|
const getCopyMethod = lazy(() => {
|
||||||
const os = platform()
|
const os = platform()
|
||||||
|
|
||||||
if (os === "darwin") {
|
if (os === "darwin" && Bun.which("oascript")) {
|
||||||
console.log("clipboard: using osascript")
|
console.log("clipboard: using osascript")
|
||||||
return async (text: string) => {
|
return async (text: string) => {
|
||||||
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||||
@@ -70,13 +70,13 @@ export namespace Clipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (os === "linux") {
|
if (os === "linux") {
|
||||||
if (process.env["WAYLAND_DISPLAY"]) {
|
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
|
||||||
console.log("clipboard: using wl-copy")
|
console.log("clipboard: using wl-copy")
|
||||||
return async (text: string) => {
|
return async (text: string) => {
|
||||||
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
||||||
proc.stdin.write(text)
|
proc.stdin.write(text)
|
||||||
proc.stdin.end()
|
proc.stdin.end()
|
||||||
await proc.exited
|
await proc.exited.catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Bun.which("xclip")) {
|
if (Bun.which("xclip")) {
|
||||||
@@ -89,7 +89,7 @@ export namespace Clipboard {
|
|||||||
})
|
})
|
||||||
proc.stdin.write(text)
|
proc.stdin.write(text)
|
||||||
proc.stdin.end()
|
proc.stdin.end()
|
||||||
await proc.exited
|
await proc.exited.catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Bun.which("xsel")) {
|
if (Bun.which("xsel")) {
|
||||||
@@ -102,7 +102,7 @@ export namespace Clipboard {
|
|||||||
})
|
})
|
||||||
proc.stdin.write(text)
|
proc.stdin.write(text)
|
||||||
proc.stdin.end()
|
proc.stdin.end()
|
||||||
await proc.exited
|
await proc.exited.catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export namespace Editor {
|
|||||||
})
|
})
|
||||||
await proc.exited
|
await proc.exited
|
||||||
const content = await Bun.file(filepath).text()
|
const content = await Bun.file(filepath).text()
|
||||||
|
opts.renderer.currentRenderBuffer.clear()
|
||||||
opts.renderer.resume()
|
opts.renderer.resume()
|
||||||
opts.renderer.requestRender()
|
opts.renderer.requestRender()
|
||||||
return content || undefined
|
return content || undefined
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { Identifier } from "../id/id"
|
||||||
|
|
||||||
export namespace Command {
|
export namespace Command {
|
||||||
|
export const Default = {
|
||||||
|
INIT: "init",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
Executed: Bus.event(
|
||||||
|
"command.executed",
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
sessionID: Identifier.schema("session"),
|
||||||
|
arguments: z.string(),
|
||||||
|
messageID: Identifier.schema("message"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
@@ -33,6 +52,14 @@ export namespace Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result[Default.INIT] === undefined) {
|
||||||
|
result[Default.INIT] = {
|
||||||
|
name: Default.INIT,
|
||||||
|
description: "create/update AGENTS.md",
|
||||||
|
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+2
@@ -6,3 +6,5 @@ The file you create will be given to agentic coding agents (such as yourself) th
|
|||||||
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
|
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
|
||||||
|
|
||||||
If there's already an AGENTS.md, improve it if it's located in ${path}
|
If there's already an AGENTS.md, improve it if it's located in ${path}
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
@@ -385,7 +385,11 @@ export namespace Config {
|
|||||||
.optional()
|
.optional()
|
||||||
.default("ctrl+x")
|
.default("ctrl+x")
|
||||||
.describe("Leader key for keybind combinations"),
|
.describe("Leader key for keybind combinations"),
|
||||||
app_exit: z.string().optional().default("ctrl+c,<leader>q").describe("Exit the application"),
|
app_exit: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("ctrl+c,ctrl+d,<leader>q")
|
||||||
|
.describe("Exit the application"),
|
||||||
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
||||||
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
||||||
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
||||||
@@ -449,18 +453,37 @@ export namespace Config {
|
|||||||
.default("<leader>h")
|
.default("<leader>h")
|
||||||
.describe("Toggle code block concealment in messages"),
|
.describe("Toggle code block concealment in messages"),
|
||||||
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
||||||
|
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
|
||||||
|
model_cycle_recent_reverse: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("shift+f2")
|
||||||
|
.describe("Previous recently used model"),
|
||||||
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
|
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
|
||||||
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
||||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||||
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||||
|
input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
|
||||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||||
input_submit: z.string().optional().default("enter").describe("Submit input"),
|
input_submit: z.string().optional().default("return").describe("Submit input"),
|
||||||
input_newline: z
|
input_newline: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.default("shift+enter,ctrl+j")
|
.default("shift+return,ctrl+j")
|
||||||
.describe("Insert newline in input"),
|
.describe("Insert newline in input"),
|
||||||
|
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
||||||
|
history_next: z.string().optional().default("down").describe("Previous history item"),
|
||||||
|
session_child_cycle: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("ctrl+right")
|
||||||
|
.describe("Next child session"),
|
||||||
|
session_child_cycle_reverse: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("ctrl+left")
|
||||||
|
.describe("Previous child session"),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ export namespace Ripgrep {
|
|||||||
args.push(`--max-count=${input.limit}`)
|
args.push(`--max-count=${input.limit}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args.push("--")
|
||||||
args.push(input.pattern)
|
args.push(input.pattern)
|
||||||
|
|
||||||
const command = args.join(" ")
|
const command = args.join(" ")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Bus } from "../bus"
|
|||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import z from "zod"
|
||||||
|
|
||||||
import * as Formatter from "./formatter"
|
import * as Formatter from "./formatter"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
@@ -11,6 +12,17 @@ import { Instance } from "../project/instance"
|
|||||||
export namespace Format {
|
export namespace Format {
|
||||||
const log = Log.create({ service: "format" })
|
const log = Log.create({ service: "format" })
|
||||||
|
|
||||||
|
export const Status = z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
extensions: z.string().array(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
ref: "FormatterStatus",
|
||||||
|
})
|
||||||
|
export type Status = z.infer<typeof Status>
|
||||||
|
|
||||||
const state = Instance.state(async () => {
|
const state = Instance.state(async () => {
|
||||||
const enabled: Record<string, boolean> = {}
|
const enabled: Record<string, boolean> = {}
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
@@ -62,6 +74,20 @@ export namespace Format {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function status() {
|
||||||
|
const s = await state()
|
||||||
|
const result: Status[] = []
|
||||||
|
for (const formatter of Object.values(s.formatters)) {
|
||||||
|
const enabled = await isEnabled(formatter)
|
||||||
|
result.push({
|
||||||
|
name: formatter.name,
|
||||||
|
extensions: formatter.extensions,
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
log.info("init")
|
log.info("init")
|
||||||
Bus.subscribe(File.Event.Edited, async (payload) => {
|
Bus.subscribe(File.Event.Edited, async (payload) => {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ process.on("uncaughtException", (e) => {
|
|||||||
const cli = yargs(hideBin(process.argv))
|
const cli = yargs(hideBin(process.argv))
|
||||||
.scriptName("opencode")
|
.scriptName("opencode")
|
||||||
.help("help", "show help")
|
.help("help", "show help")
|
||||||
|
.alias("help", "h")
|
||||||
.version("version", "show version number", Installation.VERSION)
|
.version("version", "show version number", Installation.VERSION)
|
||||||
.alias("version", "v")
|
.alias("version", "v")
|
||||||
.option("print-logs", {
|
.option("print-logs", {
|
||||||
@@ -140,5 +141,5 @@ try {
|
|||||||
// Most notably, some docker-container-based MCP servers don't handle such signals unless
|
// Most notably, some docker-container-based MCP servers don't handle such signals unless
|
||||||
// run using `docker run --init`.
|
// run using `docker run --init`.
|
||||||
// Explicitly exit to avoid any hanging subprocesses.
|
// Explicitly exit to avoid any hanging subprocesses.
|
||||||
process.exit();
|
process.exit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { LSP } from "../lsp"
|
|||||||
import { FileWatcher } from "../file/watcher"
|
import { FileWatcher } from "../file/watcher"
|
||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
|
import { Project } from "./project"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { Command } from "../command"
|
||||||
|
import { Instance } from "./instance"
|
||||||
|
|
||||||
export async function InstanceBootstrap() {
|
export async function InstanceBootstrap() {
|
||||||
if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return
|
if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return
|
||||||
@@ -14,4 +18,10 @@ export async function InstanceBootstrap() {
|
|||||||
await LSP.init()
|
await LSP.init()
|
||||||
FileWatcher.init()
|
FileWatcher.init()
|
||||||
File.init()
|
File.init()
|
||||||
|
|
||||||
|
Bus.subscribe(Command.Event.Executed, async (payload) => {
|
||||||
|
if (payload.properties.name === Command.Default.INIT) {
|
||||||
|
await Project.setInitialized(Instance.project.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ const context = Context.create<Context>("instance")
|
|||||||
const cache = new Map<string, Context>()
|
const cache = new Map<string, Context>()
|
||||||
|
|
||||||
export const Instance = {
|
export const Instance = {
|
||||||
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
async provide<R>(input: {
|
||||||
|
directory: string
|
||||||
|
init?: () => Promise<any>
|
||||||
|
fn: () => R
|
||||||
|
}): Promise<R> {
|
||||||
let existing = cache.get(input.directory)
|
let existing = cache.get(input.directory)
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
const project = await Project.fromDirectory(input.directory)
|
const project = await Project.fromDirectory(input.directory)
|
||||||
@@ -24,8 +28,8 @@ export const Instance = {
|
|||||||
}
|
}
|
||||||
return context.provide(existing, async () => {
|
return context.provide(existing, async () => {
|
||||||
if (!cache.has(input.directory)) {
|
if (!cache.has(input.directory)) {
|
||||||
await input.init?.()
|
|
||||||
cache.set(input.directory, existing)
|
cache.set(input.directory, existing)
|
||||||
|
await input.init?.()
|
||||||
}
|
}
|
||||||
return input.fn()
|
return input.fn()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { Ripgrep } from "../file/ripgrep"
|
|||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { File } from "../file"
|
import { File } from "../file"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
|
import { Format } from "../format"
|
||||||
import { MessageV2 } from "../session/message-v2"
|
import { MessageV2 } from "../session/message-v2"
|
||||||
import { TuiRoute } from "./tui"
|
import { TuiRoute } from "./tui"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
@@ -1336,6 +1337,26 @@ export namespace Server {
|
|||||||
return c.json(await LSP.status())
|
return c.json(await LSP.status())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/formatter",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get formatter status",
|
||||||
|
operationId: "formatter.status",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Formatter status",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Format.Status.array()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.json(await Format.status())
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/tui/append-prompt",
|
"/tui/append-prompt",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { Decimal } from "decimal.js"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
|
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
|
||||||
|
|
||||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
|
||||||
|
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
@@ -14,11 +12,11 @@ import { Share } from "../share/share"
|
|||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { Project } from "../project/project"
|
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { SessionPrompt } from "./prompt"
|
import { SessionPrompt } from "./prompt"
|
||||||
import { fn } from "@/util/fn"
|
import { fn } from "@/util/fn"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
|
import { Command } from "../command"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
@@ -164,7 +162,12 @@ export namespace Session {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
|
export async function createNext(input: {
|
||||||
|
id?: string
|
||||||
|
title?: string
|
||||||
|
parentID?: string
|
||||||
|
directory: string
|
||||||
|
}) {
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
id: Identifier.descending("session", input.id),
|
id: Identifier.descending("session", input.id),
|
||||||
version: Installation.VERSION,
|
version: Installation.VERSION,
|
||||||
@@ -402,7 +405,9 @@ export namespace Session {
|
|||||||
.add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000))
|
.add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000))
|
||||||
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
|
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
|
||||||
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
|
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
|
||||||
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000))
|
.add(
|
||||||
|
new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000),
|
||||||
|
)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
tokens,
|
tokens,
|
||||||
}
|
}
|
||||||
@@ -423,22 +428,13 @@ export namespace Session {
|
|||||||
messageID: Identifier.schema("message"),
|
messageID: Identifier.schema("message"),
|
||||||
}),
|
}),
|
||||||
async (input) => {
|
async (input) => {
|
||||||
await SessionPrompt.prompt({
|
await SessionPrompt.command({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: input.messageID,
|
messageID: input.messageID,
|
||||||
model: {
|
model: input.providerID + "/" + input.modelID,
|
||||||
providerID: input.providerID,
|
command: Command.Default.INIT,
|
||||||
modelID: input.modelID,
|
arguments: "",
|
||||||
},
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
id: Identifier.ascending("part"),
|
|
||||||
type: "text",
|
|
||||||
text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
await Project.setInitialized(Instance.project.id)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1593,6 +1593,7 @@ export namespace SessionPrompt {
|
|||||||
let index = 0
|
let index = 0
|
||||||
template = template.replace(bashRegex, () => results[index++])
|
template = template.replace(bashRegex, () => results[index++])
|
||||||
}
|
}
|
||||||
|
template = template.trim()
|
||||||
|
|
||||||
const parts = [
|
const parts = [
|
||||||
{
|
{
|
||||||
@@ -1657,6 +1658,8 @@ export namespace SessionPrompt {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const agent = await Agent.get(agentName)
|
const agent = await Agent.get(agentName)
|
||||||
|
let result: MessageV2.WithParts
|
||||||
|
|
||||||
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
|
if ((agent.mode === "subagent" && command.subtask !== false) || command.subtask === true) {
|
||||||
using abort = lock(input.sessionID)
|
using abort = lock(input.sessionID)
|
||||||
|
|
||||||
@@ -1732,7 +1735,7 @@ export namespace SessionPrompt {
|
|||||||
}
|
}
|
||||||
await Session.updatePart(toolPart)
|
await Session.updatePart(toolPart)
|
||||||
|
|
||||||
const result = await TaskTool.init().then((t) =>
|
const taskResult = await TaskTool.init().then((t) =>
|
||||||
t.execute(args, {
|
t.execute(args, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
abort: abort.signal,
|
abort: abort.signal,
|
||||||
@@ -1760,22 +1763,31 @@ export namespace SessionPrompt {
|
|||||||
},
|
},
|
||||||
input: toolPart.state.input,
|
input: toolPart.state.input,
|
||||||
title: "",
|
title: "",
|
||||||
metadata: result.metadata,
|
metadata: taskResult.metadata,
|
||||||
output: result.output,
|
output: taskResult.output,
|
||||||
}
|
}
|
||||||
await Session.updatePart(toolPart)
|
await Session.updatePart(toolPart)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { info: assistantMsg, parts: [toolPart] }
|
result = { info: assistantMsg, parts: [toolPart] }
|
||||||
|
} else {
|
||||||
|
result = await prompt({
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
messageID: input.messageID,
|
||||||
|
model,
|
||||||
|
agent: agentName,
|
||||||
|
parts,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return prompt({
|
Bus.publish(Command.Event.Executed, {
|
||||||
|
name: input.command,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: input.messageID,
|
arguments: input.arguments,
|
||||||
model,
|
messageID: result.info.id,
|
||||||
agent: agentName,
|
|
||||||
parts,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureTitle(input: {
|
async function ensureTitle(input: {
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { generateText, type ModelMessage } from "ai"
|
|||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { Identifier } from "@/id/id"
|
import { Identifier } from "@/id/id"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
|
|
||||||
import { ProviderTransform } from "@/provider/transform"
|
import { ProviderTransform } from "@/provider/transform"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
|
import path from "path"
|
||||||
|
import { Instance } from "@/project/instance"
|
||||||
|
|
||||||
export namespace SessionSummary {
|
export namespace SessionSummary {
|
||||||
const log = Log.create({ service: "session.summary" })
|
const log = Log.create({ service: "session.summary" })
|
||||||
@@ -33,10 +34,13 @@ export namespace SessionSummary {
|
|||||||
input.messages
|
input.messages
|
||||||
.flatMap((x) => x.parts)
|
.flatMap((x) => x.parts)
|
||||||
.filter((x) => x.type === "patch")
|
.filter((x) => x.type === "patch")
|
||||||
.flatMap((x) => x.files),
|
.flatMap((x) => x.files)
|
||||||
|
.map((x) => path.relative(Instance.worktree, x)),
|
||||||
)
|
)
|
||||||
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
|
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
|
||||||
x.filter((x) => files.has(x.file)),
|
x.filter((x) => {
|
||||||
|
return files.has(x.file)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
await Session.update(input.sessionID, (draft) => {
|
await Session.update(input.sessionID, (draft) => {
|
||||||
draft.summary = {
|
draft.summary = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const GrepTool = Tool.define("grep", {
|
|||||||
const searchPath = params.path || Instance.directory
|
const searchPath = params.path || Instance.directory
|
||||||
|
|
||||||
const rgPath = await Ripgrep.filepath()
|
const rgPath = await Ripgrep.filepath()
|
||||||
const args = ["-nH", "--field-match-separator=|", params.pattern]
|
const args = ["-nH", "--field-match-separator=|", "--regexp", params.pattern]
|
||||||
if (params.include) {
|
if (params.include) {
|
||||||
args.push("--glob", params.include)
|
args.push("--glob", params.include)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Usage notes:
|
|||||||
Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):
|
Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):
|
||||||
|
|
||||||
<example_agent_descriptions>
|
<example_agent_descriptions>
|
||||||
"code-reviewer": use this agent after you are done writing a signficant piece of code
|
"code-reviewer": use this agent after you are done writing a significant piece of code
|
||||||
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
||||||
</example_agent_description>
|
</example_agent_description>
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ function isPrime(n) {
|
|||||||
}
|
}
|
||||||
</code>
|
</code>
|
||||||
<commentary>
|
<commentary>
|
||||||
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
Since a significant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
||||||
</commentary>
|
</commentary>
|
||||||
assistant: Now let me use the code-reviewer agent to review the code
|
assistant: Now let me use the code-reviewer agent to review the code
|
||||||
assistant: Uses the Task tool to launch the code-reviewer agent
|
assistant: Uses the Task tool to launch the code-reviewer agent
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ describe("Keybind.toString", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("should convert shift modifier to string", () => {
|
test("should convert shift modifier to string", () => {
|
||||||
const info: Keybind.Info = { ctrl: false, meta: false, shift: true, leader: false, name: "enter" }
|
const info: Keybind.Info = {
|
||||||
expect(Keybind.toString(info)).toBe("shift+enter")
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: true,
|
||||||
|
leader: false,
|
||||||
|
name: "return",
|
||||||
|
}
|
||||||
|
expect(Keybind.toString(info)).toBe("shift+return")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should convert function key to string", () => {
|
test("should convert function key to string", () => {
|
||||||
@@ -38,7 +44,13 @@ describe("Keybind.toString", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("should convert special key to string", () => {
|
test("should convert special key to string", () => {
|
||||||
const info: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: false, name: "pgup" }
|
const info: Keybind.Info = {
|
||||||
|
ctrl: false,
|
||||||
|
meta: false,
|
||||||
|
shift: false,
|
||||||
|
leader: false,
|
||||||
|
name: "pgup",
|
||||||
|
}
|
||||||
expect(Keybind.toString(info)).toBe("pgup")
|
expect(Keybind.toString(info)).toBe("pgup")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,15 +232,15 @@ describe("Keybind.parse", () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should parse shift+enter combination", () => {
|
test("should parse shift+return combination", () => {
|
||||||
const result = Keybind.parse("shift+enter")
|
const result = Keybind.parse("shift+return")
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
meta: false,
|
meta: false,
|
||||||
shift: true,
|
shift: true,
|
||||||
leader: false,
|
leader: false,
|
||||||
name: "enter",
|
name: "return",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
if (process.versions.bun !== "1.3.0") {
|
const rootPkgPath = path.resolve(import.meta.dir, "../../../package.json")
|
||||||
throw new Error("This script requires bun@1.3.0")
|
const rootPkg = await Bun.file(rootPkgPath).json()
|
||||||
|
const expectedBunVersion = rootPkg.packageManager?.split("@")[1]
|
||||||
|
|
||||||
|
if (!expectedBunVersion) {
|
||||||
|
throw new Error("packageManager field not found in root package.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.versions.bun !== expectedBunVersion) {
|
||||||
|
throw new Error(
|
||||||
|
`This script requires bun@${expectedBunVersion}, but you are using bun@${process.versions.bun}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANNEL =
|
const CHANNEL =
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ import type {
|
|||||||
McpStatusResponses,
|
McpStatusResponses,
|
||||||
LspStatusData,
|
LspStatusData,
|
||||||
LspStatusResponses,
|
LspStatusResponses,
|
||||||
|
FormatterStatusData,
|
||||||
|
FormatterStatusResponses,
|
||||||
TuiAppendPromptData,
|
TuiAppendPromptData,
|
||||||
TuiAppendPromptResponses,
|
TuiAppendPromptResponses,
|
||||||
TuiAppendPromptErrors,
|
TuiAppendPromptErrors,
|
||||||
@@ -773,6 +775,20 @@ class Lsp extends _HeyApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Formatter extends _HeyApiClient {
|
||||||
|
/**
|
||||||
|
* Get formatter status
|
||||||
|
*/
|
||||||
|
public status<ThrowOnError extends boolean = false>(
|
||||||
|
options?: Options<FormatterStatusData, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
return (options?.client ?? this._client).get<FormatterStatusResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/formatter",
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Control extends _HeyApiClient {
|
class Control extends _HeyApiClient {
|
||||||
/**
|
/**
|
||||||
* Get the next TUI request from the queue
|
* Get the next TUI request from the queue
|
||||||
@@ -1023,6 +1039,7 @@ export class OpencodeClient extends _HeyApiClient {
|
|||||||
app = new App({ client: this._client })
|
app = new App({ client: this._client })
|
||||||
mcp = new Mcp({ client: this._client })
|
mcp = new Mcp({ client: this._client })
|
||||||
lsp = new Lsp({ client: this._client })
|
lsp = new Lsp({ client: this._client })
|
||||||
|
formatter = new Formatter({ client: this._client })
|
||||||
tui = new Tui({ client: this._client })
|
tui = new Tui({ client: this._client })
|
||||||
auth = new Auth({ client: this._client })
|
auth = new Auth({ client: this._client })
|
||||||
event = new Event({ client: this._client })
|
event = new Event({ client: this._client })
|
||||||
|
|||||||
@@ -114,6 +114,14 @@ export type KeybindsConfig = {
|
|||||||
* List available models
|
* List available models
|
||||||
*/
|
*/
|
||||||
model_list?: string
|
model_list?: string
|
||||||
|
/**
|
||||||
|
* Next recently used model
|
||||||
|
*/
|
||||||
|
model_cycle_recent?: string
|
||||||
|
/**
|
||||||
|
* Previous recently used model
|
||||||
|
*/
|
||||||
|
model_cycle_recent_reverse?: string
|
||||||
/**
|
/**
|
||||||
* List available commands
|
* List available commands
|
||||||
*/
|
*/
|
||||||
@@ -134,6 +142,10 @@ export type KeybindsConfig = {
|
|||||||
* Clear input field
|
* Clear input field
|
||||||
*/
|
*/
|
||||||
input_clear?: string
|
input_clear?: string
|
||||||
|
/**
|
||||||
|
* Forward delete
|
||||||
|
*/
|
||||||
|
input_forward_delete?: string
|
||||||
/**
|
/**
|
||||||
* Paste from clipboard
|
* Paste from clipboard
|
||||||
*/
|
*/
|
||||||
@@ -146,6 +158,22 @@ export type KeybindsConfig = {
|
|||||||
* Insert newline in input
|
* Insert newline in input
|
||||||
*/
|
*/
|
||||||
input_newline?: string
|
input_newline?: string
|
||||||
|
/**
|
||||||
|
* Previous history item
|
||||||
|
*/
|
||||||
|
history_previous?: string
|
||||||
|
/**
|
||||||
|
* Previous history item
|
||||||
|
*/
|
||||||
|
history_next?: string
|
||||||
|
/**
|
||||||
|
* Next child session
|
||||||
|
*/
|
||||||
|
session_child_cycle?: string
|
||||||
|
/**
|
||||||
|
* Previous child session
|
||||||
|
*/
|
||||||
|
session_child_cycle_reverse?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentConfig = {
|
export type AgentConfig = {
|
||||||
@@ -1050,6 +1078,12 @@ export type LspStatus = {
|
|||||||
status: "connected" | "error"
|
status: "connected" | "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FormatterStatus = {
|
||||||
|
name: string
|
||||||
|
extensions: Array<string>
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type EventTuiPromptAppend = {
|
export type EventTuiPromptAppend = {
|
||||||
type: "tui.prompt.append"
|
type: "tui.prompt.append"
|
||||||
properties: {
|
properties: {
|
||||||
@@ -1228,6 +1262,16 @@ export type EventTodoUpdated = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventCommandExecuted = {
|
||||||
|
type: "command.executed"
|
||||||
|
properties: {
|
||||||
|
name: string
|
||||||
|
sessionID: string
|
||||||
|
arguments: string
|
||||||
|
messageID: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type EventSessionIdle = {
|
export type EventSessionIdle = {
|
||||||
type: "session.idle"
|
type: "session.idle"
|
||||||
properties: {
|
properties: {
|
||||||
@@ -1290,6 +1334,7 @@ export type Event =
|
|||||||
| EventFileEdited
|
| EventFileEdited
|
||||||
| EventFileWatcherUpdated
|
| EventFileWatcherUpdated
|
||||||
| EventTodoUpdated
|
| EventTodoUpdated
|
||||||
|
| EventCommandExecuted
|
||||||
| EventSessionIdle
|
| EventSessionIdle
|
||||||
| EventSessionCreated
|
| EventSessionCreated
|
||||||
| EventSessionUpdated
|
| EventSessionUpdated
|
||||||
@@ -2491,6 +2536,24 @@ export type LspStatusResponses = {
|
|||||||
|
|
||||||
export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses]
|
export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses]
|
||||||
|
|
||||||
|
export type FormatterStatusData = {
|
||||||
|
body?: never
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
|
url: "/formatter"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormatterStatusResponses = {
|
||||||
|
/**
|
||||||
|
* Formatter status
|
||||||
|
*/
|
||||||
|
200: Array<FormatterStatus>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
|
||||||
|
|
||||||
export type TuiAppendPromptData = {
|
export type TuiAppendPromptData = {
|
||||||
body?: {
|
body?: {
|
||||||
text: string
|
text: string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/components/index.ts",
|
".": "./src/components/index.ts",
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
color: var(--text-base);
|
color: var(--text-base);
|
||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
|
|
||||||
/* text-14-regular */
|
/* text-12-regular */
|
||||||
font-family: var(--font-family-sans);
|
font-family: var(--font-family-sans);
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-small);
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
line-height: var(--line-height-large); /* 142.857% */
|
line-height: var(--line-height-large); /* 166.667% */
|
||||||
letter-spacing: var(--letter-spacing-normal);
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -11,3 +11,110 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fadeUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-up-text {
|
||||||
|
animation: fadeUp 0.4s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(4) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(6) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
&:nth-child(7) {
|
||||||
|
animation-delay: 0.7s;
|
||||||
|
}
|
||||||
|
&:nth-child(8) {
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
&:nth-child(9) {
|
||||||
|
animation-delay: 0.9s;
|
||||||
|
}
|
||||||
|
&:nth-child(10) {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
&:nth-child(11) {
|
||||||
|
animation-delay: 1.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(12) {
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(13) {
|
||||||
|
animation-delay: 1.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(14) {
|
||||||
|
animation-delay: 1.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(15) {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(16) {
|
||||||
|
animation-delay: 1.6s;
|
||||||
|
}
|
||||||
|
&:nth-child(17) {
|
||||||
|
animation-delay: 1.7s;
|
||||||
|
}
|
||||||
|
&:nth-child(18) {
|
||||||
|
animation-delay: 1.8s;
|
||||||
|
}
|
||||||
|
&:nth-child(19) {
|
||||||
|
animation-delay: 1.9s;
|
||||||
|
}
|
||||||
|
&:nth-child(20) {
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
&:nth-child(21) {
|
||||||
|
animation-delay: 2.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(22) {
|
||||||
|
animation-delay: 2.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(23) {
|
||||||
|
animation-delay: 2.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(24) {
|
||||||
|
animation-delay: 2.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(25) {
|
||||||
|
animation-delay: 2.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(26) {
|
||||||
|
animation-delay: 2.6s;
|
||||||
|
}
|
||||||
|
&:nth-child(27) {
|
||||||
|
animation-delay: 2.7s;
|
||||||
|
}
|
||||||
|
&:nth-child(28) {
|
||||||
|
animation-delay: 2.8s;
|
||||||
|
}
|
||||||
|
&:nth-child(29) {
|
||||||
|
animation-delay: 2.9s;
|
||||||
|
}
|
||||||
|
&:nth-child(30) {
|
||||||
|
animation-delay: 3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
--shadow-xs: var(--shadow-xs);
|
--shadow-xs: var(--shadow-xs);
|
||||||
--shadow-md: var(--shadow-md);
|
--shadow-md: var(--shadow-md);
|
||||||
--shadow-xs-border-selected: var(--shadow-xs-border-selected);
|
--shadow-xs-border-select: var(--shadow-xs-border-select);
|
||||||
|
|
||||||
--animate-pulse: var(--animate-pulse);
|
--animate-pulse: var(--animate-pulse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,3 +15,99 @@
|
|||||||
direction: rtl;
|
direction: rtl;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility fade-up-text {
|
||||||
|
animation: fadeUp 0.4s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(4) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(6) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
&:nth-child(7) {
|
||||||
|
animation-delay: 0.7s;
|
||||||
|
}
|
||||||
|
&:nth-child(8) {
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
&:nth-child(9) {
|
||||||
|
animation-delay: 0.9s;
|
||||||
|
}
|
||||||
|
&:nth-child(10) {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
&:nth-child(11) {
|
||||||
|
animation-delay: 1.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(12) {
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(13) {
|
||||||
|
animation-delay: 1.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(14) {
|
||||||
|
animation-delay: 1.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(15) {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(16) {
|
||||||
|
animation-delay: 1.6s;
|
||||||
|
}
|
||||||
|
&:nth-child(17) {
|
||||||
|
animation-delay: 1.7s;
|
||||||
|
}
|
||||||
|
&:nth-child(18) {
|
||||||
|
animation-delay: 1.8s;
|
||||||
|
}
|
||||||
|
&:nth-child(19) {
|
||||||
|
animation-delay: 1.9s;
|
||||||
|
}
|
||||||
|
&:nth-child(20) {
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
&:nth-child(21) {
|
||||||
|
animation-delay: 2.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(22) {
|
||||||
|
animation-delay: 2.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(23) {
|
||||||
|
animation-delay: 2.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(24) {
|
||||||
|
animation-delay: 2.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(25) {
|
||||||
|
animation-delay: 2.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(26) {
|
||||||
|
animation-delay: 2.6s;
|
||||||
|
}
|
||||||
|
&:nth-child(27) {
|
||||||
|
animation-delay: 2.7s;
|
||||||
|
}
|
||||||
|
&:nth-child(28) {
|
||||||
|
animation-delay: 2.8s;
|
||||||
|
}
|
||||||
|
&:nth-child(29) {
|
||||||
|
animation-delay: 2.9s;
|
||||||
|
}
|
||||||
|
&:nth-child(30) {
|
||||||
|
animation-delay: 3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ The new TUI works like the old one since it connects to the same opencode server
|
|||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
You will not be autoupgraded to 1.0 if you are currently using a previous
|
You should not be autoupgraded to 1.0 if you are currently using a previous
|
||||||
version.
|
version. However some older versions of OpenCode always grab latest.
|
||||||
|
|
||||||
|
|
||||||
To upgrade manually, run
|
To upgrade manually, run
|
||||||
@@ -22,6 +22,11 @@ To upgrade manually, run
|
|||||||
$ opencode upgrade 1.0.0
|
$ opencode upgrade 1.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To downgrade back to 0.x, run
|
||||||
|
```bash
|
||||||
|
$ opencode upgrade 0.15.31
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## UX changes
|
## UX changes
|
||||||
@@ -59,7 +64,3 @@ We removed some functionality that we weren't sure anyone actually used. If some
|
|||||||
- project_init
|
- project_init
|
||||||
- tool_details
|
- tool_details
|
||||||
- thinking_blocks
|
- thinking_blocks
|
||||||
- session_child_cycle
|
|
||||||
- session_child_cycle_reverse
|
|
||||||
- model_cycle_recent
|
|
||||||
- model_cycle_recent_reverse
|
|
||||||
|
|||||||
@@ -19,6 +19,28 @@ opencode run "Explain how closures work in JavaScript"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### tui
|
||||||
|
|
||||||
|
Start the OpenCode terminal user interface.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
opencode [project]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Flags
|
||||||
|
|
||||||
|
| Flag | Short | Description |
|
||||||
|
| ------------ | ----- | ------------------------------------------ |
|
||||||
|
| `--continue` | `-c` | Continue the last session |
|
||||||
|
| `--session` | `-s` | Session ID to continue |
|
||||||
|
| `--prompt` | `-p` | Prompt to use |
|
||||||
|
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||||
|
| `--agent` | | Agent to use |
|
||||||
|
| `--port` | | Port to listen on |
|
||||||
|
| `--hostname` | | Hostname to listen on |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
The OpenCode CLI also has the following commands.
|
The OpenCode CLI also has the following commands.
|
||||||
@@ -164,13 +186,17 @@ opencode run Explain the use of context in Go
|
|||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
|
|
||||||
| Flag | Short | Description |
|
| Flag | Short | Description |
|
||||||
| ------------ | ----- | ------------------------------------------ |
|
| ------------ | ----- | ------------------------------------------------------------------ |
|
||||||
| `--continue` | `-c` | Continue the last session |
|
| `--command` | | The command to run, use message for args |
|
||||||
| `--session` | `-s` | Session ID to continue |
|
| `--continue` | `-c` | Continue the last session |
|
||||||
| `--share` | | Share the session |
|
| `--session` | `-s` | Session ID to continue |
|
||||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
| `--share` | | Share the session |
|
||||||
| `--agent` | | Agent to use |
|
| `--model` | `-m` | Model to use in the form of provider/model |
|
||||||
|
| `--agent` | | Agent to use |
|
||||||
|
| `--file` | `-f` | File(s) to attach to message |
|
||||||
|
| `--format` | | Format: default (formatted) or json (raw JSON events) |
|
||||||
|
| `--title` | | Title for the session (uses truncated prompt if no value provided) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -189,7 +215,7 @@ This starts an HTTP server that provides API access to opencode functionality wi
|
|||||||
| Flag | Short | Description |
|
| Flag | Short | Description |
|
||||||
| ------------ | ----- | --------------------- |
|
| ------------ | ----- | --------------------- |
|
||||||
| `--port` | `-p` | Port to listen on |
|
| `--port` | `-p` | Port to listen on |
|
||||||
| `--hostname` | `-h` | Hostname to listen on |
|
| `--hostname` | | Hostname to listen on |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -221,18 +247,13 @@ opencode upgrade v0.1.48
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Flags
|
## Global Flags
|
||||||
|
|
||||||
The opencode CLI takes the following global flags.
|
The opencode CLI takes the following global flags.
|
||||||
|
|
||||||
| Flag | Short | Description |
|
| Flag | Short | Description |
|
||||||
| -------------- | ----- | ------------------------------------------ |
|
| -------------- | ----- | ------------------------------------ |
|
||||||
| `--help` | `-h` | Display help |
|
| `--help` | `-h` | Display help |
|
||||||
| `--version` | | Print version number |
|
| `--version` | `-v` | Print version number |
|
||||||
| `--print-logs` | | Print logs to stderr |
|
| `--print-logs` | | Print logs to stderr |
|
||||||
| `--log-level` | | Log level (DEBUG, INFO, WARN, ERROR) |
|
| `--log-level` | | Log level (DEBUG, INFO, WARN, ERROR) |
|
||||||
| `--prompt` | `-p` | Prompt to use |
|
|
||||||
| `--model` | `-m` | Model to use in the form of provider/model |
|
|
||||||
| `--agent` | | Agent to use |
|
|
||||||
| `--port` | | Port to listen on |
|
|
||||||
| `--hostname` | | Hostname to listen on |
|
|
||||||
|
|||||||
@@ -10,41 +10,43 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
|
|||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"keybinds": {
|
"keybinds": {
|
||||||
"leader": "ctrl+x",
|
"leader": "ctrl+x",
|
||||||
"app_help": "<leader>h",
|
"app_exit": "ctrl+c,ctrl+d,<leader>q",
|
||||||
"app_exit": "ctrl+c,<leader>q",
|
|
||||||
"editor_open": "<leader>e",
|
"editor_open": "<leader>e",
|
||||||
"theme_list": "<leader>t",
|
"theme_list": "<leader>t",
|
||||||
"project_init": "<leader>i",
|
"sidebar_toggle": "<leader>b",
|
||||||
"tool_details": "<leader>d",
|
"status_view": "<leader>s",
|
||||||
"thinking_blocks": "<leader>b",
|
|
||||||
"session_export": "<leader>x",
|
"session_export": "<leader>x",
|
||||||
"session_new": "<leader>n",
|
"session_new": "<leader>n",
|
||||||
"session_list": "<leader>l",
|
"session_list": "<leader>l",
|
||||||
"session_share": "<leader>s",
|
"session_timeline": "<leader>g",
|
||||||
|
"session_share": "none",
|
||||||
"session_unshare": "none",
|
"session_unshare": "none",
|
||||||
"session_interrupt": "esc",
|
"session_interrupt": "escape",
|
||||||
"session_compact": "<leader>c",
|
"session_compact": "<leader>c",
|
||||||
"session_child_cycle": "ctrl+right",
|
"messages_page_up": "pageup",
|
||||||
"session_child_cycle_reverse": "ctrl+left",
|
"messages_page_down": "pagedown",
|
||||||
"messages_page_up": "pgup",
|
|
||||||
"messages_page_down": "pgdown",
|
|
||||||
"messages_half_page_up": "ctrl+alt+u",
|
"messages_half_page_up": "ctrl+alt+u",
|
||||||
"messages_half_page_down": "ctrl+alt+d",
|
"messages_half_page_down": "ctrl+alt+d",
|
||||||
"messages_first": "ctrl+g",
|
"messages_first": "ctrl+g,home",
|
||||||
"messages_last": "ctrl+alt+g",
|
"messages_last": "ctrl+alt+g,end",
|
||||||
"messages_copy": "<leader>y",
|
"messages_copy": "<leader>y",
|
||||||
"messages_undo": "<leader>u",
|
"messages_undo": "<leader>u",
|
||||||
"messages_redo": "<leader>r",
|
"messages_redo": "<leader>r",
|
||||||
|
"messages_toggle_conceal": "<leader>h",
|
||||||
"model_list": "<leader>m",
|
"model_list": "<leader>m",
|
||||||
"model_cycle_recent": "f2",
|
"model_cycle_recent": "f2",
|
||||||
"model_cycle_recent_reverse": "shift+f2",
|
"model_cycle_recent_reverse": "shift+f2",
|
||||||
|
"command_list": "ctrl+p",
|
||||||
"agent_list": "<leader>a",
|
"agent_list": "<leader>a",
|
||||||
"agent_cycle": "tab",
|
"agent_cycle": "tab",
|
||||||
"agent_cycle_reverse": "shift+tab",
|
"agent_cycle_reverse": "shift+tab",
|
||||||
"input_clear": "ctrl+c",
|
"input_clear": "ctrl+c",
|
||||||
|
"input_forward_delete": "ctrl+d",
|
||||||
"input_paste": "ctrl+v",
|
"input_paste": "ctrl+v",
|
||||||
"input_submit": "enter",
|
"input_submit": "enter",
|
||||||
"input_newline": "shift+enter,ctrl+j"
|
"input_newline": "shift+enter,ctrl+j",
|
||||||
|
"history_previous": "up",
|
||||||
|
"history_next": "down"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -444,6 +444,45 @@ To use Google Vertex AI with OpenCode:
|
|||||||
|
|
||||||
4. Run the `/models` command to select the one you want.
|
4. Run the `/models` command to select the one you want.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hugging Face
|
||||||
|
|
||||||
|
[Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers) provides access to open models supported by 17+ providers.
|
||||||
|
|
||||||
|
1. Head over to [Hugging Face settings](https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained) to create a token with permission to make calls to Inference Providers.
|
||||||
|
|
||||||
|
2. Run `opencode auth login` and select **Hugging Face**.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ opencode auth login
|
||||||
|
|
||||||
|
┌ Add credential
|
||||||
|
│
|
||||||
|
◆ Select provider
|
||||||
|
│ ● Hugging Face
|
||||||
|
│ ...
|
||||||
|
└
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Enter your Hugging Face token.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ opencode auth login
|
||||||
|
|
||||||
|
┌ Add credential
|
||||||
|
│
|
||||||
|
◇ Select provider
|
||||||
|
│ Hugging Face
|
||||||
|
│
|
||||||
|
◇ Enter your API key
|
||||||
|
│ _
|
||||||
|
└
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the `/models` command to select a model like _Kimi-K2-Instruct_ or _GLM-4.6_.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### LM Studio
|
### LM Studio
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "1.0.2",
|
"version": "1.0.13",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Reference in New Issue
Block a user