Compare commits
122
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a3be31de | ||
|
|
d16c8c9f0f | ||
|
|
fffe20cbe5 | ||
|
|
f6da3c467b | ||
|
|
c0d9f21c0f | ||
|
|
a67b616139 | ||
|
|
2991547974 | ||
|
|
b59def2e4a | ||
|
|
d842353f39 | ||
|
|
d20ef569de | ||
|
|
ca2b871810 | ||
|
|
23ea8ba1ce | ||
|
|
c417fec246 | ||
|
|
5413b16b57 | ||
|
|
43c021ed80 | ||
|
|
3b005d29d7 | ||
|
|
635f70f477 | ||
|
|
adbb6037ac | ||
|
|
598d6d00e4 | ||
|
|
cf934357c9 | ||
|
|
8063e645c7 | ||
|
|
8ab206b443 | ||
|
|
ec5c96e10d | ||
|
|
d2a61290b9 | ||
|
|
10faf9e717 | ||
|
|
cba239bc8f | ||
|
|
6f5e3ddfb3 | ||
|
|
d412ba264a | ||
|
|
9780f2b792 | ||
|
|
5aa2078852 | ||
|
|
eb975bb89c | ||
|
|
9479fe3ce6 | ||
|
|
4393cf8dbe | ||
|
|
447a4ca8c3 | ||
|
|
40ac2549ff | ||
|
|
a9c56b813a | ||
|
|
b1b73c9deb | ||
|
|
774377330b | ||
|
|
e7a157ef8f | ||
|
|
3989b9fc7f | ||
|
|
8bfcdf4831 | ||
|
|
3632ba3785 | ||
|
|
b7b3824d76 | ||
|
|
b12efb2023 | ||
|
|
bd91cf220c | ||
|
|
9eb6731c21 | ||
|
|
11373755d9 | ||
|
|
00b5e9f6ca | ||
|
|
6b3f424e4d | ||
|
|
e7dfeec9c4 | ||
|
|
97893bd7e6 | ||
|
|
bfefdb3752 | ||
|
|
12b79c581e | ||
|
|
ac9b4c7ebf | ||
|
|
208af232ff | ||
|
|
600c6b4973 | ||
|
|
61007a9b94 | ||
|
|
52fe1a5ac5 | ||
|
|
468927e06a | ||
|
|
61562dd9f0 | ||
|
|
c86dd91310 | ||
|
|
9c85a37811 | ||
|
|
51bba6e634 | ||
|
|
e1089bc5de | ||
|
|
618c654aa0 | ||
|
|
4703e859bd | ||
|
|
a1dc4ebbe4 | ||
|
|
e4e6096510 | ||
|
|
c472734933 | ||
|
|
9d068c20bb | ||
|
|
48e4f2f45d | ||
|
|
bbf4574476 | ||
|
|
8bad513140 | ||
|
|
1ff5d888c2 | ||
|
|
5d25758400 | ||
|
|
16fdc90976 | ||
|
|
793542230f | ||
|
|
9de1242d9b | ||
|
|
b3afa84058 | ||
|
|
024a10bbb5 | ||
|
|
bef9ac96e2 | ||
|
|
24bb293136 | ||
|
|
45180104fe | ||
|
|
edd86e3fb7 | ||
|
|
4a72d57534 | ||
|
|
0068cb305f | ||
|
|
90044196bf | ||
|
|
963a926db2 | ||
|
|
0d3d48bb59 | ||
|
|
66eaba4bdc | ||
|
|
21b6e5404e | ||
|
|
a0fe59ab75 | ||
|
|
81ebf56cf1 | ||
|
|
429708e3d5 | ||
|
|
d50f825c6d | ||
|
|
47bfae52c0 | ||
|
|
52cf9e3423 | ||
|
|
a9b6debfa2 | ||
|
|
d6bf475749 | ||
|
|
f22580e943 | ||
|
|
6d98db57c7 | ||
|
|
59f127a250 | ||
|
|
3068e7dcf7 | ||
|
|
f83d62191a | ||
|
|
3b72857124 | ||
|
|
68cd105d9d | ||
|
|
e09af2cb4b | ||
|
|
14bd3b1d30 | ||
|
|
3a9c2152f7 | ||
|
|
7283bfa480 | ||
|
|
37d5099728 | ||
|
|
d45fc030b2 | ||
|
|
c7042c807f | ||
|
|
715265de4b | ||
|
|
2048f32491 | ||
|
|
2d7ba43a21 | ||
|
|
bd198d8550 | ||
|
|
cbffbcdd3d | ||
|
|
2be8b2269f | ||
|
|
c1fa257a92 | ||
|
|
7fd81dd93e | ||
|
|
d554e7aaef |
@@ -28,14 +28,14 @@ jobs:
|
|||||||
const versionPattern = /[v]?1\.0\./i;
|
const versionPattern = /[v]?1\.0\./i;
|
||||||
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
||||||
|
|
||||||
|
// Check for "nix" keyword
|
||||||
|
const nixPattern = /\bnix\b/i;
|
||||||
|
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
|
||||||
|
|
||||||
|
const labels = [];
|
||||||
|
|
||||||
if (isWebRelated) {
|
if (isWebRelated) {
|
||||||
// Add web label
|
labels.push('web');
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: issue.number,
|
|
||||||
labels: ['web']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assign to adamdotdevin
|
// Assign to adamdotdevin
|
||||||
await github.rest.issues.addAssignees({
|
await github.rest.issues.addAssignees({
|
||||||
@@ -46,10 +46,18 @@ jobs:
|
|||||||
});
|
});
|
||||||
} else if (isVersionRelated) {
|
} else if (isVersionRelated) {
|
||||||
// Only add opentui if NOT web-related
|
// Only add opentui if NOT web-related
|
||||||
|
labels.push('opentui');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNixRelated) {
|
||||||
|
labels.push('nix');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labels.length > 0) {
|
||||||
await github.rest.issues.addLabels({
|
await github.rest.issues.addLabels({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: issue.number,
|
issue_number: issue.number,
|
||||||
labels: ['opentui']
|
labels: labels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ jobs:
|
|||||||
git config --global user.email "action@github.com"
|
git config --global user.email "action@github.com"
|
||||||
git config --global user.name "Github Action"
|
git config --global user.name "Github Action"
|
||||||
|
|
||||||
|
- name: Update flake.lock
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix flake update
|
||||||
|
|
||||||
- name: Update node_modules hash
|
- name: Update node_modules hash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -62,7 +67,7 @@ jobs:
|
|||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
}
|
}
|
||||||
|
|
||||||
FILES=(flake.nix nix/node-modules.nix nix/hashes.json)
|
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
||||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||||
if [ -z "$STATUS" ]; then
|
if [ -z "$STATUS" ]; then
|
||||||
summarize "no changes"
|
summarize "no changes"
|
||||||
@@ -71,7 +76,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
git add "${FILES[@]}"
|
git add "${FILES[@]}"
|
||||||
git commit -m "Update Nix hashes"
|
git commit -m "Update Nix flake.lock and hashes"
|
||||||
|
|
||||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||||
git push origin HEAD:"$BRANCH"
|
git push origin HEAD:"$BRANCH"
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ dist
|
|||||||
refs
|
refs
|
||||||
Session.vim
|
Session.vim
|
||||||
opencode.json
|
opencode.json
|
||||||
|
a.out
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
|||||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although Anthropic is recommended, OpenCode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||||
- Out of the box LSP support
|
- Out of the box LSP support
|
||||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|||||||
@@ -144,3 +144,5 @@
|
|||||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||||
|
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||||
|
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"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",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"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",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"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",
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -180,6 +180,7 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.5.1",
|
||||||
|
"@ai-sdk/mcp": "0.0.8",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
@@ -190,8 +191,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.1.46",
|
"@opentui/core": "0.1.47",
|
||||||
"@opentui/solid": "0.1.46",
|
"@opentui/solid": "0.1.47",
|
||||||
"@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",
|
||||||
@@ -250,7 +251,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -270,7 +271,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.81.0",
|
"@hey-api/openapi-ts": "0.81.0",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -281,7 +282,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -294,7 +295,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -322,9 +323,19 @@
|
|||||||
"vite-plugin-solid": "catalog:",
|
"vite-plugin-solid": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/util": {
|
||||||
|
"name": "@opencode-ai/util",
|
||||||
|
"version": "1.0.85",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "catalog:",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -378,7 +389,7 @@
|
|||||||
"@types/bun": "1.3.0",
|
"@types/bun": "1.3.0",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
"@typescript/native-preview": "7.0.0-dev.20251014.1",
|
||||||
"ai": "5.0.8",
|
"ai": "5.0.97",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"hono": "4.7.10",
|
"hono": "4.7.10",
|
||||||
@@ -413,12 +424,14 @@
|
|||||||
|
|
||||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
|
||||||
|
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ=="],
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="],
|
||||||
|
|
||||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-dnVIgSz1DZD/0gVau6ifYN3HZFN15HZwC9VjevTFfvrfSfbEvpXj5x/k/zk/0XuQrlQ5g8JiwJtxc9bx24x2xw=="],
|
"@ai-sdk/google": ["@ai-sdk/google@2.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-dnVIgSz1DZD/0gVau6ifYN3HZFN15HZwC9VjevTFfvrfSfbEvpXj5x/k/zk/0XuQrlQ5g8JiwJtxc9bx24x2xw=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.16", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.9", "@ai-sdk/google": "2.0.11", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-tStlnOCRGRqKKJSCOtXhijX4r9kYVK2v+Vs7miJnfvr3sZfO8nRS0xnNhfgu17xuNi5LMMufeCYURTz4lKxzUQ=="],
|
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.16", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.9", "@ai-sdk/google": "2.0.11", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.7", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-tStlnOCRGRqKKJSCOtXhijX4r9kYVK2v+Vs7miJnfvr3sZfO8nRS0xnNhfgu17xuNi5LMMufeCYURTz4lKxzUQ=="],
|
||||||
|
|
||||||
|
"@ai-sdk/mcp": ["@ai-sdk/mcp@0.0.8", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9y9GuGcZ9/+pMIHfpOCJgZVp+AZMv6TkjX2NVT17SQZvTF2N8LXuCXyoUPyi1PxIxzxl0n463LxxaB2O6olC+Q=="],
|
||||||
|
|
||||||
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
|
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
|
||||||
|
|
||||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
|
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="],
|
||||||
@@ -965,25 +978,27 @@
|
|||||||
|
|
||||||
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
|
||||||
|
|
||||||
|
"@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"],
|
||||||
|
|
||||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||||
|
|
||||||
"@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.1.46", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.46", "@opentui/core-darwin-x64": "0.1.46", "@opentui/core-linux-arm64": "0.1.46", "@opentui/core-linux-x64": "0.1.46", "@opentui/core-win32-arm64": "0.1.46", "@opentui/core-win32-x64": "0.1.46", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-9682jrr65vYP0hPHfrZRK3xymlCSLVBMrRKNtclFasDi6bRvACUrtziFOIIyMIvPHRJCFWPbtz0MppARmN4zvQ=="],
|
"@opentui/core": ["@opentui/core@0.1.47", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.47", "@opentui/core-darwin-x64": "0.1.47", "@opentui/core-linux-arm64": "0.1.47", "@opentui/core-linux-x64": "0.1.47", "@opentui/core-win32-arm64": "0.1.47", "@opentui/core-win32-x64": "0.1.47", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-gKcYX9EJ/e5VLEwBH2kalDr5xoI9MEanzQV7uV3Sb2Z9+ndwEUShKKna3odN8g4E20c4sX2VpwmB9hhl3Tsd9w=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.46", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sp/uFS8J/1yVfhgkTJ43OZfy64hv1/9sdT+oC5yb8XTNPI3QGtg6ixjr3nRoD/Lkxuj2i5SJ30RZufqH6rkCpA=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.47", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0/u4VkJJPvW24cZzMaKf6Dm+VzeO1a94l6NV3AQ1Wb+pPTEyOmNWkRvj03ZrRLMCyQduaFVtlnor8DVCk6OHuQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.46", "", { "os": "darwin", "cpu": "x64" }, "sha512-JtxEruRyLQRK8ByPmBm1nYYSvnX6mXNC+mngvd5RDiCzLzkM6qVBQBd/m3Hxp2/s6MO5Z2+iVBzZ8XFH5T4IZw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.47", "", { "os": "darwin", "cpu": "x64" }, "sha512-y1+c/e+IaZAj5N02GnD+oaubbb5JiW5eKgF0h58kw73iXDMfynuoGOpREz58i1rUFYOMYJGdrSjEHtXk2pD2XA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.46", "", { "os": "linux", "cpu": "arm64" }, "sha512-pN8nR4CwBlkZ5uh5KvoytiXXav2GhkP9cB2d3gPe49c7MBz2XrjGexgb47xjaq0hAVbytv9XUifqdPTcFQdPaQ=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.47", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZESHmqILtfb6FFEfi40JGKl8z0+LhOSoHgfOK1PPyuyRT9Mk8uXeQgPMF5W6Ac0pp4w+uWVC4TrFjijCCSiaUQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.46", "", { "os": "linux", "cpu": "x64" }, "sha512-oH4/FEYZYce9qMQVqGl4+Btw+Mfsf6ybpWIIJUJjXMWWZlAgsTMAWM8m195Oe6WstfFLF+nRH7NUcm/YOsCHnw=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.47", "", { "os": "linux", "cpu": "x64" }, "sha512-qfvy1qshgnZMcAHQ3MS093IBjxM2pPx+kEnW7icsyud60zoJgoUugdN2kjgJiIJiYX3f3PgE68J6CVW2MCtYfQ=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.46", "", { "os": "win32", "cpu": "arm64" }, "sha512-C/rTBJ9bzBcZJRCLIxi9Ka/DANe2SaHtryotseWPk9RDydw7LTHGoi3VtRW0RFijQGqmvFg+31MeNhvY1YZ65Q=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.47", "", { "os": "win32", "cpu": "arm64" }, "sha512-f6OoPnaz303H6fudi8blS+iEcJtlFlcqdBoWnWnJQfN9rLmajW3Yf7RfpNOoLUlDcwxQLyTL/5EHwbcG8D4r7A=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.46", "", { "os": "win32", "cpu": "x64" }, "sha512-d2DXSlA93LbSriX+pDDZ5sMwkcW1+eVoeykxeW4UParSb4/3ceBCD4aSARaZ6yoq0rR1IWOdgKdiihZH4mwdJQ=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.47", "", { "os": "win32", "cpu": "x64" }, "sha512-lQnJg7FucyyTbN/ybTj5FZ7S8OAfT5KxXDR5l9Sla7R5MIDY6nBXYM3GWeF81jzDd4K4Z/0hxNFtWSopEXRFYg=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.46", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.46", "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-t+LDXS+FBT+fjQHOxuL44Bx1jGuXuLouyB25BZuypQKKT8sOhi8rJ5+Q4UwH5lrI4OoRBXmzrgsWj7DD58sHDw=="],
|
"@opentui/solid": ["@opentui/solid@0.1.47", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.47", "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-azN2sf8X/6HiLkz8ip2lcY532ApNEkl+BHd+wml/HdwdgLE7nthgA6x8Pgvi7f4qkRmpeYATU+danIzB6K6B8A=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -1495,6 +1510,8 @@
|
|||||||
|
|
||||||
"@vercel/nft": ["@vercel/nft@0.30.3", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w=="],
|
"@vercel/nft": ["@vercel/nft@0.30.3", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^10.4.5", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w=="],
|
||||||
|
|
||||||
|
"@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="],
|
||||||
|
|
||||||
"@vinxi/listhen": ["@vinxi/listhen@1.5.6", "", { "dependencies": { "@parcel/watcher": "^2.3.0", "@parcel/watcher-wasm": "2.3.0", "citty": "^0.1.5", "clipboardy": "^4.0.0", "consola": "^3.2.3", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.10.0", "http-shutdown": "^1.2.2", "jiti": "^1.21.0", "mlly": "^1.5.0", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.3.2", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw=="],
|
"@vinxi/listhen": ["@vinxi/listhen@1.5.6", "", { "dependencies": { "@parcel/watcher": "^2.3.0", "@parcel/watcher-wasm": "2.3.0", "citty": "^0.1.5", "clipboardy": "^4.0.0", "consola": "^3.2.3", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.10.0", "http-shutdown": "^1.2.2", "jiti": "^1.21.0", "mlly": "^1.5.0", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.3.2", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-WSN1z931BtasZJlgPp704zJFnQFRg7yzSjkm3MzAWQYe4uXFXlFr1hc5Ac2zae5/HDOz5x1/zDM5Cb54vTCnWw=="],
|
||||||
|
|
||||||
"@vinxi/plugin-directives": ["@vinxi/plugin-directives@0.5.1", "", { "dependencies": { "@babel/parser": "^7.23.5", "acorn": "^8.10.0", "acorn-jsx": "^5.3.2", "acorn-loose": "^8.3.0", "acorn-typescript": "^1.4.3", "astring": "^1.8.6", "magicast": "^0.2.10", "recast": "^0.23.4", "tslib": "^2.6.2" }, "peerDependencies": { "vinxi": "^0.5.5" } }, "sha512-pH/KIVBvBt7z7cXrUH/9uaqcdxjegFC7+zvkZkdOyWzs+kQD5KPf3cl8kC+5ayzXHT+OMlhGhyitytqN3cGmHg=="],
|
"@vinxi/plugin-directives": ["@vinxi/plugin-directives@0.5.1", "", { "dependencies": { "@babel/parser": "^7.23.5", "acorn": "^8.10.0", "acorn-jsx": "^5.3.2", "acorn-loose": "^8.3.0", "acorn-typescript": "^1.4.3", "astring": "^1.8.6", "magicast": "^0.2.10", "recast": "^0.23.4", "tslib": "^2.6.2" }, "peerDependencies": { "vinxi": "^0.5.5" } }, "sha512-pH/KIVBvBt7z7cXrUH/9uaqcdxjegFC7+zvkZkdOyWzs+kQD5KPf3cl8kC+5ayzXHT+OMlhGhyitytqN3cGmHg=="],
|
||||||
@@ -1529,7 +1546,7 @@
|
|||||||
|
|
||||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||||
|
|
||||||
"ai": ["ai@5.0.8", "", { "dependencies": { "@ai-sdk/gateway": "1.0.4", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-qbnhj046UvG30V1S5WhjBn+RBGEAmi8PSZWqMhRsE3EPxvO5BcePXTZFA23e9MYyWS9zr4Vm8Mv3wQXwLmtIBw=="],
|
"ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
@@ -3603,7 +3620,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"@ai-sdk/amazon-bedrock/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
|
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
|
||||||
|
|
||||||
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
|
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
|
||||||
|
|
||||||
@@ -3611,6 +3628,8 @@
|
|||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
|
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
|
||||||
|
|
||||||
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
||||||
@@ -3819,7 +3838,7 @@
|
|||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
|
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
|
||||||
|
|
||||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762156382,
|
"lastModified": 1763618868,
|
||||||
"narHash": "sha256-Yg7Ag7ov5+36jEFC1DaZh/12SEXo6OO3/8rqADRxiqs=",
|
"narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7241bcbb4f099a66aafca120d37c65e8dda32717",
|
"rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": "sha256-lAU5G92UJ998pKOd0EAt9gUa/HUXXvu6xAUdH2STmDo="
|
"nodeModules": "sha256-bPiUpHGtgwVxHQHXBprpc6fFeJqW6/x7dwtQZBq29oU="
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
"@solidjs/meta": "0.29.4",
|
"@solidjs/meta": "0.29.4",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"ai": "5.0.8",
|
"ai": "5.0.97",
|
||||||
"hono": "4.7.10",
|
"hono": "4.7.10",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
|
|||||||
@@ -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": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||||
"start": "vinxi start",
|
"start": "vinxi start",
|
||||||
"version": "1.0.75"
|
"version": "1.0.85"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
|||||||
@@ -202,6 +202,14 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IconGoogle(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
|
||||||
@@ -228,3 +236,14 @@ export function IconChevronRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IconBreakdown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M2 12L2 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M6 12L6 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M10 12L10 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M14 12L14 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export const config = {
|
|||||||
|
|
||||||
// Static stats (used on landing page)
|
// Static stats (used on landing page)
|
||||||
stats: {
|
stats: {
|
||||||
contributors: "250",
|
contributors: "300",
|
||||||
commits: "3,500",
|
commits: "4,000",
|
||||||
monthlyUsers: "300,000",
|
monthlyUsers: "300,000",
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, Database, eq, gte, inArray, isNull, lte, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
import { and, Database, eq, gte, inArray, isNull, lte, or, sql, sum } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||||
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
||||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
@@ -35,7 +35,7 @@ async function getCosts(workspaceID: string, year: number, month: number) {
|
|||||||
.select({
|
.select({
|
||||||
date: sql<string>`DATE(${UsageTable.timeCreated})`,
|
date: sql<string>`DATE(${UsageTable.timeCreated})`,
|
||||||
model: UsageTable.model,
|
model: UsageTable.model,
|
||||||
totalCost: sql<number>`SUM(${UsageTable.cost})`,
|
totalCost: sum(UsageTable.cost),
|
||||||
keyId: UsageTable.keyID,
|
keyId: UsageTable.keyID,
|
||||||
})
|
})
|
||||||
.from(UsageTable)
|
.from(UsageTable)
|
||||||
@@ -46,7 +46,13 @@ async function getCosts(workspaceID: string, year: number, month: number) {
|
|||||||
lte(UsageTable.timeCreated, endDate),
|
lte(UsageTable.timeCreated, endDate),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID),
|
.groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID)
|
||||||
|
.then((x) =>
|
||||||
|
x.map((r) => ({
|
||||||
|
...r,
|
||||||
|
totalCost: r.totalCost ? parseInt(r.totalCost) : 0,
|
||||||
|
})),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get unique key IDs from usage
|
// Get unique key IDs from usage
|
||||||
@@ -204,18 +210,16 @@ export function GraphSection() {
|
|||||||
const dates = getDates()
|
const dates = getDates()
|
||||||
if (!data?.usage?.length) return null
|
if (!data?.usage?.length) return null
|
||||||
|
|
||||||
const filteredUsageResults = store.key ? data.usage.filter((row) => row.keyId === store.key) : data.usage
|
|
||||||
|
|
||||||
const dailyData = new Map<string, Map<string, number>>()
|
const dailyData = new Map<string, Map<string, number>>()
|
||||||
for (const dateKey of dates) dailyData.set(dateKey, new Map())
|
for (const dateKey of dates) dailyData.set(dateKey, new Map())
|
||||||
|
|
||||||
for (const row of filteredUsageResults) {
|
data.usage
|
||||||
const dayMap = dailyData.get(row.date)
|
.filter((row) => (store.key ? row.keyId === store.key : true))
|
||||||
if (dayMap) {
|
.forEach((row) => {
|
||||||
const existing = dayMap.get(row.model) || 0
|
const dayMap = dailyData.get(row.date)
|
||||||
dayMap.set(row.model, existing + row.totalCost)
|
if (!dayMap) return
|
||||||
}
|
dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost)
|
||||||
}
|
})
|
||||||
|
|
||||||
const filteredModels = store.model === null ? getModels() : [store.model]
|
const filteredModels = store.model === null ? getModels() : [store.model]
|
||||||
|
|
||||||
@@ -223,7 +227,7 @@ export function GraphSection() {
|
|||||||
const color = getModelColor(model)
|
const color = getModelColor(model)
|
||||||
return {
|
return {
|
||||||
label: model,
|
label: model,
|
||||||
data: dates.map((date) => (dailyData.get(date)?.get(model) || 0) / 100000000),
|
data: dates.map((date) => (dailyData.get(date)?.get(model) || 0) / 100_000_000),
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
hoverBackgroundColor: color,
|
hoverBackgroundColor: color,
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
|
|||||||
@@ -5,11 +5,21 @@ import { withActor } from "~/context/auth.withActor"
|
|||||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||||
import styles from "./model-section.module.css"
|
import styles from "./model-section.module.css"
|
||||||
import { querySessionInfo } from "../common"
|
import { querySessionInfo } from "../common"
|
||||||
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
|
import {
|
||||||
|
IconAlibaba,
|
||||||
|
IconAnthropic,
|
||||||
|
IconGoogle,
|
||||||
|
IconMoonshotAI,
|
||||||
|
IconOpenAI,
|
||||||
|
IconStealth,
|
||||||
|
IconXai,
|
||||||
|
IconZai,
|
||||||
|
} from "~/component/icon"
|
||||||
|
|
||||||
const getModelLab = (modelId: string) => {
|
const getModelLab = (modelId: string) => {
|
||||||
if (modelId.startsWith("claude")) return "Anthropic"
|
if (modelId.startsWith("claude")) return "Anthropic"
|
||||||
if (modelId.startsWith("gpt")) return "OpenAI"
|
if (modelId.startsWith("gpt")) return "OpenAI"
|
||||||
|
if (modelId.startsWith("gemini")) return "Google"
|
||||||
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
if (modelId.startsWith("kimi")) return "Moonshot AI"
|
||||||
if (modelId.startsWith("glm")) return "Z.ai"
|
if (modelId.startsWith("glm")) return "Z.ai"
|
||||||
if (modelId.startsWith("qwen")) return "Alibaba"
|
if (modelId.startsWith("qwen")) return "Alibaba"
|
||||||
@@ -24,7 +34,17 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
|||||||
all: Object.entries(ZenData.list().models)
|
all: Object.entries(ZenData.list().models)
|
||||||
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
|
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
|
||||||
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
.filter(([id, _model]) => !id.startsWith("alpha-"))
|
||||||
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
|
.sort(([idA, modelA], [idB, modelB]) => {
|
||||||
|
const priority = ["big-pickle", "grok", "claude", "gpt", "gemini"]
|
||||||
|
const getPriority = (id: string) => {
|
||||||
|
const index = priority.findIndex((p) => id.startsWith(p))
|
||||||
|
return index === -1 ? Infinity : index
|
||||||
|
}
|
||||||
|
const pA = getPriority(idA)
|
||||||
|
const pB = getPriority(idB)
|
||||||
|
if (pA !== pB) return pA - pB
|
||||||
|
return modelA.name.localeCompare(modelB.name)
|
||||||
|
})
|
||||||
.map(([id, model]) => ({ id, name: model.name })),
|
.map(([id, model]) => ({ id, name: model.name })),
|
||||||
disabled: await Model.listDisabled(),
|
disabled: await Model.listDisabled(),
|
||||||
}
|
}
|
||||||
@@ -96,6 +116,8 @@ export function ModelSection() {
|
|||||||
return <IconOpenAI width={16} height={16} />
|
return <IconOpenAI width={16} height={16} />
|
||||||
case "Anthropic":
|
case "Anthropic":
|
||||||
return <IconAnthropic width={16} height={16} />
|
return <IconAnthropic width={16} height={16} />
|
||||||
|
case "Google":
|
||||||
|
return <IconGoogle width={16} height={16} />
|
||||||
case "Moonshot AI":
|
case "Moonshot AI":
|
||||||
return <IconMoonshotAI width={16} height={16} />
|
return <IconMoonshotAI width={16} height={16} />
|
||||||
case "Z.ai":
|
case "Z.ai":
|
||||||
|
|||||||
@@ -56,6 +56,53 @@
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="tokens-with-breakdown"] {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-button"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-popup"] {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: var(--space-2);
|
||||||
|
z-index: 10;
|
||||||
|
min-width: 180px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:last-child td {
|
tbody tr:last-child td {
|
||||||
@@ -116,4 +163,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Breakdown popup content */
|
||||||
|
[data-slot="breakdown-row"] {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-label"] {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="breakdown-value"] {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
import { createAsync, query, useParams } from "@solidjs/router"
|
import { createAsync, query, useParams } from "@solidjs/router"
|
||||||
import { createMemo, For, Show, createEffect } from "solid-js"
|
import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
|
||||||
import { formatDateUTC, formatDateForTable } from "../common"
|
import { formatDateUTC, formatDateForTable } from "../common"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { IconChevronLeft, IconChevronRight } from "~/component/icon"
|
import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon"
|
||||||
import styles from "./usage-section.module.css"
|
import styles from "./usage-section.module.css"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
@@ -22,15 +22,34 @@ export function UsageSection() {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
|
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
|
||||||
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
|
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
|
||||||
|
const [openBreakdownId, setOpenBreakdownId] = createSignal<string | null>(null)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setStore({ usage: usage() })
|
setStore({ usage: usage() })
|
||||||
}, [usage])
|
}, [usage])
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!openBreakdownId()) return
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
if (!target.closest('[data-slot="tokens-with-breakdown"]')) {
|
||||||
|
setOpenBreakdownId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", handleClickOutside)
|
||||||
|
return () => document.removeEventListener("click", handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
|
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
|
||||||
const canGoPrev = createMemo(() => store.page > 0)
|
const canGoPrev = createMemo(() => store.page > 0)
|
||||||
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
|
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
|
||||||
|
|
||||||
|
const calculateTotalInputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
|
||||||
|
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
const goPrev = async () => {
|
const goPrev = async () => {
|
||||||
const usage = await getUsageInfo(params.id!, store.page - 1)
|
const usage = await getUsageInfo(params.id!, store.page - 1)
|
||||||
setStore({
|
setStore({
|
||||||
@@ -73,15 +92,50 @@ export function UsageSection() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={store.usage}>
|
<For each={store.usage}>
|
||||||
{(usage) => {
|
{(usage, index) => {
|
||||||
const date = createMemo(() => new Date(usage.timeCreated))
|
const date = createMemo(() => new Date(usage.timeCreated))
|
||||||
|
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
|
||||||
|
const breakdownId = `breakdown-${index()}`
|
||||||
|
const isOpen = createMemo(() => openBreakdownId() === breakdownId)
|
||||||
|
const isClaude = usage.model.toLowerCase().includes("claude")
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td data-slot="usage-date" title={formatDateUTC(date())}>
|
<td data-slot="usage-date" title={formatDateUTC(date())}>
|
||||||
{formatDateForTable(date())}
|
{formatDateForTable(date())}
|
||||||
</td>
|
</td>
|
||||||
<td data-slot="usage-model">{usage.model}</td>
|
<td data-slot="usage-model">{usage.model}</td>
|
||||||
<td data-slot="usage-tokens">{usage.inputTokens}</td>
|
<td data-slot="usage-tokens">
|
||||||
|
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<button
|
||||||
|
data-slot="breakdown-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setOpenBreakdownId(isOpen() ? null : breakdownId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconBreakdown />
|
||||||
|
</button>
|
||||||
|
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
|
||||||
|
<Show when={isOpen()}>
|
||||||
|
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Input</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.inputTokens}</span>
|
||||||
|
</div>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Cache Read</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.cacheReadTokens ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={isClaude}>
|
||||||
|
<div data-slot="breakdown-row">
|
||||||
|
<span data-slot="breakdown-label">Cache Write</span>
|
||||||
|
<span data-slot="breakdown-value">{usage.cacheWrite5mTokens ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td data-slot="usage-tokens">{usage.outputTokens}</td>
|
<td data-slot="usage-tokens">{usage.outputTokens}</td>
|
||||||
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { logger } from "./logger"
|
|||||||
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
|
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
|
||||||
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
|
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
|
||||||
import { anthropicHelper } from "./provider/anthropic"
|
import { anthropicHelper } from "./provider/anthropic"
|
||||||
|
import { googleHelper } from "./provider/google"
|
||||||
import { openaiHelper } from "./provider/openai"
|
import { openaiHelper } from "./provider/openai"
|
||||||
import { oaCompatHelper } from "./provider/openai-compatible"
|
import { oaCompatHelper } from "./provider/openai-compatible"
|
||||||
import { createRateLimiter } from "./rateLimiter"
|
import { createRateLimiter } from "./rateLimiter"
|
||||||
@@ -30,6 +31,8 @@ export async function handler(
|
|||||||
opts: {
|
opts: {
|
||||||
format: ZenData.Format
|
format: ZenData.Format
|
||||||
parseApiKey: (headers: Headers) => string | undefined
|
parseApiKey: (headers: Headers) => string | undefined
|
||||||
|
parseModel: (url: string, body: any) => string
|
||||||
|
parseIsStream: (url: string, body: any) => boolean
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
|
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
|
||||||
@@ -43,15 +46,18 @@ export async function handler(
|
|||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const url = input.request.url
|
||||||
const body = await input.request.json()
|
const body = await input.request.json()
|
||||||
const ip = input.request.headers.get("x-real-ip") ?? ""
|
const ip = input.request.headers.get("x-real-ip") ?? ""
|
||||||
|
const model = opts.parseModel(url, body)
|
||||||
|
const isStream = opts.parseIsStream(url, body)
|
||||||
logger.metric({
|
logger.metric({
|
||||||
is_tream: !!body.stream,
|
is_tream: isStream,
|
||||||
session: input.request.headers.get("x-opencode-session"),
|
session: input.request.headers.get("x-opencode-session"),
|
||||||
request: input.request.headers.get("x-opencode-request"),
|
request: input.request.headers.get("x-opencode-request"),
|
||||||
})
|
})
|
||||||
const zenData = ZenData.list()
|
const zenData = ZenData.list()
|
||||||
const modelInfo = validateModel(zenData, body.model)
|
const modelInfo = validateModel(zenData, model)
|
||||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
|
|
||||||
@@ -64,7 +70,7 @@ export async function handler(
|
|||||||
logger.metric({ provider: providerInfo.id })
|
logger.metric({ provider: providerInfo.id })
|
||||||
|
|
||||||
const startTimestamp = Date.now()
|
const startTimestamp = Date.now()
|
||||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
|
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
|
||||||
const reqBody = JSON.stringify(
|
const reqBody = JSON.stringify(
|
||||||
providerInfo.modifyBody({
|
providerInfo.modifyBody({
|
||||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||||
@@ -114,7 +120,7 @@ export async function handler(
|
|||||||
logger.debug("STATUS: " + res.status + " " + res.statusText)
|
logger.debug("STATUS: " + res.status + " " + res.statusText)
|
||||||
|
|
||||||
// Handle non-streaming response
|
// Handle non-streaming response
|
||||||
if (!body.stream) {
|
if (!isStream) {
|
||||||
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
|
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
const body = JSON.stringify(responseConverter(json))
|
const body = JSON.stringify(responseConverter(json))
|
||||||
@@ -169,7 +175,7 @@ export async function handler(
|
|||||||
responseLength += value.length
|
responseLength += value.length
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
const parts = buffer.split("\n\n")
|
const parts = buffer.split(providerInfo.streamSeparator)
|
||||||
buffer = parts.pop() ?? ""
|
buffer = parts.pop() ?? ""
|
||||||
|
|
||||||
for (let part of parts) {
|
for (let part of parts) {
|
||||||
@@ -283,6 +289,7 @@ export async function handler(
|
|||||||
...(() => {
|
...(() => {
|
||||||
const format = zenData.providers[provider.id].format
|
const format = zenData.providers[provider.id].format
|
||||||
if (format === "anthropic") return anthropicHelper
|
if (format === "anthropic") return anthropicHelper
|
||||||
|
if (format === "google") return googleHelper
|
||||||
if (format === "openai") return openaiHelper
|
if (format === "openai") return openaiHelper
|
||||||
return oaCompatHelper
|
return oaCompatHelper
|
||||||
})(),
|
})(),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const anthropicHelper = {
|
|||||||
service_tier: "standard_only",
|
service_tier: "standard_only",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { ProviderHelper } from "./provider"
|
||||||
|
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
promptTokenCount: 11453,
|
||||||
|
candidatesTokenCount: 71,
|
||||||
|
totalTokenCount: 11625,
|
||||||
|
cachedContentTokenCount: 8100,
|
||||||
|
promptTokensDetails: [
|
||||||
|
{modality: "TEXT",tokenCount: 11453}
|
||||||
|
],
|
||||||
|
cacheTokensDetails: [
|
||||||
|
{modality: "TEXT",tokenCount: 8100}
|
||||||
|
],
|
||||||
|
thoughtsTokenCount: 101
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Usage = {
|
||||||
|
promptTokenCount?: number
|
||||||
|
candidatesTokenCount?: number
|
||||||
|
totalTokenCount?: number
|
||||||
|
cachedContentTokenCount?: number
|
||||||
|
promptTokensDetails?: { modality: string; tokenCount: number }[]
|
||||||
|
cacheTokensDetails?: { modality: string; tokenCount: number }[]
|
||||||
|
thoughtsTokenCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const googleHelper = {
|
||||||
|
format: "google",
|
||||||
|
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
|
||||||
|
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
|
||||||
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||||
|
headers.set("x-goog-api-key", apiKey)
|
||||||
|
},
|
||||||
|
modifyBody: (body: Record<string, any>) => {
|
||||||
|
return body
|
||||||
|
},
|
||||||
|
streamSeparator: "\r\n\r\n",
|
||||||
|
createUsageParser: () => {
|
||||||
|
let usage: Usage
|
||||||
|
|
||||||
|
return {
|
||||||
|
parse: (chunk: string) => {
|
||||||
|
if (!chunk.startsWith("data: ")) return
|
||||||
|
|
||||||
|
let json
|
||||||
|
try {
|
||||||
|
json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage }
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.usageMetadata) return
|
||||||
|
usage = json.usageMetadata
|
||||||
|
},
|
||||||
|
retrieve: () => usage,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeUsage: (usage: Usage) => {
|
||||||
|
const inputTokens = usage.promptTokenCount ?? 0
|
||||||
|
const outputTokens = usage.candidatesTokenCount ?? 0
|
||||||
|
const reasoningTokens = usage.thoughtsTokenCount ?? 0
|
||||||
|
const cacheReadTokens = usage.cachedContentTokenCount ?? 0
|
||||||
|
return {
|
||||||
|
inputTokens: inputTokens - cacheReadTokens,
|
||||||
|
outputTokens,
|
||||||
|
reasoningTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWrite5mTokens: undefined,
|
||||||
|
cacheWrite1hTokens: undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} satisfies ProviderHelper
|
||||||
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
|
|||||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const openaiHelper = {
|
|||||||
modifyBody: (body: Record<string, any>) => {
|
modifyBody: (body: Record<string, any>) => {
|
||||||
return body
|
return body
|
||||||
},
|
},
|
||||||
|
streamSeparator: "\n\n",
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
let usage: Usage
|
let usage: Usage
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ import {
|
|||||||
|
|
||||||
export type ProviderHelper = {
|
export type ProviderHelper = {
|
||||||
format: ZenData.Format
|
format: ZenData.Format
|
||||||
modifyUrl: (providerApi: string) => string
|
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
|
||||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
|
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
|
||||||
modifyBody: (body: Record<string, any>) => Record<string, any>
|
modifyBody: (body: Record<string, any>) => Record<string, any>
|
||||||
|
streamSeparator: string
|
||||||
createUsageParser: () => {
|
createUsageParser: () => {
|
||||||
parse: (chunk: string) => void
|
parse: (chunk: string) => void
|
||||||
retrieve: () => any
|
retrieve: () => any
|
||||||
|
|||||||
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
|
|||||||
return handler(input, {
|
return handler(input, {
|
||||||
format: "oa-compat",
|
format: "oa-compat",
|
||||||
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
||||||
|
parseModel: (url: string, body: any) => body.model,
|
||||||
|
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
|
|||||||
return handler(input, {
|
return handler(input, {
|
||||||
format: "anthropic",
|
format: "anthropic",
|
||||||
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
|
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
|
||||||
|
parseModel: (url: string, body: any) => body.model,
|
||||||
|
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
|
import { handler } from "~/routes/zen/util/handler"
|
||||||
|
|
||||||
|
export function POST(input: APIEvent) {
|
||||||
|
return handler(input, {
|
||||||
|
format: "google",
|
||||||
|
parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
|
||||||
|
parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
|
||||||
|
parseIsStream: (url: string, body: any) =>
|
||||||
|
// ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
|
||||||
|
url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
|
|||||||
return handler(input, {
|
return handler(input, {
|
||||||
format: "openai",
|
format: "openai",
|
||||||
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
|
||||||
|
parseModel: (url: string, body: any) => body.model,
|
||||||
|
parseIsStream: (url: string, body: any) => !!body.stream,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.75",
|
"version": "1.0.85",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Actor } from "./actor"
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
|
|
||||||
export namespace ZenData {
|
export namespace ZenData {
|
||||||
const FormatSchema = z.enum(["anthropic", "openai", "oa-compat"])
|
const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
|
||||||
export type Format = z.infer<typeof FormatSchema>
|
export type Format = z.infer<typeof FormatSchema>
|
||||||
|
|
||||||
const ModelCostSchema = z.object({
|
const ModelCostSchema = z.object({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export default {
|
|||||||
if (
|
if (
|
||||||
url.pathname !== "/zen/v1/chat/completions" &&
|
url.pathname !== "/zen/v1/chat/completions" &&
|
||||||
url.pathname !== "/zen/v1/messages" &&
|
url.pathname !== "/zen/v1/messages" &&
|
||||||
url.pathname !== "/zen/v1/responses"
|
url.pathname !== "/zen/v1/responses" &&
|
||||||
|
!url.pathname.startsWith("/zen/v1/models/")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"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.75",
|
"version": "1.0.85",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
|
|
||||||
sdk.global.event().then(async (events) => {
|
sdk.global.event().then(async (events) => {
|
||||||
for await (const event of events.stream) {
|
for await (const event of events.stream) {
|
||||||
console.log("event", event)
|
// console.log("event", event)
|
||||||
// console.log("event", event.payload.type)
|
|
||||||
emitter.emit(event.directory, event.payload)
|
emitter.emit(event.directory, event.payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const name = createMemo(
|
const name = createMemo(
|
||||||
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
|
() => `___${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const [store, setStore] = makePersisted(
|
const [store, setStore] = makePersisted(
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { createSignal, onCleanup, onMount } from "solid-js"
|
||||||
|
import { isServer } from "solid-js/web"
|
||||||
|
|
||||||
|
export function createSessionSeen(key: string, delay = 1000) {
|
||||||
|
// 1. Initialize state based on storage (default to true on server to avoid flash)
|
||||||
|
const storageKey = `app:seen:${key}`
|
||||||
|
const [hasSeen] = createSignal(!isServer && sessionStorage.getItem(storageKey) === "true")
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// 2. If we haven't seen it, mark it as seen for NEXT time
|
||||||
|
if (!hasSeen()) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
sessionStorage.setItem(storageKey, "true")
|
||||||
|
}, delay)
|
||||||
|
onCleanup(() => clearTimeout(timer))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return hasSeen
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { LayoutProvider } from "./context/layout"
|
|||||||
import { GlobalSDKProvider } from "./context/global-sdk"
|
import { GlobalSDKProvider } from "./context/global-sdk"
|
||||||
import { SessionProvider } from "./context/session"
|
import { SessionProvider } from "./context/session"
|
||||||
import { base64Encode } from "./utils"
|
import { base64Encode } from "./utils"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
|
|
||||||
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
|
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
|
||||||
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
|
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
|
||||||
@@ -51,10 +51,12 @@ render(
|
|||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
<Route path="/" component={() => <Navigate href="session" />} />
|
||||||
<Route
|
<Route
|
||||||
path="/session/:id?"
|
path="/session/:id?"
|
||||||
component={() => (
|
component={(p) => (
|
||||||
<SessionProvider>
|
<Show when={p.params.id || true} keyed>
|
||||||
<Session />
|
<SessionProvider>
|
||||||
</SessionProvider>
|
<Session />
|
||||||
|
</SessionProvider>
|
||||||
|
</Show>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createMemo, Show, type ParentProps } from "solid-js"
|
import { createMemo, type ParentProps } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
import { SyncProvider } from "@/context/sync"
|
import { SyncProvider } from "@/context/sync"
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
?.replace(" hr.", "h")}
|
?.replace(" hr.", "h")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden flex justify-between items-center self-stretch">
|
<div class="hidden _flex justify-between items-center self-stretch">
|
||||||
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
|
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
|
||||||
<Show when={session.summary}>
|
<Show when={session.summary}>
|
||||||
{(summary) => <DiffChanges changes={summary()} />}
|
{(summary) => <DiffChanges changes={summary()} />}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { useSession } from "@/context/session"
|
|||||||
import { StickyAccordionHeader } from "@/components/sticky-accordion-header"
|
import { StickyAccordionHeader } from "@/components/sticky-accordion-header"
|
||||||
import { SessionReview } from "@/components/session-review"
|
import { SessionReview } from "@/components/session-review"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { createSessionSeen } from "@/hooks/create-session-seen"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -451,7 +452,9 @@ export default function Page() {
|
|||||||
<For each={session.messages.user()}>
|
<For each={session.messages.user()}>
|
||||||
{(message) => {
|
{(message) => {
|
||||||
const isActive = createMemo(() => session.messages.active()?.id === message.id)
|
const isActive = createMemo(() => session.messages.active()?.id === message.id)
|
||||||
const [titled, setTitled] = createSignal(!!message.summary?.title)
|
const titleSeen = createSessionSeen(`message-title-${message.id}`)
|
||||||
|
const contentSeen = createSessionSeen(`message-content-${message.id}`)
|
||||||
|
const [titled, setTitled] = createSignal(titleSeen())
|
||||||
const assistantMessages = createMemo(() => {
|
const assistantMessages = createMemo(() => {
|
||||||
if (!session.id) return []
|
if (!session.id) return []
|
||||||
return sync.data.message[session.id]?.filter(
|
return sync.data.message[session.id]?.filter(
|
||||||
@@ -459,7 +462,6 @@ export default function Page() {
|
|||||||
) as AssistantMessageType[]
|
) as AssistantMessageType[]
|
||||||
})
|
})
|
||||||
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
||||||
const [completed, setCompleted] = createSignal(!!message.summary?.body || !!error())
|
|
||||||
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
|
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
|
||||||
const parts = createMemo(() => sync.data.part[message.id])
|
const parts = createMemo(() => sync.data.part[message.id])
|
||||||
const hasToolPart = createMemo(() =>
|
const hasToolPart = createMemo(() =>
|
||||||
@@ -470,16 +472,18 @@ export default function Page() {
|
|||||||
const working = createMemo(
|
const working = createMemo(
|
||||||
() => message.id === session.messages.last()?.id && session.working(),
|
() => message.id === session.messages.last()?.id && session.working(),
|
||||||
)
|
)
|
||||||
|
const initialCompleted = !(message.id === session.messages.last()?.id && session.working())
|
||||||
|
const [completed, setCompleted] = createSignal(initialCompleted)
|
||||||
|
|
||||||
// allowing time for the animations to finish
|
// allowing time for the animations to finish
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
if (titleSeen()) return
|
||||||
const title = message.summary?.title
|
const title = message.summary?.title
|
||||||
setTimeout(() => setTitled(!!title), 10_000)
|
if (title) setTimeout(() => setTitled(true), 10_000)
|
||||||
})
|
})
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const summary = message.summary?.body
|
const completed = !working()
|
||||||
const complete = !!summary || !!error()
|
setTimeout(() => setCompleted(completed), 1200)
|
||||||
setTimeout(() => setCompleted(complete), 1200)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -523,7 +527,7 @@ export default function Page() {
|
|||||||
<Markdown
|
<Markdown
|
||||||
classList={{
|
classList={{
|
||||||
"text-14-regular": !!message.summary?.diffs?.length,
|
"text-14-regular": !!message.summary?.diffs?.length,
|
||||||
"[&>*]:fade-up-text": !message.summary?.diffs?.length,
|
"[&>*]:fade-up-text": !message.summary?.diffs?.length && !contentSeen(),
|
||||||
}}
|
}}
|
||||||
text={summary()}
|
text={summary()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The AI coding agent built for the terminal"
|
description = "The AI coding agent built for the terminal"
|
||||||
version = "1.0.75"
|
version = "1.0.85"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/sst/opencode"
|
repository = "https://github.com/sst/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.75/opencode-darwin-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.75/opencode-darwin-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.75/opencode-linux-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-linux-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.75/opencode-linux-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-linux-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.75/opencode-windows-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"$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.75",
|
"version": "1.0.85",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.5.1",
|
||||||
|
"@ai-sdk/mcp": "0.0.8",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
@@ -54,8 +55,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.1.46",
|
"@opentui/core": "0.1.47",
|
||||||
"@opentui/solid": "0.1.46",
|
"@opentui/solid": "0.1.47",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -167,6 +167,15 @@ export default {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filetype: "yaml",
|
||||||
|
wasm: "https://github.com/tree-sitter-grammars/tree-sitter-yaml/releases/download/v0.7.2/tree-sitter-yaml.wasm",
|
||||||
|
queries: {
|
||||||
|
highlights: [
|
||||||
|
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/yaml/highlights.scm",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
filetype: "haskell",
|
filetype: "haskell",
|
||||||
wasm: "https://github.com/tree-sitter/tree-sitter-haskell/releases/download/v0.23.1/tree-sitter-haskell.wasm",
|
wasm: "https://github.com/tree-sitter/tree-sitter-haskell/releases/download/v0.23.1/tree-sitter-haskell.wasm",
|
||||||
|
|||||||
@@ -131,7 +131,34 @@ if (!Script.preview) {
|
|||||||
"",
|
"",
|
||||||
"package() {",
|
"package() {",
|
||||||
` cd "opencode-\${pkgver}/packages/opencode"`,
|
` cd "opencode-\${pkgver}/packages/opencode"`,
|
||||||
' install -Dm755 $(find dist/*/bin/opencode) "${pkgdir}/usr/bin/opencode"',
|
' mkdir -p "${pkgdir}/usr/bin"',
|
||||||
|
' target_arch="x64"',
|
||||||
|
' case "$CARCH" in',
|
||||||
|
' x86_64) target_arch="x64" ;;',
|
||||||
|
' aarch64) target_arch="arm64" ;;',
|
||||||
|
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
|
||||||
|
" esac",
|
||||||
|
' libc=""',
|
||||||
|
" if command -v ldd >/dev/null 2>&1; then",
|
||||||
|
" if ldd --version 2>&1 | grep -qi musl; then",
|
||||||
|
' libc="-musl"',
|
||||||
|
" fi",
|
||||||
|
" fi",
|
||||||
|
' if [ -z "$libc" ] && ls /lib/ld-musl-* >/dev/null 2>&1; then',
|
||||||
|
' libc="-musl"',
|
||||||
|
" fi",
|
||||||
|
' base=""',
|
||||||
|
' if [ "$target_arch" = "x64" ]; then',
|
||||||
|
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
|
||||||
|
' base="-baseline"',
|
||||||
|
" fi",
|
||||||
|
" fi",
|
||||||
|
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
|
||||||
|
' if [ ! -f "$bin" ]; then',
|
||||||
|
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
|
||||||
|
" return 1",
|
||||||
|
" fi",
|
||||||
|
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',
|
||||||
"}",
|
"}",
|
||||||
"",
|
"",
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|||||||
@@ -79,16 +79,48 @@ export namespace BunProc {
|
|||||||
version,
|
version,
|
||||||
})
|
})
|
||||||
|
|
||||||
await BunProc.run(args, {
|
const total = 3
|
||||||
cwd: Global.Path.cache,
|
const wait = 500
|
||||||
}).catch((e) => {
|
|
||||||
throw new InstallFailedError(
|
const runInstall = async (count: number = 1): Promise<void> => {
|
||||||
{ pkg, version },
|
log.info("bun install attempt", {
|
||||||
{
|
pkg,
|
||||||
cause: e,
|
version,
|
||||||
},
|
attempt: count,
|
||||||
)
|
total,
|
||||||
})
|
})
|
||||||
|
await BunProc.run(args, {
|
||||||
|
cwd: Global.Path.cache,
|
||||||
|
}).catch(async (error) => {
|
||||||
|
log.warn("bun install failed", {
|
||||||
|
pkg,
|
||||||
|
version,
|
||||||
|
attempt: count,
|
||||||
|
total,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
if (count >= total) {
|
||||||
|
throw new InstallFailedError(
|
||||||
|
{ pkg, version },
|
||||||
|
{
|
||||||
|
cause: error,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const delay = wait * count
|
||||||
|
log.info("bun install retrying", {
|
||||||
|
pkg,
|
||||||
|
version,
|
||||||
|
next: count + 1,
|
||||||
|
delay,
|
||||||
|
})
|
||||||
|
await Bun.sleep(delay)
|
||||||
|
return runInstall(count + 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await runInstall()
|
||||||
|
|
||||||
parsed.dependencies[pkg] = version
|
parsed.dependencies[pkg] = version
|
||||||
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
||||||
return mod
|
return mod
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Agent } from "../../agent/agent"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import matter from "gray-matter"
|
import matter from "gray-matter"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
|
import { EOL } from "os"
|
||||||
|
|
||||||
const AgentCreateCommand = cmd({
|
const AgentCreateCommand = cmd({
|
||||||
command: "create",
|
command: "create",
|
||||||
@@ -133,9 +134,32 @@ const AgentCreateCommand = cmd({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const AgentListCommand = cmd({
|
||||||
|
command: "list",
|
||||||
|
describe: "list all available agents",
|
||||||
|
async handler() {
|
||||||
|
await Instance.provide({
|
||||||
|
directory: process.cwd(),
|
||||||
|
async fn() {
|
||||||
|
const agents = await Agent.list()
|
||||||
|
const sortedAgents = agents.sort((a, b) => {
|
||||||
|
if (a.builtIn !== b.builtIn) {
|
||||||
|
return a.builtIn ? -1 : 1
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const agent of sortedAgents) {
|
||||||
|
process.stdout.write(`${agent.name} (${agent.mode})${EOL}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const AgentCommand = cmd({
|
export const AgentCommand = cmd({
|
||||||
command: "agent",
|
command: "agent",
|
||||||
describe: "manage agents",
|
describe: "manage agents",
|
||||||
builder: (yargs) => yargs.command(AgentCreateCommand).demandCommand(),
|
builder: (yargs) => yargs.command(AgentCreateCommand).command(AgentListCommand).demandCommand(),
|
||||||
async handler() {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
|
|||||||
import { Clipboard } from "@tui/util/clipboard"
|
import { Clipboard } from "@tui/util/clipboard"
|
||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch } from "solid-js"
|
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show } from "solid-js"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||||
|
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
||||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||||
import { SyncProvider, useSync } from "@tui/context/sync"
|
import { SyncProvider, useSync } from "@tui/context/sync"
|
||||||
import { LocalProvider, useLocal } from "@tui/context/local"
|
import { LocalProvider, useLocal } from "@tui/context/local"
|
||||||
@@ -293,6 +294,14 @@ function App() {
|
|||||||
},
|
},
|
||||||
category: "System",
|
category: "System",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Connect provider",
|
||||||
|
value: "provider.connect",
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.replace(() => <DialogProviderList />)
|
||||||
|
},
|
||||||
|
category: "System",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
|
title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`,
|
||||||
value: "theme.switch_mode",
|
value: "theme.switch_mode",
|
||||||
@@ -451,16 +460,18 @@ function App() {
|
|||||||
<text fg={theme.textMuted}>{process.cwd().replace(Global.Path.home, "~")}</text>
|
<text fg={theme.textMuted}>{process.cwd().replace(Global.Path.home, "~")}</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" flexShrink={0}>
|
<Show when={false}>
|
||||||
<text fg={theme.textMuted} paddingRight={1}>
|
<box flexDirection="row" flexShrink={0}>
|
||||||
tab
|
<text fg={theme.textMuted} paddingRight={1}>
|
||||||
</text>
|
tab
|
||||||
<text fg={local.agent.color(local.agent.current().name)}>{""}</text>
|
</text>
|
||||||
<text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
|
<text fg={local.agent.color(local.agent.current().name)}>{""}</text>
|
||||||
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
<text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
|
||||||
<span> AGENT </span>
|
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
||||||
</text>
|
<span> AGENT </span>
|
||||||
</box>
|
</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
|
export const EmptyBorder = {
|
||||||
|
topLeft: "",
|
||||||
|
bottomLeft: "",
|
||||||
|
vertical: "",
|
||||||
|
topRight: "",
|
||||||
|
bottomRight: "",
|
||||||
|
horizontal: " ",
|
||||||
|
bottomT: "",
|
||||||
|
topT: "",
|
||||||
|
cross: "",
|
||||||
|
leftT: "",
|
||||||
|
rightT: "",
|
||||||
|
}
|
||||||
|
|
||||||
export const SplitBorder = {
|
export const SplitBorder = {
|
||||||
border: ["left" as const, "right" as const],
|
border: ["left" as const, "right" as const],
|
||||||
customBorderChars: {
|
customBorderChars: {
|
||||||
topLeft: "",
|
...EmptyBorder,
|
||||||
bottomLeft: "",
|
|
||||||
vertical: "┃",
|
vertical: "┃",
|
||||||
topRight: "",
|
|
||||||
bottomRight: "",
|
|
||||||
horizontal: "",
|
|
||||||
bottomT: "",
|
|
||||||
topT: "",
|
|
||||||
cross: "",
|
|
||||||
leftT: "",
|
|
||||||
rightT: "",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { createMemo, createSignal } from "solid-js"
|
import { createMemo, createSignal } from "solid-js"
|
||||||
import { useLocal } from "@tui/context/local"
|
import { useLocal } from "@tui/context/local"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy } from "remeda"
|
import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy, take } from "remeda"
|
||||||
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
||||||
import { useDialog } from "@tui/ui/dialog"
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
import { useTheme } from "../context/theme"
|
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||||
|
|
||||||
function Free() {
|
|
||||||
const { theme } = useTheme()
|
|
||||||
return <span style={{ fg: theme.secondary }}>Free</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DialogModel() {
|
export function DialogModel() {
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
@@ -17,9 +12,16 @@ export function DialogModel() {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
|
const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
|
||||||
|
|
||||||
|
const connected = createMemo(() =>
|
||||||
|
sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const showRecent = createMemo(() => !ref()?.filter && local.model.recent().length > 0 && connected())
|
||||||
|
const providers = createDialogProviderOptions()
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
return [
|
return [
|
||||||
...(!ref()?.filter
|
...(showRecent()
|
||||||
? local.model.recent().flatMap((item) => {
|
? local.model.recent().flatMap((item) => {
|
||||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)!
|
const provider = sync.data.provider.find((x) => x.id === item.providerID)!
|
||||||
if (!provider) return []
|
if (!provider) return []
|
||||||
@@ -35,7 +37,17 @@ export function DialogModel() {
|
|||||||
title: model.name ?? item.modelID,
|
title: model.name ?? item.modelID,
|
||||||
description: provider.name,
|
description: provider.name,
|
||||||
category: "Recent",
|
category: "Recent",
|
||||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
|
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.clear()
|
||||||
|
local.model.set(
|
||||||
|
{
|
||||||
|
providerID: provider.id,
|
||||||
|
modelID: model.id,
|
||||||
|
},
|
||||||
|
{ recent: true },
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -56,28 +68,56 @@ export function DialogModel() {
|
|||||||
modelID: model,
|
modelID: model,
|
||||||
},
|
},
|
||||||
title: info.name ?? model,
|
title: info.name ?? model,
|
||||||
description: provider.name,
|
description: connected() ? provider.name : undefined,
|
||||||
category: provider.name,
|
category: connected() ? provider.name : undefined,
|
||||||
footer: info.cost?.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
|
disabled: provider.id === "opencode" && model.includes("-nano"),
|
||||||
|
footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||||
|
onSelect() {
|
||||||
|
dialog.clear()
|
||||||
|
local.model.set(
|
||||||
|
{
|
||||||
|
providerID: provider.id,
|
||||||
|
modelID: model,
|
||||||
|
},
|
||||||
|
{ recent: true },
|
||||||
|
)
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
|
filter((x) => !showRecent() || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
|
||||||
sortBy((x) => x.title),
|
sortBy((x) => x.title),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
...(!connected()
|
||||||
|
? pipe(
|
||||||
|
providers(),
|
||||||
|
map((option) => {
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
category: "Popular providers",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
take(6),
|
||||||
|
)
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogSelect
|
<DialogSelect
|
||||||
|
keybind={[
|
||||||
|
{
|
||||||
|
keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
|
||||||
|
title: connected() ? "Connect provider" : "More providers",
|
||||||
|
onTrigger() {
|
||||||
|
dialog.replace(() => <DialogProvider />)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
ref={setRef}
|
ref={setRef}
|
||||||
title="Select model"
|
title="Select model"
|
||||||
current={local.model.current()}
|
current={local.model.current()}
|
||||||
options={options()}
|
options={options()}
|
||||||
onSelect={(option) => {
|
|
||||||
dialog.clear()
|
|
||||||
local.model.set(option.value, { recent: true })
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import { createMemo, createSignal, onMount, Show } from "solid-js"
|
||||||
|
import { useSync } from "@tui/context/sync"
|
||||||
|
import { map, pipe, sortBy } from "remeda"
|
||||||
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
|
import { useSDK } from "../context/sdk"
|
||||||
|
import { DialogPrompt } from "../ui/dialog-prompt"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
|
import { TextAttributes } from "@opentui/core"
|
||||||
|
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk"
|
||||||
|
import { DialogModel } from "./dialog-model"
|
||||||
|
|
||||||
|
const PROVIDER_PRIORITY: Record<string, number> = {
|
||||||
|
opencode: 0,
|
||||||
|
anthropic: 1,
|
||||||
|
"github-copilot": 2,
|
||||||
|
openai: 3,
|
||||||
|
google: 4,
|
||||||
|
openrouter: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDialogProviderOptions() {
|
||||||
|
const sync = useSync()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const options = createMemo(() => {
|
||||||
|
return pipe(
|
||||||
|
sync.data.provider_next.all,
|
||||||
|
map((provider) => ({
|
||||||
|
title: provider.name,
|
||||||
|
value: provider.id,
|
||||||
|
footer: {
|
||||||
|
opencode: "Recommended",
|
||||||
|
anthropic: "Claude Max or API key",
|
||||||
|
}[provider.id],
|
||||||
|
async onSelect() {
|
||||||
|
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||||
|
{
|
||||||
|
type: "api",
|
||||||
|
label: "API key",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
let index: number | null = 0
|
||||||
|
if (methods.length > 1) {
|
||||||
|
index = await new Promise<number | null>((resolve) => {
|
||||||
|
dialog.replace(
|
||||||
|
() => (
|
||||||
|
<DialogSelect
|
||||||
|
title="Select auth method"
|
||||||
|
options={methods.map((x, index) => ({
|
||||||
|
title: x.label,
|
||||||
|
value: index,
|
||||||
|
}))}
|
||||||
|
onSelect={(option) => resolve(option.value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
() => resolve(null),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (index == null) return
|
||||||
|
const method = methods[index]
|
||||||
|
if (method.type === "oauth") {
|
||||||
|
const result = await sdk.client.provider.oauth.authorize({
|
||||||
|
path: {
|
||||||
|
id: provider.id,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
method: index,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (result.data?.method === "code") {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if (result.data?.method === "auto") {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (method.type === "api") {
|
||||||
|
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogProvider() {
|
||||||
|
const options = createDialogProviderOptions()
|
||||||
|
return <DialogSelect title="Connect a provider" options={options()} />
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoMethodProps {
|
||||||
|
index: number
|
||||||
|
providerID: string
|
||||||
|
title: string
|
||||||
|
authorization: ProviderAuthAuthorization
|
||||||
|
}
|
||||||
|
function AutoMethod(props: AutoMethodProps) {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const sync = useSync()
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const result = await sdk.client.provider.oauth.callback({
|
||||||
|
path: {
|
||||||
|
id: props.providerID,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
method: props.index,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (result.error) {
|
||||||
|
dialog.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await sdk.client.instance.dispose()
|
||||||
|
await sync.bootstrap()
|
||||||
|
dialog.replace(() => <DialogModel />)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
||||||
|
<text fg={theme.textMuted}>esc</text>
|
||||||
|
</box>
|
||||||
|
<box gap={1}>
|
||||||
|
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||||
|
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||||
|
</box>
|
||||||
|
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeMethodProps {
|
||||||
|
index: number
|
||||||
|
title: string
|
||||||
|
providerID: string
|
||||||
|
authorization: ProviderAuthAuthorization
|
||||||
|
}
|
||||||
|
function CodeMethod(props: CodeMethodProps) {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const sync = useSync()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const [error, setError] = createSignal(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogPrompt
|
||||||
|
title={props.title}
|
||||||
|
placeholder="Authorization code"
|
||||||
|
onConfirm={async (value) => {
|
||||||
|
const { error } = await sdk.client.provider.oauth.callback({
|
||||||
|
path: {
|
||||||
|
id: props.providerID,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
method: props.index,
|
||||||
|
code: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!error) {
|
||||||
|
await sdk.client.instance.dispose()
|
||||||
|
await sync.bootstrap()
|
||||||
|
dialog.replace(() => <DialogModel />)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError(true)
|
||||||
|
}}
|
||||||
|
description={() => (
|
||||||
|
<box gap={1}>
|
||||||
|
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||||
|
<text fg={theme.primary}>{props.authorization.url}</text>
|
||||||
|
<Show when={error()}>
|
||||||
|
<text fg={theme.error}>Invalid code</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiMethodProps {
|
||||||
|
providerID: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
function ApiMethod(props: ApiMethodProps) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const sync = useSync()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogPrompt
|
||||||
|
title={props.title}
|
||||||
|
placeholder="API key"
|
||||||
|
onConfirm={async (value) => {
|
||||||
|
if (!value) return
|
||||||
|
sdk.client.auth.set({
|
||||||
|
path: {
|
||||||
|
id: props.providerID,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
type: "api",
|
||||||
|
key: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await sdk.client.instance.dispose()
|
||||||
|
await sync.bootstrap()
|
||||||
|
dialog.replace(() => <DialogModel />)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -53,15 +53,7 @@ export function Autocomplete(props: {
|
|||||||
// Track props.value to make memo reactive to text changes
|
// Track props.value to make memo reactive to text changes
|
||||||
props.value // <- there surely is a better way to do this, like making .input() reactive
|
props.value // <- there surely is a better way to do this, like making .input() reactive
|
||||||
|
|
||||||
const val = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
|
return props.input().getTextRange(store.index + 1, props.input().cursorOffset)
|
||||||
|
|
||||||
// If the filter contains a space, hide the autocomplete
|
|
||||||
if (val.includes(" ")) {
|
|
||||||
hide()
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return val
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function insertPart(text: string, part: PromptInfo["parts"][number]) {
|
function insertPart(text: string, part: PromptInfo["parts"][number]) {
|
||||||
@@ -387,17 +379,19 @@ export function Autocomplete(props: {
|
|||||||
get visible() {
|
get visible() {
|
||||||
return store.visible
|
return store.visible
|
||||||
},
|
},
|
||||||
onInput() {
|
onInput(value) {
|
||||||
if (store.visible) {
|
if (store.visible) {
|
||||||
if (props.input().cursorOffset <= store.index) {
|
if (
|
||||||
|
// Typed text before the trigger
|
||||||
|
props.input().cursorOffset <= store.index ||
|
||||||
|
// There is a space between the trigger and the cursor
|
||||||
|
props.input().getTextRange(store.index, props.input().cursorOffset).match(/\s/) ||
|
||||||
|
// "/<command>" is not the sole content
|
||||||
|
(store.visible === "/" && value.match(/^\S+\s+\S+\s*$/))
|
||||||
|
) {
|
||||||
hide()
|
hide()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Check if a space was typed after the trigger character
|
|
||||||
const currentText = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
|
|
||||||
if (currentText.includes(" ")) {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyDown(e: KeyEvent) {
|
onKeyDown(e: KeyEvent) {
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
import {
|
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, type KeyBinding } from "@opentui/core"
|
||||||
TextAttributes,
|
import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js"
|
||||||
BoxRenderable,
|
|
||||||
TextareaRenderable,
|
|
||||||
MouseEvent,
|
|
||||||
PasteEvent,
|
|
||||||
t,
|
|
||||||
dim,
|
|
||||||
fg,
|
|
||||||
type KeyBinding,
|
|
||||||
} from "@opentui/core"
|
|
||||||
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 { useTheme } from "@tui/context/theme"
|
import { useTheme } from "@tui/context/theme"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
import { EmptyBorder } 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"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
@@ -29,6 +19,8 @@ import { Clipboard } from "../../util/clipboard"
|
|||||||
import type { FilePart } from "@opencode-ai/sdk"
|
import type { FilePart } from "@opencode-ai/sdk"
|
||||||
import { TuiEvent } from "../../event"
|
import { TuiEvent } from "../../event"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
|
import { Locale } from "@/util/locale"
|
||||||
|
import { Shimmer } from "../../ui/shimmer"
|
||||||
|
|
||||||
export type PromptProps = {
|
export type PromptProps = {
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
@@ -57,7 +49,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const status = createMemo(() => (props.sessionID ? sync.session.status(props.sessionID) : "idle"))
|
const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" })
|
||||||
const history = usePromptHistory()
|
const history = usePromptHistory()
|
||||||
const command = useCommandDialog()
|
const command = useCommandDialog()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
@@ -222,12 +214,17 @@ export function Prompt(props: PromptProps) {
|
|||||||
title: "Interrupt session",
|
title: "Interrupt session",
|
||||||
value: "session.interrupt",
|
value: "session.interrupt",
|
||||||
keybind: "session_interrupt",
|
keybind: "session_interrupt",
|
||||||
disabled: status() !== "working",
|
disabled: status().type === "idle",
|
||||||
category: "Session",
|
category: "Session",
|
||||||
onSelect: (dialog) => {
|
onSelect: (dialog) => {
|
||||||
if (!props.sessionID) return
|
|
||||||
if (autocomplete.visible) return
|
if (autocomplete.visible) return
|
||||||
if (!input.focused) return
|
if (!input.focused) return
|
||||||
|
// TODO: this should be its own command
|
||||||
|
if (store.mode === "shell") {
|
||||||
|
setStore("mode", "normal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!props.sessionID) return
|
||||||
|
|
||||||
setStore("interrupt", store.interrupt + 1)
|
setStore("interrupt", store.interrupt + 1)
|
||||||
|
|
||||||
@@ -425,6 +422,10 @@ export function Prompt(props: PromptProps) {
|
|||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
agent: local.agent.current().name,
|
agent: local.agent.current().name,
|
||||||
|
model: {
|
||||||
|
providerID: local.model.current().providerID,
|
||||||
|
modelID: local.model.current().modelID,
|
||||||
|
},
|
||||||
command: inputText,
|
command: inputText,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -538,6 +539,16 @@ export function Prompt(props: PromptProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const highlight = createMemo(() => {
|
||||||
|
if (keybind.leader) return theme.border
|
||||||
|
if (store.mode === "shell") return theme.primary
|
||||||
|
return local.agent.color(local.agent.current().name)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
renderer.setCursorColor(highlight())
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
@@ -562,17 +573,22 @@ export function Prompt(props: PromptProps) {
|
|||||||
/>
|
/>
|
||||||
<box ref={(r) => (anchor = r)}>
|
<box ref={(r) => (anchor = r)}>
|
||||||
<box
|
<box
|
||||||
flexDirection="row"
|
border={["left"]}
|
||||||
{...SplitBorder}
|
borderColor={highlight()}
|
||||||
borderColor={keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border}
|
customBorderChars={{
|
||||||
justifyContent="space-evenly"
|
...EmptyBorder,
|
||||||
|
vertical: "┃",
|
||||||
|
bottomLeft: "╹",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<box backgroundColor={theme.backgroundElement} width={3} height="100%" alignItems="center" paddingTop={1}>
|
<box
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.primary}>
|
paddingLeft={2}
|
||||||
{store.mode === "normal" ? ">" : "!"}
|
paddingRight={1}
|
||||||
</text>
|
paddingTop={1}
|
||||||
</box>
|
flexShrink={0}
|
||||||
<box paddingTop={1} paddingBottom={1} backgroundColor={theme.backgroundElement} flexGrow={1}>
|
backgroundColor={theme.backgroundElement}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder={
|
placeholder={
|
||||||
props.showPlaceholder
|
props.showPlaceholder
|
||||||
@@ -747,37 +763,151 @@ export function Prompt(props: PromptProps) {
|
|||||||
cursorColor={theme.primary}
|
cursorColor={theme.primary}
|
||||||
syntaxStyle={syntax()}
|
syntaxStyle={syntax()}
|
||||||
/>
|
/>
|
||||||
|
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
|
||||||
|
<text fg={highlight()}>
|
||||||
|
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
|
||||||
|
</text>
|
||||||
|
<Show when={store.mode === "normal"}>
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||||
|
<text flexShrink={0} fg={theme.text}>
|
||||||
|
{local.model.parsed().model}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<box backgroundColor={theme.backgroundElement} width={1} justifyContent="center" alignItems="center"></box>
|
</box>
|
||||||
|
<box
|
||||||
|
height={1}
|
||||||
|
border={["left"]}
|
||||||
|
borderColor={highlight()}
|
||||||
|
customBorderChars={{
|
||||||
|
...EmptyBorder,
|
||||||
|
vertical: "╹",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box
|
||||||
|
height={1}
|
||||||
|
border={["bottom"]}
|
||||||
|
borderColor={theme.backgroundElement}
|
||||||
|
customBorderChars={{
|
||||||
|
...EmptyBorder,
|
||||||
|
horizontal: "▀",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text flexShrink={0} wrapMode="none" fg={theme.text}>
|
<Show when={status().type !== "idle"} fallback={<text />}>
|
||||||
<span style={{ fg: theme.textMuted }}>{local.model.parsed().provider}</span>{" "}
|
<box
|
||||||
<span style={{ bold: true }}>{local.model.parsed().model}</span>
|
flexDirection="row"
|
||||||
</text>
|
gap={1}
|
||||||
<Switch>
|
flexGrow={1}
|
||||||
<Match when={status() === "compacting"}>
|
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||||
<text fg={theme.textMuted}>compacting...</text>
|
>
|
||||||
</Match>
|
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||||
<Match when={status() === "working"}>
|
<Loader />
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
{(() => {
|
||||||
esc{" "}
|
const retry = createMemo(() => {
|
||||||
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
const s = status()
|
||||||
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
if (s.type !== "retry") return
|
||||||
</span>
|
return s
|
||||||
</text>
|
})
|
||||||
|
const message = createMemo(() => {
|
||||||
|
const r = retry()
|
||||||
|
if (!r) return
|
||||||
|
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
||||||
|
return "gemini 3 way too hot right now"
|
||||||
|
if (r.message.length > 50) return r.message.slice(0, 50) + "..."
|
||||||
|
return r.message
|
||||||
|
})
|
||||||
|
const [seconds, setSeconds] = createSignal(0)
|
||||||
|
onMount(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const next = retry()?.next
|
||||||
|
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
clearInterval(timer)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<Show when={retry()}>
|
||||||
|
<text fg={theme.error}>
|
||||||
|
{message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
|
||||||
|
attempt #{retry()!.attempt}]
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Match>
|
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
|
||||||
<Match when={props.hint}>{props.hint!}</Match>
|
esc{" "}
|
||||||
<Match when={true}>
|
<span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
|
||||||
<text fg={theme.text}>
|
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
|
||||||
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
</span>
|
||||||
</text>
|
</text>
|
||||||
</Match>
|
</box>
|
||||||
</Switch>
|
</Show>
|
||||||
|
<Show when={status().type !== "retry"}>
|
||||||
|
<box gap={2} flexDirection="row">
|
||||||
|
<Switch>
|
||||||
|
<Match when={store.mode === "normal"}>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
{keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>switch agent</span>
|
||||||
|
</text>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
{keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
|
||||||
|
</text>
|
||||||
|
</Match>
|
||||||
|
<Match when={store.mode === "shell"}>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
|
||||||
|
</text>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Loader() {
|
||||||
|
const FRAMES = [
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
"▰▱▱▱▱▱▱",
|
||||||
|
"▰▰▱▱▱▱▱",
|
||||||
|
"▰▰▰▱▱▱▱",
|
||||||
|
"▱▰▰▰▱▱▱",
|
||||||
|
"▱▱▰▰▰▱▱",
|
||||||
|
"▱▱▱▰▰▰▱",
|
||||||
|
"▱▱▱▱▰▰▰",
|
||||||
|
"▱▱▱▱▱▰▰",
|
||||||
|
"▱▱▱▱▱▱▰",
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
"▱▱▱▱▱▱▱",
|
||||||
|
]
|
||||||
|
const [frame, setFrame] = createSignal(0)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setFrame((frame() + 1) % FRAMES.length)
|
||||||
|
}, 100)
|
||||||
|
onCleanup(() => {
|
||||||
|
clearInterval(timer)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const { theme } = useTheme()
|
||||||
|
return <text fg={theme.diffAdded}>{FRAMES[frame()]}</text>
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createSimpleContext } from "./helper"
|
|||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
import { useArgs } from "./args"
|
import { useArgs } from "./args"
|
||||||
|
import { RGBA } from "@opentui/core"
|
||||||
|
|
||||||
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||||
name: "Local",
|
name: "Local",
|
||||||
@@ -91,7 +92,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
color(name: string) {
|
color(name: string) {
|
||||||
const agent = agents().find((x) => x.name === name)
|
const agent = agents().find((x) => x.name === name)
|
||||||
if (agent?.color) return agent.color
|
if (agent?.color) return RGBA.fromHex(agent.color)
|
||||||
const index = agents().findIndex((x) => x.name === name)
|
const index = agents().findIndex((x) => x.name === name)
|
||||||
return colors()[index % colors().length]
|
return colors()[index % colors().length]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
|
|
||||||
sdk.event.subscribe().then(async (events) => {
|
sdk.event.subscribe().then(async (events) => {
|
||||||
for await (const event of events.stream) {
|
for await (const event of events.stream) {
|
||||||
console.log("event", event.type)
|
|
||||||
emitter.emit(event.type, event)
|
emitter.emit(event.type, event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type {
|
|||||||
McpStatus,
|
McpStatus,
|
||||||
FormatterStatus,
|
FormatterStatus,
|
||||||
SessionStatus,
|
SessionStatus,
|
||||||
|
ProviderListResponse,
|
||||||
|
ProviderAuthMethod,
|
||||||
} 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"
|
||||||
@@ -28,6 +30,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
status: "loading" | "partial" | "complete"
|
status: "loading" | "partial" | "complete"
|
||||||
provider: Provider[]
|
provider: Provider[]
|
||||||
provider_default: Record<string, string>
|
provider_default: Record<string, string>
|
||||||
|
provider_next: ProviderListResponse
|
||||||
|
provider_auth: Record<string, ProviderAuthMethod[]>
|
||||||
agent: Agent[]
|
agent: Agent[]
|
||||||
command: Command[]
|
command: Command[]
|
||||||
permission: {
|
permission: {
|
||||||
@@ -56,6 +60,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
formatter: FormatterStatus[]
|
formatter: FormatterStatus[]
|
||||||
}>({
|
}>({
|
||||||
|
provider_next: {
|
||||||
|
all: [],
|
||||||
|
default: {},
|
||||||
|
connected: [],
|
||||||
|
},
|
||||||
|
provider_auth: {},
|
||||||
config: {},
|
config: {},
|
||||||
status: "loading",
|
status: "loading",
|
||||||
agent: [],
|
agent: [],
|
||||||
@@ -232,20 +242,25 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const exit = useExit()
|
const exit = useExit()
|
||||||
|
|
||||||
onMount(() => {
|
async function bootstrap() {
|
||||||
// blocking
|
// blocking
|
||||||
Promise.all([
|
await Promise.all([
|
||||||
sdk.client.config.providers({ throwOnError: true }).then((x) => {
|
sdk.client.config.providers({ throwOnError: true }).then((x) => {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setStore("provider", x.data!.providers)
|
setStore("provider", x.data!.providers)
|
||||||
setStore("provider_default", x.data!.default)
|
setStore("provider_default", x.data!.default)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
sdk.client.provider.list({ throwOnError: true }).then((x) => {
|
||||||
|
batch(() => {
|
||||||
|
setStore("provider_next", x.data!)
|
||||||
|
})
|
||||||
|
}),
|
||||||
sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
|
sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])),
|
||||||
sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
|
sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)),
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setStore("status", "partial")
|
if (store.status !== "complete") setStore("status", "partial")
|
||||||
// non-blocking
|
// non-blocking
|
||||||
Promise.all([
|
Promise.all([
|
||||||
sdk.client.session.list().then((x) =>
|
sdk.client.session.list().then((x) =>
|
||||||
@@ -259,6 +274,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
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!)),
|
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
|
||||||
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
|
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
|
||||||
|
sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
setStore("status", "complete")
|
setStore("status", "complete")
|
||||||
})
|
})
|
||||||
@@ -266,6 +282,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
await exit(e)
|
await exit(e)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
bootstrap()
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
@@ -320,6 +340,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
console.log("synced in " + (Date.now() - now), sessionID)
|
console.log("synced in " + (Date.now() - now), sessionID)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bootstrap,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -132,7 +132,16 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
|
|||||||
if (c instanceof RGBA) return c
|
if (c instanceof RGBA) return c
|
||||||
if (typeof c === "string") {
|
if (typeof c === "string") {
|
||||||
if (c === "transparent" || c === "none") return RGBA.fromInts(0, 0, 0, 0)
|
if (c === "transparent" || c === "none") return RGBA.fromInts(0, 0, 0, 0)
|
||||||
return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c])
|
|
||||||
|
if (c.startsWith("#")) return RGBA.fromHex(c)
|
||||||
|
|
||||||
|
if (defs[c]) {
|
||||||
|
return resolveColor(defs[c])
|
||||||
|
} else if (theme.theme[c as keyof Theme]) {
|
||||||
|
return resolveColor(theme.theme[c as keyof Theme])
|
||||||
|
} else {
|
||||||
|
throw new Error(`Color reference "${c}" not found in defs or theme`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return resolveColor(c[mode])
|
return resolveColor(c[mode])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import { DialogMessage } from "./dialog-message"
|
|||||||
import type { PromptInfo } from "../../component/prompt/history"
|
import type { PromptInfo } from "../../component/prompt/history"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { DialogConfirm } from "@tui/ui/dialog-confirm"
|
import { DialogConfirm } from "@tui/ui/dialog-confirm"
|
||||||
|
import { DialogPrompt } from "@tui/ui/dialog-prompt"
|
||||||
import { DialogTimeline } from "./dialog-timeline"
|
import { DialogTimeline } from "./dialog-timeline"
|
||||||
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||||
import { Sidebar } from "./sidebar"
|
import { Sidebar } from "./sidebar"
|
||||||
@@ -105,11 +106,6 @@ export function Session() {
|
|||||||
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
|
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
|
||||||
})
|
})
|
||||||
|
|
||||||
const lastUserMessage = createMemo(() => {
|
|
||||||
const p = pending()
|
|
||||||
return messages().findLast((x) => x.role === "user" && (!p || x.id < p)) as UserMessage
|
|
||||||
})
|
|
||||||
|
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
|
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
|
||||||
const [conceal, setConceal] = createSignal(true)
|
const [conceal, setConceal] = createSignal(true)
|
||||||
@@ -146,9 +142,30 @@ export function Session() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
|
// Auto-navigate to whichever session currently needs permission input
|
||||||
|
createEffect(() => {
|
||||||
|
const currentSession = session()
|
||||||
|
if (!currentSession) return
|
||||||
|
const currentPermissions = permissions()
|
||||||
|
let targetID = currentPermissions.length > 0 ? currentSession.id : undefined
|
||||||
|
|
||||||
|
if (!targetID) {
|
||||||
|
const child = sync.data.session.find(
|
||||||
|
(x) => x.parentID === currentSession.id && (sync.data.permission[x.id]?.length ?? 0) > 0,
|
||||||
|
)
|
||||||
|
if (child) targetID = child.id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetID && targetID !== currentSession.id) {
|
||||||
|
navigate({
|
||||||
|
type: "session",
|
||||||
|
sessionID: targetID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let scroll: ScrollBoxRenderable
|
let scroll: ScrollBoxRenderable
|
||||||
let prompt: PromptRef
|
let prompt: PromptRef
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
@@ -583,12 +600,17 @@ export function Session() {
|
|||||||
transcript += `---\n\n`
|
transcript += `---\n\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to file in data directory
|
// Prompt for optional filename
|
||||||
const exportDir = path.join(Global.Path.data, "exports")
|
const customFilename = await DialogPrompt.show(dialog, "Export filename", {
|
||||||
await fs.mkdir(exportDir, { recursive: true })
|
value: `session-${sessionData.id.slice(0, 8)}.md`,
|
||||||
|
})
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
// Cancel if user pressed escape
|
||||||
const filename = `session-${sessionData.id.slice(0, 8)}-${timestamp}.md`
|
if (customFilename === null) return
|
||||||
|
|
||||||
|
// Save to file in current working directory
|
||||||
|
const exportDir = process.cwd()
|
||||||
|
const filename = customFilename.trim()
|
||||||
const filepath = path.join(exportDir, filename)
|
const filepath = path.join(exportDir, filename)
|
||||||
|
|
||||||
await Bun.write(filepath, transcript)
|
await Bun.write(filepath, transcript)
|
||||||
@@ -880,52 +902,55 @@ function UserMessage(props: {
|
|||||||
<Show when={text()}>
|
<Show when={text()}>
|
||||||
<box
|
<box
|
||||||
id={props.message.id}
|
id={props.message.id}
|
||||||
onMouseOver={() => {
|
|
||||||
setHover(true)
|
|
||||||
}}
|
|
||||||
onMouseOut={() => {
|
|
||||||
setHover(false)
|
|
||||||
}}
|
|
||||||
onMouseUp={props.onMouseUp}
|
|
||||||
border={["left"]}
|
border={["left"]}
|
||||||
paddingTop={1}
|
|
||||||
paddingBottom={1}
|
|
||||||
paddingLeft={2}
|
|
||||||
marginTop={props.index === 0 ? 0 : 1}
|
|
||||||
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
|
||||||
borderColor={color()}
|
borderColor={color()}
|
||||||
flexShrink={0}
|
customBorderChars={SplitBorder.customBorderChars}
|
||||||
|
marginTop={props.index === 0 ? 0 : 1}
|
||||||
>
|
>
|
||||||
<text fg={theme.text}>{text()?.text}</text>
|
<box
|
||||||
<Show when={files().length}>
|
onMouseOver={() => {
|
||||||
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
|
setHover(true)
|
||||||
<For each={files()}>
|
}}
|
||||||
{(file) => {
|
onMouseOut={() => {
|
||||||
const bg = createMemo(() => {
|
setHover(false)
|
||||||
if (file.mime.startsWith("image/")) return theme.accent
|
}}
|
||||||
if (file.mime === "application/pdf") return theme.primary
|
onMouseUp={props.onMouseUp}
|
||||||
return theme.secondary
|
paddingTop={1}
|
||||||
})
|
paddingBottom={1}
|
||||||
return (
|
paddingLeft={1}
|
||||||
<text fg={theme.text}>
|
backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
|
||||||
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
flexShrink={0}
|
||||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
|
>
|
||||||
</text>
|
<text fg={theme.text}>{text()?.text}</text>
|
||||||
)
|
<Show when={files().length}>
|
||||||
}}
|
<box flexDirection="row" paddingBottom={1} paddingTop={1} gap={1} flexWrap="wrap">
|
||||||
</For>
|
<For each={files()}>
|
||||||
</box>
|
{(file) => {
|
||||||
</Show>
|
const bg = createMemo(() => {
|
||||||
<text fg={theme.text}>
|
if (file.mime.startsWith("image/")) return theme.accent
|
||||||
{sync.data.config.username ?? "You"}{" "}
|
if (file.mime === "application/pdf") return theme.primary
|
||||||
<Show
|
return theme.secondary
|
||||||
when={queued()}
|
})
|
||||||
fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
|
return (
|
||||||
>
|
<text fg={theme.text}>
|
||||||
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
<span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
|
||||||
|
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</text>
|
<text fg={theme.textMuted}>
|
||||||
|
{sync.data.config.username ?? "You"}{" "}
|
||||||
|
<Show
|
||||||
|
when={queued()}
|
||||||
|
fallback={<span style={{ fg: theme.textMuted }}>{Locale.time(props.message.time.created)}</span>}
|
||||||
|
>
|
||||||
|
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
||||||
|
</Show>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={compaction()}>
|
<Show when={compaction()}>
|
||||||
@@ -982,51 +1007,62 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
|||||||
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
|
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.last && status().type !== "idle"}>
|
<Switch>
|
||||||
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
|
<Match when={props.last && status().type !== "idle" && false}>
|
||||||
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
|
<box paddingLeft={3} flexDirection="row" gap={1} marginTop={1}>
|
||||||
<Shimmer text={props.message.modelID} color={theme.text} />
|
<text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
|
||||||
{(() => {
|
<Shimmer text={props.message.modelID} color={theme.text} />
|
||||||
const retry = createMemo(() => {
|
{(() => {
|
||||||
const s = status()
|
const retry = createMemo(() => {
|
||||||
if (s.type !== "retry") return
|
const s = status()
|
||||||
return s
|
if (s.type !== "retry") return
|
||||||
})
|
return s
|
||||||
const [seconds, setSeconds] = createSignal(0)
|
|
||||||
onMount(() => {
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
const next = retry()?.next
|
|
||||||
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
clearInterval(timer)
|
|
||||||
})
|
})
|
||||||
})
|
const message = createMemo(() => {
|
||||||
return (
|
const r = retry()
|
||||||
<Show when={retry()}>
|
if (!r) return
|
||||||
<text fg={theme.error}>
|
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
|
||||||
{retry()!.message} [attempt #{retry()!.attempt}
|
return "gemini 3 way too hot right now"
|
||||||
{seconds() > 0 ? `, retrying in ${seconds()}s` : ""}]
|
if (r.message.length > 50) return r.message.slice(0, 50) + "..."
|
||||||
</text>
|
return r.message
|
||||||
</Show>
|
})
|
||||||
)
|
const [seconds, setSeconds] = createSignal(0)
|
||||||
})()}
|
onMount(() => {
|
||||||
</box>
|
const timer = setInterval(() => {
|
||||||
</Show>
|
const next = retry()?.next
|
||||||
<Show
|
if (next) setSeconds(Math.round((next - Date.now()) / 1000))
|
||||||
when={
|
}, 1000)
|
||||||
props.message.time.completed &&
|
|
||||||
props.parts.some((item) => item.type === "step-finish" && item.reason !== "tool-calls")
|
onCleanup(() => {
|
||||||
}
|
clearInterval(timer)
|
||||||
>
|
})
|
||||||
<box paddingLeft={3}>
|
})
|
||||||
<text marginTop={1}>
|
return (
|
||||||
<span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
|
<Show when={retry()}>
|
||||||
<span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
|
<text fg={theme.error}>
|
||||||
</text>
|
{message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
|
||||||
</box>
|
attempt #{retry()!.attempt}]
|
||||||
</Show>
|
</text>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</box>
|
||||||
|
</Match>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
(props.message.time.completed &&
|
||||||
|
props.parts.some((item) => item.type === "step-finish" && item.reason !== "tool-calls")) ||
|
||||||
|
props.last
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<box paddingLeft={3}>
|
||||||
|
<text marginTop={1}>
|
||||||
|
<span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
|
||||||
|
<span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1275,7 +1311,10 @@ ToolRegistry.register<typeof WriteTool>({
|
|||||||
container: "block",
|
container: "block",
|
||||||
render(props) {
|
render(props) {
|
||||||
const { theme, syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
const lines = createMemo(() => props.input.content?.split("\n") ?? [], [] as string[])
|
const lines = createMemo(
|
||||||
|
() => (typeof props.input.content === "string" ? props.input.content.split("\n") : []),
|
||||||
|
[] as string[],
|
||||||
|
)
|
||||||
const code = createMemo(() => {
|
const code = createMemo(() => {
|
||||||
if (!props.input.content) return ""
|
if (!props.input.content) return ""
|
||||||
const text = props.input.content
|
const text = props.input.content
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useDialog, type DialogContext } from "./dialog"
|
import { useDialog, type DialogContext } from "./dialog"
|
||||||
import { onMount } from "solid-js"
|
import { onMount, type JSX } from "solid-js"
|
||||||
import { useKeyboard } from "@opentui/solid"
|
import { useKeyboard } from "@opentui/solid"
|
||||||
|
|
||||||
export type DialogPromptProps = {
|
export type DialogPromptProps = {
|
||||||
title: string
|
title: string
|
||||||
|
description?: () => JSX.Element
|
||||||
|
placeholder?: string
|
||||||
value?: string
|
value?: string
|
||||||
onConfirm?: (value: string) => void
|
onConfirm?: (value: string) => void
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
@@ -19,12 +21,11 @@ export function DialogPrompt(props: DialogPromptProps) {
|
|||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
if (evt.name === "return") {
|
if (evt.name === "return") {
|
||||||
props.onConfirm?.(textarea.plainText)
|
props.onConfirm?.(textarea.plainText)
|
||||||
dialog.clear()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
dialog.setSize("large")
|
dialog.setSize("medium")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
}, 1)
|
}, 1)
|
||||||
@@ -37,35 +38,36 @@ export function DialogPrompt(props: DialogPromptProps) {
|
|||||||
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
<text attributes={TextAttributes.BOLD}>{props.title}</text>
|
||||||
<text fg={theme.textMuted}>esc</text>
|
<text fg={theme.textMuted}>esc</text>
|
||||||
</box>
|
</box>
|
||||||
<box>
|
<box gap={1}>
|
||||||
|
{props.description}
|
||||||
<textarea
|
<textarea
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
props.onConfirm?.(textarea.plainText)
|
props.onConfirm?.(textarea.plainText)
|
||||||
dialog.clear()
|
|
||||||
}}
|
}}
|
||||||
|
height={3}
|
||||||
keyBindings={[{ name: "return", action: "submit" }]}
|
keyBindings={[{ name: "return", action: "submit" }]}
|
||||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||||
initialValue={props.value}
|
initialValue={props.value}
|
||||||
placeholder="Enter text"
|
placeholder={props.placeholder ?? "Enter text"}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box paddingBottom={1}>
|
<box paddingBottom={1} gap={1} flexDirection="row">
|
||||||
<text fg={theme.textMuted}>Press enter to confirm, esc to cancel</text>
|
<text fg={theme.text}>
|
||||||
|
enter <span style={{ fg: theme.textMuted }}>submit</span>
|
||||||
|
</text>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
esc <span style={{ fg: theme.textMuted }}>cancel</span>
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogPrompt.show = (dialog: DialogContext, title: string, value?: string) => {
|
DialogPrompt.show = (dialog: DialogContext, title: string, options?: Omit<DialogPromptProps, "title">) => {
|
||||||
return new Promise<string | null>((resolve) => {
|
return new Promise<string | null>((resolve) => {
|
||||||
dialog.replace(
|
dialog.replace(
|
||||||
() => (
|
() => (
|
||||||
<DialogPrompt
|
<DialogPrompt title={title} {...options} onConfirm={(value) => resolve(value)} onCancel={() => resolve(null)} />
|
||||||
title={title}
|
|
||||||
value={value}
|
|
||||||
onConfirm={(value) => resolve(value)}
|
|
||||||
onCancel={() => resolve(null)}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
() => resolve(null),
|
() => resolve(null),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<box paddingLeft={3} paddingRight={2}>
|
<box paddingLeft={4} paddingRight={4}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||||
{props.title}
|
{props.title}
|
||||||
@@ -184,8 +184,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<scrollbox
|
<scrollbox
|
||||||
paddingLeft={2}
|
paddingLeft={1}
|
||||||
paddingRight={2}
|
paddingRight={1}
|
||||||
scrollbarOptions={{ visible: false }}
|
scrollbarOptions={{ visible: false }}
|
||||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
||||||
maxHeight={height()}
|
maxHeight={height()}
|
||||||
@@ -194,7 +194,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
{([category, options], index) => (
|
{([category, options], index) => (
|
||||||
<>
|
<>
|
||||||
<Show when={category}>
|
<Show when={category}>
|
||||||
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={1}>
|
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
|
||||||
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
||||||
{category}
|
{category}
|
||||||
</text>
|
</text>
|
||||||
@@ -203,6 +203,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
<For each={options}>
|
<For each={options}>
|
||||||
{(option) => {
|
{(option) => {
|
||||||
const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
|
const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
|
||||||
|
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
id={JSON.stringify(option.value)}
|
id={JSON.stringify(option.value)}
|
||||||
@@ -217,8 +218,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
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={current() ? 1 : 3}
|
||||||
paddingRight={1}
|
paddingRight={3}
|
||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
<Option
|
<Option
|
||||||
@@ -226,7 +227,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
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={current()}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
@@ -236,12 +237,14 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
<box paddingRight={2} paddingLeft={3} flexDirection="row" paddingBottom={1} gap={1}>
|
<box paddingRight={2} paddingLeft={4} 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 }}>
|
||||||
<span style={{ fg: theme.textMuted }}> {item.title}</span>
|
<b>{item.title}</b>{" "}
|
||||||
|
</span>
|
||||||
|
<span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span>
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -268,7 +271,7 @@ function Option(props: {
|
|||||||
fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
|
fg={props.active ? theme.background : props.current ? theme.primary : theme.text}
|
||||||
marginRight={0.5}
|
marginRight={0.5}
|
||||||
>
|
>
|
||||||
●
|
◆
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
<text
|
<text
|
||||||
@@ -277,6 +280,7 @@ function Option(props: {
|
|||||||
attributes={props.active ? TextAttributes.BOLD : undefined}
|
attributes={props.active ? TextAttributes.BOLD : undefined}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
wrapMode="none"
|
wrapMode="none"
|
||||||
|
paddingLeft={3}
|
||||||
>
|
>
|
||||||
{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>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { batch, createContext, Show, useContext, type JSX, type ParentProps } fr
|
|||||||
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 { Clipboard } from "@tui/util/clipboard"
|
||||||
|
import { useToast } from "./toast"
|
||||||
|
|
||||||
export function Dialog(
|
export function Dialog(
|
||||||
props: ParentProps<{
|
props: ParentProps<{
|
||||||
@@ -12,10 +14,12 @@ export function Dialog(
|
|||||||
) {
|
) {
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const renderer = useRenderer()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
onMouseUp={async () => {
|
onMouseUp={async () => {
|
||||||
|
if (renderer.getSelection()) return
|
||||||
props.onClose?.()
|
props.onClose?.()
|
||||||
}}
|
}}
|
||||||
width={dimensions().width}
|
width={dimensions().width}
|
||||||
@@ -29,6 +33,7 @@ export function Dialog(
|
|||||||
>
|
>
|
||||||
<box
|
<box
|
||||||
onMouseUp={async (e) => {
|
onMouseUp={async (e) => {
|
||||||
|
if (renderer.getSelection()) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
width={props.size === "large" ? 80 : 60}
|
width={props.size === "large" ? 80 : 60}
|
||||||
@@ -124,10 +129,28 @@ const ctx = createContext<DialogContext>()
|
|||||||
|
|
||||||
export function DialogProvider(props: ParentProps) {
|
export function DialogProvider(props: ParentProps) {
|
||||||
const value = init()
|
const value = init()
|
||||||
|
const renderer = useRenderer()
|
||||||
|
const toast = useToast()
|
||||||
return (
|
return (
|
||||||
<ctx.Provider value={value}>
|
<ctx.Provider value={value}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<box position="absolute">
|
<box
|
||||||
|
position="absolute"
|
||||||
|
onMouseUp={async () => {
|
||||||
|
const text = renderer.getSelection()?.getSelectedText()
|
||||||
|
if (text && text.length > 0) {
|
||||||
|
const base64 = Buffer.from(text).toString("base64")
|
||||||
|
const osc52 = `\x1b]52;c;${base64}\x07`
|
||||||
|
const finalOsc52 = process.env["TMUX"] ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
|
||||||
|
/* @ts-expect-error */
|
||||||
|
renderer.writeOut(finalOsc52)
|
||||||
|
await Clipboard.copy(text)
|
||||||
|
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
|
||||||
|
.catch(toast.error)
|
||||||
|
renderer.clearSelection()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Show when={value.stack.length}>
|
<Show when={value.stack.length}>
|
||||||
<Dialog onClose={() => value.clear()} size={value.size}>
|
<Dialog onClose={() => value.clear()} size={value.size}>
|
||||||
{value.stack.at(-1)!.element}
|
{value.stack.at(-1)!.element}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export function Toast() {
|
|||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
top={2}
|
top={2}
|
||||||
right={2}
|
right={2}
|
||||||
width={Math.min(60, dimensions().width - 6)}
|
|
||||||
maxWidth={Math.min(60, dimensions().width - 6)}
|
maxWidth={Math.min(60, dimensions().width - 6)}
|
||||||
paddingLeft={2}
|
paddingLeft={2}
|
||||||
paddingRight={2}
|
paddingRight={2}
|
||||||
@@ -39,7 +38,9 @@ export function Toast() {
|
|||||||
{current().title}
|
{current().title}
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
<text fg={theme.text}>{current().message}</text>
|
<text fg={theme.text} wrapMode="word" width="100%">
|
||||||
|
{current().message}
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CliRenderer } from "@opentui/core"
|
|||||||
|
|
||||||
export namespace Editor {
|
export namespace Editor {
|
||||||
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
|
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
|
||||||
const editor = process.env["EDITOR"]
|
const editor = process.env["VISUAL"] || process.env["EDITOR"]
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
|
|
||||||
const filepath = join(tmpdir(), `${Date.now()}.md`)
|
const filepath = join(tmpdir(), `${Date.now()}.md`)
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
import { ConfigMarkdown } from "@/config/markdown"
|
import { ConfigMarkdown } from "@/config/markdown"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { MCP } from "../mcp"
|
import { MCP } from "../mcp"
|
||||||
|
import { Provider } from "../provider/provider"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
|
|
||||||
export function FormatError(input: unknown) {
|
export function FormatError(input: unknown) {
|
||||||
if (MCP.Failed.isInstance(input))
|
if (MCP.Failed.isInstance(input))
|
||||||
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||||
|
if (Provider.ModelNotFoundError.isInstance(input)) {
|
||||||
|
const { providerID, modelID, suggestions } = input.data
|
||||||
|
return [
|
||||||
|
`Model not found: ${providerID}/${modelID}`,
|
||||||
|
...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
|
||||||
|
`Try: \`opencode models\` to list available models`,
|
||||||
|
`Or check your config (opencode.json) provider/model names`,
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
|
if (Provider.InitError.isInstance(input)) {
|
||||||
|
return `Failed to initialize provider "${input.data.providerID}". Check credentials and configuration.`
|
||||||
|
}
|
||||||
if (Config.JsonError.isInstance(input)) {
|
if (Config.JsonError.isInstance(input)) {
|
||||||
return (
|
return (
|
||||||
`Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
|
`Config file at ${input.data.path} is not valid JSON(C)` + (input.data.message ? `: ${input.data.message}` : "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (Config.ConfigDirectoryTypoError.isInstance(input)) {
|
if (Config.ConfigDirectoryTypoError.isInstance(input)) {
|
||||||
return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Use "${input.data.suggestion}" instead. This is a common typo.`
|
return `Directory "${input.data.dir}" in ${input.data.path} is not valid. Rename the directory to "${input.data.suggestion}" or remove it. This is a common typo.`
|
||||||
}
|
}
|
||||||
if (ConfigMarkdown.FrontmatterError.isInstance(input)) {
|
if (ConfigMarkdown.FrontmatterError.isInstance(input)) {
|
||||||
return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}`
|
return `Failed to parse frontmatter in ${input.data.path}:\n${input.data.message}`
|
||||||
|
|||||||
@@ -428,8 +428,8 @@ export namespace Config {
|
|||||||
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
|
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
|
||||||
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
||||||
history_next: z.string().optional().default("down").describe("Next history item"),
|
history_next: z.string().optional().default("down").describe("Next history item"),
|
||||||
session_child_cycle: z.string().optional().default("ctrl+right").describe("Next child session"),
|
session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
|
||||||
session_child_cycle_reverse: z.string().optional().default("ctrl+left").describe("Previous child session"),
|
session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.meta({
|
.meta({
|
||||||
@@ -480,6 +480,10 @@ export namespace Config {
|
|||||||
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
|
.describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
|
||||||
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
||||||
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
||||||
|
enabled_providers: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
|
||||||
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
||||||
small_model: z
|
small_model: z
|
||||||
.string()
|
.string()
|
||||||
@@ -511,6 +515,8 @@ export namespace Config {
|
|||||||
z.string(),
|
z.string(),
|
||||||
ModelsDev.Provider.partial()
|
ModelsDev.Provider.partial()
|
||||||
.extend({
|
.extend({
|
||||||
|
whitelist: z.array(z.string()).optional(),
|
||||||
|
blacklist: z.array(z.string()).optional(),
|
||||||
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
||||||
options: z
|
options: z
|
||||||
.object({
|
.object({
|
||||||
@@ -542,36 +548,43 @@ export namespace Config {
|
|||||||
.describe("Custom provider configurations and model overrides"),
|
.describe("Custom provider configurations and model overrides"),
|
||||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||||
formatter: z
|
formatter: z
|
||||||
.record(
|
.union([
|
||||||
z.string(),
|
z.literal(false),
|
||||||
z.object({
|
z.record(
|
||||||
disabled: z.boolean().optional(),
|
z.string(),
|
||||||
command: z.array(z.string()).optional(),
|
z.object({
|
||||||
environment: z.record(z.string(), z.string()).optional(),
|
disabled: z.boolean().optional(),
|
||||||
extensions: z.array(z.string()).optional(),
|
command: z.array(z.string()).optional(),
|
||||||
}),
|
environment: z.record(z.string(), z.string()).optional(),
|
||||||
)
|
extensions: z.array(z.string()).optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
lsp: z
|
lsp: z
|
||||||
.record(
|
.union([
|
||||||
z.string(),
|
z.literal(false),
|
||||||
z.union([
|
z.record(
|
||||||
z.object({
|
z.string(),
|
||||||
disabled: z.literal(true),
|
z.union([
|
||||||
}),
|
z.object({
|
||||||
z.object({
|
disabled: z.literal(true),
|
||||||
command: z.array(z.string()),
|
}),
|
||||||
extensions: z.array(z.string()).optional(),
|
z.object({
|
||||||
disabled: z.boolean().optional(),
|
command: z.array(z.string()),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
extensions: z.array(z.string()).optional(),
|
||||||
initialization: z.record(z.string(), z.any()).optional(),
|
disabled: z.boolean().optional(),
|
||||||
}),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
]),
|
initialization: z.record(z.string(), z.any()).optional(),
|
||||||
)
|
}),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
])
|
||||||
.optional()
|
.optional()
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (!data) return true
|
if (!data) return true
|
||||||
|
if (typeof data === "boolean") return true
|
||||||
const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
|
const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
|
||||||
|
|
||||||
return Object.entries(data).every(([id, config]) => {
|
return Object.entries(data).every(([id, config]) => {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export namespace Flag {
|
|||||||
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
|
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
|
||||||
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
|
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
|
||||||
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
||||||
|
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH =
|
||||||
|
process.env["OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH"]
|
||||||
|
|
||||||
// Experimental
|
// Experimental
|
||||||
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ export namespace Format {
|
|||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
|
|
||||||
const formatters: Record<string, Formatter.Info> = {}
|
const formatters: Record<string, Formatter.Info> = {}
|
||||||
|
if (cfg.formatter === false) {
|
||||||
|
log.info("all formatters are disabled")
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
formatters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const item of Object.values(Formatter)) {
|
for (const item of Object.values(Formatter)) {
|
||||||
formatters[item.name] = item
|
formatters[item.name] = item
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,21 @@ export namespace LSP {
|
|||||||
async () => {
|
async () => {
|
||||||
const clients: LSPClient.Info[] = []
|
const clients: LSPClient.Info[] = []
|
||||||
const servers: Record<string, LSPServer.Info> = {}
|
const servers: Record<string, LSPServer.Info> = {}
|
||||||
|
const cfg = await Config.get()
|
||||||
|
|
||||||
|
if (cfg.lsp === false) {
|
||||||
|
log.info("all LSPs are disabled")
|
||||||
|
return {
|
||||||
|
broken: new Set<string>(),
|
||||||
|
servers,
|
||||||
|
clients,
|
||||||
|
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const server of Object.values(LSPServer)) {
|
for (const server of Object.values(LSPServer)) {
|
||||||
servers[server.id] = server
|
servers[server.id] = server
|
||||||
}
|
}
|
||||||
const cfg = await Config.get()
|
|
||||||
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
|
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
|
||||||
const existing = servers[name]
|
const existing = servers[name]
|
||||||
if (item.disabled) {
|
if (item.disabled) {
|
||||||
|
|||||||
@@ -250,12 +250,12 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RubyLsp: Info = {
|
export const Rubocop: Info = {
|
||||||
id: "ruby-lsp",
|
id: "ruby-lsp",
|
||||||
root: NearestRoot(["Gemfile"]),
|
root: NearestRoot(["Gemfile"]),
|
||||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
let bin = Bun.which("ruby-lsp", {
|
let bin = Bun.which("rubocop", {
|
||||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||||
})
|
})
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
@@ -266,25 +266,25 @@ export namespace LSPServer {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
log.info("installing ruby-lsp")
|
log.info("installing rubocop")
|
||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
|
cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
stdin: "pipe",
|
stdin: "pipe",
|
||||||
})
|
})
|
||||||
const exit = await proc.exited
|
const exit = await proc.exited
|
||||||
if (exit !== 0) {
|
if (exit !== 0) {
|
||||||
log.error("Failed to install ruby-lsp")
|
log.error("Failed to install rubocop")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
|
bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
|
||||||
log.info(`installed ruby-lsp`, {
|
log.info(`installed rubocop`, {
|
||||||
bin,
|
bin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
process: spawn(bin!, ["--stdio"], {
|
process: spawn(bin!, ["--lsp"], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -945,6 +945,54 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const YamlLS: Info = {
|
||||||
|
id: "yaml-ls",
|
||||||
|
extensions: [".yaml", ".yml"],
|
||||||
|
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
|
||||||
|
async spawn(root) {
|
||||||
|
let binary = Bun.which("yaml-language-server")
|
||||||
|
const args: string[] = []
|
||||||
|
if (!binary) {
|
||||||
|
const js = path.join(
|
||||||
|
Global.Path.bin,
|
||||||
|
"node_modules",
|
||||||
|
"yaml-language-server",
|
||||||
|
"out",
|
||||||
|
"server",
|
||||||
|
"src",
|
||||||
|
"server.js",
|
||||||
|
)
|
||||||
|
const exists = await Bun.file(js).exists()
|
||||||
|
if (!exists) {
|
||||||
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
|
||||||
|
cwd: Global.Path.bin,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
stdin: "pipe",
|
||||||
|
}).exited
|
||||||
|
}
|
||||||
|
binary = BunProc.which()
|
||||||
|
args.push("run", js)
|
||||||
|
}
|
||||||
|
args.push("--stdio")
|
||||||
|
const proc = spawn(binary, args, {
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
process: proc,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const LuaLS: Info = {
|
export const LuaLS: Info = {
|
||||||
id: "lua-ls",
|
id: "lua-ls",
|
||||||
root: NearestRoot([
|
root: NearestRoot([
|
||||||
@@ -1077,4 +1125,44 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PHPIntelephense: Info = {
|
||||||
|
id: "php intelephense",
|
||||||
|
extensions: [".php"],
|
||||||
|
root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
|
||||||
|
async spawn(root) {
|
||||||
|
let binary = Bun.which("intelephense")
|
||||||
|
const args: string[] = []
|
||||||
|
if (!binary) {
|
||||||
|
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
|
||||||
|
if (!(await Bun.file(js).exists())) {
|
||||||
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
await Bun.spawn([BunProc.which(), "install", "intelephense"], {
|
||||||
|
cwd: Global.Path.bin,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
stdin: "pipe",
|
||||||
|
}).exited
|
||||||
|
}
|
||||||
|
binary = BunProc.which()
|
||||||
|
args.push("run", js)
|
||||||
|
}
|
||||||
|
args.push("--stdio")
|
||||||
|
const proc = spawn(binary, args, {
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
process: proc,
|
||||||
|
initialization: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { experimental_createMCPClient, type Tool } from "ai"
|
import { type Tool } from "ai"
|
||||||
|
import { experimental_createMCPClient } from "@ai-sdk/mcp"
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||||
|
|||||||
@@ -186,8 +186,13 @@ export namespace Permission {
|
|||||||
public readonly permissionID: string,
|
public readonly permissionID: string,
|
||||||
public readonly toolCallID?: string,
|
public readonly toolCallID?: string,
|
||||||
public readonly metadata?: Record<string, any>,
|
public readonly metadata?: Record<string, any>,
|
||||||
|
public readonly reason?: string,
|
||||||
) {
|
) {
|
||||||
super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
|
super(
|
||||||
|
reason !== undefined
|
||||||
|
? reason
|
||||||
|
: `The user rejected permission to use this specific tool call. You may try again with different parameters.`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const Instance = {
|
|||||||
async dispose() {
|
async dispose() {
|
||||||
Log.Default.info("disposing instance", { directory: Instance.directory })
|
Log.Default.info("disposing instance", { directory: Instance.directory })
|
||||||
await State.dispose(Instance.directory)
|
await State.dispose(Instance.directory)
|
||||||
|
cache.delete(Instance.directory)
|
||||||
},
|
},
|
||||||
async disposeAll() {
|
async disposeAll() {
|
||||||
Log.Default.info("disposing all instances")
|
Log.Default.info("disposing all instances")
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export namespace State {
|
|||||||
|
|
||||||
tasks.push(task)
|
tasks.push(task)
|
||||||
}
|
}
|
||||||
entries.delete(key)
|
entries.clear()
|
||||||
await Promise.all(tasks)
|
await Promise.all(tasks)
|
||||||
disposalFinished = true
|
disposalFinished = true
|
||||||
log.info("state disposal completed", { key })
|
log.info("state disposal completed", { key })
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { Instance } from "@/project/instance"
|
||||||
|
import { Plugin } from "../plugin"
|
||||||
|
import { map, filter, pipe, fromEntries, mapValues } from "remeda"
|
||||||
|
import z from "zod"
|
||||||
|
import { fn } from "@/util/fn"
|
||||||
|
import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin"
|
||||||
|
import { NamedError } from "@/util/error"
|
||||||
|
import { Auth } from "@/auth"
|
||||||
|
|
||||||
|
export namespace ProviderAuth {
|
||||||
|
const state = Instance.state(async () => {
|
||||||
|
const methods = pipe(
|
||||||
|
await Plugin.list(),
|
||||||
|
filter((x) => x.auth?.provider !== undefined),
|
||||||
|
map((x) => [x.auth!.provider, x.auth!] as const),
|
||||||
|
fromEntries(),
|
||||||
|
)
|
||||||
|
return { methods, pending: {} as Record<string, AuthOuathResult> }
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Method = z
|
||||||
|
.object({
|
||||||
|
type: z.union([z.literal("oauth"), z.literal("api")]),
|
||||||
|
label: z.string(),
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
ref: "ProviderAuthMethod",
|
||||||
|
})
|
||||||
|
export type Method = z.infer<typeof Method>
|
||||||
|
|
||||||
|
export async function methods() {
|
||||||
|
const s = await state().then((x) => x.methods)
|
||||||
|
return mapValues(s, (x) =>
|
||||||
|
x.methods.map(
|
||||||
|
(y): Method => ({
|
||||||
|
type: y.type,
|
||||||
|
label: y.label,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Authorization = z
|
||||||
|
.object({
|
||||||
|
url: z.string(),
|
||||||
|
method: z.union([z.literal("auto"), z.literal("code")]),
|
||||||
|
instructions: z.string(),
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
ref: "ProviderAuthAuthorization",
|
||||||
|
})
|
||||||
|
export type Authorization = z.infer<typeof Authorization>
|
||||||
|
|
||||||
|
export const authorize = fn(
|
||||||
|
z.object({
|
||||||
|
providerID: z.string(),
|
||||||
|
method: z.number(),
|
||||||
|
}),
|
||||||
|
async (input): Promise<Authorization | undefined> => {
|
||||||
|
const auth = await state().then((s) => s.methods[input.providerID])
|
||||||
|
const method = auth.methods[input.method]
|
||||||
|
if (method.type === "oauth") {
|
||||||
|
const result = await method.authorize()
|
||||||
|
await state().then((s) => (s.pending[input.providerID] = result))
|
||||||
|
return {
|
||||||
|
url: result.url,
|
||||||
|
method: result.method,
|
||||||
|
instructions: result.instructions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const callback = fn(
|
||||||
|
z.object({
|
||||||
|
providerID: z.string(),
|
||||||
|
method: z.number(),
|
||||||
|
code: z.string().optional(),
|
||||||
|
}),
|
||||||
|
async (input) => {
|
||||||
|
const match = await state().then((s) => s.pending[input.providerID])
|
||||||
|
if (!match) throw new OauthMissing({ providerID: input.providerID })
|
||||||
|
let result
|
||||||
|
|
||||||
|
if (match.method === "code") {
|
||||||
|
if (!input.code) throw new OauthCodeMissing({ providerID: input.providerID })
|
||||||
|
result = await match.callback(input.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.method === "auto") {
|
||||||
|
result = await match.callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.type === "success") {
|
||||||
|
if ("key" in result) {
|
||||||
|
await Auth.set(input.providerID, {
|
||||||
|
type: "api",
|
||||||
|
key: result.key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if ("refresh" in result) {
|
||||||
|
await Auth.set(input.providerID, {
|
||||||
|
type: "oauth",
|
||||||
|
access: result.access,
|
||||||
|
refresh: result.refresh,
|
||||||
|
expires: result.expires,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new OauthCallbackFailed({})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const api = fn(
|
||||||
|
z.object({
|
||||||
|
providerID: z.string(),
|
||||||
|
key: z.string(),
|
||||||
|
}),
|
||||||
|
async (input) => {
|
||||||
|
await Auth.set(input.providerID, {
|
||||||
|
type: "api",
|
||||||
|
key: input.key,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const OauthMissing = NamedError.create(
|
||||||
|
"ProviderAuthOauthMissing",
|
||||||
|
z.object({
|
||||||
|
providerID: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
export const OauthCodeMissing = NamedError.create(
|
||||||
|
"ProviderAuthOauthCodeMissing",
|
||||||
|
z.object({
|
||||||
|
providerID: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import path from "path"
|
import fuzzysort from "fuzzysort"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { mergeDeep, sortBy } from "remeda"
|
import { mergeDeep, sortBy } from "remeda"
|
||||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||||
@@ -10,7 +10,6 @@ import { ModelsDev } from "./models"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Global } from "../global"
|
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
|
|
||||||
@@ -243,6 +242,15 @@ export namespace Provider {
|
|||||||
const config = await Config.get()
|
const config = await Config.get()
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
|
const disabled = new Set(config.disabled_providers ?? [])
|
||||||
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
|
||||||
|
|
||||||
|
function isProviderAllowed(providerID: string): boolean {
|
||||||
|
if (enabled && !enabled.has(providerID)) return false
|
||||||
|
if (disabled.has(providerID)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const providers: {
|
const providers: {
|
||||||
[providerID: string]: {
|
[providerID: string]: {
|
||||||
source: Source
|
source: Source
|
||||||
@@ -367,10 +375,10 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
parsed.models[modelID] = parsedModel
|
parsed.models[modelID] = parsedModel
|
||||||
}
|
}
|
||||||
|
|
||||||
database[providerID] = parsed
|
database[providerID] = parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabled = await Config.get().then((cfg) => new Set(cfg.disabled_providers ?? []))
|
|
||||||
// load env
|
// load env
|
||||||
for (const [providerID, provider] of Object.entries(database)) {
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
if (disabled.has(providerID)) continue
|
if (disabled.has(providerID)) continue
|
||||||
@@ -448,6 +456,12 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [providerID, provider] of Object.entries(providers)) {
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
|
if (!isProviderAllowed(providerID)) {
|
||||||
|
delete providers[providerID]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const configProvider = config.provider?.[providerID]
|
||||||
const filteredModels = Object.fromEntries(
|
const filteredModels = Object.fromEntries(
|
||||||
Object.entries(provider.info.models)
|
Object.entries(provider.info.models)
|
||||||
// Filter out blacklisted models
|
// Filter out blacklisted models
|
||||||
@@ -460,15 +474,32 @@ export namespace Provider {
|
|||||||
([, model]) =>
|
([, model]) =>
|
||||||
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
((!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) &&
|
||||||
model.status !== "deprecated",
|
model.status !== "deprecated",
|
||||||
),
|
)
|
||||||
|
// Filter by provider's whitelist/blacklist from config
|
||||||
|
.filter(([modelID]) => {
|
||||||
|
if (!configProvider) return true
|
||||||
|
|
||||||
|
return (
|
||||||
|
(!configProvider.blacklist || !configProvider.blacklist.includes(modelID)) &&
|
||||||
|
(!configProvider.whitelist || configProvider.whitelist.includes(modelID))
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
provider.info.models = filteredModels
|
provider.info.models = filteredModels
|
||||||
|
|
||||||
if (Object.keys(provider.info.models).length === 0) {
|
if (Object.keys(provider.info.models).length === 0) {
|
||||||
delete providers[providerID]
|
delete providers[providerID]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.info("found", { providerID })
|
|
||||||
|
// TODO: set this in models.dev, not set due to breaking issues on older OC versions
|
||||||
|
// u have to set include usage to true w/ this provider, setting in models.dev would cause undefined issue when accessing usage in older versions
|
||||||
|
if (providerID === "openrouter") {
|
||||||
|
provider.info.npm = "@openrouter/ai-sdk-provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("found", { providerID, npm: provider.info.npm })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -494,6 +525,7 @@ export namespace Provider {
|
|||||||
if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
|
if (pkg.includes("@ai-sdk/openai-compatible") && options["includeUsage"] === undefined) {
|
||||||
options["includeUsage"] = true
|
options["includeUsage"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
|
const key = Bun.hash.xxHash32(JSON.stringify({ pkg, options }))
|
||||||
const existing = s.sdk.get(key)
|
const existing = s.sdk.get(key)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
@@ -515,26 +547,29 @@ export namespace Provider {
|
|||||||
const modPath =
|
const modPath =
|
||||||
provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath
|
provider.id === "google-vertex-anthropic" ? `${installedPath}/dist/anthropic/index.mjs` : installedPath
|
||||||
const mod = await import(modPath)
|
const mod = await import(modPath)
|
||||||
if (options["timeout"] !== undefined && options["timeout"] !== null) {
|
|
||||||
// Preserve custom fetch if it exists, wrap it with timeout logic
|
|
||||||
const customFetch = options["fetch"]
|
|
||||||
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
|
||||||
const { signal, ...rest } = init ?? {}
|
|
||||||
|
|
||||||
|
const customFetch = options["fetch"]
|
||||||
|
|
||||||
|
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
|
||||||
|
// Preserve custom fetch if it exists, wrap it with timeout logic
|
||||||
|
const fetchFn = customFetch ?? fetch
|
||||||
|
const opts = init ?? {}
|
||||||
|
|
||||||
|
if (options["timeout"] !== undefined && options["timeout"] !== null) {
|
||||||
const signals: AbortSignal[] = []
|
const signals: AbortSignal[] = []
|
||||||
if (signal) signals.push(signal)
|
if (opts.signal) signals.push(opts.signal)
|
||||||
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
|
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
|
||||||
|
|
||||||
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
|
||||||
|
|
||||||
const fetchFn = customFetch ?? fetch
|
opts.signal = combined
|
||||||
return fetchFn(input, {
|
|
||||||
...rest,
|
|
||||||
signal: combined,
|
|
||||||
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
|
||||||
timeout: false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fetchFn(input, {
|
||||||
|
...opts,
|
||||||
|
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
|
||||||
|
timeout: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||||
const loaded = fn({
|
const loaded = fn({
|
||||||
@@ -563,9 +598,78 @@ export namespace Provider {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const provider = s.providers[providerID]
|
const provider = s.providers[providerID]
|
||||||
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
|
if (!provider) {
|
||||||
|
let suggestions: string[] = []
|
||||||
|
const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, "")
|
||||||
|
const levenshtein = (a: string, b: string) => {
|
||||||
|
const m = a.length,
|
||||||
|
n = b.length
|
||||||
|
const dp = Array.from({ length: m + 1 }, () => new Array<number>(n + 1).fill(0))
|
||||||
|
for (let i = 0; i <= m; i++) dp[i][0] = i
|
||||||
|
for (let j = 0; j <= n; j++) dp[0][j] = j
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
||||||
|
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dp[m][n]
|
||||||
|
}
|
||||||
|
if (!modelID || modelID.trim() === "") {
|
||||||
|
// Treat single-token input as an unqualified model; search across all providers' models.
|
||||||
|
const q = normalize(providerID)
|
||||||
|
const entries: { combo: string; norm: string }[] = []
|
||||||
|
for (const [pid, prov] of Object.entries(s.providers)) {
|
||||||
|
for (const mid of Object.keys(prov.info.models)) {
|
||||||
|
entries.push({ combo: pid + "/" + mid, norm: normalize(mid) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const byNorm = fuzzysort.go(q, entries as any, { limit: 5, key: "norm" }).map((r: any) => r.obj.combo)
|
||||||
|
const combos = entries.map((e) => e.combo)
|
||||||
|
const byRaw = fuzzysort.go(providerID, combos, { limit: 5 }).map((r) => r.target)
|
||||||
|
let merged = Array.from(new Set([...byNorm, ...byRaw]))
|
||||||
|
if (merged.length === 0) {
|
||||||
|
// fallback to edit distance on normalized mid
|
||||||
|
const scored = entries
|
||||||
|
.map((e) => ({ combo: e.combo, d: levenshtein(q, e.norm) }))
|
||||||
|
.sort((a, b) => a.d - b.d)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((x) => x.combo)
|
||||||
|
merged = scored
|
||||||
|
}
|
||||||
|
suggestions = merged.slice(0, 3)
|
||||||
|
} else {
|
||||||
|
const pcands = Object.keys(s.providers)
|
||||||
|
const corpus = pcands.map((raw) => ({ raw, norm: normalize(raw) }))
|
||||||
|
const q = normalize(providerID)
|
||||||
|
const hits = fuzzysort.go(q, corpus as any, { limit: 5, key: "norm" })
|
||||||
|
let ranked = hits.map((r: any) => r.obj.raw)
|
||||||
|
if (ranked.length === 0) {
|
||||||
|
ranked = pcands
|
||||||
|
.map((p) => ({ p, d: levenshtein(q, normalize(p)) }))
|
||||||
|
.sort((a, b) => a.d - b.d)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((x) => x.p)
|
||||||
|
}
|
||||||
|
const providerSuggestions = ranked.map((r) => r + "/" + modelID)
|
||||||
|
suggestions = providerSuggestions
|
||||||
|
}
|
||||||
|
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||||
|
}
|
||||||
const info = provider.info.models[modelID]
|
const info = provider.info.models[modelID]
|
||||||
if (!info) throw new ModelNotFoundError({ providerID, modelID })
|
if (!info) {
|
||||||
|
const candidates = Object.keys(provider.info.models)
|
||||||
|
// Normalize punctuation differences like '-' vs '.' by stripping non-alphanumerics
|
||||||
|
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "")
|
||||||
|
const corpus = candidates.map((raw) => ({ raw, norm: normalize(raw) }))
|
||||||
|
const query = normalize(modelID)
|
||||||
|
const results = fuzzysort.go(query, corpus as any, { limit: 5, key: "norm" })
|
||||||
|
const ranked = results.map((r) => ("obj" in r ? (r as any).obj.raw : (r as any).target)) as string[]
|
||||||
|
const fallback = fuzzysort.go(modelID, candidates, { limit: 5 }).map((r) => r.target)
|
||||||
|
const merged = Array.from(new Set([...ranked, ...fallback]))
|
||||||
|
const suggestions = merged.slice(0, 3).map((m) => providerID + "/" + m)
|
||||||
|
throw new ModelNotFoundError({ providerID, modelID, suggestions })
|
||||||
|
}
|
||||||
const sdk = await getSDK(provider.info, info)
|
const sdk = await getSDK(provider.info, info)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -666,6 +770,7 @@ export namespace Provider {
|
|||||||
z.object({
|
z.object({
|
||||||
providerID: z.string(),
|
providerID: z.string(),
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
|
suggestions: z.array(z.string()).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export namespace ProviderTransform {
|
|||||||
const result: ModelMessage[] = []
|
const result: ModelMessage[] = []
|
||||||
for (let i = 0; i < msgs.length; i++) {
|
for (let i = 0; i < msgs.length; i++) {
|
||||||
const msg = msgs[i]
|
const msg = msgs[i]
|
||||||
const prevMsg = msgs[i - 1]
|
|
||||||
const nextMsg = msgs[i + 1]
|
const nextMsg = msgs[i + 1]
|
||||||
|
|
||||||
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
|
||||||
@@ -137,10 +136,23 @@ export namespace ProviderTransform {
|
|||||||
): Record<string, any> | undefined {
|
): Record<string, any> | undefined {
|
||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
|
|
||||||
|
// switch to providerID later, for now use this
|
||||||
|
if (npm === "@openrouter/ai-sdk-provider") {
|
||||||
|
result["usage"] = {
|
||||||
|
include: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (providerID === "openai") {
|
if (providerID === "openai") {
|
||||||
result["promptCacheKey"] = sessionID
|
result["promptCacheKey"] = sessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerID === "google" || (providerID === "opencode" && modelID.includes("gemini-3"))) {
|
||||||
|
result["thinkingConfig"] = {
|
||||||
|
includeThoughts: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
|
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
|
||||||
if (modelID.includes("codex")) {
|
if (modelID.includes("codex")) {
|
||||||
result["store"] = false
|
result["store"] = false
|
||||||
@@ -178,10 +190,18 @@ export namespace ProviderTransform {
|
|||||||
return {
|
return {
|
||||||
["anthropic" as string]: options,
|
["anthropic" as string]: options,
|
||||||
}
|
}
|
||||||
|
case "@ai-sdk/google":
|
||||||
|
return {
|
||||||
|
["google" as string]: options,
|
||||||
|
}
|
||||||
case "@ai-sdk/gateway":
|
case "@ai-sdk/gateway":
|
||||||
return {
|
return {
|
||||||
["gateway" as string]: options,
|
["gateway" as string]: options,
|
||||||
}
|
}
|
||||||
|
case "@openrouter/ai-sdk-provider":
|
||||||
|
return {
|
||||||
|
["openrouter" as string]: options,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
[providerID]: options,
|
[providerID]: options,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { Instance } from "../project/instance"
|
|||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { Command } from "../command"
|
import { Command } from "../command"
|
||||||
|
import { ProviderAuth } from "../provider/auth"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { ProjectRoute } from "./project"
|
import { ProjectRoute } from "./project"
|
||||||
import { ToolRegistry } from "../tool/registry"
|
import { ToolRegistry } from "../tool/registry"
|
||||||
@@ -42,6 +43,9 @@ import { SessionSummary } from "@/session/summary"
|
|||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
|
|
||||||
|
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||||
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
description: "Bad request",
|
description: "Bad request",
|
||||||
@@ -303,6 +307,27 @@ export namespace Server {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/instance/dispose",
|
||||||
|
describeRoute({
|
||||||
|
description: "Dispose the current instance",
|
||||||
|
operationId: "instance.dispose",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Instance disposed",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
await Instance.dispose()
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/path",
|
"/path",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -1160,6 +1185,138 @@ export namespace Server {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/provider",
|
||||||
|
describeRoute({
|
||||||
|
description: "List all providers",
|
||||||
|
operationId: "provider.list",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of providers",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(
|
||||||
|
z.object({
|
||||||
|
all: ModelsDev.Provider.array(),
|
||||||
|
default: z.record(z.string(), z.string()),
|
||||||
|
connected: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const providers = await ModelsDev.get()
|
||||||
|
const connected = await Provider.list().then((x) => Object.keys(x))
|
||||||
|
return c.json({
|
||||||
|
all: Object.values(providers),
|
||||||
|
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||||
|
connected,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/provider/auth",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get provider authentication methods",
|
||||||
|
operationId: "provider.auth",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Provider auth methods",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.json(await ProviderAuth.methods())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/provider/:id/oauth/authorize",
|
||||||
|
describeRoute({
|
||||||
|
description: "Authorize a provider using OAuth",
|
||||||
|
operationId: "provider.oauth.authorize",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Authorization URL and method",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(ProviderAuth.Authorization.optional()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string().meta({ description: "Provider ID" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
validator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
method: z.number().meta({ description: "Auth method index" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const id = c.req.valid("param").id
|
||||||
|
const { method } = c.req.valid("json")
|
||||||
|
const result = await ProviderAuth.authorize({
|
||||||
|
providerID: id,
|
||||||
|
method,
|
||||||
|
})
|
||||||
|
return c.json(result)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/provider/:id/oauth/callback",
|
||||||
|
describeRoute({
|
||||||
|
description: "Handle OAuth callback for a provider",
|
||||||
|
operationId: "provider.oauth.callback",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "OAuth callback processed successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string().meta({ description: "Provider ID" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
validator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
method: z.number().meta({ description: "Auth method index" }),
|
||||||
|
code: z.string().optional().meta({ description: "OAuth authorization code" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const id = c.req.valid("param").id
|
||||||
|
const { method, code } = c.req.valid("json")
|
||||||
|
await ProviderAuth.callback({
|
||||||
|
providerID: id,
|
||||||
|
method,
|
||||||
|
code,
|
||||||
|
})
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/find",
|
"/find",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
|
|||||||
@@ -382,17 +382,23 @@ export namespace Session {
|
|||||||
const adjustedInputTokens = excludesCachedTokens
|
const adjustedInputTokens = excludesCachedTokens
|
||||||
? (input.usage.inputTokens ?? 0)
|
? (input.usage.inputTokens ?? 0)
|
||||||
: (input.usage.inputTokens ?? 0) - cachedInputTokens
|
: (input.usage.inputTokens ?? 0) - cachedInputTokens
|
||||||
|
const safe = (value: number) => {
|
||||||
|
if (!Number.isFinite(value)) return 0
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
const tokens = {
|
const tokens = {
|
||||||
input: adjustedInputTokens,
|
input: safe(adjustedInputTokens),
|
||||||
output: input.usage.outputTokens ?? 0,
|
output: safe(input.usage.outputTokens ?? 0),
|
||||||
reasoning: input.usage?.reasoningTokens ?? 0,
|
reasoning: safe(input.usage?.reasoningTokens ?? 0),
|
||||||
cache: {
|
cache: {
|
||||||
write: (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
write: safe(
|
||||||
// @ts-expect-error
|
(input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||||
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
// @ts-expect-error
|
||||||
0) as number,
|
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
||||||
read: cachedInputTokens,
|
0) as number,
|
||||||
|
),
|
||||||
|
read: safe(cachedInputTokens),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,15 +407,17 @@ export namespace Session {
|
|||||||
? input.model.cost.context_over_200k
|
? input.model.cost.context_over_200k
|
||||||
: input.model.cost
|
: input.model.cost
|
||||||
return {
|
return {
|
||||||
cost: new Decimal(0)
|
cost: safe(
|
||||||
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
|
new Decimal(0)
|
||||||
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
|
.add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000))
|
||||||
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000))
|
.add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000))
|
||||||
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000))
|
.add(new Decimal(tokens.cache.read).mul(costInfo?.cache_read ?? 0).div(1_000_000))
|
||||||
// TODO: update models.dev to have better pricing model, for now:
|
.add(new Decimal(tokens.cache.write).mul(costInfo?.cache_write ?? 0).div(1_000_000))
|
||||||
// charge reasoning tokens at the same rate as output tokens
|
// TODO: update models.dev to have better pricing model, for now:
|
||||||
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
|
// charge reasoning tokens at the same rate as output tokens
|
||||||
.toNumber(),
|
.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
|
||||||
|
.toNumber(),
|
||||||
|
),
|
||||||
tokens,
|
tokens,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export namespace MessageV2 {
|
|||||||
type: z.literal("text"),
|
type: z.literal("text"),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
synthetic: z.boolean().optional(),
|
synthetic: z.boolean().optional(),
|
||||||
|
ignored: z.boolean().optional(),
|
||||||
time: z
|
time: z
|
||||||
.object({
|
.object({
|
||||||
start: z.number(),
|
start: z.number(),
|
||||||
@@ -566,7 +567,7 @@ export namespace MessageV2 {
|
|||||||
}
|
}
|
||||||
result.push(userMessage)
|
result.push(userMessage)
|
||||||
for (const part of msg.parts) {
|
for (const part of msg.parts) {
|
||||||
if (part.type === "text")
|
if (part.type === "text" && !part.ignored)
|
||||||
userMessage.parts.push({
|
userMessage.parts.push({
|
||||||
type: "text",
|
type: "text",
|
||||||
text: part.text,
|
text: part.text,
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export namespace SessionProcessor {
|
|||||||
|
|
||||||
const parts = await MessageV2.parts(input.assistantMessage.id)
|
const parts = await MessageV2.parts(input.assistantMessage.id)
|
||||||
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
|
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastThree.length === DOOM_LOOP_THRESHOLD &&
|
lastThree.length === DOOM_LOOP_THRESHOLD &&
|
||||||
lastThree.every(
|
lastThree.every(
|
||||||
@@ -160,6 +161,17 @@ export namespace SessionProcessor {
|
|||||||
input: value.input,
|
input: value.input,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (permission.doom_loop === "deny") {
|
||||||
|
throw new Permission.RejectedError(
|
||||||
|
input.assistantMessage.sessionID,
|
||||||
|
"doom_loop",
|
||||||
|
value.toolCallId,
|
||||||
|
{
|
||||||
|
tool: value.toolName,
|
||||||
|
input: value.input,
|
||||||
|
},
|
||||||
|
`You seem to be stuck in a doom loop, please stop repeating the same action`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ import { SessionProcessor } from "./processor"
|
|||||||
import { TaskTool } from "@/tool/task"
|
import { TaskTool } from "@/tool/task"
|
||||||
import { SessionStatus } from "./status"
|
import { SessionStatus } from "./status"
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
|
|
||||||
export namespace SessionPrompt {
|
export namespace SessionPrompt {
|
||||||
const log = Log.create({ service: "session.prompt" })
|
const log = Log.create({ service: "session.prompt" })
|
||||||
export const OUTPUT_TOKEN_MAX = 32_000
|
export const OUTPUT_TOKEN_MAX = 32_000
|
||||||
@@ -239,6 +242,7 @@ export namespace SessionPrompt {
|
|||||||
|
|
||||||
let step = 0
|
let step = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
|
SessionStatus.set(sessionID, { type: "busy" })
|
||||||
log.info("loop", { step, sessionID })
|
log.info("loop", { step, sessionID })
|
||||||
if (abort.aborted) break
|
if (abort.aborted) break
|
||||||
let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||||
@@ -600,12 +604,9 @@ export namespace SessionPrompt {
|
|||||||
throw new Error("Impossible")
|
throw new Error("Impossible")
|
||||||
})
|
})
|
||||||
|
|
||||||
async function resolveModel(input: { model: PromptInput["model"]; agent: Agent.Info }) {
|
async function lastModel(sessionID: string) {
|
||||||
if (input.model) {
|
for await (const item of MessageV2.stream(sessionID)) {
|
||||||
return input.model
|
if (item.info.role === "user" && item.info.model) return item.info.model
|
||||||
}
|
|
||||||
if (input.agent.model) {
|
|
||||||
return input.agent.model
|
|
||||||
}
|
}
|
||||||
return Provider.defaultModel()
|
return Provider.defaultModel()
|
||||||
}
|
}
|
||||||
@@ -794,10 +795,7 @@ export namespace SessionPrompt {
|
|||||||
tools: input.tools,
|
tools: input.tools,
|
||||||
system: input.system,
|
system: input.system,
|
||||||
agent: agent.name,
|
agent: agent.name,
|
||||||
model: await resolveModel({
|
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
|
||||||
model: input.model,
|
|
||||||
agent,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = await Promise.all(
|
const parts = await Promise.all(
|
||||||
@@ -899,7 +897,7 @@ export namespace SessionPrompt {
|
|||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
agent: input.agent!,
|
agent: input.agent!,
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
extra: { bypassCwdCheck: true },
|
extra: { bypassCwdCheck: true, ...info.model },
|
||||||
metadata: async () => {},
|
metadata: async () => {},
|
||||||
})
|
})
|
||||||
pieces.push(
|
pieces.push(
|
||||||
@@ -1091,6 +1089,12 @@ export namespace SessionPrompt {
|
|||||||
export const ShellInput = z.object({
|
export const ShellInput = z.object({
|
||||||
sessionID: Identifier.schema("session"),
|
sessionID: Identifier.schema("session"),
|
||||||
agent: z.string(),
|
agent: z.string(),
|
||||||
|
model: z
|
||||||
|
.object({
|
||||||
|
providerID: z.string(),
|
||||||
|
modelID: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
})
|
})
|
||||||
export type ShellInput = z.infer<typeof ShellInput>
|
export type ShellInput = z.infer<typeof ShellInput>
|
||||||
@@ -1100,7 +1104,7 @@ export namespace SessionPrompt {
|
|||||||
SessionRevert.cleanup(session)
|
SessionRevert.cleanup(session)
|
||||||
}
|
}
|
||||||
const agent = await Agent.get(input.agent)
|
const agent = await Agent.get(input.agent)
|
||||||
const model = await resolveModel({ agent, model: undefined })
|
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
|
||||||
const userMsg: MessageV2.User = {
|
const userMsg: MessageV2.User = {
|
||||||
id: Identifier.ascending("message"),
|
id: Identifier.ascending("message"),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -1338,10 +1342,8 @@ export namespace SessionPrompt {
|
|||||||
return cmdAgent.model
|
return cmdAgent.model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (input.model) {
|
if (input.model) return Provider.parseModel(input.model)
|
||||||
return Provider.parseModel(input.model)
|
return await lastModel(input.sessionID)
|
||||||
}
|
|
||||||
return await Provider.defaultModel()
|
|
||||||
})()
|
})()
|
||||||
const agent = await Agent.get(agentName)
|
const agent = await Agent.get(agentName)
|
||||||
|
|
||||||
@@ -1397,7 +1399,11 @@ export namespace SessionPrompt {
|
|||||||
...small.info.options,
|
...small.info.options,
|
||||||
}
|
}
|
||||||
if (small.providerID === "openai" || small.modelID.includes("gpt-5")) {
|
if (small.providerID === "openai" || small.modelID.includes("gpt-5")) {
|
||||||
options["reasoningEffort"] = "minimal"
|
if (small.modelID.includes("5.1")) {
|
||||||
|
options["reasoningEffort"] = "low"
|
||||||
|
} else {
|
||||||
|
options["reasoningEffort"] = "minimal"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (small.providerID === "google") {
|
if (small.providerID === "google") {
|
||||||
options["thinkingConfig"] = {
|
options["thinkingConfig"] = {
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ import { Filesystem } from "@/util/filesystem"
|
|||||||
import { Wildcard } from "@/util/wildcard"
|
import { Wildcard } from "@/util/wildcard"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
|
import { Flag } from "@/flag/flag.ts"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
const MAX_OUTPUT_LENGTH = 30_000
|
const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
|
||||||
|
const MAX_OUTPUT_LENGTH = (() => {
|
||||||
|
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
|
||||||
|
})()
|
||||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||||
const SIGKILL_TIMEOUT_MS = 200
|
const SIGKILL_TIMEOUT_MS = 200
|
||||||
@@ -22,7 +28,7 @@ export const log = Log.create({ service: "bash-tool" })
|
|||||||
|
|
||||||
const resolveWasm = (asset: string) => {
|
const resolveWasm = (asset: string) => {
|
||||||
if (asset.startsWith("file://")) return fileURLToPath(asset)
|
if (asset.startsWith("file://")) return fileURLToPath(asset)
|
||||||
if (asset.startsWith("/")) return asset
|
if (asset.startsWith("/") || /^[a-z]:/i.test(asset)) return asset
|
||||||
const url = new URL(asset, import.meta.url)
|
const url = new URL(asset, import.meta.url)
|
||||||
return fileURLToPath(url)
|
return fileURLToPath(url)
|
||||||
}
|
}
|
||||||
@@ -68,7 +74,8 @@ export const BashTool = Tool.define("bash", {
|
|||||||
if (!tree) {
|
if (!tree) {
|
||||||
throw new Error("Failed to parse command")
|
throw new Error("Failed to parse command")
|
||||||
}
|
}
|
||||||
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
|
const agent = await Agent.get(ctx.agent)
|
||||||
|
const permissions = agent.permission.bash
|
||||||
|
|
||||||
const askPatterns = new Set<string>()
|
const askPatterns = new Set<string>()
|
||||||
for (const node of tree.rootNode.descendantsOfType("command")) {
|
for (const node of tree.rootNode.descendantsOfType("command")) {
|
||||||
@@ -107,9 +114,30 @@ export const BashTool = Tool.define("bash", {
|
|||||||
: resolved
|
: resolved
|
||||||
|
|
||||||
if (!Filesystem.contains(Instance.directory, normalized)) {
|
if (!Filesystem.contains(Instance.directory, normalized)) {
|
||||||
throw new Error(
|
const parentDir = path.dirname(normalized)
|
||||||
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
|
if (agent.permission.external_directory === "ask") {
|
||||||
)
|
await Permission.ask({
|
||||||
|
type: "external_directory",
|
||||||
|
pattern: [parentDir, path.join(parentDir, "*")],
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
messageID: ctx.messageID,
|
||||||
|
callID: ctx.callID,
|
||||||
|
title: `This command references paths outside of ${Instance.directory}`,
|
||||||
|
metadata: {
|
||||||
|
command: params.command,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (agent.permission.external_directory === "deny") {
|
||||||
|
throw new Permission.RejectedError(
|
||||||
|
ctx.sessionID,
|
||||||
|
"external_directory",
|
||||||
|
ctx.callID,
|
||||||
|
{
|
||||||
|
command: params.command,
|
||||||
|
},
|
||||||
|
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,7 +299,7 @@ export const BashTool = Tool.define("bash", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: params.command,
|
title: params.description,
|
||||||
metadata: {
|
metadata: {
|
||||||
output,
|
output,
|
||||||
exit: proc.exitCode,
|
exit: proc.exitCode,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Usage notes:
|
|||||||
|
|
||||||
# Committing changes with git
|
# Committing changes with git
|
||||||
|
|
||||||
When the user asks you to create a new git commit, follow these steps carefully:
|
If and only if the user asks you to create a new git commit, follow these steps carefully:
|
||||||
|
|
||||||
1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool:
|
1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool:
|
||||||
- Run a git status command to see all untracked files.
|
- Run a git status command to see all untracked files.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
if (agent.permission.external_directory === "ask") {
|
if (agent.permission.external_directory === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "external_directory",
|
type: "external_directory",
|
||||||
pattern: parentDir,
|
pattern: [parentDir, path.join(parentDir, "*")],
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
messageID: ctx.messageID,
|
messageID: ctx.messageID,
|
||||||
callID: ctx.callID,
|
callID: ctx.callID,
|
||||||
@@ -57,6 +57,17 @@ export const EditTool = Tool.define("edit", {
|
|||||||
parentDir,
|
parentDir,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (agent.permission.external_directory === "deny") {
|
||||||
|
throw new Permission.RejectedError(
|
||||||
|
ctx.sessionID,
|
||||||
|
"external_directory",
|
||||||
|
ctx.callID,
|
||||||
|
{
|
||||||
|
filepath: filePath,
|
||||||
|
parentDir,
|
||||||
|
},
|
||||||
|
`File ${filePath} is not in the current working directory`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const PatchTool = Tool.define("patch", {
|
|||||||
if (agent.permission.external_directory === "ask") {
|
if (agent.permission.external_directory === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "external_directory",
|
type: "external_directory",
|
||||||
pattern: parentDir,
|
pattern: [parentDir, path.join(parentDir, "*")],
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
messageID: ctx.messageID,
|
messageID: ctx.messageID,
|
||||||
callID: ctx.callID,
|
callID: ctx.callID,
|
||||||
@@ -68,6 +68,17 @@ export const PatchTool = Tool.define("patch", {
|
|||||||
parentDir,
|
parentDir,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (agent.permission.external_directory === "deny") {
|
||||||
|
throw new Permission.RejectedError(
|
||||||
|
ctx.sessionID,
|
||||||
|
"external_directory",
|
||||||
|
ctx.callID,
|
||||||
|
{
|
||||||
|
filepath: filePath,
|
||||||
|
parentDir,
|
||||||
|
},
|
||||||
|
`File ${filePath} is not in the current working directory`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const ReadTool = Tool.define("read", {
|
|||||||
if (agent.permission.external_directory === "ask") {
|
if (agent.permission.external_directory === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "external_directory",
|
type: "external_directory",
|
||||||
pattern: parentDir,
|
pattern: [parentDir, path.join(parentDir, "*")],
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
messageID: ctx.messageID,
|
messageID: ctx.messageID,
|
||||||
callID: ctx.callID,
|
callID: ctx.callID,
|
||||||
@@ -46,6 +46,17 @@ export const ReadTool = Tool.define("read", {
|
|||||||
parentDir,
|
parentDir,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (agent.permission.external_directory === "deny") {
|
||||||
|
throw new Permission.RejectedError(
|
||||||
|
ctx.sessionID,
|
||||||
|
"external_directory",
|
||||||
|
ctx.callID,
|
||||||
|
{
|
||||||
|
filepath: filepath,
|
||||||
|
parentDir,
|
||||||
|
},
|
||||||
|
`File ${filepath} is not in the current working directory`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MessageV2 } from "../session/message-v2"
|
|||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import { SessionPrompt } from "../session/prompt"
|
import { SessionPrompt } from "../session/prompt"
|
||||||
|
import { iife } from "@/util/iife"
|
||||||
import { defer } from "@/util/defer"
|
import { defer } from "@/util/defer"
|
||||||
|
|
||||||
export const TaskTool = Tool.define("task", async () => {
|
export const TaskTool = Tool.define("task", async () => {
|
||||||
@@ -23,13 +24,21 @@ export const TaskTool = Tool.define("task", async () => {
|
|||||||
description: z.string().describe("A short (3-5 words) description of the task"),
|
description: z.string().describe("A short (3-5 words) description of the task"),
|
||||||
prompt: z.string().describe("The task for the agent to perform"),
|
prompt: z.string().describe("The task for the agent to perform"),
|
||||||
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
|
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
|
||||||
|
session_id: z.string().describe("Existing Task session to continue").optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
const agent = await Agent.get(params.subagent_type)
|
const agent = await Agent.get(params.subagent_type)
|
||||||
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
|
if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
|
||||||
const session = await Session.create({
|
const session = await iife(async () => {
|
||||||
parentID: ctx.sessionID,
|
if (params.session_id) {
|
||||||
title: params.description + ` (@${agent.name} subagent)`,
|
const found = await Session.get(params.session_id).catch(() => {})
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Session.create({
|
||||||
|
parentID: ctx.sessionID,
|
||||||
|
title: params.description + ` (@${agent.name} subagent)`,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
|
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
|
||||||
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
||||||
@@ -89,13 +98,17 @@ export const TaskTool = Tool.define("task", async () => {
|
|||||||
all = await Session.messages({ sessionID: session.id })
|
all = await Session.messages({ sessionID: session.id })
|
||||||
all = all.filter((x) => x.info.role === "assistant")
|
all = all.filter((x) => x.info.role === "assistant")
|
||||||
all = all.flatMap((msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[])
|
all = all.flatMap((msg) => msg.parts.filter((x: any) => x.type === "tool") as MessageV2.ToolPart[])
|
||||||
|
const text = result.parts.findLast((x) => x.type === "text")?.text ?? ""
|
||||||
|
|
||||||
|
const output = text + "\n\n" + ["<task_metadata>", `session_id: ${session.id}`, "</task_metadata>"].join("\n")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: params.description,
|
title: params.description,
|
||||||
metadata: {
|
metadata: {
|
||||||
summary: all,
|
summary: all,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
},
|
},
|
||||||
output: (result.parts.findLast((x: any) => x.type === "text") as any)?.text ?? "",
|
output,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ When NOT to use the Task tool:
|
|||||||
Usage notes:
|
Usage notes:
|
||||||
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
||||||
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
||||||
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
3. Each agent invocation is stateless unless you provide a session_id. Your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
||||||
4. The agent's outputs should generally be trusted
|
4. The agent's outputs should generally be trusted
|
||||||
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
||||||
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const WriteTool = Tool.define("write", {
|
|||||||
if (agent.permission.external_directory === "ask") {
|
if (agent.permission.external_directory === "ask") {
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
type: "external_directory",
|
type: "external_directory",
|
||||||
pattern: parentDir,
|
pattern: [parentDir, path.join(parentDir, "*")],
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
messageID: ctx.messageID,
|
messageID: ctx.messageID,
|
||||||
callID: ctx.callID,
|
callID: ctx.callID,
|
||||||
@@ -36,6 +36,17 @@ export const WriteTool = Tool.define("write", {
|
|||||||
parentDir,
|
parentDir,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else if (agent.permission.external_directory === "deny") {
|
||||||
|
throw new Permission.RejectedError(
|
||||||
|
ctx.sessionID,
|
||||||
|
"external_directory",
|
||||||
|
ctx.callID,
|
||||||
|
{
|
||||||
|
filepath: filepath,
|
||||||
|
parentDir,
|
||||||
|
},
|
||||||
|
`File ${filepath} is not in the current working directory`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { BashTool } from "../../src/tool/bash"
|
import { BashTool } from "../../src/tool/bash"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
import { Permission } from "../../src/permission"
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
sessionID: "test",
|
sessionID: "test",
|
||||||
@@ -33,20 +34,22 @@ describe("tool.bash", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("cd ../ should fail outside of project root", async () => {
|
// TODO: better test
|
||||||
await Instance.provide({
|
// test("cd ../ should ask for permission for external directory", async () => {
|
||||||
directory: projectRoot,
|
// await Instance.provide({
|
||||||
fn: async () => {
|
// directory: projectRoot,
|
||||||
expect(
|
// fn: async () => {
|
||||||
bash.execute(
|
// bash.execute(
|
||||||
{
|
// {
|
||||||
command: "cd ../",
|
// command: "cd ../",
|
||||||
description: "Try to cd to parent directory",
|
// description: "Try to cd to parent directory",
|
||||||
},
|
// },
|
||||||
ctx,
|
// ctx,
|
||||||
),
|
// )
|
||||||
).rejects.toThrow("This command references paths outside of")
|
// // Give time for permission to be asked
|
||||||
},
|
// await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
})
|
// expect(Permission.pending()[ctx.sessionID]).toBeDefined()
|
||||||
})
|
// },
|
||||||
|
// })
|
||||||
|
// })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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.75",
|
"version": "1.0.85",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
@@ -24,4 +24,4 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:"
|
"@typescript/native-preview": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-108
@@ -26,120 +26,122 @@ export type PluginInput = {
|
|||||||
|
|
||||||
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
||||||
|
|
||||||
|
export type AuthHook = {
|
||||||
|
provider: string
|
||||||
|
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
|
||||||
|
methods: (
|
||||||
|
| {
|
||||||
|
type: "oauth"
|
||||||
|
label: string
|
||||||
|
prompts?: Array<
|
||||||
|
| {
|
||||||
|
type: "text"
|
||||||
|
key: string
|
||||||
|
message: string
|
||||||
|
placeholder?: string
|
||||||
|
validate?: (value: string) => string | undefined
|
||||||
|
condition?: (inputs: Record<string, string>) => boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "select"
|
||||||
|
key: string
|
||||||
|
message: string
|
||||||
|
options: Array<{
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
hint?: string
|
||||||
|
}>
|
||||||
|
condition?: (inputs: Record<string, string>) => boolean
|
||||||
|
}
|
||||||
|
>
|
||||||
|
authorize(inputs?: Record<string, string>): Promise<AuthOuathResult>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "api"
|
||||||
|
label: string
|
||||||
|
prompts?: Array<
|
||||||
|
| {
|
||||||
|
type: "text"
|
||||||
|
key: string
|
||||||
|
message: string
|
||||||
|
placeholder?: string
|
||||||
|
validate?: (value: string) => string | undefined
|
||||||
|
condition?: (inputs: Record<string, string>) => boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "select"
|
||||||
|
key: string
|
||||||
|
message: string
|
||||||
|
options: Array<{
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
hint?: string
|
||||||
|
}>
|
||||||
|
condition?: (inputs: Record<string, string>) => boolean
|
||||||
|
}
|
||||||
|
>
|
||||||
|
authorize?(inputs?: Record<string, string>): Promise<
|
||||||
|
| {
|
||||||
|
type: "success"
|
||||||
|
key: string
|
||||||
|
provider?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "failed"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthOuathResult = { url: string; instructions: string } & (
|
||||||
|
| {
|
||||||
|
method: "auto"
|
||||||
|
callback(): Promise<
|
||||||
|
| ({
|
||||||
|
type: "success"
|
||||||
|
provider?: string
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
refresh: string
|
||||||
|
access: string
|
||||||
|
expires: number
|
||||||
|
}
|
||||||
|
| { key: string }
|
||||||
|
))
|
||||||
|
| {
|
||||||
|
type: "failed"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
method: "code"
|
||||||
|
callback(code: string): Promise<
|
||||||
|
| ({
|
||||||
|
type: "success"
|
||||||
|
provider?: string
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
refresh: string
|
||||||
|
access: string
|
||||||
|
expires: number
|
||||||
|
}
|
||||||
|
| { key: string }
|
||||||
|
))
|
||||||
|
| {
|
||||||
|
type: "failed"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export interface Hooks {
|
export interface Hooks {
|
||||||
event?: (input: { event: Event }) => Promise<void>
|
event?: (input: { event: Event }) => Promise<void>
|
||||||
config?: (input: Config) => Promise<void>
|
config?: (input: Config) => Promise<void>
|
||||||
tool?: {
|
tool?: {
|
||||||
[key: string]: ToolDefinition
|
[key: string]: ToolDefinition
|
||||||
}
|
}
|
||||||
auth?: {
|
auth?: AuthHook
|
||||||
provider: string
|
|
||||||
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
|
|
||||||
methods: (
|
|
||||||
| {
|
|
||||||
type: "oauth"
|
|
||||||
label: string
|
|
||||||
prompts?: Array<
|
|
||||||
| {
|
|
||||||
type: "text"
|
|
||||||
key: string
|
|
||||||
message: string
|
|
||||||
placeholder?: string
|
|
||||||
validate?: (value: string) => string | undefined
|
|
||||||
condition?: (inputs: Record<string, string>) => boolean
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "select"
|
|
||||||
key: string
|
|
||||||
message: string
|
|
||||||
options: Array<{
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
hint?: string
|
|
||||||
}>
|
|
||||||
condition?: (inputs: Record<string, string>) => boolean
|
|
||||||
}
|
|
||||||
>
|
|
||||||
authorize(inputs?: Record<string, string>): Promise<
|
|
||||||
{ url: string; instructions: string } & (
|
|
||||||
| {
|
|
||||||
method: "auto"
|
|
||||||
callback(): Promise<
|
|
||||||
| ({
|
|
||||||
type: "success"
|
|
||||||
provider?: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
refresh: string
|
|
||||||
access: string
|
|
||||||
expires: number
|
|
||||||
}
|
|
||||||
| { key: string }
|
|
||||||
))
|
|
||||||
| {
|
|
||||||
type: "failed"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
method: "code"
|
|
||||||
callback(code: string): Promise<
|
|
||||||
| ({
|
|
||||||
type: "success"
|
|
||||||
provider?: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
refresh: string
|
|
||||||
access: string
|
|
||||||
expires: number
|
|
||||||
}
|
|
||||||
| { key: string }
|
|
||||||
))
|
|
||||||
| {
|
|
||||||
type: "failed"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
|
||||||
)
|
|
||||||
>
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "api"
|
|
||||||
label: string
|
|
||||||
prompts?: Array<
|
|
||||||
| {
|
|
||||||
type: "text"
|
|
||||||
key: string
|
|
||||||
message: string
|
|
||||||
placeholder?: string
|
|
||||||
validate?: (value: string) => string | undefined
|
|
||||||
condition?: (inputs: Record<string, string>) => boolean
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "select"
|
|
||||||
key: string
|
|
||||||
message: string
|
|
||||||
options: Array<{
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
hint?: string
|
|
||||||
}>
|
|
||||||
condition?: (inputs: Record<string, string>) => boolean
|
|
||||||
}
|
|
||||||
>
|
|
||||||
authorize?(inputs?: Record<string, string>): Promise<
|
|
||||||
| {
|
|
||||||
type: "success"
|
|
||||||
key: string
|
|
||||||
provider?: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "failed"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
|
||||||
)[]
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Called when a new message is received
|
* Called when a new message is received
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.75",
|
"version": "1.0.85",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
@@ -26,4 +26,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"directory": "dist"
|
"directory": "dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import type {
|
|||||||
ToolListData,
|
ToolListData,
|
||||||
ToolListResponses,
|
ToolListResponses,
|
||||||
ToolListErrors,
|
ToolListErrors,
|
||||||
|
InstanceDisposeData,
|
||||||
|
InstanceDisposeResponses,
|
||||||
PathGetData,
|
PathGetData,
|
||||||
PathGetResponses,
|
PathGetResponses,
|
||||||
SessionListData,
|
SessionListData,
|
||||||
@@ -92,6 +94,16 @@ import type {
|
|||||||
CommandListResponses,
|
CommandListResponses,
|
||||||
ConfigProvidersData,
|
ConfigProvidersData,
|
||||||
ConfigProvidersResponses,
|
ConfigProvidersResponses,
|
||||||
|
ProviderListData,
|
||||||
|
ProviderListResponses,
|
||||||
|
ProviderAuthData,
|
||||||
|
ProviderAuthResponses,
|
||||||
|
ProviderOauthAuthorizeData,
|
||||||
|
ProviderOauthAuthorizeResponses,
|
||||||
|
ProviderOauthAuthorizeErrors,
|
||||||
|
ProviderOauthCallbackData,
|
||||||
|
ProviderOauthCallbackResponses,
|
||||||
|
ProviderOauthCallbackErrors,
|
||||||
FindTextData,
|
FindTextData,
|
||||||
FindTextResponses,
|
FindTextResponses,
|
||||||
FindFilesData,
|
FindFilesData,
|
||||||
@@ -272,6 +284,18 @@ class Tool extends _HeyApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Instance extends _HeyApiClient {
|
||||||
|
/**
|
||||||
|
* Dispose the current instance
|
||||||
|
*/
|
||||||
|
public dispose<ThrowOnError extends boolean = false>(options?: Options<InstanceDisposeData, ThrowOnError>) {
|
||||||
|
return (options?.client ?? this._client).post<InstanceDisposeResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/instance/dispose",
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Path extends _HeyApiClient {
|
class Path extends _HeyApiClient {
|
||||||
/**
|
/**
|
||||||
* Get the current path
|
* Get the current path
|
||||||
@@ -554,6 +578,67 @@ class Command extends _HeyApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Oauth extends _HeyApiClient {
|
||||||
|
/**
|
||||||
|
* Authorize a provider using OAuth
|
||||||
|
*/
|
||||||
|
public authorize<ThrowOnError extends boolean = false>(options: Options<ProviderOauthAuthorizeData, ThrowOnError>) {
|
||||||
|
return (options.client ?? this._client).post<
|
||||||
|
ProviderOauthAuthorizeResponses,
|
||||||
|
ProviderOauthAuthorizeErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
url: "/provider/{id}/oauth/authorize",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle OAuth callback for a provider
|
||||||
|
*/
|
||||||
|
public callback<ThrowOnError extends boolean = false>(options: Options<ProviderOauthCallbackData, ThrowOnError>) {
|
||||||
|
return (options.client ?? this._client).post<
|
||||||
|
ProviderOauthCallbackResponses,
|
||||||
|
ProviderOauthCallbackErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
url: "/provider/{id}/oauth/callback",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Provider extends _HeyApiClient {
|
||||||
|
/**
|
||||||
|
* List all providers
|
||||||
|
*/
|
||||||
|
public list<ThrowOnError extends boolean = false>(options?: Options<ProviderListData, ThrowOnError>) {
|
||||||
|
return (options?.client ?? this._client).get<ProviderListResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/provider",
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get provider authentication methods
|
||||||
|
*/
|
||||||
|
public auth<ThrowOnError extends boolean = false>(options?: Options<ProviderAuthData, ThrowOnError>) {
|
||||||
|
return (options?.client ?? this._client).get<ProviderAuthResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/provider/auth",
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
oauth = new Oauth({ client: this._client })
|
||||||
|
}
|
||||||
|
|
||||||
class Find extends _HeyApiClient {
|
class Find extends _HeyApiClient {
|
||||||
/**
|
/**
|
||||||
* Find text in files
|
* Find text in files
|
||||||
@@ -891,9 +976,11 @@ export class OpencodeClient extends _HeyApiClient {
|
|||||||
project = new Project({ client: this._client })
|
project = new Project({ client: this._client })
|
||||||
config = new Config({ client: this._client })
|
config = new Config({ client: this._client })
|
||||||
tool = new Tool({ client: this._client })
|
tool = new Tool({ client: this._client })
|
||||||
|
instance = new Instance({ client: this._client })
|
||||||
path = new Path({ client: this._client })
|
path = new Path({ client: this._client })
|
||||||
session = new Session({ client: this._client })
|
session = new Session({ client: this._client })
|
||||||
command = new Command({ client: this._client })
|
command = new Command({ client: this._client })
|
||||||
|
provider = new Provider({ client: this._client })
|
||||||
find = new Find({ client: this._client })
|
find = new Find({ client: this._client })
|
||||||
file = new File({ client: this._client })
|
file = new File({ client: this._client })
|
||||||
app = new App({ client: this._client })
|
app = new App({ client: this._client })
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export type TextPart = {
|
|||||||
type: "text"
|
type: "text"
|
||||||
text: string
|
text: string
|
||||||
synthetic?: boolean
|
synthetic?: boolean
|
||||||
|
ignored?: boolean
|
||||||
time?: {
|
time?: {
|
||||||
start: number
|
start: number
|
||||||
end?: number
|
end?: number
|
||||||
@@ -1015,6 +1016,10 @@ export type Config = {
|
|||||||
* Disable providers that are loaded automatically
|
* Disable providers that are loaded automatically
|
||||||
*/
|
*/
|
||||||
disabled_providers?: Array<string>
|
disabled_providers?: Array<string>
|
||||||
|
/**
|
||||||
|
* When set, ONLY these providers will be enabled. All other providers will be ignored
|
||||||
|
*/
|
||||||
|
enabled_providers?: Array<string>
|
||||||
/**
|
/**
|
||||||
* Model to use in the format of provider/model, eg anthropic/claude-2
|
* Model to use in the format of provider/model, eg anthropic/claude-2
|
||||||
*/
|
*/
|
||||||
@@ -1096,6 +1101,8 @@ export type Config = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
whitelist?: Array<string>
|
||||||
|
blacklist?: Array<string>
|
||||||
options?: {
|
options?: {
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
baseURL?: string
|
baseURL?: string
|
||||||
@@ -1117,33 +1124,37 @@ export type Config = {
|
|||||||
mcp?: {
|
mcp?: {
|
||||||
[key: string]: McpLocalConfig | McpRemoteConfig
|
[key: string]: McpLocalConfig | McpRemoteConfig
|
||||||
}
|
}
|
||||||
formatter?: {
|
formatter?:
|
||||||
[key: string]: {
|
| false
|
||||||
disabled?: boolean
|
| {
|
||||||
command?: Array<string>
|
[key: string]: {
|
||||||
environment?: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
extensions?: Array<string>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lsp?: {
|
|
||||||
[key: string]:
|
|
||||||
| {
|
|
||||||
disabled: true
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
command: Array<string>
|
|
||||||
extensions?: Array<string>
|
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
env?: {
|
command?: Array<string>
|
||||||
|
environment?: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
initialization?: {
|
extensions?: Array<string>
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lsp?:
|
||||||
|
| false
|
||||||
|
| {
|
||||||
|
[key: string]:
|
||||||
|
| {
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
command: Array<string>
|
||||||
|
extensions?: Array<string>
|
||||||
|
disabled?: boolean
|
||||||
|
env?: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
initialization?: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Additional instruction files or patterns to include
|
* Additional instruction files or patterns to include
|
||||||
*/
|
*/
|
||||||
@@ -1229,6 +1240,7 @@ export type TextPartInput = {
|
|||||||
type: "text"
|
type: "text"
|
||||||
text: string
|
text: string
|
||||||
synthetic?: boolean
|
synthetic?: boolean
|
||||||
|
ignored?: boolean
|
||||||
time?: {
|
time?: {
|
||||||
start: number
|
start: number
|
||||||
end?: number
|
end?: number
|
||||||
@@ -1327,6 +1339,17 @@ export type Provider = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProviderAuthMethod = {
|
||||||
|
type: "oauth" | "api"
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderAuthAuthorization = {
|
||||||
|
url: string
|
||||||
|
method: "auto" | "code"
|
||||||
|
instructions: string
|
||||||
|
}
|
||||||
|
|
||||||
export type Symbol = {
|
export type Symbol = {
|
||||||
name: string
|
name: string
|
||||||
kind: number
|
kind: number
|
||||||
@@ -1605,6 +1628,24 @@ export type ToolListResponses = {
|
|||||||
|
|
||||||
export type ToolListResponse = ToolListResponses[keyof ToolListResponses]
|
export type ToolListResponse = ToolListResponses[keyof ToolListResponses]
|
||||||
|
|
||||||
|
export type InstanceDisposeData = {
|
||||||
|
body?: never
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
|
url: "/instance/dispose"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstanceDisposeResponses = {
|
||||||
|
/**
|
||||||
|
* Instance disposed
|
||||||
|
*/
|
||||||
|
200: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses]
|
||||||
|
|
||||||
export type PathGetData = {
|
export type PathGetData = {
|
||||||
body?: never
|
body?: never
|
||||||
path?: never
|
path?: never
|
||||||
@@ -2290,6 +2331,10 @@ export type SessionCommandResponse = SessionCommandResponses[keyof SessionComman
|
|||||||
export type SessionShellData = {
|
export type SessionShellData = {
|
||||||
body?: {
|
body?: {
|
||||||
agent: string
|
agent: string
|
||||||
|
model?: {
|
||||||
|
providerID: string
|
||||||
|
modelID: string
|
||||||
|
}
|
||||||
command: string
|
command: string
|
||||||
}
|
}
|
||||||
path: {
|
path: {
|
||||||
@@ -2474,6 +2519,128 @@ export type ConfigProvidersResponses = {
|
|||||||
|
|
||||||
export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses]
|
export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses]
|
||||||
|
|
||||||
|
export type ProviderListData = {
|
||||||
|
body?: never
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
|
url: "/provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderListResponses = {
|
||||||
|
/**
|
||||||
|
* List of providers
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
all: Array<Provider>
|
||||||
|
default: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
connected: Array<string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses]
|
||||||
|
|
||||||
|
export type ProviderAuthData = {
|
||||||
|
body?: never
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
|
url: "/provider/auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderAuthResponses = {
|
||||||
|
/**
|
||||||
|
* Provider auth methods
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
[key: string]: Array<ProviderAuthMethod>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses]
|
||||||
|
|
||||||
|
export type ProviderOauthAuthorizeData = {
|
||||||
|
body?: {
|
||||||
|
/**
|
||||||
|
* Auth method index
|
||||||
|
*/
|
||||||
|
method: number
|
||||||
|
}
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Provider ID
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
|
url: "/provider/{id}/oauth/authorize"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderOauthAuthorizeErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request
|
||||||
|
*/
|
||||||
|
400: BadRequestError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors]
|
||||||
|
|
||||||
|
export type ProviderOauthAuthorizeResponses = {
|
||||||
|
/**
|
||||||
|
* Authorization URL and method
|
||||||
|
*/
|
||||||
|
200: ProviderAuthAuthorization
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses]
|
||||||
|
|
||||||
|
export type ProviderOauthCallbackData = {
|
||||||
|
body?: {
|
||||||
|
/**
|
||||||
|
* Auth method index
|
||||||
|
*/
|
||||||
|
method: number
|
||||||
|
/**
|
||||||
|
* OAuth authorization code
|
||||||
|
*/
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Provider ID
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
}
|
||||||
|
url: "/provider/{id}/oauth/callback"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderOauthCallbackErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request
|
||||||
|
*/
|
||||||
|
400: BadRequestError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors]
|
||||||
|
|
||||||
|
export type ProviderOauthCallbackResponses = {
|
||||||
|
/**
|
||||||
|
* OAuth callback processed successfully
|
||||||
|
*/
|
||||||
|
200: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses]
|
||||||
|
|
||||||
export type FindTextData = {
|
export type FindTextData = {
|
||||||
body?: never
|
body?: never
|
||||||
path?: never
|
path?: never
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ class KeybindsConfig:
|
|||||||
session_unshare: Union[Unset, str] = "none"
|
session_unshare: Union[Unset, str] = "none"
|
||||||
session_interrupt: Union[Unset, str] = "esc"
|
session_interrupt: Union[Unset, str] = "esc"
|
||||||
session_compact: Union[Unset, str] = "<leader>c"
|
session_compact: Union[Unset, str] = "<leader>c"
|
||||||
session_child_cycle: Union[Unset, str] = "ctrl+right"
|
session_child_cycle: Union[Unset, str] = "<leader>right"
|
||||||
session_child_cycle_reverse: Union[Unset, str] = "ctrl+left"
|
session_child_cycle_reverse: Union[Unset, str] = "<leader>left"
|
||||||
messages_page_up: Union[Unset, str] = "pgup"
|
messages_page_up: Union[Unset, str] = "pgup"
|
||||||
messages_page_down: Union[Unset, str] = "pgdown"
|
messages_page_down: Union[Unset, str] = "pgdown"
|
||||||
messages_half_page_up: Union[Unset, str] = "ctrl+alt+u"
|
messages_half_page_up: Union[Unset, str] = "ctrl+alt+u"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.75",
|
"version": "1.0.85",
|
||||||
"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.75",
|
"version": "1.0.85",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/components/index.ts",
|
".": "./src/components/index.ts",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
[data-component="markdown"] {
|
[data-component="markdown"] {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
scrollbar-width: none;
|
|
||||||
color: var(--text-base);
|
color: var(--text-base);
|
||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
|
|
||||||
@@ -14,10 +13,6 @@
|
|||||||
line-height: var(--line-height-large); /* 166.667% */
|
line-height: var(--line-height-large); /* 166.667% */
|
||||||
letter-spacing: var(--letter-spacing-normal);
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
@@ -41,4 +36,15 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
border-color: var(--border-weaker-base);
|
border-color: var(--border-weaker-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-component="text-part"] {
|
[data-component="text-part"] {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
[data-component="markdown"] {
|
[data-component="markdown"] {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user