mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-04 15:50:44 +08:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb4d97d51 | ||
|
|
b1b82977ec | ||
|
|
f6262460ff | ||
|
|
560a610384 | ||
|
|
0308b2ff98 | ||
|
|
5b92d49be7 | ||
|
|
0386d0ae09 | ||
|
|
28bec57e1d | ||
|
|
aaa31f02af | ||
|
|
ff609a52c1 | ||
|
|
1e30793f0a | ||
|
|
5268eb479d | ||
|
|
a4eba2e6e9 | ||
|
|
0f30115205 | ||
|
|
ae500ea01d | ||
|
|
087479d459 | ||
|
|
6e2379a28c | ||
|
|
262fa184fd | ||
|
|
9a7e1c154d | ||
|
|
5bf9193dfa | ||
|
|
180fb3f39d | ||
|
|
7e6b7314f4 | ||
|
|
a262508fb8 | ||
|
|
80ff24b65a | ||
|
|
012aa67e42 | ||
|
|
0a1f12a583 | ||
|
|
f17dc812d0 | ||
|
|
1854d85ccc | ||
|
|
2c4d1fb8b4 | ||
|
|
d8fa7cf65d | ||
|
|
7d8d360138 | ||
|
|
d80880350d | ||
|
|
b693ed0dbd | ||
|
|
83f961a7c2 | ||
|
|
a093917db1 | ||
|
|
52716db649 | ||
|
|
9ca4b464ea | ||
|
|
204a31b6bb | ||
|
|
813d287a09 | ||
|
|
4dd9f33eba | ||
|
|
5953378a12 | ||
|
|
b419eed295 | ||
|
|
52deb7f352 | ||
|
|
a4f3aecbaa | ||
|
|
49ff6a852a | ||
|
|
7f537d2e98 | ||
|
|
753443b16f | ||
|
|
33c63be980 | ||
|
|
b6efca42b4 | ||
|
|
fa6eadc39a | ||
|
|
8789acefa6 | ||
|
|
0e280017e6 | ||
|
|
17e8322c29 | ||
|
|
96eda740cd | ||
|
|
fa84612357 | ||
|
|
cf1f63eda3 | ||
|
|
9704f5ce89 | ||
|
|
0eaec2af82 | ||
|
|
398d35dc97 | ||
|
|
5efeaae093 | ||
|
|
cb2dd34a5e | ||
|
|
7112a706b8 | ||
|
|
025a47d01f | ||
|
|
13f89fdb8f | ||
|
|
cc78d50ef6 | ||
|
|
a8985b1849 | ||
|
|
6a1552f65c | ||
|
|
776091cc23 | ||
|
|
f385524f48 | ||
|
|
350982e636 | ||
|
|
5854455815 | ||
|
|
9ecaf618db | ||
|
|
95b667d21e | ||
|
|
a0b689c140 | ||
|
|
ea52ed41be | ||
|
|
5a50d54fda | ||
|
|
35d118b0c4 | ||
|
|
ea7c213f5d | ||
|
|
70dd6dd394 | ||
|
|
049510afbd | ||
|
|
c120447fd0 | ||
|
|
feb1f36126 | ||
|
|
d6ef47bb2d | ||
|
|
50fd416d49 | ||
|
|
aef6904247 | ||
|
|
0bf40faf95 | ||
|
|
c90987c4b0 | ||
|
|
0e08655407 | ||
|
|
427887db9c | ||
|
|
a718622498 | ||
|
|
4e83107d79 | ||
|
|
04b6e72820 | ||
|
|
501a2539c7 | ||
|
|
6a9856d480 | ||
|
|
2c8d42d997 | ||
|
|
9c237f0bfb | ||
|
|
63bfe76720 | ||
|
|
99d7ff47c4 | ||
|
|
3ff0eb3065 | ||
|
|
4d2b265dc4 | ||
|
|
1854245bd3 | ||
|
|
4d07034930 | ||
|
|
98031173b6 | ||
|
|
e8e474597c | ||
|
|
382758790c | ||
|
|
c33920f59d | ||
|
|
33f004d4b6 | ||
|
|
8963b536ee | ||
|
|
51455e2a1e | ||
|
|
30d6a26e3e | ||
|
|
cd4fabd11b | ||
|
|
9a8b8f26ac | ||
|
|
2f73b16b57 | ||
|
|
df9952c291 | ||
|
|
ee946d8128 | ||
|
|
ec8f2e078e | ||
|
|
335f46122b | ||
|
|
73eae191e9 | ||
|
|
14e823e938 | ||
|
|
2fbd462e6e | ||
|
|
e1cc98d448 | ||
|
|
0ce64962d4 | ||
|
|
338229193f | ||
|
|
57644a4be8 | ||
|
|
da2099137a | ||
|
|
09bc8d9ca4 | ||
|
|
d95f724303 | ||
|
|
c413c3ed8f | ||
|
|
5f56be0ad4 | ||
|
|
ef441d5cff | ||
|
|
16a188c524 | ||
|
|
50c40a8d99 | ||
|
|
4114c8715c | ||
|
|
ced5fdbe70 | ||
|
|
b16aa81e0d | ||
|
|
b44971668c | ||
|
|
0ff4c284e2 | ||
|
|
e8db95be16 | ||
|
|
69c2dd53ad | ||
|
|
14a910bd64 | ||
|
|
52f97ffdc9 | ||
|
|
a1e87f6cd9 | ||
|
|
c2fc41dcd5 | ||
|
|
b62c7943e7 | ||
|
|
64caeeb12d | ||
|
|
e8ac4a1e99 | ||
|
|
19c8654195 | ||
|
|
00d7aed797 | ||
|
|
4477132448 | ||
|
|
eaeea45ace | ||
|
|
e404bf33b1 | ||
|
|
79a7edea5e | ||
|
|
2b05fe2859 | ||
|
|
f8996f0a90 | ||
|
|
eb04cdac41 | ||
|
|
125938c7a1 |
4
.github/workflows/opencode.yml
vendored
4
.github/workflows/opencode.yml
vendored
@@ -3,6 +3,8 @@ name: opencode
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
@@ -28,4 +30,4 @@ jobs:
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: opencode/glm-4.6
|
||||
model: opencode/claude-haiku-4-5
|
||||
|
||||
5
.github/workflows/snapshot.yml
vendored
5
.github/workflows/snapshot.yml
vendored
@@ -1,11 +1,14 @@
|
||||
name: snapshot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- fix-snapshot-2
|
||||
- test-bedrock
|
||||
- v0
|
||||
- otui-diffs
|
||||
- snapshot-*
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
|
||||
2
.github/workflows/update-nix-hashes.yml
vendored
2
.github/workflows/update-nix-hashes.yml
vendored
@@ -18,6 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SYSTEM: x86_64-linux
|
||||
@@ -29,6 +30,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Check if bun version matches package.json
|
||||
EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
|
||||
CURRENT_VERSION=$(bun --version)
|
||||
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
|
||||
exit 1
|
||||
fi
|
||||
bun typecheck
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-openai-codex-auth"],
|
||||
// "enterprise": {
|
||||
// "url": "http://localhost:3000",
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"provider": {
|
||||
"opencode": {
|
||||
@@ -11,4 +11,17 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
"mcp": {
|
||||
"exa": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.exa.ai/mcp",
|
||||
},
|
||||
"morph": {
|
||||
"type": "local",
|
||||
"command": ["bunx", "@morphllm/morphmcp"],
|
||||
"environment": {
|
||||
"ENABLED_TOOLS": "warp_grep",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
5
STATS.md
5
STATS.md
@@ -151,3 +151,8 @@
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
|
||||
93
bun.lock
93
bun.lock
@@ -8,6 +8,7 @@
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
@@ -19,7 +20,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -47,7 +48,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -74,7 +75,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -98,7 +99,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -122,7 +123,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -163,7 +164,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -191,7 +192,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
@@ -207,7 +208,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -216,13 +217,15 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||
"@ai-sdk/anthropic": "2.0.45",
|
||||
"@ai-sdk/anthropic": "2.0.50",
|
||||
"@ai-sdk/azure": "2.0.73",
|
||||
"@ai-sdk/google": "2.0.42",
|
||||
"@ai-sdk/google-vertex": "3.0.74",
|
||||
"@ai-sdk/google": "2.0.44",
|
||||
"@ai-sdk/google-vertex": "3.0.81",
|
||||
"@ai-sdk/mcp": "0.0.8",
|
||||
"@ai-sdk/openai": "2.0.71",
|
||||
"@ai-sdk/openai-compatible": "1.0.27",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
@@ -234,9 +237,9 @@
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.2.5",
|
||||
"@opentui/core": "0.1.50",
|
||||
"@opentui/solid": "0.1.50",
|
||||
"@openrouter/ai-sdk-provider": "1.2.8",
|
||||
"@opentui/core": "0.1.54",
|
||||
"@opentui/solid": "0.1.54",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -255,7 +258,7 @@
|
||||
"jsonc-parser": "3.3.1",
|
||||
"minimatch": "10.0.3",
|
||||
"open": "10.1.2",
|
||||
"opentui-spinner": "0.0.5",
|
||||
"opentui-spinner": "0.0.6",
|
||||
"partial-json": "0.1.7",
|
||||
"remeda": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
@@ -294,7 +297,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -314,7 +317,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -325,7 +328,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -338,7 +341,7 @@
|
||||
},
|
||||
"packages/tauri": {
|
||||
"name": "@opencode-ai/tauri",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
@@ -351,7 +354,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -383,7 +386,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -393,7 +396,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -439,7 +442,7 @@
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.5.5",
|
||||
"@pierre/precision-diffs": "0.5.7",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
@@ -490,9 +493,9 @@
|
||||
|
||||
"@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.42", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Jdn+3TZm4iIt62CUjjUoIOshqFIXyzNmUDfkSVV4FcjlSo5+AuhzI1KC7QiNHlqPNejzR6NLIqGJx96VAES34g=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.44", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.74", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.45", "@ai-sdk/google": "2.0.42", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W0375p41RQOheAmy7iJGtuJWQWX/aKkO4sJHf6eIYa3bkz93Cbo1aRG1X7ocyMusLZ3dIaW7x6X9WHD8IHkNfg=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.81", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/google": "2.0.44", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yrl5Ug0Mqwo9ya45oxczgy2RWgpEA/XQQCSFYP+3NZMQ4yA3Iim1vkOjVCsGaZZ8rjVk395abi1ZMZV0/6rqVA=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -502,7 +505,7 @@
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "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-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "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-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
@@ -1078,27 +1081,27 @@
|
||||
|
||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.5", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-NrvJFPvdEUo6DYUQIVWPGfhafuZ2PAIX7+CUMKGknv8TcTNVo0TyP1y5SU7Bgjf/Wup9/74UFKUB07icOhVZjQ=="],
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.8", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-pQT8AzZBKg9f4bkt4doF486ZlhK0XjKkevrLkiqYgfh1Jplovieu28nK4Y+xy3sF18/mxjqh9/2y6jh01qzLrA=="],
|
||||
|
||||
"@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.50", "", { "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.50", "@opentui/core-darwin-x64": "0.1.50", "@opentui/core-linux-arm64": "0.1.50", "@opentui/core-linux-x64": "0.1.50", "@opentui/core-win32-arm64": "0.1.50", "@opentui/core-win32-x64": "0.1.50", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-QhjwT2f8AIQj0gbL/WQ2M93sl2/qp9+Kqxyh4dOhp8z3qnTc5D7J105VrMyeWZW7/P27ubgbFAqqWXrZ4FsuLw=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.54", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.54", "@opentui/core-darwin-x64": "0.1.54", "@opentui/core-linux-arm64": "0.1.54", "@opentui/core-linux-x64": "0.1.54", "@opentui/core-win32-arm64": "0.1.54", "@opentui/core-win32-x64": "0.1.54", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-NYBVOmAa3JB+bSTxFTUc3Ej3B8Gc364DMsoktVRCYdjQ+AyeMRNdOTj9sMWdDJbXqITK/atrsUXouoE5bL9sWA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FKqTDOsZl9TXF7KN2SdZKoRHQNvqKSY27AG3jhKCoiyLGdaNCAsaeBWqAmpnL4E4kMkV3aiQSCrKTrYsaevvOg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.54", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LyJ2AI9XA3upckiXLaX/P6tB40pB6euZA5MCikyhwcDVew3Z7NpTI0xHp4gUWHwsSTwmx6u851ofgkf7nMBEJg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.50", "", { "os": "darwin", "cpu": "x64" }, "sha512-GczVNqqpM/HtsgeBB08K6zL1B7oc6Y5G2cMklo06LrYRdDkFdDtY5fNNnJR2/psZWzTrI3M+sLnKWgUGD5CxUQ=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.54", "", { "os": "darwin", "cpu": "x64" }, "sha512-LZAjXR1OoUr8pewBTHqxupdPS4PC4qQCht0HHGEr1+zN7zu5c+kcp3Uop+NeQTYLjHJftiULukzAfLJMXFuDNA=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-+CKMhweEXH0tLGM6qqaqk6DyCEmwrTVubTtez/pSM3GgcROSXIBui9TEZpIlPgSCVmjbotGS6eSIg4oU+p9o7w=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.54", "", { "os": "linux", "cpu": "arm64" }, "sha512-AC7qBYc4shY28/eR1BvguvfCjLzZJy0mdvadSdDF0XJJIe5vhWJ9UyKsoyLo7pwrd0iFBf5d7fg4kJcbLbZNWg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.50", "", { "os": "linux", "cpu": "x64" }, "sha512-yv5KWiMohAK9bsi1gth9DDZDpoJA1EDHexjhThsPT8EH82g13T088dnJZuJWUE9dr1OwTCQG8DyorNxX3ViEGQ=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.54", "", { "os": "linux", "cpu": "x64" }, "sha512-S7BAq4CUV6ZfoXDSClJT75XxVSKPOOuRVYvBSHMOgwkBwLHpJZdz0Z4mzcGBBP1nJQ5Gz92TWPYfYMbZvR1lpw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.50", "", { "os": "win32", "cpu": "arm64" }, "sha512-6/6pURTRNTLFKF8IhYVi7U+T/HGMeURav9LIYw7yfcOibd0kLMthmemhS0Lzyk5dmtp0T4V4NmRmtlq/fIzyjQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.54", "", { "os": "win32", "cpu": "arm64" }, "sha512-K+E9i8t6YfN0Ly9moHefRQfR5GSbQUAsqzfrW4TD4bpJBy5y7EKh1mz8ZdES/RwWOSGUCb+JN7/ZQm0OubbdvQ=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.50", "", { "os": "win32", "cpu": "x64" }, "sha512-EME8GBFq9uCLbH5js8fH7/xY4ZtLIZlt3bkYKT6lPiCNdaf/6ebg+F/ObPXFkJrc8VeV1ql2bXhQ6RLi7izvAA=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.54", "", { "os": "win32", "cpu": "x64" }, "sha512-0KsBPRtcqmPM1AoN2Ez9au6uf7y8631W9tXTWnVZRlI2/G3j1AfGReR6vuwrd8O4JfxFXiYWWPfSxYZvWLSRNg=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.50", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.50", "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-q778kp/eksh8UOPSQO2h8h9CGGDqepTf9u2WYTS2HYHRAI2SRtUWpN9L7Euyt3BtG9L/wpsIOHK/ufPhQH1X6A=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.54", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.54", "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-rr9moI+W3meoD57t4Flyfw33mRSFIX0FOx+t4T3sLNA3Pz5FGU2n4qyxPc21OkAuc23S+YxuEuwbZebnIaLGWA=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1214,7 +1217,7 @@
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
|
||||
|
||||
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.5.5", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-mmDHEWWQ6fmXY5qRNHqodzOxHPwLqVNbbnO/MOpXteOTjd0nVIGy5IcaNwU2WSxhxQRwaUepKyx5+wwPcZLEmw=="],
|
||||
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.5.7", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-Y+e4kJ9pT2I4NS5fE39KdoiXtwMkVPRvrwLM6O2IqO7PDCRWLBS7CYxcSgSyngEndccUll2krx66I2QnfO0Ovg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
@@ -2968,7 +2971,7 @@
|
||||
|
||||
"openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
|
||||
|
||||
"opentui-spinner": ["opentui-spinner@0.0.5", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-abSWzWA7eyuD0PjerAWbBznLmOQn+8xRDaLGCVIs4ctETi2laNFr5KwicYnPXsHZpPc2neV7WtQm+diCEfOhLA=="],
|
||||
"opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
@@ -3736,20 +3739,22 @@
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@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/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "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-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
|
||||
|
||||
"@ai-sdk/azure/@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/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.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-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@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-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "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-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
|
||||
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "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-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -4072,7 +4077,7 @@
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ipv62vavDCmrV/oE/lXehL9FzwQuZOnnlhPEftWizx464Wb6lvnBTJx8uhmEYruFSzOWTI95Z33ncZ4tA8E6RQ=="],
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
|
||||
|
||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
|
||||
|
||||
@@ -4618,8 +4623,6 @@
|
||||
|
||||
"jsonwebtoken/jws/jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic/@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=="],
|
||||
|
||||
"opencode/@ai-sdk/openai/@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=="],
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible/@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=="],
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763934636,
|
||||
"narHash": "sha256-9glbI7f1uU+yzQCq5LwLgdZqx6svOhZWkd4JRY265fc=",
|
||||
"lastModified": 1764527385,
|
||||
"narHash": "sha256-nA5ywiGKl76atrbdZ5Aucd8SjF/v8ew9b9QsC+MKL14=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ee09932cedcef15aaf476f9343d1dea2cb77e261",
|
||||
"rev": "23258e03aaa49b3a68597e3e50eb0cbce7e42e9d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -30,6 +30,24 @@ Leave the following comment on a GitHub PR. opencode will implement the requeste
|
||||
Delete the attachment from S3 when the note is removed /oc
|
||||
```
|
||||
|
||||
#### Review specific code lines
|
||||
|
||||
Leave a comment directly on code lines in the PR's "Files" tab. opencode will automatically detect the file, line numbers, and diff context to provide precise responses.
|
||||
|
||||
```
|
||||
[Comment on specific lines in Files tab]
|
||||
/oc add error handling here
|
||||
```
|
||||
|
||||
When commenting on specific lines, opencode receives:
|
||||
|
||||
- The exact file being reviewed
|
||||
- The specific lines of code
|
||||
- The surrounding diff context
|
||||
- Line number information
|
||||
|
||||
This allows for more targeted requests without needing to specify file paths or line numbers manually.
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command in the terminal from your GitHub repo:
|
||||
@@ -51,6 +69,8 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
@@ -135,3 +155,9 @@ Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
### PR review comment event
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"pull_request_review_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"pull_request":{"number":7},"comment":{"id":1,"body":"hey opencode, add error handling","path":"src/components/Button.tsx","diff_hunk":"@@ -45,8 +45,11 @@\n- const handleClick = () => {\n- console.log('clicked')\n+ const handleClick = useCallback(() => {\n+ console.log('clicked')\n+ doSomething()\n+ }, [doSomething])","line":47,"original_line":45,"position":10,"commit_id":"abc123","original_commit_id":"def456"}}}'
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ import { graphql } from "@octokit/graphql"
|
||||
import * as core from "@actions/core"
|
||||
import * as github from "@actions/github"
|
||||
import type { Context as GitHubContext } from "@actions/github/lib/context"
|
||||
import type { IssueCommentEvent } from "@octokit/webhooks-types"
|
||||
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { spawn } from "node:child_process"
|
||||
|
||||
@@ -124,7 +124,7 @@ let exitCode = 0
|
||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||
|
||||
try {
|
||||
assertContextEvent("issue_comment")
|
||||
assertContextEvent("issue_comment", "pull_request_review_comment")
|
||||
assertPayloadKeyword()
|
||||
await assertOpencodeConnected()
|
||||
|
||||
@@ -241,19 +241,43 @@ function createOpencode() {
|
||||
}
|
||||
|
||||
function assertPayloadKeyword() {
|
||||
const payload = useContext().payload as IssueCommentEvent
|
||||
const payload = useContext().payload as IssueCommentEvent | PullRequestReviewCommentEvent
|
||||
const body = payload.comment.body.trim()
|
||||
if (!body.match(/(?:^|\s)(?:\/opencode|\/oc)(?=$|\s)/)) {
|
||||
throw new Error("Comments must mention `/opencode` or `/oc`")
|
||||
}
|
||||
}
|
||||
|
||||
function getReviewCommentContext() {
|
||||
const context = useContext()
|
||||
if (context.eventName !== "pull_request_review_comment") {
|
||||
return null
|
||||
}
|
||||
|
||||
const payload = context.payload as PullRequestReviewCommentEvent
|
||||
return {
|
||||
file: payload.comment.path,
|
||||
diffHunk: payload.comment.diff_hunk,
|
||||
line: payload.comment.line,
|
||||
originalLine: payload.comment.original_line,
|
||||
position: payload.comment.position,
|
||||
commitId: payload.comment.commit_id,
|
||||
originalCommitId: payload.comment.original_commit_id,
|
||||
}
|
||||
}
|
||||
|
||||
async function assertOpencodeConnected() {
|
||||
let retry = 0
|
||||
let connected = false
|
||||
do {
|
||||
try {
|
||||
await client.app.get<true>()
|
||||
await client.app.log<true>({
|
||||
body: {
|
||||
service: "github-workflow",
|
||||
level: "info",
|
||||
message: "Prepare to react to Github Workflow event",
|
||||
},
|
||||
})
|
||||
connected = true
|
||||
break
|
||||
} catch (e) {}
|
||||
@@ -383,11 +407,24 @@ async function createComment() {
|
||||
}
|
||||
|
||||
async function getUserPrompt() {
|
||||
const context = useContext()
|
||||
const payload = context.payload as IssueCommentEvent | PullRequestReviewCommentEvent
|
||||
const reviewContext = getReviewCommentContext()
|
||||
|
||||
let prompt = (() => {
|
||||
const payload = useContext().payload as IssueCommentEvent
|
||||
const body = payload.comment.body.trim()
|
||||
if (body === "/opencode" || body === "/oc") return "Summarize this thread"
|
||||
if (body.includes("/opencode") || body.includes("/oc")) return body
|
||||
if (body === "/opencode" || body === "/oc") {
|
||||
if (reviewContext) {
|
||||
return `Review this code change and suggest improvements for the commented lines:\n\nFile: ${reviewContext.file}\nLines: ${reviewContext.line}\n\n${reviewContext.diffHunk}`
|
||||
}
|
||||
return "Summarize this thread"
|
||||
}
|
||||
if (body.includes("/opencode") || body.includes("/oc")) {
|
||||
if (reviewContext) {
|
||||
return `${body}\n\nContext: You are reviewing a comment on file "${reviewContext.file}" at line ${reviewContext.line}.\n\nDiff context:\n${reviewContext.diffHunk}`
|
||||
}
|
||||
return body
|
||||
}
|
||||
throw new Error("Comments must mention `/opencode` or `/oc`")
|
||||
})()
|
||||
|
||||
|
||||
81
install
81
install
@@ -11,43 +11,82 @@ requested_version=${VERSION:-}
|
||||
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
# Normalize various Unix-like identifiers
|
||||
case "$raw_os" in
|
||||
Darwin*) os="darwin" ;;
|
||||
Linux*) os="linux" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
||||
esac
|
||||
arch=$(uname -m)
|
||||
esac
|
||||
|
||||
arch=$(uname -m)
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
elif [[ "$arch" == "x86_64" ]]; then
|
||||
fi
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
arch="x64"
|
||||
fi
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
filename="$APP-$os-$arch.tar.gz"
|
||||
else
|
||||
filename="$APP-$os-$arch.zip"
|
||||
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
|
||||
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
|
||||
if [ "$rosetta_flag" = "1" ]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
case "$filename" in
|
||||
*"-linux-"*)
|
||||
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||
combo="$os-$arch"
|
||||
case "$combo" in
|
||||
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
|
||||
;;
|
||||
*"-darwin-"*)
|
||||
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||
;;
|
||||
*"-windows-"*)
|
||||
[[ "$arch" == "x64" ]] || exit 1
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
*)
|
||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive_ext=".zip"
|
||||
if [ "$os" = "linux" ]; then
|
||||
archive_ext=".tar.gz"
|
||||
fi
|
||||
|
||||
is_musl=false
|
||||
if [ "$os" = "linux" ]; then
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
is_musl=true
|
||||
fi
|
||||
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
is_musl=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
needs_baseline=false
|
||||
if [ "$arch" = "x64" ]; then
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ]; then
|
||||
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
|
||||
if [ "$avx2" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
target="$os-$arch"
|
||||
if [ "$needs_baseline" = "true" ]; then
|
||||
target="$target-baseline"
|
||||
fi
|
||||
if [ "$is_musl" = "true" ]; then
|
||||
target="$target-musl"
|
||||
fi
|
||||
|
||||
filename="$APP-$target$archive_ext"
|
||||
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-cieNNPXZd4Bg9bZtRq2H8L99e24U8p5d+d76SE7SeJc="
|
||||
"nodeModules": "sha256-CrewwFMa3zxKTM/XpnRVQm75FB7Jk1tXX40D+kYt0YE="
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.5.5",
|
||||
"@pierre/precision-diffs": "0.5.7",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.97",
|
||||
@@ -63,7 +63,8 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -36,6 +36,7 @@ ${body.email}`.trim()
|
||||
to: "contact@anoma.ly",
|
||||
subject: `Enterprise Inquiry from ${body.name}`,
|
||||
body: emailContent,
|
||||
replyTo: body.email,
|
||||
})
|
||||
|
||||
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
|
||||
|
||||
@@ -13,13 +13,20 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
|
||||
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
|
||||
import { logger } from "./logger"
|
||||
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
|
||||
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
|
||||
import {
|
||||
createBodyConverter,
|
||||
createStreamPartConverter,
|
||||
createResponseConverter,
|
||||
ProviderHelper,
|
||||
UsageInfo,
|
||||
} from "./provider/provider"
|
||||
import { anthropicHelper } from "./provider/anthropic"
|
||||
import { googleHelper } from "./provider/google"
|
||||
import { openaiHelper } from "./provider/openai"
|
||||
import { oaCompatHelper } from "./provider/openai-compatible"
|
||||
import { createRateLimiter } from "./rateLimiter"
|
||||
import { createDataDumper } from "./dataDumper"
|
||||
import { createTrialLimiter } from "./trialLimiter"
|
||||
|
||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||
type RetryOptions = {
|
||||
@@ -62,11 +69,13 @@ export async function handler(
|
||||
const zenData = ZenData.list()
|
||||
const modelInfo = validateModel(zenData, model)
|
||||
const dataDumper = createDataDumper(sessionId, requestId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||
await rateLimiter?.check()
|
||||
|
||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||
const providerInfo = selectProvider(zenData, modelInfo, sessionId, retry)
|
||||
const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry)
|
||||
const authInfo = await authenticate(modelInfo, providerInfo)
|
||||
validateBilling(authInfo, modelInfo)
|
||||
validateModelSettings(authInfo)
|
||||
@@ -136,8 +145,10 @@ export async function handler(
|
||||
logger.debug("RESPONSE: " + body)
|
||||
dataDumper?.provideResponse(body)
|
||||
dataDumper?.flush()
|
||||
const tokensInfo = providerInfo.normalizeUsage(json.usage)
|
||||
await trialLimiter?.track(tokensInfo)
|
||||
await rateLimiter?.track()
|
||||
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
|
||||
await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
|
||||
await reload(authInfo)
|
||||
return new Response(body, {
|
||||
status: res.status,
|
||||
@@ -169,7 +180,9 @@ export async function handler(
|
||||
await rateLimiter?.track()
|
||||
const usage = usageParser.retrieve()
|
||||
if (usage) {
|
||||
await trackUsage(authInfo, modelInfo, providerInfo, usage)
|
||||
const tokensInfo = providerInfo.normalizeUsage(usage)
|
||||
await trialLimiter?.track(tokensInfo)
|
||||
await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
|
||||
await reload(authInfo)
|
||||
}
|
||||
c.close()
|
||||
@@ -275,8 +288,18 @@ export async function handler(
|
||||
return { id: modelId, ...modelData }
|
||||
}
|
||||
|
||||
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, sessionId: string, retry: RetryOptions) {
|
||||
function selectProvider(
|
||||
zenData: ZenData,
|
||||
modelInfo: ModelInfo,
|
||||
sessionId: string,
|
||||
isTrial: boolean,
|
||||
retry: RetryOptions,
|
||||
) {
|
||||
const provider = (() => {
|
||||
if (isTrial) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
|
||||
}
|
||||
|
||||
if (retry.retryCount === MAX_RETRIES) {
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
}
|
||||
@@ -432,9 +455,14 @@ export async function handler(
|
||||
providerInfo.apiKey = authInfo.provider.credentials
|
||||
}
|
||||
|
||||
async function trackUsage(authInfo: AuthInfo, modelInfo: ModelInfo, providerInfo: ProviderInfo, usage: any) {
|
||||
async function trackUsage(
|
||||
authInfo: AuthInfo,
|
||||
modelInfo: ModelInfo,
|
||||
providerInfo: ProviderInfo,
|
||||
usageInfo: UsageInfo,
|
||||
) {
|
||||
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
|
||||
providerInfo.normalizeUsage(usage)
|
||||
usageInfo
|
||||
|
||||
const modelCost =
|
||||
modelInfo.cost200K &&
|
||||
|
||||
@@ -24,6 +24,15 @@ import {
|
||||
toOaCompatibleResponse,
|
||||
} from "./openai-compatible"
|
||||
|
||||
export type UsageInfo = {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens?: number
|
||||
cacheReadTokens?: number
|
||||
cacheWrite5mTokens?: number
|
||||
cacheWrite1hTokens?: number
|
||||
}
|
||||
|
||||
export type ProviderHelper = {
|
||||
format: ZenData.Format
|
||||
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
|
||||
@@ -34,14 +43,7 @@ export type ProviderHelper = {
|
||||
parse: (chunk: string) => void
|
||||
retrieve: () => any
|
||||
}
|
||||
normalizeUsage: (usage: any) => {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
reasoningTokens?: number
|
||||
cacheReadTokens?: number
|
||||
cacheWrite5mTokens?: number
|
||||
cacheWrite1hTokens?: number
|
||||
}
|
||||
normalizeUsage: (usage: any) => UsageInfo
|
||||
}
|
||||
|
||||
export interface CommonMessage {
|
||||
|
||||
43
packages/console/app/src/routes/zen/util/trialLimiter.ts
Normal file
43
packages/console/app/src/routes/zen/util/trialLimiter.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { UsageInfo } from "./provider/provider"
|
||||
|
||||
export function createTrialLimiter(limit: number | undefined, ip: string) {
|
||||
if (!limit) return
|
||||
if (!ip) return
|
||||
|
||||
let trial: boolean
|
||||
|
||||
return {
|
||||
isTrial: async () => {
|
||||
const data = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
usage: IpTable.usage,
|
||||
})
|
||||
.from(IpTable)
|
||||
.where(eq(IpTable.ip, ip))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
trial = (data?.usage ?? 0) < limit
|
||||
return trial
|
||||
},
|
||||
track: async (usageInfo: UsageInfo) => {
|
||||
if (!trial) return
|
||||
const usage =
|
||||
usageInfo.inputTokens +
|
||||
usageInfo.outputTokens +
|
||||
(usageInfo.reasoningTokens ?? 0) +
|
||||
(usageInfo.cacheReadTokens ?? 0) +
|
||||
(usageInfo.cacheWrite5mTokens ?? 0) +
|
||||
(usageInfo.cacheWrite1hTokens ?? 0)
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.insert(IpTable)
|
||||
.values({ ip, usage })
|
||||
.onDuplicateKeyUpdate({ set: { usage: sql`${IpTable.usage} + ${usage}` } }),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
8
packages/console/core/migrations/0038_famous_magik.sql
Normal file
8
packages/console/core/migrations/0038_famous_magik.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE `ip` (
|
||||
`ip` varchar(45) NOT NULL,
|
||||
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
`time_deleted` timestamp(3),
|
||||
`usage` int,
|
||||
CONSTRAINT `ip_ip_pk` PRIMARY KEY(`ip`)
|
||||
);
|
||||
981
packages/console/core/migrations/meta/0038_snapshot.json
Normal file
981
packages/console/core/migrations/meta/0038_snapshot.json
Normal file
@@ -0,0 +1,981 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "9d5d9885-7ec5-45f6-ac53-45a8e25dede7",
|
||||
"prevId": "8b7fa839-a088-408e-84a4-1a07325c0290",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"account_id_pk": {
|
||||
"name": "account_id_pk",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"auth": {
|
||||
"name": "auth",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "enum('email','github','google')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"subject": {
|
||||
"name": "subject",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"columns": ["provider", "subject"],
|
||||
"isUnique": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"columns": ["account_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"auth_id_pk": {
|
||||
"name": "auth_id_pk",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"billing": {
|
||||
"name": "billing",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_type": {
|
||||
"name": "payment_method_type",
|
||||
"type": "varchar(32)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_limit": {
|
||||
"name": "monthly_limit",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_usage": {
|
||||
"name": "monthly_usage",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_monthly_usage_updated": {
|
||||
"name": "time_monthly_usage_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_trigger": {
|
||||
"name": "reload_trigger",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_amount": {
|
||||
"name": "reload_amount",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reload_error": {
|
||||
"name": "reload_error",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_error": {
|
||||
"name": "time_reload_error",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_reload_locked_till": {
|
||||
"name": "time_reload_locked_till",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_customer_id": {
|
||||
"name": "global_customer_id",
|
||||
"columns": ["customer_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"payment": {
|
||||
"name": "payment",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"invoice_id": {
|
||||
"name": "invoice_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_refunded": {
|
||||
"name": "time_refunded",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_5m_tokens": {
|
||||
"name": "cache_write_5m_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cache_write_1h_tokens": {
|
||||
"name": "cache_write_1h_tokens",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key_id": {
|
||||
"name": "key_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"ip": {
|
||||
"name": "ip",
|
||||
"columns": {
|
||||
"ip": {
|
||||
"name": "ip",
|
||||
"type": "varchar(45)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"usage": {
|
||||
"name": "usage",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"ip_ip_pk": {
|
||||
"name": "ip_ip_pk",
|
||||
"columns": ["ip"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": ["key"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"model_workspace_model": {
|
||||
"name": "model_workspace_model",
|
||||
"columns": ["workspace_id", "model"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"model_workspace_id_id_pk": {
|
||||
"name": "model_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"credentials": {
|
||||
"name": "credentials",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"workspace_provider": {
|
||||
"name": "workspace_provider",
|
||||
"columns": ["workspace_id", "provider"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"provider_workspace_id_id_pk": {
|
||||
"name": "provider_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "enum('admin','member')",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_limit": {
|
||||
"name": "monthly_limit",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"monthly_usage": {
|
||||
"name": "monthly_usage",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_monthly_usage_updated": {
|
||||
"name": "time_monthly_usage_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_account_id": {
|
||||
"name": "user_account_id",
|
||||
"columns": ["workspace_id", "account_id"],
|
||||
"isUnique": true
|
||||
},
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": ["workspace_id", "email"],
|
||||
"isUnique": true
|
||||
},
|
||||
"global_account_id": {
|
||||
"name": "global_account_id",
|
||||
"columns": ["account_id"],
|
||||
"isUnique": false
|
||||
},
|
||||
"global_email": {
|
||||
"name": "global_email",
|
||||
"columns": ["email"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": ["workspace_id", "id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"workspace": {
|
||||
"name": "workspace",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": ["slug"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -267,6 +267,13 @@
|
||||
"when": 1761928273807,
|
||||
"tag": "0037_messy_jackal",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "5",
|
||||
"when": 1764110043942,
|
||||
"tag": "0038_famous_magik",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -22,6 +22,7 @@ export namespace AWS {
|
||||
to: z.string(),
|
||||
subject: z.string(),
|
||||
body: z.string(),
|
||||
replyTo: z.string().optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
|
||||
@@ -35,6 +36,7 @@ export namespace AWS {
|
||||
Destination: {
|
||||
ToAddresses: [input.to],
|
||||
},
|
||||
...(input.replyTo && { ReplyToAddresses: [input.replyTo] }),
|
||||
Content: {
|
||||
Simple: {
|
||||
Subject: {
|
||||
|
||||
@@ -24,6 +24,12 @@ export namespace ZenData {
|
||||
cost: ModelCostSchema,
|
||||
cost200K: ModelCostSchema.optional(),
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
trial: z
|
||||
.object({
|
||||
limit: z.number(),
|
||||
provider: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
rateLimit: z.number().optional(),
|
||||
fallbackProvider: z.string().optional(),
|
||||
providers: z.array(
|
||||
|
||||
12
packages/console/core/src/schema/ip.sql.ts
Normal file
12
packages/console/core/src/schema/ip.sql.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { mysqlTable, int, primaryKey, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps } from "../drizzle/types"
|
||||
|
||||
export const IpTable = mysqlTable(
|
||||
"ip",
|
||||
{
|
||||
ip: varchar("ip", { length: 45 }).notNull(),
|
||||
...timestamps,
|
||||
usage: int("usage"),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.ip] })],
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
</head>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { Collapsible } from "@/ui"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
|
||||
@@ -76,6 +76,7 @@ export default function FileTree(props: {
|
||||
<Switch>
|
||||
<Match when={node.type === "directory"}>
|
||||
<Collapsible
|
||||
variant="ghost"
|
||||
class="w-full"
|
||||
forceMount={false}
|
||||
// open={local.file.node(node.path)?.expanded}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { getDirectory, getFilename } from "@/utils"
|
||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { DateTime } from "luxon"
|
||||
@@ -16,6 +15,7 @@ import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Part } from "@opencode-ai/sdk"
|
||||
import { produce } from "solid-js/store"
|
||||
import { createMemo } from "solid-js"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
@@ -34,29 +33,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
|
||||
Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
|
||||
|
||||
const sanitizer = createMemo(() => new RegExp(`${store.path.directory}/`, "g"))
|
||||
const sanitize = (text: string) => text.replace(sanitizer(), "")
|
||||
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
|
||||
const sanitizePart = (part: Part) => {
|
||||
if (part.type === "tool") {
|
||||
if (part.state.status === "completed" || part.state.status === "error") {
|
||||
for (const key in part.state.metadata) {
|
||||
if (typeof part.state.metadata[key] === "string") {
|
||||
part.state.metadata[key] = sanitize(part.state.metadata[key] as string)
|
||||
}
|
||||
}
|
||||
for (const key in part.state.input) {
|
||||
if (typeof part.state.input[key] === "string") {
|
||||
part.state.input[key] = sanitize(part.state.input[key] as string)
|
||||
}
|
||||
}
|
||||
if ("error" in part.state) {
|
||||
part.state.error = sanitize(part.state.error as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
return part
|
||||
}
|
||||
|
||||
return {
|
||||
data: store,
|
||||
@@ -88,10 +65,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
for (const message of messages.data!) {
|
||||
draft.part[message.info.id] = message.parts
|
||||
.slice()
|
||||
.map(sanitizePart)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
draft.session_diff[sessionID] = diff.data ?? []
|
||||
}),
|
||||
@@ -105,7 +79,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
},
|
||||
load,
|
||||
absolute,
|
||||
sanitize,
|
||||
get directory() {
|
||||
return store.path.directory
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Layout(props: ParentProps) {
|
||||
{iife(() => {
|
||||
const sync = useSync()
|
||||
return (
|
||||
<DataProvider data={sync.data}>
|
||||
<DataProvider data={sync.data} directory={directory()}>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
</DataProvider>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { base64Encode, getFilename } from "@/utils"
|
||||
import { base64Encode } from "@/utils"
|
||||
import { For } from "solid-js"
|
||||
import { A } from "@solidjs/router"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
export default function Home() {
|
||||
const sync = useGlobalSync()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DateTime } from "luxon"
|
||||
import { A, useParams } from "@solidjs/router"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { base64Encode, getFilename } from "@/utils"
|
||||
import { base64Encode } from "@/utils"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -11,6 +11,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const params = useParams()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
|
||||
import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { getDirectory, getFilename } from "@/utils"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { DateTime } from "luxon"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
@@ -13,7 +12,7 @@ import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
||||
import {
|
||||
@@ -30,6 +29,7 @@ import type { JSX } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSession } from "@/context/session"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
@@ -333,43 +333,35 @@ export default function Page() {
|
||||
flex: layout.review.state() === "pane",
|
||||
}}
|
||||
>
|
||||
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-2xl mx-auto">
|
||||
<div
|
||||
classList={{
|
||||
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
|
||||
"max-w-146 mx-auto": !wide(),
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={session.id}>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<Show when={session.messages.user().length > 1}>
|
||||
<>
|
||||
<MessageNav
|
||||
class="@6xl:hidden mt-3 mr-8"
|
||||
messages={session.messages.user()}
|
||||
current={session.messages.active()}
|
||||
onMessageSelect={session.messages.setActive}
|
||||
size="compact"
|
||||
working={session.working()}
|
||||
/>
|
||||
<MessageNav
|
||||
classList={{
|
||||
"hidden @6xl:flex": true,
|
||||
"mt-0.5 mr-3 absolute right-full": wide(),
|
||||
"mt-3 mr-8": !wide(),
|
||||
}}
|
||||
messages={session.messages.user()}
|
||||
current={session.messages.active()}
|
||||
onMessageSelect={session.messages.setActive}
|
||||
size={wide() ? "normal" : "compact"}
|
||||
working={session.working()}
|
||||
/>
|
||||
</>
|
||||
</Show>
|
||||
<SessionMessageRail
|
||||
messages={session.messages.user()}
|
||||
current={session.messages.active()}
|
||||
onMessageSelect={session.messages.setActive}
|
||||
working={session.working()}
|
||||
wide={wide()}
|
||||
/>
|
||||
<SessionTurn
|
||||
sessionID={session.id!}
|
||||
messageID={session.messages.active()?.id!}
|
||||
classes={{ root: "pb-20 flex-1 min-w-0", content: "pb-20" }}
|
||||
classes={{
|
||||
root: "pb-20 flex-1 min-w-0",
|
||||
content: "pb-20",
|
||||
container: "w-full " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
|
||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-146 mx-auto px-6">
|
||||
<div class="text-20-medium text-text-weaker">New session</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
@@ -390,12 +382,14 @@ export default function Page() {
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
}}
|
||||
/>
|
||||
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
|
||||
<div class="w-full max-w-146 px-6">
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={layout.review.state() === "pane" && session.diffs().length}>
|
||||
@@ -498,7 +492,7 @@ export default function Page() {
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
<Show when={session.layout.tabs.active}>
|
||||
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
|
||||
<div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Collapsible as KobalteCollapsible } from "@kobalte/core/collapsible"
|
||||
import { Icon, IconProps } from "@opencode-ai/ui/icon"
|
||||
import { splitProps } from "solid-js"
|
||||
import type { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export interface CollapsibleProps extends ComponentProps<typeof KobalteCollapsible> {}
|
||||
export interface CollapsibleTriggerProps extends ComponentProps<typeof KobalteCollapsible.Trigger> {}
|
||||
export interface CollapsibleContentProps extends ComponentProps<typeof KobalteCollapsible.Content> {}
|
||||
|
||||
function CollapsibleRoot(props: CollapsibleProps) {
|
||||
return <KobalteCollapsible forceMount {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger(props: CollapsibleTriggerProps) {
|
||||
const [local, others] = splitProps(props, ["class"])
|
||||
return (
|
||||
<KobalteCollapsible.Trigger
|
||||
classList={{
|
||||
"w-full group/collapsible": true,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent(props: ParentProps<CollapsibleContentProps>) {
|
||||
const [local, others] = splitProps(props, ["class", "children"])
|
||||
return (
|
||||
<KobalteCollapsible.Content
|
||||
classList={{
|
||||
"h-0 overflow-hidden transition-all duration-100 ease-out": true,
|
||||
"data-expanded:h-fit": true,
|
||||
[local.class]: !!local.class,
|
||||
}}
|
||||
{...others}
|
||||
>
|
||||
{local.children}
|
||||
</KobalteCollapsible.Content>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleArrow(props: Partial<IconProps>) {
|
||||
const [local, others] = splitProps(props, ["class", "name"])
|
||||
return (
|
||||
<Icon
|
||||
name={local.name ?? "chevron-right"}
|
||||
classList={{
|
||||
"flex-none text-text-muted transition-transform duration-100": true,
|
||||
"group-data-[expanded]/collapsible:rotate-90": true,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const Collapsible = Object.assign(CollapsibleRoot, {
|
||||
Trigger: CollapsibleTrigger,
|
||||
Content: CollapsibleContent,
|
||||
Arrow: CollapsibleArrow,
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
export {
|
||||
Collapsible,
|
||||
type CollapsibleProps,
|
||||
type CollapsibleTriggerProps,
|
||||
type CollapsibleContentProps,
|
||||
} from "./collapsible"
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./path"
|
||||
export * from "./dom"
|
||||
export * from "./encode"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useSync } from "@/context/sync"
|
||||
|
||||
export function getFilename(path: string) {
|
||||
if (!path) return ""
|
||||
const trimmed = path.replace(/[\/]+$/, "")
|
||||
const parts = trimmed.split("/")
|
||||
return parts[parts.length - 1] ?? ""
|
||||
}
|
||||
|
||||
export function getDirectory(path: string) {
|
||||
const sync = useSync()
|
||||
const parts = path.split("/")
|
||||
const dir = parts.slice(0, parts.length - 1).join("/")
|
||||
return dir ? sync.sanitize(dir + "/") : ""
|
||||
}
|
||||
|
||||
export function getFileExtension(path: string) {
|
||||
const parts = path.split(".")
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,6 +9,8 @@ export default createHandler(() => (
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenCode</title>
|
||||
<meta name="theme-color" content="#F8F7F7" />
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
{assets}
|
||||
|
||||
@@ -7,11 +7,12 @@ import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } fro
|
||||
import { Share } from "~/core/share"
|
||||
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { createDefaultOptions } from "@opencode-ai/ui/pierre"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { DateTime } from "luxon"
|
||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
|
||||
import { createStore } from "solid-js/store"
|
||||
import z from "zod"
|
||||
import NotFound from "../[...404]"
|
||||
@@ -40,6 +41,9 @@ const getData = query(async (shareID) => {
|
||||
session_diff_preload: {
|
||||
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
|
||||
}
|
||||
session_diff_preload_split: {
|
||||
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
|
||||
}
|
||||
session_status: {
|
||||
[sessionID: string]: SessionStatus
|
||||
}
|
||||
@@ -61,6 +65,9 @@ const getData = query(async (shareID) => {
|
||||
session_diff_preload: {
|
||||
[share.sessionID]: [],
|
||||
},
|
||||
session_diff_preload_split: {
|
||||
[share.sessionID]: [],
|
||||
},
|
||||
session_status: {
|
||||
[share.sessionID]: {
|
||||
type: "idle",
|
||||
@@ -77,29 +84,28 @@ const getData = query(async (shareID) => {
|
||||
break
|
||||
case "session_diff":
|
||||
result.session_diff[share.sessionID] = item.data
|
||||
result.session_diff_preload[share.sessionID] = await Promise.all(
|
||||
item.data.map(async (diff) =>
|
||||
preloadMultiFileDiff<any>({
|
||||
oldFile: { name: diff.file, contents: diff.before },
|
||||
newFile: { name: diff.file, contents: diff.after },
|
||||
options: {
|
||||
theme: "OpenCode",
|
||||
themeType: "system",
|
||||
disableLineNumbers: false,
|
||||
overflow: "wrap",
|
||||
diffStyle: "unified",
|
||||
diffIndicators: "bars",
|
||||
disableBackground: false,
|
||||
expansionLineCount: 20,
|
||||
lineDiffType: "none",
|
||||
maxLineDiffLength: 1000,
|
||||
maxLineLengthForHighlighting: 1000,
|
||||
disableFileHeader: true,
|
||||
},
|
||||
// annotations,
|
||||
}),
|
||||
),
|
||||
)
|
||||
await Promise.all([
|
||||
Promise.all(
|
||||
item.data.map(async (diff) =>
|
||||
preloadMultiFileDiff<any>({
|
||||
oldFile: { name: diff.file, contents: diff.before },
|
||||
newFile: { name: diff.file, contents: diff.after },
|
||||
options: createDefaultOptions("unified"),
|
||||
// annotations,
|
||||
}),
|
||||
),
|
||||
).then((r) => (result.session_diff_preload[share.sessionID] = r)),
|
||||
Promise.all(
|
||||
item.data.map(async (diff) =>
|
||||
preloadMultiFileDiff<any>({
|
||||
oldFile: { name: diff.file, contents: diff.before },
|
||||
newFile: { name: diff.file, contents: diff.after },
|
||||
options: createDefaultOptions("split"),
|
||||
// annotations,
|
||||
}),
|
||||
),
|
||||
).then((r) => (result.session_diff_preload_split[share.sessionID] = r)),
|
||||
])
|
||||
break
|
||||
case "message":
|
||||
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
|
||||
@@ -141,219 +147,234 @@ export default function () {
|
||||
}}
|
||||
>
|
||||
<Show when={data()}>
|
||||
{(data) => (
|
||||
<DataProvider data={data()}>
|
||||
{iife(() => {
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
})
|
||||
const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
|
||||
if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
|
||||
const info = createMemo(() => data().session[match().index])
|
||||
const messages = createMemo(() =>
|
||||
data().sessionID
|
||||
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
||||
(a, b) => b.time.created - a.time.created,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const firstUserMessage = createMemo(() => messages().at(0))
|
||||
const activeMessage = createMemo(
|
||||
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
||||
)
|
||||
function setActiveMessage(message: UserMessage | undefined) {
|
||||
if (message) {
|
||||
setStore("messageId", message.id)
|
||||
} else {
|
||||
setStore("messageId", undefined)
|
||||
{(data) => {
|
||||
const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
|
||||
if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
|
||||
const info = createMemo(() => data().session[match().index])
|
||||
|
||||
return (
|
||||
<DataProvider data={data()} directory={info().directory}>
|
||||
{iife(() => {
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
})
|
||||
const messages = createMemo(() =>
|
||||
data().sessionID
|
||||
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
||||
(a, b) => b.time.created - a.time.created,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const firstUserMessage = createMemo(() => messages().at(0))
|
||||
const activeMessage = createMemo(
|
||||
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
||||
)
|
||||
function setActiveMessage(message: UserMessage | undefined) {
|
||||
if (message) {
|
||||
setStore("messageId", message.id)
|
||||
} else {
|
||||
setStore("messageId", undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
||||
const diffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
||||
const diffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const splitDiffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
|
||||
const title = () => (
|
||||
<div class="flex flex-col gap-4 shrink-0">
|
||||
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
|
||||
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
|
||||
<Mark class="shrink-0 w-3 my-0.5" />
|
||||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<img src={`https://models.dev/logos/${provider()}.svg`} class="size-3.5 shrink-0 dark:invert" />
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
||||
const title = () => (
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
|
||||
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
|
||||
<Mark class="shrink-0 w-3 my-0.5" />
|
||||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<img src={`https://models.dev/logos/${provider()}.svg`} class="size-3.5 shrink-0 dark:invert" />
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||
</div>
|
||||
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
const turns = () => (
|
||||
<div class="relative mt-2 pt-6 pb-8 px-4 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
||||
{title()}
|
||||
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={message.id}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="flex items-center justify-center pt-20 pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const wide = createMemo(() => diffs().length === 0)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
||||
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
||||
<div class="">
|
||||
<a href="https://opencode.ai">
|
||||
<Mark />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center">
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://github.com/sst/opencode"
|
||||
target="_blank"
|
||||
icon="github"
|
||||
variant="ghost"
|
||||
/>
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://opencode.ai/discord"
|
||||
target="_blank"
|
||||
icon="discord"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="select-text flex flex-col flex-1 min-h-0">
|
||||
<div class="hidden md:flex w-full flex-1 min-h-0">
|
||||
<div
|
||||
classList={{
|
||||
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full mx-auto": true,
|
||||
"px-21 @4xl:px-6 max-w-2xl": !wide(),
|
||||
"px-6 max-w-2xl": wide(),
|
||||
}}
|
||||
>
|
||||
{title()}
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<Show when={messages().length > 1}>
|
||||
<>
|
||||
<div class="md:hidden absolute right-full">
|
||||
<MessageNav
|
||||
class="mt-2 mr-3"
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
size="compact"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
classList={{
|
||||
"hidden md:block": true,
|
||||
"absolute right-[90%]": !wide(),
|
||||
"absolute right-full": wide(),
|
||||
}}
|
||||
>
|
||||
<MessageNav
|
||||
classList={{
|
||||
"mt-2.5 mr-3": !wide(),
|
||||
"mt-0.5 mr-8": wide(),
|
||||
}}
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
size={wide() ? "normal" : "compact"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
const turns = () => (
|
||||
<div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
||||
<div class="px-4">{title()}</div>
|
||||
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
|
||||
>
|
||||
<div class="flex items-center justify-center pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</SessionTurn>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={diffs().length > 0}>
|
||||
<div class="relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
messageID={message.id}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={diffs().length > 0}>
|
||||
<Tabs class="md:hidden">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||
Session
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
|
||||
5 Files Changed
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="!overflow-hidden">
|
||||
{turns()}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content forceMount value="review" class="!overflow-hidden hidden data-[selected]:block">
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="md:hidden !overflow-hidden">{turns()}</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
)}
|
||||
)
|
||||
|
||||
const wide = createMemo(() => diffs().length === 0)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
||||
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
||||
<div class="">
|
||||
<a href="https://opencode.ai">
|
||||
<Mark />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center">
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://github.com/sst/opencode"
|
||||
target="_blank"
|
||||
icon="github"
|
||||
variant="ghost"
|
||||
/>
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://opencode.ai/discord"
|
||||
target="_blank"
|
||||
icon="discord"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="select-text flex flex-col flex-1 min-h-0">
|
||||
<div classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}>
|
||||
<div
|
||||
classList={{
|
||||
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
|
||||
"mx-auto max-w-146": !wide(),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"w-full flex justify-start items-start min-w-0": true,
|
||||
"max-w-146 mx-auto px-6": wide(),
|
||||
"pr-6 pl-18": !wide(),
|
||||
}}
|
||||
>
|
||||
{title()}
|
||||
</div>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={wide()}
|
||||
/>
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||
classes={{
|
||||
root: "grow",
|
||||
content: "flex flex-col justify-between items-start",
|
||||
container: "w-full pb-20 " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"),
|
||||
}}
|
||||
>
|
||||
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</SessionTurn>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={diffs().length > 0}>
|
||||
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||
<SessionReview
|
||||
class="@4xl:hidden"
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
<SessionReview
|
||||
class="hidden @4xl:flex"
|
||||
split
|
||||
diffs={splitDiffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={diffs().length > 0}>
|
||||
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||
Session
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
|
||||
5 Files Changed
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="!overflow-hidden">
|
||||
{turns()}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
forceMount
|
||||
value="review"
|
||||
class="!overflow-hidden hidden data-[selected]:block"
|
||||
>
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
{turns()}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The AI coding agent built for the terminal"
|
||||
version = "1.0.111"
|
||||
version = "1.0.124"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.111/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.124/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.111/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.124/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.111/opencode-linux-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.124/opencode-linux-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.111/opencode-linux-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.124/opencode-linux-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.111/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.124/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.111",
|
||||
"version": "1.0.124",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -43,13 +43,15 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||
"@ai-sdk/anthropic": "2.0.45",
|
||||
"@ai-sdk/anthropic": "2.0.50",
|
||||
"@ai-sdk/azure": "2.0.73",
|
||||
"@ai-sdk/google": "2.0.42",
|
||||
"@ai-sdk/google-vertex": "3.0.74",
|
||||
"@ai-sdk/google": "2.0.44",
|
||||
"@ai-sdk/google-vertex": "3.0.81",
|
||||
"@ai-sdk/mcp": "0.0.8",
|
||||
"@ai-sdk/openai": "2.0.71",
|
||||
"@ai-sdk/openai-compatible": "1.0.27",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.18",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
@@ -61,9 +63,9 @@
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.2.5",
|
||||
"@opentui/core": "0.1.50",
|
||||
"@opentui/solid": "0.1.50",
|
||||
"@openrouter/ai-sdk-provider": "1.2.8",
|
||||
"@opentui/core": "0.1.54",
|
||||
"@opentui/solid": "0.1.54",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -82,7 +84,7 @@
|
||||
"jsonc-parser": "3.3.1",
|
||||
"minimatch": "10.0.3",
|
||||
"open": "10.1.2",
|
||||
"opentui-spinner": "0.0.5",
|
||||
"opentui-spinner": "0.0.6",
|
||||
"partial-json": "0.1.7",
|
||||
"remeda": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
|
||||
@@ -102,8 +102,7 @@ export namespace Agent {
|
||||
const result: Record<string, Info> = {
|
||||
general: {
|
||||
name: "general",
|
||||
description:
|
||||
"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
|
||||
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
|
||||
tools: {
|
||||
todoread: false,
|
||||
todowrite: false,
|
||||
@@ -114,6 +113,41 @@ export namespace Agent {
|
||||
mode: "subagent",
|
||||
builtIn: true,
|
||||
},
|
||||
explore: {
|
||||
name: "explore",
|
||||
tools: {
|
||||
todoread: false,
|
||||
todowrite: false,
|
||||
edit: false,
|
||||
write: false,
|
||||
...defaultTools,
|
||||
},
|
||||
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
|
||||
prompt: [
|
||||
`You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`,
|
||||
``,
|
||||
`Your strengths:`,
|
||||
`- Rapidly finding files using glob patterns`,
|
||||
`- Searching code and text with powerful regex patterns`,
|
||||
`- Reading and analyzing file contents`,
|
||||
``,
|
||||
`Guidelines:`,
|
||||
`- Use Glob for broad file pattern matching`,
|
||||
`- Use Grep for searching file contents with regex`,
|
||||
`- Use Read when you know the specific file path you need to read`,
|
||||
`- Use Bash for file operations like copying, moving, or listing directory contents`,
|
||||
`- Adapt your search approach based on the thoroughness level specified by the caller`,
|
||||
`- Return file paths as absolute paths in your final response`,
|
||||
`- For clear communication, avoid using emojis`,
|
||||
`- Do not create any files, or run bash commands that modify the user's system state in any way`,
|
||||
``,
|
||||
`Complete the user's search request efficiently and report your findings clearly.`,
|
||||
].join("\n"),
|
||||
options: {},
|
||||
permission: agentPermission,
|
||||
mode: "subagent",
|
||||
builtIn: true,
|
||||
},
|
||||
build: {
|
||||
name: "build",
|
||||
tools: { ...defaultTools },
|
||||
|
||||
@@ -7,7 +7,7 @@ import { graphql } from "@octokit/graphql"
|
||||
import * as core from "@actions/core"
|
||||
import * as github from "@actions/github"
|
||||
import type { Context } from "@actions/github/lib/context"
|
||||
import type { IssueCommentEvent } from "@octokit/webhooks-types"
|
||||
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
|
||||
import { UI } from "../ui"
|
||||
import { cmd } from "./cmd"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
@@ -328,6 +328,8 @@ export const GithubInstallCommand = cmd({
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
@@ -378,7 +380,7 @@ export const GithubRunCommand = cmd({
|
||||
const isMock = args.token || args.event
|
||||
|
||||
const context = isMock ? (JSON.parse(args.event!) as Context) : github.context
|
||||
if (context.eventName !== "issue_comment") {
|
||||
if (context.eventName !== "issue_comment" && context.eventName !== "pull_request_review_comment") {
|
||||
core.setFailed(`Unsupported event type: ${context.eventName}`)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -387,9 +389,14 @@ export const GithubRunCommand = cmd({
|
||||
const runId = normalizeRunId()
|
||||
const share = normalizeShare()
|
||||
const { owner, repo } = context.repo
|
||||
const payload = context.payload as IssueCommentEvent
|
||||
const payload = context.payload as IssueCommentEvent | PullRequestReviewCommentEvent
|
||||
const issueEvent = isIssueCommentEvent(payload) ? payload : undefined
|
||||
const actor = context.actor
|
||||
const issueId = payload.issue.number
|
||||
|
||||
const issueId =
|
||||
context.eventName === "pull_request_review_comment"
|
||||
? (payload as PullRequestReviewCommentEvent).pull_request.number
|
||||
: (payload as IssueCommentEvent).issue.number
|
||||
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
|
||||
const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
|
||||
|
||||
@@ -434,7 +441,7 @@ export const GithubRunCommand = cmd({
|
||||
// 1. Issue
|
||||
// 2. Local PR
|
||||
// 3. Fork PR
|
||||
if (payload.issue.pull_request) {
|
||||
if (context.eventName === "pull_request_review_comment" || issueEvent?.issue.pull_request) {
|
||||
const prData = await fetchPR()
|
||||
// Local PR
|
||||
if (prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner) {
|
||||
@@ -531,11 +538,45 @@ export const GithubRunCommand = cmd({
|
||||
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
|
||||
}
|
||||
|
||||
function isIssueCommentEvent(
|
||||
event: IssueCommentEvent | PullRequestReviewCommentEvent,
|
||||
): event is IssueCommentEvent {
|
||||
return "issue" in event
|
||||
}
|
||||
|
||||
function getReviewCommentContext() {
|
||||
if (context.eventName !== "pull_request_review_comment") {
|
||||
return null
|
||||
}
|
||||
|
||||
const reviewPayload = payload as PullRequestReviewCommentEvent
|
||||
return {
|
||||
file: reviewPayload.comment.path,
|
||||
diffHunk: reviewPayload.comment.diff_hunk,
|
||||
line: reviewPayload.comment.line,
|
||||
originalLine: reviewPayload.comment.original_line,
|
||||
position: reviewPayload.comment.position,
|
||||
commitId: reviewPayload.comment.commit_id,
|
||||
originalCommitId: reviewPayload.comment.original_commit_id,
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserPrompt() {
|
||||
const reviewContext = getReviewCommentContext()
|
||||
let prompt = (() => {
|
||||
const body = payload.comment.body.trim()
|
||||
if (body === "/opencode" || body === "/oc") return "Summarize this thread"
|
||||
if (body.includes("/opencode") || body.includes("/oc")) return body
|
||||
if (body === "/opencode" || body === "/oc") {
|
||||
if (reviewContext) {
|
||||
return `Review this code change and suggest improvements for the commented lines:\n\nFile: ${reviewContext.file}\nLines: ${reviewContext.line}\n\n${reviewContext.diffHunk}`
|
||||
}
|
||||
return "Summarize this thread"
|
||||
}
|
||||
if (body.includes("/opencode") || body.includes("/oc")) {
|
||||
if (reviewContext) {
|
||||
return `${body}\n\nContext: You are reviewing a comment on file "${reviewContext.file}" at line ${reviewContext.line}.\n\nDiff context:\n${reviewContext.diffHunk}`
|
||||
}
|
||||
return body
|
||||
}
|
||||
throw new Error("Comments must mention `/opencode` or `/oc`")
|
||||
})()
|
||||
|
||||
@@ -652,7 +693,10 @@ export const GithubRunCommand = cmd({
|
||||
try {
|
||||
return await chat(`Summarize the following in less than 40 characters:\n\n${response}`)
|
||||
} catch (e) {
|
||||
return `Fix issue: ${payload.issue.title}`
|
||||
const title = issueEvent
|
||||
? issueEvent.issue.title
|
||||
: (payload as PullRequestReviewCommentEvent).pull_request.title
|
||||
return `Fix issue: ${title}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { TuiEvent } from "./event"
|
||||
import { KVProvider, useKV } from "./context/kv"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import open from "open"
|
||||
|
||||
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
// can't set raw mode if not a TTY
|
||||
@@ -186,16 +187,13 @@ function App() {
|
||||
})
|
||||
})
|
||||
|
||||
let continued = false
|
||||
createEffect(() => {
|
||||
if (sync.status !== "complete") return
|
||||
if (args.continue) {
|
||||
const match = sync.data.session.at(0)?.id
|
||||
if (match) {
|
||||
route.navigate({
|
||||
type: "session",
|
||||
sessionID: match,
|
||||
})
|
||||
}
|
||||
if (continued || sync.status !== "complete" || !args.continue) return
|
||||
const match = sync.data.session.at(0)?.id
|
||||
if (match) {
|
||||
continued = true
|
||||
route.navigate({ type: "session", sessionID: match })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -318,6 +316,15 @@ function App() {
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Open docs",
|
||||
value: "docs.open",
|
||||
onSelect: () => {
|
||||
open("https://opencode.ai/docs").catch(() => {})
|
||||
dialog.clear()
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Exit the app",
|
||||
value: "app.exit",
|
||||
@@ -455,48 +462,14 @@ function App() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<box flexDirection="column" flexGrow={1}>
|
||||
<Switch>
|
||||
<Match when={route.data.type === "home"}>
|
||||
<Home />
|
||||
</Match>
|
||||
<Match when={route.data.type === "session"}>
|
||||
<Session />
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
<box
|
||||
height={1}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
flexShrink={0}
|
||||
>
|
||||
<box flexDirection="row">
|
||||
<box flexDirection="row" backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
|
||||
<text fg={theme.textMuted}>open</text>
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
code{" "}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>v{Installation.VERSION}</text>
|
||||
</box>
|
||||
<box paddingLeft={1} paddingRight={1}>
|
||||
<text fg={theme.textMuted}>{process.cwd().replace(Global.Path.home, "~")}</text>
|
||||
</box>
|
||||
</box>
|
||||
<Show when={false}>
|
||||
<box flexDirection="row" flexShrink={0}>
|
||||
<text fg={theme.textMuted} paddingRight={1}>
|
||||
tab
|
||||
</text>
|
||||
<text fg={local.agent.color(local.agent.current().name)}>{""}</text>
|
||||
<text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
|
||||
<span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
|
||||
<span> AGENT </span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
<Switch>
|
||||
<Match when={route.data.type === "home"}>
|
||||
<Home />
|
||||
</Match>
|
||||
<Match when={route.data.type === "session"}>
|
||||
<Session />
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createMemo, createSignal } from "solid-js"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy, take } from "remeda"
|
||||
import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
|
||||
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
export function DialogModel() {
|
||||
const local = useLocal()
|
||||
@@ -16,42 +18,86 @@ export function DialogModel() {
|
||||
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(() => {
|
||||
return [
|
||||
...(showRecent()
|
||||
? local.model.recent().flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)!
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Recent",
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
const query = ref()?.filter
|
||||
const favorites = connected() ? local.model.favorite() : []
|
||||
const recents = local.model.recent()
|
||||
|
||||
const recentList = recents
|
||||
.filter((item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID))
|
||||
.slice(0, 5)
|
||||
|
||||
const favoriteOptions = !query
|
||||
? favorites.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
]
|
||||
})
|
||||
: []),
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Favorites",
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
: []
|
||||
|
||||
const recentOptions = !query
|
||||
? recentList.flatMap((item) => {
|
||||
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
||||
if (!provider) return []
|
||||
const model = provider.models[item.modelID]
|
||||
if (!model) return []
|
||||
return [
|
||||
{
|
||||
key: item,
|
||||
value: {
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
title: model.name ?? item.modelID,
|
||||
description: provider.name,
|
||||
category: "Recent",
|
||||
disabled: provider.id === "opencode" && model.id.includes("-nano"),
|
||||
footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
|
||||
onSelect: () => {
|
||||
dialog.clear()
|
||||
local.model.set(
|
||||
{
|
||||
providerID: provider.id,
|
||||
modelID: model.id,
|
||||
},
|
||||
{ recent: true },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
: []
|
||||
|
||||
return [
|
||||
...favoriteOptions,
|
||||
...recentOptions,
|
||||
...pipe(
|
||||
sync.data.provider,
|
||||
sortBy(
|
||||
@@ -62,28 +108,47 @@ export function DialogModel() {
|
||||
pipe(
|
||||
provider.models,
|
||||
entries(),
|
||||
map(([model, info]) => ({
|
||||
value: {
|
||||
map(([model, info]) => {
|
||||
const value = {
|
||||
providerID: provider.id,
|
||||
modelID: model,
|
||||
},
|
||||
title: info.name ?? model,
|
||||
description: connected() ? provider.name : undefined,
|
||||
category: connected() ? provider.name : 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 },
|
||||
}
|
||||
return {
|
||||
value,
|
||||
title: info.name ?? model,
|
||||
description: favorites.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
},
|
||||
})),
|
||||
filter((x) => !showRecent() || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
|
||||
? "(Favorite)"
|
||||
: undefined,
|
||||
category: connected() ? provider.name : 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) => {
|
||||
if (query) return true
|
||||
const value = x.value
|
||||
const inFavorites = favorites.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inFavorites) return false
|
||||
const inRecents = recents.some(
|
||||
(item) => item.providerID === value.providerID && item.modelID === value.modelID,
|
||||
)
|
||||
if (inRecents) return false
|
||||
return true
|
||||
}),
|
||||
sortBy((x) => x.title),
|
||||
),
|
||||
),
|
||||
@@ -108,11 +173,19 @@ export function DialogModel() {
|
||||
keybind={[
|
||||
{
|
||||
keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
|
||||
title: connected() ? "Connect provider" : "More providers",
|
||||
title: connected() ? "Connect provider" : "View all providers",
|
||||
onTrigger() {
|
||||
dialog.replace(() => <DialogProvider />)
|
||||
},
|
||||
},
|
||||
{
|
||||
keybind: Keybind.parse("ctrl+f")[0],
|
||||
title: "Favorite",
|
||||
disabled: !connected(),
|
||||
onTrigger: (option) => {
|
||||
local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
|
||||
},
|
||||
},
|
||||
]}
|
||||
ref={setRef}
|
||||
title="Select model"
|
||||
|
||||
@@ -26,13 +26,15 @@ export function createDialogProviderOptions() {
|
||||
const options = createMemo(() => {
|
||||
return pipe(
|
||||
sync.data.provider_next.all,
|
||||
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
|
||||
map((provider) => ({
|
||||
title: provider.name,
|
||||
value: provider.id,
|
||||
footer: {
|
||||
opencode: "Recommended",
|
||||
anthropic: "Claude Max or API key",
|
||||
description: {
|
||||
opencode: "(Recommended)",
|
||||
anthropic: "(Claude Max or API key)",
|
||||
}[provider.id],
|
||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||
async onSelect() {
|
||||
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||
{
|
||||
@@ -85,7 +87,6 @@ export function createDialogProviderOptions() {
|
||||
}
|
||||
},
|
||||
})),
|
||||
sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99),
|
||||
)
|
||||
})
|
||||
return options
|
||||
@@ -197,11 +198,24 @@ function ApiMethod(props: ApiMethodProps) {
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<DialogPrompt
|
||||
title={props.title}
|
||||
placeholder="API key"
|
||||
description={
|
||||
props.providerID === "opencode" ? (
|
||||
<box gap={1}>
|
||||
<text fg={theme.textMuted}>
|
||||
OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
|
||||
</text>
|
||||
<text>
|
||||
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
|
||||
</text>
|
||||
</box>
|
||||
) : undefined
|
||||
}
|
||||
onConfirm={async (value) => {
|
||||
if (!value) return
|
||||
sdk.client.auth.set({
|
||||
|
||||
@@ -81,6 +81,7 @@ export function Autocomplete(props: {
|
||||
const extmarkId = input.extmarks.create({
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
virtual: true,
|
||||
styleId,
|
||||
typeId: props.promptPartTypeId(),
|
||||
})
|
||||
@@ -238,7 +239,7 @@ export function Autocomplete(props: {
|
||||
},
|
||||
{
|
||||
display: "/thinking",
|
||||
description: "toggle thinking blocks",
|
||||
description: "toggle thinking visibility",
|
||||
onSelect: () => command.trigger("session.toggle.thinking"),
|
||||
},
|
||||
)
|
||||
@@ -291,6 +292,11 @@ export function Autocomplete(props: {
|
||||
description: "open editor",
|
||||
onSelect: () => command.trigger("prompt.editor", "prompt"),
|
||||
},
|
||||
{
|
||||
display: "/connect",
|
||||
description: "connect to a provider",
|
||||
onSelect: () => command.trigger("provider.connect"),
|
||||
},
|
||||
{
|
||||
display: "/help",
|
||||
description: "show help",
|
||||
|
||||
@@ -310,6 +310,7 @@ export function Prompt(props: PromptProps) {
|
||||
const extmarkId = input.extmarks.create({
|
||||
start,
|
||||
end,
|
||||
virtual: true,
|
||||
styleId,
|
||||
typeId: promptPartTypeId,
|
||||
})
|
||||
@@ -496,6 +497,40 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
const exit = useExit()
|
||||
|
||||
function pasteText(text: string, virtualText: string) {
|
||||
const currentOffset = input.visualCursor.offset
|
||||
const extmarkStart = currentOffset
|
||||
const extmarkEnd = extmarkStart + virtualText.length
|
||||
|
||||
input.insertText(virtualText + " ")
|
||||
|
||||
const extmarkId = input.extmarks.create({
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
virtual: true,
|
||||
styleId: pasteStyleId,
|
||||
typeId: promptPartTypeId,
|
||||
})
|
||||
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const partIndex = draft.prompt.parts.length
|
||||
draft.prompt.parts.push({
|
||||
type: "text" as const,
|
||||
text,
|
||||
source: {
|
||||
text: {
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
value: virtualText,
|
||||
},
|
||||
},
|
||||
})
|
||||
draft.extmarkToPartIndex.set(extmarkId, partIndex)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function pasteImage(file: { filename?: string; content: string; mime: string }) {
|
||||
const currentOffset = input.visualCursor.offset
|
||||
const extmarkStart = currentOffset
|
||||
@@ -551,12 +586,16 @@ export function Prompt(props: PromptProps) {
|
||||
frames: createFrames({
|
||||
color,
|
||||
style: "blocks",
|
||||
inactiveFactor: 0.25,
|
||||
inactiveFactor: 0.6,
|
||||
// enableFading: false,
|
||||
minAlpha: 0.3,
|
||||
}),
|
||||
color: createColors({
|
||||
color,
|
||||
style: "blocks",
|
||||
inactiveFactor: 0.25,
|
||||
inactiveFactor: 0.6,
|
||||
// enableFading: false,
|
||||
minAlpha: 0.3,
|
||||
}),
|
||||
}
|
||||
})
|
||||
@@ -602,11 +641,7 @@ export function Prompt(props: PromptProps) {
|
||||
flexGrow={1}
|
||||
>
|
||||
<textarea
|
||||
placeholder={
|
||||
props.showPlaceholder
|
||||
? t`${dim(fg(theme.primary)(" → up/down"))} ${dim(fg("#64748b")("history"))} ${dim(fg("#a78bfa")("•"))} ${dim(fg(theme.primary)(keybind.print("input_newline")))} ${dim(fg("#64748b")("newline"))} ${dim(fg("#a78bfa")("•"))} ${dim(fg(theme.primary)(keybind.print("input_submit")))} ${dim(fg("#64748b")("submit"))}`
|
||||
: undefined
|
||||
}
|
||||
placeholder={props.sessionID ? undefined : "Build anything..."}
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
minHeight={1}
|
||||
@@ -705,25 +740,36 @@ export function Prompt(props: PromptProps) {
|
||||
// trim ' from the beginning and end of the pasted content. just
|
||||
// ' and nothing else
|
||||
const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
|
||||
console.log(pastedContent, filepath)
|
||||
try {
|
||||
const file = Bun.file(filepath)
|
||||
if (file.type.startsWith("image/")) {
|
||||
event.preventDefault()
|
||||
const content = await file
|
||||
.arrayBuffer()
|
||||
.then((buffer) => Buffer.from(buffer).toString("base64"))
|
||||
.catch(console.error)
|
||||
if (content) {
|
||||
await pasteImage({
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
content,
|
||||
})
|
||||
return
|
||||
const isUrl = /^(https?):\/\//.test(filepath)
|
||||
if (!isUrl) {
|
||||
try {
|
||||
const file = Bun.file(filepath)
|
||||
// Handle SVG as raw text content, not as base64 image
|
||||
if (file.type === "image/svg+xml") {
|
||||
event.preventDefault()
|
||||
const content = await file.text().catch(() => {})
|
||||
if (content) {
|
||||
pasteText(content, `[SVG: ${file.name ?? "image"}]`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
if (file.type.startsWith("image/")) {
|
||||
event.preventDefault()
|
||||
const content = await file
|
||||
.arrayBuffer()
|
||||
.then((buffer) => Buffer.from(buffer).toString("base64"))
|
||||
.catch(() => {})
|
||||
if (content) {
|
||||
await pasteImage({
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
content,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
|
||||
if (
|
||||
@@ -731,45 +777,16 @@ export function Prompt(props: PromptProps) {
|
||||
!sync.data.config.experimental?.disable_paste_summary
|
||||
) {
|
||||
event.preventDefault()
|
||||
const currentOffset = input.visualCursor.offset
|
||||
const virtualText = `[Pasted ~${lineCount} lines]`
|
||||
const textToInsert = virtualText + " "
|
||||
const extmarkStart = currentOffset
|
||||
const extmarkEnd = extmarkStart + virtualText.length
|
||||
|
||||
input.insertText(textToInsert)
|
||||
|
||||
const extmarkId = input.extmarks.create({
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
virtual: true,
|
||||
styleId: pasteStyleId,
|
||||
typeId: promptPartTypeId,
|
||||
})
|
||||
|
||||
const part = {
|
||||
type: "text" as const,
|
||||
text: pastedContent,
|
||||
source: {
|
||||
text: {
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
value: virtualText,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const partIndex = draft.prompt.parts.length
|
||||
draft.prompt.parts.push(part)
|
||||
draft.extmarkToPartIndex.set(extmarkId, partIndex)
|
||||
}),
|
||||
)
|
||||
pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
|
||||
return
|
||||
}
|
||||
}}
|
||||
ref={(r: TextareaRenderable) => (input = r)}
|
||||
ref={(r: TextareaRenderable) => {
|
||||
input = r
|
||||
setTimeout(() => {
|
||||
input.cursorColor = highlight()
|
||||
}, 0)
|
||||
}}
|
||||
onMouseDown={(r: MouseEvent) => r.target?.focus()}
|
||||
focusedBackgroundColor={theme.backgroundElement}
|
||||
cursorColor={highlight()}
|
||||
@@ -796,7 +813,8 @@ export function Prompt(props: PromptProps) {
|
||||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
vertical: "╹",
|
||||
// when the background is transparent, don't draw the vertical line
|
||||
vertical: theme.background.a != 0 ? "╹" : " ",
|
||||
}}
|
||||
>
|
||||
<box
|
||||
@@ -825,7 +843,8 @@ export function Prompt(props: PromptProps) {
|
||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||
>
|
||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
{/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
|
||||
<spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
{(() => {
|
||||
const retry = createMemo(() => {
|
||||
@@ -837,7 +856,7 @@ export function Prompt(props: PromptProps) {
|
||||
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"
|
||||
return "gemini is way too hot right now"
|
||||
if (r.message.length > 50) return r.message.slice(0, 50) + "..."
|
||||
return r.message
|
||||
})
|
||||
|
||||
12
packages/opencode/src/cli/cmd/tui/context/directory.ts
Normal file
12
packages/opencode/src/cli/cmd/tui/context/directory.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { useSync } from "./sync"
|
||||
import { Global } from "@/global"
|
||||
|
||||
export function useDirectory() {
|
||||
const sync = useSync()
|
||||
return createMemo(() => {
|
||||
const result = process.cwd().replace(Global.Path.home, "~")
|
||||
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
|
||||
return result
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { FormatError } from "@/cli/error"
|
||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||
|
||||
export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
||||
name: "Exit",
|
||||
@@ -10,8 +10,10 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
||||
renderer.destroy()
|
||||
await input.onExit?.()
|
||||
if (reason) {
|
||||
const formatted = FormatError(reason) ?? JSON.stringify(reason)
|
||||
process.stderr.write(formatted + "\n")
|
||||
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
|
||||
if (formatted) {
|
||||
process.stderr.write(formatted + "\n")
|
||||
}
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
@@ -114,18 +114,34 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
providerID: string
|
||||
modelID: string
|
||||
}[]
|
||||
favorite: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}[]
|
||||
}>({
|
||||
ready: false,
|
||||
model: {},
|
||||
recent: [],
|
||||
favorite: [],
|
||||
})
|
||||
|
||||
const file = Bun.file(path.join(Global.Path.state, "model.json"))
|
||||
|
||||
function save() {
|
||||
Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
recent: modelStore.recent,
|
||||
favorite: modelStore.favorite,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
file
|
||||
.json()
|
||||
.then((x) => {
|
||||
setModelStore("recent", x.recent)
|
||||
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
|
||||
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
@@ -184,6 +200,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
recent() {
|
||||
return modelStore.recent
|
||||
},
|
||||
favorite() {
|
||||
return modelStore.favorite
|
||||
},
|
||||
parsed: createMemo(() => {
|
||||
const value = currentModel()
|
||||
const provider = sync.data.provider.find((x) => x.id === value.providerID)!
|
||||
@@ -206,6 +225,33 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
if (!val) return
|
||||
setModelStore("model", agent.current().name, { ...val })
|
||||
},
|
||||
cycleFavorite(direction: 1 | -1) {
|
||||
const favorites = modelStore.favorite.filter((item) => isModelValid(item))
|
||||
if (!favorites.length) {
|
||||
toast.show({
|
||||
variant: "info",
|
||||
message: "Add a favorite model to use this shortcut",
|
||||
duration: 3000,
|
||||
})
|
||||
return
|
||||
}
|
||||
const current = currentModel()
|
||||
let index = favorites.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
|
||||
if (index === -1) {
|
||||
index = direction === 1 ? 0 : favorites.length - 1
|
||||
} else {
|
||||
index += direction
|
||||
if (index < 0) index = favorites.length - 1
|
||||
if (index >= favorites.length) index = 0
|
||||
}
|
||||
const next = favorites[index]
|
||||
if (!next) return
|
||||
setModelStore("model", agent.current().name, { ...next })
|
||||
const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 10) uniq.pop()
|
||||
setModelStore("recent", uniq)
|
||||
save()
|
||||
},
|
||||
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
if (!isModelValid(model)) {
|
||||
@@ -219,17 +265,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setModelStore("model", agent.current().name, model)
|
||||
if (options?.recent) {
|
||||
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
if (uniq.length > 10) uniq.pop()
|
||||
setModelStore("recent", uniq)
|
||||
Bun.write(
|
||||
file,
|
||||
JSON.stringify({
|
||||
recent: modelStore.recent,
|
||||
}),
|
||||
)
|
||||
save()
|
||||
}
|
||||
})
|
||||
},
|
||||
toggleFavorite(model: { providerID: string; modelID: string }) {
|
||||
batch(() => {
|
||||
if (!isModelValid(model)) {
|
||||
toast.show({
|
||||
message: `Model ${model.providerID}/${model.modelID} is not valid`,
|
||||
variant: "warning",
|
||||
duration: 3000,
|
||||
})
|
||||
return
|
||||
}
|
||||
const exists = modelStore.favorite.some(
|
||||
(x) => x.providerID === model.providerID && x.modelID === model.modelID,
|
||||
)
|
||||
const next = exists
|
||||
? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
|
||||
: [model, ...modelStore.favorite]
|
||||
setModelStore("favorite", next)
|
||||
save()
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { batch, onCleanup } from "solid-js"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
@@ -16,43 +17,49 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
||||
}>()
|
||||
|
||||
sdk.event.subscribe().then(async (events) => {
|
||||
let queue: Event[] = []
|
||||
let timer: Timer | undefined
|
||||
let last = 0
|
||||
|
||||
const flush = () => {
|
||||
if (queue.length === 0) return
|
||||
const events = queue
|
||||
queue = []
|
||||
timer = undefined
|
||||
last = Date.now()
|
||||
// Batch all event emissions so all store updates result in a single render
|
||||
batch(() => {
|
||||
for (const event of events) {
|
||||
emitter.emit(event.type, event)
|
||||
}
|
||||
onMount(async () => {
|
||||
while (true) {
|
||||
if (abort.signal.aborted) break
|
||||
const events = await sdk.event.subscribe({
|
||||
signal: abort.signal,
|
||||
})
|
||||
}
|
||||
let queue: Event[] = []
|
||||
let timer: Timer | undefined
|
||||
let last = 0
|
||||
|
||||
for await (const event of events.stream) {
|
||||
queue.push(event)
|
||||
const elapsed = Date.now() - last
|
||||
|
||||
if (timer) continue
|
||||
// If we just flushed recently (within 16ms), batch this with future events
|
||||
// Otherwise, process immediately to avoid latency
|
||||
if (elapsed < 16) {
|
||||
timer = setTimeout(flush, 16)
|
||||
continue
|
||||
const flush = () => {
|
||||
if (queue.length === 0) return
|
||||
const events = queue
|
||||
queue = []
|
||||
timer = undefined
|
||||
last = Date.now()
|
||||
// Batch all event emissions so all store updates result in a single render
|
||||
batch(() => {
|
||||
for (const event of events) {
|
||||
emitter.emit(event.type, event)
|
||||
}
|
||||
})
|
||||
}
|
||||
flush()
|
||||
}
|
||||
|
||||
// Flush any remaining events
|
||||
if (timer) clearTimeout(timer)
|
||||
if (queue.length > 0) {
|
||||
flush()
|
||||
for await (const event of events.stream) {
|
||||
queue.push(event)
|
||||
const elapsed = Date.now() - last
|
||||
|
||||
if (timer) continue
|
||||
// If we just flushed recently (within 16ms), batch this with future events
|
||||
// Otherwise, process immediately to avoid latency
|
||||
if (elapsed < 16) {
|
||||
timer = setTimeout(flush, 16)
|
||||
continue
|
||||
}
|
||||
flush()
|
||||
}
|
||||
|
||||
// Flush any remaining events
|
||||
if (timer) clearTimeout(timer)
|
||||
if (queue.length > 0) {
|
||||
flush()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
SessionStatus,
|
||||
ProviderListResponse,
|
||||
ProviderAuthMethod,
|
||||
VcsInfo,
|
||||
} from "@opencode-ai/sdk"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
@@ -22,6 +23,7 @@ import { createSimpleContext } from "./helper"
|
||||
import type { Snapshot } from "@/snapshot"
|
||||
import { useExit } from "./exit"
|
||||
import { batch, onMount } from "solid-js"
|
||||
import { Log } from "@/util/log"
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
@@ -59,6 +61,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
[key: string]: McpStatus
|
||||
}
|
||||
formatter: FormatterStatus[]
|
||||
vcs: VcsInfo | undefined
|
||||
}>({
|
||||
provider_next: {
|
||||
all: [],
|
||||
@@ -82,6 +85,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
lsp: [],
|
||||
mcp: {},
|
||||
formatter: [],
|
||||
vcs: undefined,
|
||||
})
|
||||
|
||||
const sdk = useSDK()
|
||||
@@ -238,6 +242,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!))
|
||||
break
|
||||
}
|
||||
|
||||
case "vcs.branch.updated": {
|
||||
setStore("vcs", { branch: event.properties.branch })
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -276,11 +285,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
|
||||
sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
sdk.client.provider.auth().then((x) => setStore("provider_auth", x.data ?? {})),
|
||||
sdk.client.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||
]).then(() => {
|
||||
setStore("status", "complete")
|
||||
})
|
||||
})
|
||||
.catch(async (e) => {
|
||||
Log.Default.error("tui bootstrap failed", {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
name: e instanceof Error ? e.name : undefined,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
})
|
||||
await exit(e)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Locale } from "@/util/locale"
|
||||
import { useSync } from "../context/sync"
|
||||
import { Toast } from "../ui/toast"
|
||||
import { useArgs } from "../context/args"
|
||||
import { Global } from "@/global"
|
||||
import { useDirectory } from "../context/directory"
|
||||
|
||||
// TODO: what is the best way to do this?
|
||||
let once = false
|
||||
@@ -15,6 +17,7 @@ let once = false
|
||||
export function Home() {
|
||||
const sync = useSync()
|
||||
const { theme } = useTheme()
|
||||
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
|
||||
const mcpError = createMemo(() => {
|
||||
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
|
||||
})
|
||||
@@ -47,31 +50,36 @@ export function Home() {
|
||||
once = true
|
||||
}
|
||||
})
|
||||
const directory = useDirectory()
|
||||
|
||||
return (
|
||||
<box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<Logo />
|
||||
<box width={39}>
|
||||
<HelpRow keybind="command_list">Commands</HelpRow>
|
||||
<HelpRow keybind="session_list">List sessions</HelpRow>
|
||||
<HelpRow keybind="model_list">Switch model</HelpRow>
|
||||
<HelpRow keybind="agent_cycle">Switch agent</HelpRow>
|
||||
<>
|
||||
<box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<Logo />
|
||||
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
|
||||
<Prompt ref={(r) => (prompt = r)} hint={Hint} />
|
||||
</box>
|
||||
<Toast />
|
||||
</box>
|
||||
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
|
||||
<Prompt ref={(r) => (prompt = r)} hint={Hint} />
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}>
|
||||
<text fg={theme.textMuted}>{directory()}</text>
|
||||
<box gap={1} flexDirection="row" flexShrink={0}>
|
||||
<Show when={mcp()}>
|
||||
<text fg={theme.text}>
|
||||
<Switch>
|
||||
<Match when={mcpError()}>
|
||||
<span style={{ fg: theme.error }}>⊙ </span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span style={{ fg: theme.success }}>⊙ </span>
|
||||
</Match>
|
||||
</Switch>
|
||||
{Object.keys(sync.data.mcp).length} MCP
|
||||
</text>
|
||||
<text fg={theme.textMuted}>/status</text>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
<Toast />
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) {
|
||||
const keybind = useKeybind()
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<box flexDirection="row" justifyContent="space-between" width="100%">
|
||||
<text fg={theme.text}>{props.children}</text>
|
||||
<text fg={theme.primary}>{keybind.print(props.keybind)}</text>
|
||||
</box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
37
packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
Normal file
37
packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createMemo, Match, Show, Switch } from "solid-js"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useSync } from "../../context/sync"
|
||||
import { useDirectory } from "../../context/directory"
|
||||
|
||||
export function Footer() {
|
||||
const { theme } = useTheme()
|
||||
const sync = useSync()
|
||||
const mcp = createMemo(() => Object.keys(sync.data.mcp))
|
||||
const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
|
||||
const lsp = createMemo(() => Object.keys(sync.data.lsp))
|
||||
const directory = useDirectory()
|
||||
return (
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1} flexShrink={0}>
|
||||
<text fg={theme.textMuted}>{directory()}</text>
|
||||
<box gap={2} flexDirection="row" flexShrink={0}>
|
||||
<text fg={theme.text}>
|
||||
<span style={{ fg: theme.success }}>•</span> {lsp().length} LSP
|
||||
</text>
|
||||
<Show when={mcp().length}>
|
||||
<text fg={theme.text}>
|
||||
<Switch>
|
||||
<Match when={mcpError()}>
|
||||
<span style={{ fg: theme.error }}>⊙ </span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span style={{ fg: theme.success }}>⊙ </span>
|
||||
</Match>
|
||||
</Switch>
|
||||
{mcp().length} MCP
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.textMuted}>/status</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -3,15 +3,16 @@ import { useRouteData } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { pipe, sumBy } from "remeda"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { SplitBorder } from "@tui/component/border"
|
||||
import { SplitBorder, EmptyBorder } from "@tui/component/border"
|
||||
import type { AssistantMessage, Session } from "@opencode-ai/sdk"
|
||||
import { useDirectory } from "../../context/directory"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
|
||||
const Title = (props: { session: Accessor<Session> }) => {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bold: true, fg: theme.accent }}>#</span>{" "}
|
||||
<span style={{ bold: true }}>{props.session().title}</span>
|
||||
<span style={{ bold: true }}>#</span> <span style={{ bold: true }}>{props.session().title}</span>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
@@ -53,43 +54,71 @@ export function Header() {
|
||||
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
|
||||
let result = total.toLocaleString()
|
||||
if (model?.limit.context) {
|
||||
result += "/" + Math.round((total / model.limit.context) * 100) + "%"
|
||||
result += " " + Math.round((total / model.limit.context) * 100) + "%"
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
const keybind = useKeybind()
|
||||
|
||||
return (
|
||||
<box paddingLeft={1} paddingRight={1} {...SplitBorder} borderColor={theme.backgroundElement} flexShrink={0}>
|
||||
<Show
|
||||
when={shareEnabled()}
|
||||
fallback={
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
||||
<Title session={session} />
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
</box>
|
||||
}
|
||||
<box flexShrink={0}>
|
||||
<box
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={1}
|
||||
{...SplitBorder}
|
||||
border={["left"]}
|
||||
borderColor={theme.border}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
>
|
||||
<Title session={session} />
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
||||
<box flexGrow={1} flexShrink={1}>
|
||||
<Switch>
|
||||
<Match when={session().share?.url}>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
{session().share!.url}
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
/share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
</box>
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={session()?.parentID}>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<text fg={theme.text}>
|
||||
<b>Subagent session</b>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
|
||||
</text>
|
||||
<box flexGrow={1} flexShrink={1} />
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={!shareEnabled()}>
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
||||
<Title session={session} />
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Title session={session} />
|
||||
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
||||
<box flexGrow={1} flexShrink={1}>
|
||||
<Switch>
|
||||
<Match when={session().share?.url}>
|
||||
<text fg={theme.textMuted} wrapMode="word">
|
||||
{session().share!.url}
|
||||
</text>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
/share <span style={{ fg: theme.textMuted }}>to create a shareable link</span>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
<ContextInfo context={context} cost={cost} />
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import { Toast, useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv.tsx"
|
||||
import { Editor } from "../../util/editor"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { Footer } from "./footer.tsx"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -80,6 +81,8 @@ const context = createContext<{
|
||||
conceal: () => boolean
|
||||
showThinking: () => boolean
|
||||
showTimestamps: () => boolean
|
||||
diffWrapMode: () => "word" | "none"
|
||||
sync: ReturnType<typeof useSync>
|
||||
}>()
|
||||
|
||||
function use() {
|
||||
@@ -109,11 +112,17 @@ export function Session() {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
|
||||
const [conceal, setConceal] = createSignal(true)
|
||||
const [showThinking, setShowThinking] = createSignal(true)
|
||||
const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true))
|
||||
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
|
||||
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
|
||||
|
||||
const wide = createMemo(() => dimensions().width > 120)
|
||||
const sidebarVisible = createMemo(() => sidebar() === "show" || (sidebar() === "auto" && wide()))
|
||||
const sidebarVisible = createMemo(() => {
|
||||
if (session()?.parentID) return false
|
||||
if (sidebar() === "show") return true
|
||||
if (sidebar() === "auto" && wide()) return true
|
||||
return false
|
||||
})
|
||||
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
|
||||
|
||||
const scrollAcceleration = createMemo(() => {
|
||||
@@ -319,7 +328,9 @@ export function Session() {
|
||||
value: "session.undo",
|
||||
keybind: "messages_undo",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
onSelect: async (dialog) => {
|
||||
const status = sync.data.session_status[route.sessionID]
|
||||
if (status?.type !== "idle") await sdk.client.session.abort({ path: { id: route.sessionID } }).catch(() => {})
|
||||
const revert = session().revert?.messageID
|
||||
const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
|
||||
if (!message) return
|
||||
@@ -421,11 +432,24 @@ export function Session() {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Toggle thinking blocks",
|
||||
title: showThinking() ? "Hide thinking" : "Show thinking",
|
||||
value: "session.toggle.thinking",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
setShowThinking((prev) => !prev)
|
||||
setShowThinking((prev) => {
|
||||
const next = !prev
|
||||
kv.set("thinking_visibility", next)
|
||||
return next
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Toggle diff wrapping",
|
||||
value: "session.toggle.diffwrap",
|
||||
category: "Session",
|
||||
onSelect: (dialog) => {
|
||||
setDiffWrapMode((prev) => (prev === "word" ? "none" : "word"))
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
@@ -730,33 +754,13 @@ export function Session() {
|
||||
conceal,
|
||||
showThinking,
|
||||
showTimestamps,
|
||||
diffWrapMode,
|
||||
sync,
|
||||
}}
|
||||
>
|
||||
<box flexDirection="row" paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={2}>
|
||||
<box flexGrow={1} gap={1}>
|
||||
<box flexDirection="row">
|
||||
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<Show when={session()}>
|
||||
<Show when={session().parentID}>
|
||||
<box
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
justifyContent="space-between"
|
||||
flexDirection="row"
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
flexShrink={0}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
Previous <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
<b>Viewing subagent session</b>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
<span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> Next
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={!sidebarVisible()}>
|
||||
<Header />
|
||||
</Show>
|
||||
@@ -881,6 +885,9 @@ export function Session() {
|
||||
sessionID={route.sessionID}
|
||||
/>
|
||||
</box>
|
||||
<Show when={!sidebarVisible()}>
|
||||
<Footer />
|
||||
</Show>
|
||||
</Show>
|
||||
<Toast />
|
||||
</box>
|
||||
@@ -1194,15 +1201,15 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
|
||||
<box gap={1}>
|
||||
<text fg={theme.text}>Permission required to run this tool:</text>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<text>
|
||||
<text fg={theme.text}>
|
||||
<b>enter</b>
|
||||
<span style={{ fg: theme.textMuted }}> accept</span>
|
||||
</text>
|
||||
<text>
|
||||
<text fg={theme.text}>
|
||||
<b>a</b>
|
||||
<span style={{ fg: theme.textMuted }}> accept always</span>
|
||||
</text>
|
||||
<text>
|
||||
<text fg={theme.text}>
|
||||
<b>d</b>
|
||||
<span style={{ fg: theme.textMuted }}> deny</span>
|
||||
</text>
|
||||
@@ -1307,21 +1314,9 @@ ToolRegistry.register<typeof WriteTool>({
|
||||
container: "block",
|
||||
render(props) {
|
||||
const { theme, syntax } = useTheme()
|
||||
const lines = createMemo(
|
||||
() => (typeof props.input.content === "string" ? props.input.content.split("\n") : []),
|
||||
[] as string[],
|
||||
)
|
||||
const code = createMemo(() => {
|
||||
if (!props.input.content) return ""
|
||||
const text = props.input.content
|
||||
return text
|
||||
})
|
||||
|
||||
const numbers = createMemo(() => {
|
||||
const pad = lines().length.toString().length
|
||||
return lines()
|
||||
.map((_, index) => index + 1)
|
||||
.map((x) => x.toString().padStart(pad, " "))
|
||||
return props.input.content
|
||||
})
|
||||
|
||||
const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [])
|
||||
@@ -1331,14 +1326,9 @@ ToolRegistry.register<typeof WriteTool>({
|
||||
<ToolTitle icon="←" fallback="Preparing write..." when={props.input.filePath}>
|
||||
Wrote {props.input.filePath}
|
||||
</ToolTitle>
|
||||
<box flexDirection="row">
|
||||
<box flexShrink={0}>
|
||||
<For each={numbers()}>{(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}</For>
|
||||
</box>
|
||||
<box paddingLeft={1} flexGrow={1}>
|
||||
<code fg={theme.text} filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
|
||||
</box>
|
||||
</box>
|
||||
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
|
||||
<code fg={theme.text} filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
|
||||
</line_number>
|
||||
<Show when={diagnostics().length}>
|
||||
<For each={diagnostics()}>
|
||||
{(diagnostic) => (
|
||||
@@ -1410,15 +1400,15 @@ ToolRegistry.register<typeof TaskTool>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolTitle icon="%" fallback="Delegating..." when={props.input.subagent_type ?? props.input.description}>
|
||||
Task [{props.input.subagent_type ?? "unknown"}] {props.input.description}
|
||||
<ToolTitle icon="◉" fallback="Delegating..." when={props.input.subagent_type ?? props.input.description}>
|
||||
{Locale.titlecase(props.input.subagent_type ?? "unknown")} Task "{props.input.description}"
|
||||
</ToolTitle>
|
||||
<Show when={props.metadata.summary?.length}>
|
||||
<box>
|
||||
<For each={props.metadata.summary ?? []}>
|
||||
{(task) => (
|
||||
<text style={{ fg: theme.textMuted }}>
|
||||
∟ {task.tool} {task.state.status === "completed" ? task.state.title : ""}
|
||||
∟ {Locale.titlecase(task.tool)} {task.state.status === "completed" ? task.state.title : ""}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
@@ -1445,6 +1435,34 @@ ToolRegistry.register<typeof WebFetchTool>({
|
||||
},
|
||||
})
|
||||
|
||||
ToolRegistry.register({
|
||||
name: "codesearch",
|
||||
container: "inline",
|
||||
render(props: ToolProps<any>) {
|
||||
const input = props.input as any
|
||||
const metadata = props.metadata as any
|
||||
return (
|
||||
<ToolTitle icon="◇" fallback="Searching code..." when={input.query}>
|
||||
Exa Code Search "{input.query}" <Show when={metadata.results}>({metadata.results} results)</Show>
|
||||
</ToolTitle>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
ToolRegistry.register({
|
||||
name: "websearch",
|
||||
container: "inline",
|
||||
render(props: ToolProps<any>) {
|
||||
const input = props.input as any
|
||||
const metadata = props.metadata as any
|
||||
return (
|
||||
<ToolTitle icon="◈" fallback="Searching web..." when={input.query}>
|
||||
Exa Web Search "{input.query}" <Show when={metadata.numResults}>({metadata.numResults} results)</Show>
|
||||
</ToolTitle>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
ToolRegistry.register<typeof EditTool>({
|
||||
name: "edit",
|
||||
container: "block",
|
||||
@@ -1452,79 +1470,17 @@ ToolRegistry.register<typeof EditTool>({
|
||||
const ctx = use()
|
||||
const { theme, syntax } = useTheme()
|
||||
|
||||
const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked"))
|
||||
|
||||
const diff = createMemo(() => {
|
||||
const diff = props.metadata.diff ?? props.permission["diff"]
|
||||
if (!diff) return null
|
||||
|
||||
try {
|
||||
const patches = parsePatch(diff)
|
||||
if (patches.length === 0) return null
|
||||
|
||||
const patch = patches[0]
|
||||
const oldLines: string[] = []
|
||||
const newLines: string[] = []
|
||||
|
||||
for (const hunk of patch.hunks) {
|
||||
let i = 0
|
||||
while (i < hunk.lines.length) {
|
||||
const line = hunk.lines[i]
|
||||
|
||||
if (line.startsWith("-")) {
|
||||
const removedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("-")) {
|
||||
removedLines.push("- " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
const addedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
|
||||
addedLines.push("+ " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
const maxLen = Math.max(removedLines.length, addedLines.length)
|
||||
for (let j = 0; j < maxLen; j++) {
|
||||
oldLines.push(removedLines[j] ?? "")
|
||||
newLines.push(addedLines[j] ?? "")
|
||||
}
|
||||
} else if (line.startsWith("+")) {
|
||||
const addedLines: string[] = []
|
||||
while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
|
||||
addedLines.push("+ " + hunk.lines[i].slice(1))
|
||||
i++
|
||||
}
|
||||
|
||||
for (const added of addedLines) {
|
||||
oldLines.push("")
|
||||
newLines.push(added)
|
||||
}
|
||||
} else {
|
||||
oldLines.push(" " + line.slice(1))
|
||||
newLines.push(" " + line.slice(1))
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oldContent: oldLines.join("\n"),
|
||||
newContent: newLines.join("\n"),
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const code = createMemo(() => {
|
||||
if (!props.metadata.diff) return ""
|
||||
const text = props.metadata.diff.split("\n").slice(5).join("\n")
|
||||
return text.trim()
|
||||
const view = createMemo(() => {
|
||||
const diffStyle = ctx.sync.data.config.tui?.diff_style
|
||||
if (diffStyle === "stacked") return "unified"
|
||||
// Default to "auto" behavior
|
||||
return ctx.width > 120 ? "split" : "unified"
|
||||
})
|
||||
|
||||
const ft = createMemo(() => filetype(props.input.filePath))
|
||||
|
||||
const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"])
|
||||
|
||||
const diagnostics = createMemo(() => {
|
||||
const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []
|
||||
return arr.filter((x) => x.severity === 1).slice(0, 3)
|
||||
@@ -1538,26 +1494,28 @@ ToolRegistry.register<typeof EditTool>({
|
||||
replaceAll: props.input.replaceAll,
|
||||
})}
|
||||
</ToolTitle>
|
||||
<Switch>
|
||||
<Match when={props.permission["diff"]}>
|
||||
<text fg={theme.text}>{props.permission["diff"]?.trim()}</text>
|
||||
</Match>
|
||||
<Match when={diff() && style() === "split"}>
|
||||
<box paddingLeft={1} flexDirection="row" gap={2}>
|
||||
<box flexGrow={1} flexBasis={0}>
|
||||
<code fg={theme.text} filetype={ft()} syntaxStyle={syntax()} content={diff()!.oldContent} />
|
||||
</box>
|
||||
<box flexGrow={1} flexBasis={0}>
|
||||
<code fg={theme.text} filetype={ft()} syntaxStyle={syntax()} content={diff()!.newContent} />
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={code()}>
|
||||
<box paddingLeft={1}>
|
||||
<code fg={theme.text} filetype={ft()} syntaxStyle={syntax()} content={code()} />
|
||||
</box>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={diffContent()}>
|
||||
<box paddingLeft={1}>
|
||||
<diff
|
||||
diff={diffContent()}
|
||||
view={view()}
|
||||
filetype={ft()}
|
||||
syntaxStyle={syntax()}
|
||||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode={ctx.diffWrapMode()}
|
||||
addedBg={theme.diffAddedBg}
|
||||
removedBg={theme.diffRemovedBg}
|
||||
contextBg={theme.diffContextBg}
|
||||
addedSignColor={theme.diffHighlightAdded}
|
||||
removedSignColor={theme.diffHighlightRemoved}
|
||||
lineNumberFg={theme.diffLineNumber}
|
||||
lineNumberBg={theme.diffContextBg}
|
||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={diagnostics().length}>
|
||||
<box>
|
||||
<For each={diagnostics()}>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createMemo, For, Show, Switch, Match, createSignal } from "solid-js"
|
||||
import { createMemo, For, Show, Switch, Match } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { Locale } from "@/util/locale"
|
||||
import path from "path"
|
||||
import type { AssistantMessage } from "@opencode-ai/sdk"
|
||||
import { Global } from "@/global"
|
||||
import { Installation } from "@/installation"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
import { useDirectory } from "../../context/directory"
|
||||
|
||||
export function Sidebar(props: { sessionID: string }) {
|
||||
const sync = useSync()
|
||||
@@ -13,10 +18,12 @@ export function Sidebar(props: { sessionID: string }) {
|
||||
const todo = createMemo(() => sync.data.todo[props.sessionID] ?? [])
|
||||
const messages = createMemo(() => sync.data.message[props.sessionID] ?? [])
|
||||
|
||||
const [mcpExpanded, setMcpExpanded] = createSignal(true)
|
||||
const [diffExpanded, setDiffExpanded] = createSignal(true)
|
||||
const [todoExpanded, setTodoExpanded] = createSignal(true)
|
||||
const [lspExpanded, setLspExpanded] = createSignal(true)
|
||||
const [expanded, setExpanded] = createStore({
|
||||
mcp: true,
|
||||
diff: true,
|
||||
todo: true,
|
||||
lsp: true,
|
||||
})
|
||||
|
||||
// Sort MCP servers alphabetically for consistent display order
|
||||
const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b)))
|
||||
@@ -41,87 +48,104 @@ export function Sidebar(props: { sessionID: string }) {
|
||||
}
|
||||
})
|
||||
|
||||
const keybind = useKeybind()
|
||||
const directory = useDirectory()
|
||||
|
||||
const hasProviders = createMemo(() =>
|
||||
sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)),
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={session()}>
|
||||
<scrollbox width={40}>
|
||||
<box flexShrink={0} gap={1} paddingRight={1}>
|
||||
<box>
|
||||
<text fg={theme.text}>
|
||||
<b>{session().title}</b>
|
||||
</text>
|
||||
<Show when={session().share?.url}>
|
||||
<text fg={theme.textMuted}>{session().share!.url}</text>
|
||||
</Show>
|
||||
</box>
|
||||
<box>
|
||||
<text fg={theme.text}>
|
||||
<b>Context</b>
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
|
||||
<text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
|
||||
<text fg={theme.textMuted}>{cost()} spent</text>
|
||||
</box>
|
||||
<Show when={mcpEntries().length > 0}>
|
||||
<box
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
width={42}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
>
|
||||
<scrollbox flexGrow={1}>
|
||||
<box flexShrink={0} gap={1} paddingRight={1}>
|
||||
<box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
onMouseDown={() => mcpEntries().length > 2 && setMcpExpanded(!mcpExpanded())}
|
||||
>
|
||||
<Show when={mcpEntries().length > 2}>
|
||||
<text fg={theme.text}>{mcpExpanded() ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>MCP</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={mcpEntries().length <= 2 || mcpExpanded()}>
|
||||
<For each={mcpEntries()}>
|
||||
{([key, item]) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text
|
||||
flexShrink={0}
|
||||
style={{
|
||||
fg: {
|
||||
connected: theme.success,
|
||||
failed: theme.error,
|
||||
disabled: theme.textMuted,
|
||||
}[item.status],
|
||||
}}
|
||||
>
|
||||
•
|
||||
</text>
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
{key}{" "}
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
<Switch>
|
||||
<Match when={item.status === "connected"}>Connected</Match>
|
||||
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
|
||||
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
|
||||
</Switch>
|
||||
</span>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
<text fg={theme.text}>
|
||||
<b>{session().title}</b>
|
||||
</text>
|
||||
<Show when={session().share?.url}>
|
||||
<text fg={theme.textMuted}>{session().share!.url}</text>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={sync.data.lsp.length > 0}>
|
||||
<box>
|
||||
<text fg={theme.text}>
|
||||
<b>Context</b>
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
|
||||
<text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
|
||||
<text fg={theme.textMuted}>{cost()} spent</text>
|
||||
</box>
|
||||
<Show when={mcpEntries().length > 0}>
|
||||
<box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
onMouseDown={() => mcpEntries().length > 2 && setExpanded("mcp", !expanded.mcp)}
|
||||
>
|
||||
<Show when={mcpEntries().length > 2}>
|
||||
<text fg={theme.text}>{expanded.mcp ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>MCP</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={mcpEntries().length <= 2 || expanded.mcp}>
|
||||
<For each={mcpEntries()}>
|
||||
{([key, item]) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text
|
||||
flexShrink={0}
|
||||
style={{
|
||||
fg: {
|
||||
connected: theme.success,
|
||||
failed: theme.error,
|
||||
disabled: theme.textMuted,
|
||||
}[item.status],
|
||||
}}
|
||||
>
|
||||
•
|
||||
</text>
|
||||
<text fg={theme.text} wrapMode="word">
|
||||
{key}{" "}
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
<Switch>
|
||||
<Match when={item.status === "connected"}>Connected</Match>
|
||||
<Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
|
||||
<Match when={item.status === "disabled"}>Disabled in configuration</Match>
|
||||
</Switch>
|
||||
</span>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
onMouseDown={() => sync.data.lsp.length > 2 && setLspExpanded(!lspExpanded())}
|
||||
onMouseDown={() => sync.data.lsp.length > 2 && setExpanded("lsp", !expanded.lsp)}
|
||||
>
|
||||
<Show when={sync.data.lsp.length > 2}>
|
||||
<text fg={theme.text}>{lspExpanded() ? "▼" : "▶"}</text>
|
||||
<text fg={theme.text}>{expanded.lsp ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>LSP</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={sync.data.lsp.length <= 2 || lspExpanded()}>
|
||||
<Show when={sync.data.lsp.length <= 2 || expanded.lsp}>
|
||||
<Show when={sync.data.lsp.length === 0}>
|
||||
<text fg={theme.textMuted}>LSPs will activate as files are read</text>
|
||||
</Show>
|
||||
<For each={sync.data.lsp}>
|
||||
{(item) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
@@ -144,78 +168,115 @@ export function Sidebar(props: { sessionID: string }) {
|
||||
</For>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={todo().length > 0}>
|
||||
<box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
onMouseDown={() => todo().length > 2 && setTodoExpanded(!todoExpanded())}
|
||||
>
|
||||
<Show when={todo().length > 2}>
|
||||
<text fg={theme.text}>{todoExpanded() ? "▼" : "▶"}</text>
|
||||
<Show when={todo().length > 0 && todo().some((t) => t.status !== "completed")}>
|
||||
<box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
onMouseDown={() => todo().length > 2 && setExpanded("todo", !expanded.todo)}
|
||||
>
|
||||
<Show when={todo().length > 2}>
|
||||
<text fg={theme.text}>{expanded.todo ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>Todo</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={todo().length <= 2 || expanded.todo}>
|
||||
<For each={todo()}>
|
||||
{(todo) => (
|
||||
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
|
||||
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>Todo</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={todo().length <= 2 || todoExpanded()}>
|
||||
<For each={todo()}>
|
||||
{(todo) => (
|
||||
<text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
|
||||
[{todo.status === "completed" ? "✓" : " "}] {todo.content}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={diff().length > 0}>
|
||||
<box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
onMouseDown={() => diff().length > 2 && setDiffExpanded(!diffExpanded())}
|
||||
>
|
||||
<Show when={diff().length > 2}>
|
||||
<text fg={theme.text}>{diffExpanded() ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>Modified Files</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={diff().length <= 2 || diffExpanded()}>
|
||||
<For each={diff() || []}>
|
||||
{(item) => {
|
||||
const file = createMemo(() => {
|
||||
const splits = item.file.split(path.sep).filter(Boolean)
|
||||
const last = splits.at(-1)!
|
||||
const rest = splits.slice(0, -1).join(path.sep)
|
||||
if (!rest) return last
|
||||
return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
|
||||
})
|
||||
return (
|
||||
<box flexDirection="row" gap={1} justifyContent="space-between">
|
||||
<text fg={theme.textMuted} wrapMode="char">
|
||||
{file()}
|
||||
</text>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<Show when={item.additions}>
|
||||
<text fg={theme.diffAdded}>+{item.additions}</text>
|
||||
</Show>
|
||||
<Show when={item.deletions}>
|
||||
<text fg={theme.diffRemoved}>-{item.deletions}</text>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={diff().length > 0}>
|
||||
<box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
onMouseDown={() => diff().length > 2 && setExpanded("diff", !expanded.diff)}
|
||||
>
|
||||
<Show when={diff().length > 2}>
|
||||
<text fg={theme.text}>{expanded.diff ? "▼" : "▶"}</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<b>Modified Files</b>
|
||||
</text>
|
||||
</box>
|
||||
<Show when={diff().length <= 2 || expanded.diff}>
|
||||
<For each={diff() || []}>
|
||||
{(item) => {
|
||||
const file = createMemo(() => {
|
||||
const splits = item.file.split(path.sep).filter(Boolean)
|
||||
const last = splits.at(-1)!
|
||||
const rest = splits.slice(0, -1).join(path.sep)
|
||||
if (!rest) return last
|
||||
return Locale.truncateMiddle(rest, 30 - last.length) + "/" + last
|
||||
})
|
||||
return (
|
||||
<box flexDirection="row" gap={1} justifyContent="space-between">
|
||||
<text fg={theme.textMuted} wrapMode="char">
|
||||
{file()}
|
||||
</text>
|
||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||
<Show when={item.additions}>
|
||||
<text fg={theme.diffAdded}>+{item.additions}</text>
|
||||
</Show>
|
||||
<Show when={item.deletions}>
|
||||
<text fg={theme.diffRemoved}>-{item.deletions}</text>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</scrollbox>
|
||||
|
||||
<box flexShrink={0} gap={1}>
|
||||
<Show when={!hasProviders()}>
|
||||
<box
|
||||
backgroundColor={theme.backgroundElement}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
>
|
||||
<text flexShrink={0}>⬖</text>
|
||||
<box flexGrow={1} gap={1}>
|
||||
<text>
|
||||
<b>Getting started</b>
|
||||
</text>
|
||||
<text fg={theme.textMuted}>OpenCode includes free models so you can start immediately.</text>
|
||||
<text fg={theme.textMuted}>
|
||||
Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
|
||||
</text>
|
||||
<box flexDirection="row" gap={1} justifyContent="space-between">
|
||||
<text>Connect provider</text>
|
||||
<text fg={theme.textMuted}>/connect</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.textMuted}>{directory()}</text>
|
||||
<text fg={theme.textMuted}>
|
||||
<span style={{ fg: theme.success }}>•</span> <b>Open</b>
|
||||
<span style={{ fg: theme.text }}>
|
||||
<b>Code</b>
|
||||
</span>{" "}
|
||||
<span>{Installation.VERSION}</span>
|
||||
</text>
|
||||
</box>
|
||||
</scrollbox>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,9 +55,6 @@ export function DialogPrompt(props: DialogPromptProps) {
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Locale } from "@/util/locale"
|
||||
|
||||
export interface DialogSelectProps<T> {
|
||||
title: string
|
||||
placeholder?: string
|
||||
options: DialogSelectOption<T>[]
|
||||
ref?: (ref: DialogSelectRef<T>) => void
|
||||
onMove?: (option: DialogSelectOption<T>) => void
|
||||
@@ -21,6 +22,7 @@ export interface DialogSelectProps<T> {
|
||||
keybind?: {
|
||||
keybind: Keybind.Info
|
||||
title: string
|
||||
disabled?: boolean
|
||||
onTrigger: (option: DialogSelectOption<T>) => void
|
||||
}[]
|
||||
current?: T
|
||||
@@ -150,6 +152,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
}
|
||||
|
||||
for (const item of props.keybind ?? []) {
|
||||
if (item.disabled) continue
|
||||
if (Keybind.match(item.keybind, keybind.parse(evt))) {
|
||||
const s = selected()
|
||||
if (s) {
|
||||
@@ -171,8 +174,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
}
|
||||
props.ref?.(ref)
|
||||
|
||||
const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled) ?? [])
|
||||
|
||||
return (
|
||||
<box gap={1}>
|
||||
<box gap={1} paddingBottom={1}>
|
||||
<box paddingLeft={4} paddingRight={4}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
@@ -195,7 +200,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
input = r
|
||||
setTimeout(() => input.focus(), 1)
|
||||
}}
|
||||
placeholder="Enter search term"
|
||||
placeholder={props.placeholder ?? "Search"}
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
@@ -253,18 +258,20 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
<box paddingRight={2} paddingLeft={4} flexDirection="row" paddingBottom={1} gap={1}>
|
||||
<For each={props.keybind ?? []}>
|
||||
{(item) => (
|
||||
<text>
|
||||
<span style={{ fg: theme.text }}>
|
||||
<b>{item.title}</b>{" "}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
<Show when={keybinds().length} fallback={<box flexShrink={0} />}>
|
||||
<box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
|
||||
<For each={keybinds()}>
|
||||
{(item) => (
|
||||
<text>
|
||||
<span style={{ fg: theme.text }}>
|
||||
<b>{item.title}</b>{" "}
|
||||
</span>
|
||||
<span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ interface AdvancedGradientOptions {
|
||||
defaultColor?: ColorInput
|
||||
direction?: "forward" | "backward" | "bidirectional"
|
||||
holdFrames?: { start?: number; end?: number }
|
||||
enableFading?: boolean
|
||||
minAlpha?: number
|
||||
}
|
||||
|
||||
interface ScannerState {
|
||||
@@ -137,13 +139,16 @@ function calculateColorIndex(
|
||||
}
|
||||
|
||||
function createKnightRiderTrail(options: AdvancedGradientOptions): ColorGenerator {
|
||||
const { colors, defaultColor } = options
|
||||
const { colors, defaultColor, enableFading = true, minAlpha = 0 } = options
|
||||
|
||||
// Use the provided defaultColor if it's an RGBA instance, otherwise convert/default
|
||||
// We use RGBA.fromHex for the fallback to ensure we have an RGBA object.
|
||||
// Note: If defaultColor is a string, we convert it once here.
|
||||
const defaultRgba = defaultColor instanceof RGBA ? defaultColor : RGBA.fromHex((defaultColor as string) || "#000000")
|
||||
|
||||
// Store the base alpha from the inactive factor
|
||||
const baseInactiveAlpha = defaultRgba.a
|
||||
|
||||
let cachedFrameIndex = -1
|
||||
let cachedState: ScannerState | null = null
|
||||
|
||||
@@ -160,22 +165,22 @@ function createKnightRiderTrail(options: AdvancedGradientOptions): ColorGenerato
|
||||
// Calculate global fade for inactive dots during hold or movement
|
||||
const { isHolding, holdProgress, holdTotal, movementProgress, movementTotal } = state
|
||||
|
||||
let alpha = 1.0
|
||||
if (isHolding && holdTotal > 0) {
|
||||
// Fade out linearly
|
||||
const progress = Math.min(holdProgress / holdTotal, 1)
|
||||
alpha = Math.max(0, 1 - progress)
|
||||
} else if (!isHolding && movementTotal > 0) {
|
||||
// Fade in linearly during movement
|
||||
const progress = Math.min(movementProgress / Math.max(1, movementTotal - 1), 1)
|
||||
alpha = progress
|
||||
let fadeFactor = 1.0
|
||||
if (enableFading) {
|
||||
if (isHolding && holdTotal > 0) {
|
||||
// Fade out linearly to minAlpha
|
||||
const progress = Math.min(holdProgress / holdTotal, 1)
|
||||
fadeFactor = Math.max(minAlpha, 1 - progress * (1 - minAlpha))
|
||||
} else if (!isHolding && movementTotal > 0) {
|
||||
// Fade in linearly from minAlpha during movement
|
||||
const progress = Math.min(movementProgress / Math.max(1, movementTotal - 1), 1)
|
||||
fadeFactor = minAlpha + progress * (1 - minAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
// Mutate the alpha of the default RGBA object
|
||||
// This assumes single-threaded, synchronous rendering per frame
|
||||
// where we can modify the state for the current frame.
|
||||
// Since this is run for every char in the frame, setting it repeatedly to the same value is fine.
|
||||
defaultRgba.a = alpha
|
||||
// Combine base inactive alpha with the fade factor
|
||||
// This ensures inactiveFactor is respected while still allowing fading animation
|
||||
defaultRgba.a = baseInactiveAlpha * fadeFactor
|
||||
|
||||
if (index === -1) {
|
||||
return defaultRgba
|
||||
@@ -186,10 +191,10 @@ function createKnightRiderTrail(options: AdvancedGradientOptions): ColorGenerato
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a gradient of tail colors from a single bright color
|
||||
* Derives a gradient of tail colors from a single bright color using alpha falloff
|
||||
* @param brightColor The brightest color (center/head of the scanner)
|
||||
* @param steps Number of gradient steps (default: 6)
|
||||
* @returns Array of RGBA colors from brightest to darkest
|
||||
* @returns Array of RGBA colors with alpha-based trail fade (background-independent)
|
||||
*/
|
||||
export function deriveTrailColors(brightColor: ColorInput, steps: number = 6): RGBA[] {
|
||||
const baseRgba = brightColor instanceof RGBA ? brightColor : RGBA.fromHex(brightColor as string)
|
||||
@@ -197,45 +202,45 @@ export function deriveTrailColors(brightColor: ColorInput, steps: number = 6): R
|
||||
const colors: RGBA[] = []
|
||||
|
||||
for (let i = 0; i < steps; i++) {
|
||||
// Progressive darkening:
|
||||
// i=0: 100% brightness (original color)
|
||||
// i=1: add slight bloom/glare (lighten)
|
||||
// i=2+: progressively darken
|
||||
let factor: number
|
||||
// Alpha-based falloff with optional bloom effect
|
||||
let alpha: number
|
||||
let brightnessFactor: number
|
||||
|
||||
if (i === 0) {
|
||||
factor = 1.0 // Original brightness
|
||||
// Lead position: full brightness and opacity
|
||||
alpha = 1.0
|
||||
brightnessFactor = 1.0
|
||||
} else if (i === 1) {
|
||||
factor = 1.2 // Slight bloom/glare effect
|
||||
// Slight bloom/glare effect: brighten color but reduce opacity slightly
|
||||
alpha = 0.9
|
||||
brightnessFactor = 1.15
|
||||
} else {
|
||||
// Exponential decay for natural-looking trail fade
|
||||
factor = Math.pow(0.6, i - 1)
|
||||
// Exponential alpha decay for natural-looking trail fade
|
||||
alpha = Math.pow(0.65, i - 1)
|
||||
brightnessFactor = 1.0
|
||||
}
|
||||
|
||||
const r = Math.min(1.0, baseRgba.r * factor)
|
||||
const g = Math.min(1.0, baseRgba.g * factor)
|
||||
const b = Math.min(1.0, baseRgba.b * factor)
|
||||
const r = Math.min(1.0, baseRgba.r * brightnessFactor)
|
||||
const g = Math.min(1.0, baseRgba.g * brightnessFactor)
|
||||
const b = Math.min(1.0, baseRgba.b * brightnessFactor)
|
||||
|
||||
colors.push(RGBA.fromValues(r, g, b, 1.0))
|
||||
colors.push(RGBA.fromValues(r, g, b, alpha))
|
||||
}
|
||||
|
||||
return colors
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the inactive/default color from a bright color
|
||||
* Derives the inactive/default color from a bright color using alpha
|
||||
* @param brightColor The brightest color (center/head of the scanner)
|
||||
* @param factor Brightness factor for inactive color (default: 0.2)
|
||||
* @returns A much darker version suitable for inactive dots
|
||||
* @param factor Alpha factor for inactive color (default: 0.2, range: 0-1)
|
||||
* @returns The same color with reduced alpha for background-independent dimming
|
||||
*/
|
||||
export function deriveInactiveColor(brightColor: ColorInput, factor: number = 0.2): RGBA {
|
||||
const baseRgba = brightColor instanceof RGBA ? brightColor : RGBA.fromHex(brightColor as string)
|
||||
|
||||
const r = baseRgba.r * factor
|
||||
const g = baseRgba.g * factor
|
||||
const b = baseRgba.b * factor
|
||||
|
||||
return RGBA.fromValues(r, g, b, 1.0)
|
||||
// Use the full color brightness but adjust alpha for background-independent dimming
|
||||
return RGBA.fromValues(baseRgba.r, baseRgba.g, baseRgba.b, factor)
|
||||
}
|
||||
|
||||
export type KnightRiderStyle = "blocks" | "diamonds"
|
||||
@@ -251,8 +256,12 @@ export interface KnightRiderOptions {
|
||||
/** Number of trail steps when using single color (default: 6) */
|
||||
trailSteps?: number
|
||||
defaultColor?: ColorInput
|
||||
/** Brightness factor for inactive color when using single color (default: 0.2) */
|
||||
/** Alpha factor for inactive color when using single color (default: 0.2, range: 0-1) */
|
||||
inactiveFactor?: number
|
||||
/** Enable fading of inactive dots during hold and movement (default: true) */
|
||||
enableFading?: boolean
|
||||
/** Minimum alpha value when fading (default: 0, range: 0-1) */
|
||||
minAlpha?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,6 +298,8 @@ export function createFrames(options: KnightRiderOptions = {}): string[] {
|
||||
defaultColor,
|
||||
direction: "bidirectional" as const,
|
||||
holdFrames: { start: holdStart, end: holdEnd },
|
||||
enableFading: options.enableFading,
|
||||
minAlpha: options.minAlpha,
|
||||
}
|
||||
|
||||
// Bidirectional cycle: Forward (width) + Hold End + Backward (width-1) + Hold Start
|
||||
@@ -349,6 +360,8 @@ export function createColors(options: KnightRiderOptions = {}): ColorGenerator {
|
||||
defaultColor,
|
||||
direction: "bidirectional" as const,
|
||||
holdFrames: { start: holdStart, end: holdEnd },
|
||||
enableFading: options.enableFading,
|
||||
minAlpha: options.minAlpha,
|
||||
}
|
||||
|
||||
return createKnightRiderTrail(trailOptions)
|
||||
|
||||
@@ -38,3 +38,18 @@ export function FormatError(input: unknown) {
|
||||
|
||||
if (UI.CancelledError.isInstance(input)) return ""
|
||||
}
|
||||
|
||||
export function FormatUnknownError(input: unknown): string {
|
||||
if (input instanceof Error) {
|
||||
return input.stack ?? `${input.name}: ${input.message}`
|
||||
}
|
||||
|
||||
if (typeof input === "object" && input !== null) {
|
||||
try {
|
||||
const json = JSON.stringify(input, null, 2)
|
||||
if (json && json !== "{}") return json
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return String(input)
|
||||
}
|
||||
|
||||
@@ -456,6 +456,10 @@ export namespace Config {
|
||||
})
|
||||
.optional()
|
||||
.describe("Scroll acceleration settings"),
|
||||
diff_style: z
|
||||
.enum(["auto", "stacked"])
|
||||
.optional()
|
||||
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
|
||||
})
|
||||
|
||||
export const Layout = z.enum(["auto", "stretch"]).meta({
|
||||
@@ -523,6 +527,7 @@ export namespace Config {
|
||||
plan: Agent.optional(),
|
||||
build: Agent.optional(),
|
||||
general: Agent.optional(),
|
||||
explore: Agent.optional(),
|
||||
})
|
||||
.catchall(Agent)
|
||||
.optional()
|
||||
@@ -540,6 +545,10 @@ export namespace Config {
|
||||
apiKey: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
|
||||
setCacheKey: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable promptCacheKey for this provider (default false)"),
|
||||
timeout: z
|
||||
.union([
|
||||
z
|
||||
|
||||
@@ -6,6 +6,7 @@ export namespace FileIgnore {
|
||||
"bower_components",
|
||||
".pnpm-store",
|
||||
"vendor",
|
||||
".npm",
|
||||
"dist",
|
||||
"build",
|
||||
"out",
|
||||
@@ -22,12 +23,21 @@ export namespace FileIgnore {
|
||||
".output",
|
||||
"desktop",
|
||||
".sst",
|
||||
".cache",
|
||||
".webkit-cache",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"mypy_cache",
|
||||
".history",
|
||||
".gradle",
|
||||
])
|
||||
|
||||
const FILES = [
|
||||
"**/*.swp",
|
||||
"**/*.swo",
|
||||
|
||||
"**/*.pyc",
|
||||
|
||||
// OS
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import z from "zod"
|
||||
import { Bus } from "../bus"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Log } from "../util/log"
|
||||
import { FileIgnore } from "./ignore"
|
||||
@@ -8,6 +7,7 @@ import { Config } from "../config/config"
|
||||
// @ts-ignore
|
||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import type ParcelWatcher from "@parcel/watcher"
|
||||
|
||||
export namespace FileWatcher {
|
||||
const log = Log.create({ service: "file.watcher" })
|
||||
@@ -44,32 +44,44 @@ export namespace FileWatcher {
|
||||
return {}
|
||||
}
|
||||
log.info("watcher backend", { platform: process.platform, backend })
|
||||
const sub = await watcher().subscribe(
|
||||
Instance.directory,
|
||||
(err, evts) => {
|
||||
if (err) return
|
||||
for (const evt of evts) {
|
||||
log.info("event", evt)
|
||||
if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
|
||||
if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
|
||||
if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
|
||||
}
|
||||
},
|
||||
{
|
||||
ignore: [...FileIgnore.PATTERNS, ...(cfg.watcher?.ignore ?? [])],
|
||||
const subscribe: ParcelWatcher.SubscribeCallback = (err, evts) => {
|
||||
if (err) return
|
||||
for (const evt of evts) {
|
||||
if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
|
||||
if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
|
||||
if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
|
||||
}
|
||||
}
|
||||
|
||||
const subs = []
|
||||
const cfgIgnores = cfg.watcher?.ignore ?? []
|
||||
|
||||
subs.push(
|
||||
await watcher().subscribe(Instance.directory, subscribe, {
|
||||
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
|
||||
backend,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return { sub }
|
||||
|
||||
const vcsDir = Instance.project.vcsDir
|
||||
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
||||
subs.push(
|
||||
await watcher().subscribe(vcsDir, subscribe, {
|
||||
ignore: ["hooks", "info", "logs", "objects", "refs", "worktrees", "modules", "lfs"],
|
||||
backend,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return { subs }
|
||||
},
|
||||
async (state) => {
|
||||
if (!state.sub) return
|
||||
await state.sub?.unsubscribe()
|
||||
if (!state.subs) return
|
||||
await Promise.all(state.subs.map((sub) => sub?.unsubscribe()))
|
||||
},
|
||||
)
|
||||
|
||||
export function init() {
|
||||
if (!Flag.OPENCODE_EXPERIMENTAL_WATCHER) return
|
||||
state()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ export namespace Flag {
|
||||
// Experimental
|
||||
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
||||
export const OPENCODE_EXPERIMENTAL_WATCHER = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WATCHER")
|
||||
export const OPENCODE_EXPERIMENTAL_EXA = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
|
||||
@@ -246,3 +246,12 @@ export const htmlbeautifier: Info = {
|
||||
return Bun.which("htmlbeautifier") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const dart: Info = {
|
||||
name: "dart",
|
||||
command: ["dart", "format", "$FILE"],
|
||||
extensions: [".dart"],
|
||||
async enabled() {
|
||||
return Bun.which("dart") !== null
|
||||
},
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ await Promise.all([
|
||||
fs.mkdir(Global.Path.bin, { recursive: true }),
|
||||
])
|
||||
|
||||
const CACHE_VERSION = "11"
|
||||
const CACHE_VERSION = "12"
|
||||
|
||||
const version = await Bun.file(path.join(Global.Path.cache, "version"))
|
||||
.text()
|
||||
|
||||
@@ -89,6 +89,7 @@ export namespace LSPServer {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||
async spawn(root) {
|
||||
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
|
||||
log.info("typescript server", { tsserver })
|
||||
if (!tsserver) return
|
||||
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
||||
cwd: root,
|
||||
@@ -1165,4 +1166,22 @@ export namespace LSPServer {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const Dart: Info = {
|
||||
id: "dart",
|
||||
extensions: [".dart"],
|
||||
root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
|
||||
async spawn(root) {
|
||||
const dart = Bun.which("dart")
|
||||
if (!dart) {
|
||||
log.info("dart not found, please install dart first")
|
||||
return
|
||||
}
|
||||
return {
|
||||
process: spawn(dart, ["language-server", "--lsp"], {
|
||||
cwd: root,
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export namespace Plugin {
|
||||
const plugins = [...(config.plugin ?? [])]
|
||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||
plugins.push("opencode-copilot-auth@0.0.7")
|
||||
plugins.push("opencode-anthropic-auth@0.0.2")
|
||||
plugins.push("opencode-anthropic-auth@0.0.3")
|
||||
}
|
||||
for (let plugin of plugins) {
|
||||
log.info("loading plugin", { path: plugin })
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { File } from "../file"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Project } from "./project"
|
||||
import { Bus } from "../bus"
|
||||
import { Command } from "../command"
|
||||
import { Instance } from "./instance"
|
||||
import { Vcs } from "./vcs"
|
||||
import { Log } from "@/util/log"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function InstanceBootstrap() {
|
||||
await LSP.init()
|
||||
FileWatcher.init()
|
||||
File.init()
|
||||
Vcs.init()
|
||||
|
||||
Bus.subscribe(Command.Event.Executed, async (payload) => {
|
||||
if (payload.properties.name === Command.Default.INIT) {
|
||||
|
||||
@@ -12,6 +12,7 @@ export namespace Project {
|
||||
.object({
|
||||
id: z.string(),
|
||||
worktree: z.string(),
|
||||
vcsDir: z.string().optional(),
|
||||
vcs: z.literal("git").optional(),
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
@@ -74,15 +75,22 @@ export namespace Project {
|
||||
await Storage.write<Info>(["project", "global"], project)
|
||||
return project
|
||||
}
|
||||
worktree = await $`git rev-parse --path-format=absolute --show-toplevel`
|
||||
worktree = await $`git rev-parse --show-toplevel`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
.then((x) => path.resolve(worktree, x.trim()))
|
||||
const vcsDir = await $`git rev-parse --git-dir`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(worktree)
|
||||
.text()
|
||||
.then((x) => path.resolve(worktree, x.trim()))
|
||||
const project: Info = {
|
||||
id,
|
||||
worktree,
|
||||
vcsDir,
|
||||
vcs: "git",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
|
||||
77
packages/opencode/src/project/vcs.ts
Normal file
77
packages/opencode/src/project/vcs.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { $ } from "bun"
|
||||
import path from "path"
|
||||
import z from "zod"
|
||||
import { Log } from "@/util/log"
|
||||
import { Bus } from "@/bus"
|
||||
import { Instance } from "./instance"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
|
||||
const log = Log.create({ service: "vcs" })
|
||||
|
||||
export namespace Vcs {
|
||||
export const Event = {
|
||||
BranchUpdated: Bus.event(
|
||||
"vcs.branch.updated",
|
||||
z.object({
|
||||
branch: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
branch: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "VcsInfo",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
async function currentBranch() {
|
||||
return $`git rev-parse --abbrev-ref HEAD`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(Instance.worktree)
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const vcsDir = Instance.project.vcsDir
|
||||
if (Instance.project.vcs !== "git" || !vcsDir) {
|
||||
return { branch: async () => undefined, unsubscribe: undefined }
|
||||
}
|
||||
let current = await currentBranch()
|
||||
log.info("initialized", { branch: current })
|
||||
|
||||
const head = path.join(vcsDir, "HEAD")
|
||||
const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, async (evt) => {
|
||||
if (evt.properties.file !== head) return
|
||||
const next = await currentBranch()
|
||||
if (next !== current) {
|
||||
log.info("branch changed", { from: current, to: next })
|
||||
current = next
|
||||
Bus.publish(Event.BranchUpdated, { branch: next })
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
branch: async () => current,
|
||||
unsubscribe,
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
state.unsubscribe?.()
|
||||
},
|
||||
)
|
||||
|
||||
export async function init() {
|
||||
return state()
|
||||
}
|
||||
|
||||
export async function branch() {
|
||||
return await state().then((s) => s.branch())
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
|
||||
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
@@ -37,6 +38,8 @@ export namespace Provider {
|
||||
"@ai-sdk/openai": createOpenAI,
|
||||
"@ai-sdk/openai-compatible": createOpenAICompatible,
|
||||
"@openrouter/ai-sdk-provider": createOpenRouter,
|
||||
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
|
||||
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
||||
}
|
||||
|
||||
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
|
||||
@@ -87,6 +90,30 @@ export namespace Provider {
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
"github-copilot": async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
if (modelID.includes("gpt-5")) {
|
||||
return sdk.responses(modelID)
|
||||
}
|
||||
return sdk.chat(modelID)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
"github-copilot-enterprise": async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
if (modelID.includes("gpt-5")) {
|
||||
return sdk.responses(modelID)
|
||||
}
|
||||
return sdk.chat(modelID)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
azure: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
@@ -130,6 +157,11 @@ export namespace Provider {
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
},
|
||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
||||
// Skip region prefixing if model already has global prefix
|
||||
if (modelID.startsWith("global.")) {
|
||||
return sdk.languageModel(modelID)
|
||||
}
|
||||
|
||||
let regionPrefix = region.split("-")[0]
|
||||
|
||||
switch (regionPrefix) {
|
||||
@@ -423,15 +455,6 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
|
||||
// load custom
|
||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
||||
}
|
||||
}
|
||||
|
||||
for (const plugin of await Plugin.list()) {
|
||||
if (!plugin.auth) continue
|
||||
const providerID = plugin.auth.provider
|
||||
@@ -473,6 +496,14 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
|
||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(providerID, result.options ?? {}, "custom", result.getModel)
|
||||
}
|
||||
}
|
||||
|
||||
// load config
|
||||
for (const [providerID, provider] of configProviders) {
|
||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||
@@ -484,6 +515,10 @@ export namespace Provider {
|
||||
continue
|
||||
}
|
||||
|
||||
if (providerID === "github-copilot") {
|
||||
provider.info.npm = "@ai-sdk/github-copilot"
|
||||
}
|
||||
|
||||
const configProvider = config.provider?.[providerID]
|
||||
const filteredModels = Object.fromEntries(
|
||||
Object.entries(provider.info.models)
|
||||
@@ -672,6 +707,21 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
|
||||
export async function closest(providerID: string, query: string[]) {
|
||||
const s = await state()
|
||||
const provider = s.providers[providerID]
|
||||
if (!provider) return undefined
|
||||
for (const item of query) {
|
||||
for (const modelID of Object.keys(provider.info.models)) {
|
||||
if (modelID.includes(item))
|
||||
return {
|
||||
providerID,
|
||||
modelID,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSmallModel(providerID: string) {
|
||||
const cfg = await Config.get()
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
This is a temporary package used primarily for github copilot compatibility.
|
||||
|
||||
Avoid making changes to these files unless you want to only affect Copilot provider.
|
||||
|
||||
Also this should ONLY be used for Copilot provider.
|
||||
@@ -0,0 +1,2 @@
|
||||
export { createOpenaiCompatible, openaiCompatible } from "./openai-compatible-provider"
|
||||
export type { OpenaiCompatibleProvider, OpenaiCompatibleProviderSettings } from "./openai-compatible-provider"
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { LanguageModelV2 } from "@ai-sdk/provider"
|
||||
import { OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible"
|
||||
import { type FetchFunction, withoutTrailingSlash, withUserAgentSuffix } from "@ai-sdk/provider-utils"
|
||||
import { OpenAIResponsesLanguageModel } from "./responses/openai-responses-language-model"
|
||||
|
||||
// Import the version or define it
|
||||
const VERSION = "0.1.0"
|
||||
|
||||
export type OpenaiCompatibleModelId = string
|
||||
|
||||
export interface OpenaiCompatibleProviderSettings {
|
||||
/**
|
||||
* API key for authenticating requests.
|
||||
*/
|
||||
apiKey?: string
|
||||
|
||||
/**
|
||||
* Base URL for the OpenAI Compatible API calls.
|
||||
*/
|
||||
baseURL?: string
|
||||
|
||||
/**
|
||||
* Name of the provider.
|
||||
*/
|
||||
name?: string
|
||||
|
||||
/**
|
||||
* Custom headers to include in the requests.
|
||||
*/
|
||||
headers?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Custom fetch implementation.
|
||||
*/
|
||||
fetch?: FetchFunction
|
||||
}
|
||||
|
||||
export interface OpenaiCompatibleProvider {
|
||||
(modelId: OpenaiCompatibleModelId): LanguageModelV2
|
||||
chat(modelId: OpenaiCompatibleModelId): LanguageModelV2
|
||||
responses(modelId: OpenaiCompatibleModelId): LanguageModelV2
|
||||
languageModel(modelId: OpenaiCompatibleModelId): LanguageModelV2
|
||||
|
||||
// embeddingModel(modelId: any): EmbeddingModelV2
|
||||
|
||||
// imageModel(modelId: any): ImageModelV2
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OpenAI Compatible provider instance.
|
||||
*/
|
||||
export function createOpenaiCompatible(options: OpenaiCompatibleProviderSettings = {}): OpenaiCompatibleProvider {
|
||||
const baseURL = withoutTrailingSlash(options.baseURL ?? "https://api.openai.com/v1")
|
||||
|
||||
if (!baseURL) {
|
||||
throw new Error("baseURL is required")
|
||||
}
|
||||
|
||||
// Merge headers: defaults first, then user overrides
|
||||
const headers = {
|
||||
// Default OpenAI Compatible headers (can be overridden by user)
|
||||
...(options.apiKey && { Authorization: `Bearer ${options.apiKey}` }),
|
||||
...options.headers,
|
||||
}
|
||||
|
||||
const getHeaders = () => withUserAgentSuffix(headers, `ai-sdk/openai-compatible/${VERSION}`)
|
||||
|
||||
const createChatModel = (modelId: OpenaiCompatibleModelId) => {
|
||||
return new OpenAICompatibleChatLanguageModel(modelId, {
|
||||
provider: `${options.name ?? "openai-compatible"}.chat`,
|
||||
headers: getHeaders,
|
||||
url: ({ path }) => `${baseURL}${path}`,
|
||||
fetch: options.fetch,
|
||||
})
|
||||
}
|
||||
|
||||
const createResponsesModel = (modelId: OpenaiCompatibleModelId) => {
|
||||
return new OpenAIResponsesLanguageModel(modelId, {
|
||||
provider: `${options.name ?? "openai-compatible"}.responses`,
|
||||
headers: getHeaders,
|
||||
url: ({ path }) => `${baseURL}${path}`,
|
||||
fetch: options.fetch,
|
||||
})
|
||||
}
|
||||
|
||||
const createLanguageModel = (modelId: OpenaiCompatibleModelId) => createChatModel(modelId)
|
||||
|
||||
const provider = function (modelId: OpenaiCompatibleModelId) {
|
||||
return createChatModel(modelId)
|
||||
}
|
||||
|
||||
provider.languageModel = createLanguageModel
|
||||
provider.chat = createChatModel
|
||||
provider.responses = createResponsesModel
|
||||
|
||||
return provider as OpenaiCompatibleProvider
|
||||
}
|
||||
|
||||
// Default OpenAI Compatible provider instance
|
||||
export const openaiCompatible = createOpenaiCompatible()
|
||||
@@ -0,0 +1,303 @@
|
||||
import {
|
||||
type LanguageModelV2CallWarning,
|
||||
type LanguageModelV2Prompt,
|
||||
type LanguageModelV2ToolCallPart,
|
||||
UnsupportedFunctionalityError,
|
||||
} from "@ai-sdk/provider"
|
||||
import { convertToBase64, parseProviderOptions } from "@ai-sdk/provider-utils"
|
||||
import { z } from "zod/v4"
|
||||
import type { OpenAIResponsesInput, OpenAIResponsesReasoning } from "./openai-responses-api-types"
|
||||
import { localShellInputSchema, localShellOutputSchema } from "./tool/local-shell"
|
||||
|
||||
/**
|
||||
* Check if a string is a file ID based on the given prefixes
|
||||
* Returns false if prefixes is undefined (disables file ID detection)
|
||||
*/
|
||||
function isFileId(data: string, prefixes?: readonly string[]): boolean {
|
||||
if (!prefixes) return false
|
||||
return prefixes.some((prefix) => data.startsWith(prefix))
|
||||
}
|
||||
|
||||
export async function convertToOpenAIResponsesInput({
|
||||
prompt,
|
||||
systemMessageMode,
|
||||
fileIdPrefixes,
|
||||
store,
|
||||
hasLocalShellTool = false,
|
||||
}: {
|
||||
prompt: LanguageModelV2Prompt
|
||||
systemMessageMode: "system" | "developer" | "remove"
|
||||
fileIdPrefixes?: readonly string[]
|
||||
store: boolean
|
||||
hasLocalShellTool?: boolean
|
||||
}): Promise<{
|
||||
input: OpenAIResponsesInput
|
||||
warnings: Array<LanguageModelV2CallWarning>
|
||||
}> {
|
||||
const input: OpenAIResponsesInput = []
|
||||
const warnings: Array<LanguageModelV2CallWarning> = []
|
||||
|
||||
for (const { role, content } of prompt) {
|
||||
switch (role) {
|
||||
case "system": {
|
||||
switch (systemMessageMode) {
|
||||
case "system": {
|
||||
input.push({ role: "system", content })
|
||||
break
|
||||
}
|
||||
case "developer": {
|
||||
input.push({ role: "developer", content })
|
||||
break
|
||||
}
|
||||
case "remove": {
|
||||
warnings.push({
|
||||
type: "other",
|
||||
message: "system messages are removed for this model",
|
||||
})
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = systemMessageMode
|
||||
throw new Error(`Unsupported system message mode: ${_exhaustiveCheck}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "user": {
|
||||
input.push({
|
||||
role: "user",
|
||||
content: content.map((part, index) => {
|
||||
switch (part.type) {
|
||||
case "text": {
|
||||
return { type: "input_text", text: part.text }
|
||||
}
|
||||
case "file": {
|
||||
if (part.mediaType.startsWith("image/")) {
|
||||
const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType
|
||||
|
||||
return {
|
||||
type: "input_image",
|
||||
...(part.data instanceof URL
|
||||
? { image_url: part.data.toString() }
|
||||
: typeof part.data === "string" && isFileId(part.data, fileIdPrefixes)
|
||||
? { file_id: part.data }
|
||||
: {
|
||||
image_url: `data:${mediaType};base64,${convertToBase64(part.data)}`,
|
||||
}),
|
||||
detail: part.providerOptions?.openai?.imageDetail,
|
||||
}
|
||||
} else if (part.mediaType === "application/pdf") {
|
||||
if (part.data instanceof URL) {
|
||||
return {
|
||||
type: "input_file",
|
||||
file_url: part.data.toString(),
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "input_file",
|
||||
...(typeof part.data === "string" && isFileId(part.data, fileIdPrefixes)
|
||||
? { file_id: part.data }
|
||||
: {
|
||||
filename: part.filename ?? `part-${index}.pdf`,
|
||||
file_data: `data:application/pdf;base64,${convertToBase64(part.data)}`,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedFunctionalityError({
|
||||
functionality: `file part media type ${part.mediaType}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "assistant": {
|
||||
const reasoningMessages: Record<string, OpenAIResponsesReasoning> = {}
|
||||
const toolCallParts: Record<string, LanguageModelV2ToolCallPart> = {}
|
||||
|
||||
for (const part of content) {
|
||||
switch (part.type) {
|
||||
case "text": {
|
||||
input.push({
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: part.text }],
|
||||
id: (part.providerOptions?.openai?.itemId as string) ?? undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "tool-call": {
|
||||
toolCallParts[part.toolCallId] = part
|
||||
|
||||
if (part.providerExecuted) {
|
||||
break
|
||||
}
|
||||
|
||||
if (hasLocalShellTool && part.toolName === "local_shell") {
|
||||
const parsedInput = localShellInputSchema.parse(part.input)
|
||||
input.push({
|
||||
type: "local_shell_call",
|
||||
call_id: part.toolCallId,
|
||||
id: (part.providerOptions?.openai?.itemId as string) ?? undefined,
|
||||
action: {
|
||||
type: "exec",
|
||||
command: parsedInput.action.command,
|
||||
timeout_ms: parsedInput.action.timeoutMs,
|
||||
user: parsedInput.action.user,
|
||||
working_directory: parsedInput.action.workingDirectory,
|
||||
env: parsedInput.action.env,
|
||||
},
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
input.push({
|
||||
type: "function_call",
|
||||
call_id: part.toolCallId,
|
||||
name: part.toolName,
|
||||
arguments: JSON.stringify(part.input),
|
||||
id: (part.providerOptions?.openai?.itemId as string) ?? undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// assistant tool result parts are from provider-executed tools:
|
||||
case "tool-result": {
|
||||
if (store) {
|
||||
// use item references to refer to tool results from built-in tools
|
||||
input.push({ type: "item_reference", id: part.toolCallId })
|
||||
} else {
|
||||
warnings.push({
|
||||
type: "other",
|
||||
message: `Results for OpenAI tool ${part.toolName} are not sent to the API when store is false`,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "reasoning": {
|
||||
const providerOptions = await parseProviderOptions({
|
||||
provider: "openai",
|
||||
providerOptions: part.providerOptions,
|
||||
schema: openaiResponsesReasoningProviderOptionsSchema,
|
||||
})
|
||||
|
||||
const reasoningId = providerOptions?.itemId
|
||||
|
||||
if (reasoningId != null) {
|
||||
const reasoningMessage = reasoningMessages[reasoningId]
|
||||
|
||||
if (store) {
|
||||
if (reasoningMessage === undefined) {
|
||||
// use item references to refer to reasoning (single reference)
|
||||
input.push({ type: "item_reference", id: reasoningId })
|
||||
|
||||
// store unused reasoning message to mark id as used
|
||||
reasoningMessages[reasoningId] = {
|
||||
type: "reasoning",
|
||||
id: reasoningId,
|
||||
summary: [],
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const summaryParts: Array<{
|
||||
type: "summary_text"
|
||||
text: string
|
||||
}> = []
|
||||
|
||||
if (part.text.length > 0) {
|
||||
summaryParts.push({
|
||||
type: "summary_text",
|
||||
text: part.text,
|
||||
})
|
||||
} else if (reasoningMessage !== undefined) {
|
||||
warnings.push({
|
||||
type: "other",
|
||||
message: `Cannot append empty reasoning part to existing reasoning sequence. Skipping reasoning part: ${JSON.stringify(part)}.`,
|
||||
})
|
||||
}
|
||||
|
||||
if (reasoningMessage === undefined) {
|
||||
reasoningMessages[reasoningId] = {
|
||||
type: "reasoning",
|
||||
id: reasoningId,
|
||||
encrypted_content: providerOptions?.reasoningEncryptedContent,
|
||||
summary: summaryParts,
|
||||
}
|
||||
input.push(reasoningMessages[reasoningId])
|
||||
} else {
|
||||
reasoningMessage.summary.push(...summaryParts)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warnings.push({
|
||||
type: "other",
|
||||
message: `Non-OpenAI reasoning parts are not supported. Skipping reasoning part: ${JSON.stringify(part)}.`,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "tool": {
|
||||
for (const part of content) {
|
||||
const output = part.output
|
||||
|
||||
if (hasLocalShellTool && part.toolName === "local_shell" && output.type === "json") {
|
||||
input.push({
|
||||
type: "local_shell_call_output",
|
||||
call_id: part.toolCallId,
|
||||
output: localShellOutputSchema.parse(output.value).output,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
let contentValue: string
|
||||
switch (output.type) {
|
||||
case "text":
|
||||
case "error-text":
|
||||
contentValue = output.value
|
||||
break
|
||||
case "content":
|
||||
case "json":
|
||||
case "error-json":
|
||||
contentValue = JSON.stringify(output.value)
|
||||
break
|
||||
}
|
||||
|
||||
input.push({
|
||||
type: "function_call_output",
|
||||
call_id: part.toolCallId,
|
||||
output: contentValue,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
const _exhaustiveCheck: never = role
|
||||
throw new Error(`Unsupported role: ${_exhaustiveCheck}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { input, warnings }
|
||||
}
|
||||
|
||||
const openaiResponsesReasoningProviderOptionsSchema = z.object({
|
||||
itemId: z.string().nullish(),
|
||||
reasoningEncryptedContent: z.string().nullish(),
|
||||
})
|
||||
|
||||
export type OpenAIResponsesReasoningProviderOptions = z.infer<typeof openaiResponsesReasoningProviderOptionsSchema>
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { LanguageModelV2FinishReason } from "@ai-sdk/provider"
|
||||
|
||||
export function mapOpenAIResponseFinishReason({
|
||||
finishReason,
|
||||
hasFunctionCall,
|
||||
}: {
|
||||
finishReason: string | null | undefined
|
||||
// flag that checks if there have been client-side tool calls (not executed by openai)
|
||||
hasFunctionCall: boolean
|
||||
}): LanguageModelV2FinishReason {
|
||||
switch (finishReason) {
|
||||
case undefined:
|
||||
case null:
|
||||
return hasFunctionCall ? "tool-calls" : "stop"
|
||||
case "max_output_tokens":
|
||||
return "length"
|
||||
case "content_filter":
|
||||
return "content-filter"
|
||||
default:
|
||||
return hasFunctionCall ? "tool-calls" : "unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { FetchFunction } from "@ai-sdk/provider-utils"
|
||||
|
||||
export type OpenAIConfig = {
|
||||
provider: string
|
||||
url: (options: { modelId: string; path: string }) => string
|
||||
headers: () => Record<string, string | undefined>
|
||||
fetch?: FetchFunction
|
||||
generateId?: () => string
|
||||
/**
|
||||
* File ID prefixes used to identify file IDs in Responses API.
|
||||
* When undefined, all file data is treated as base64 content.
|
||||
*
|
||||
* Examples:
|
||||
* - OpenAI: ['file-'] for IDs like 'file-abc123'
|
||||
* - Azure OpenAI: ['assistant-'] for IDs like 'assistant-abc123'
|
||||
*/
|
||||
fileIdPrefixes?: readonly string[]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod/v4"
|
||||
import { createJsonErrorResponseHandler } from "@ai-sdk/provider-utils"
|
||||
|
||||
export const openaiErrorDataSchema = z.object({
|
||||
error: z.object({
|
||||
message: z.string(),
|
||||
|
||||
// The additional information below is handled loosely to support
|
||||
// OpenAI-compatible providers that have slightly different error
|
||||
// responses:
|
||||
type: z.string().nullish(),
|
||||
param: z.any().nullish(),
|
||||
code: z.union([z.string(), z.number()]).nullish(),
|
||||
}),
|
||||
})
|
||||
|
||||
export type OpenAIErrorData = z.infer<typeof openaiErrorDataSchema>
|
||||
|
||||
export const openaiFailedResponseHandler: any = createJsonErrorResponseHandler({
|
||||
errorSchema: openaiErrorDataSchema,
|
||||
errorToMessage: (data) => data.error.message,
|
||||
})
|
||||
@@ -0,0 +1,207 @@
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
|
||||
export type OpenAIResponsesInput = Array<OpenAIResponsesInputItem>
|
||||
|
||||
export type OpenAIResponsesInputItem =
|
||||
| OpenAIResponsesSystemMessage
|
||||
| OpenAIResponsesUserMessage
|
||||
| OpenAIResponsesAssistantMessage
|
||||
| OpenAIResponsesFunctionCall
|
||||
| OpenAIResponsesFunctionCallOutput
|
||||
| OpenAIResponsesComputerCall
|
||||
| OpenAIResponsesLocalShellCall
|
||||
| OpenAIResponsesLocalShellCallOutput
|
||||
| OpenAIResponsesReasoning
|
||||
| OpenAIResponsesItemReference
|
||||
|
||||
export type OpenAIResponsesIncludeValue =
|
||||
| "web_search_call.action.sources"
|
||||
| "code_interpreter_call.outputs"
|
||||
| "computer_call_output.output.image_url"
|
||||
| "file_search_call.results"
|
||||
| "message.input_image.image_url"
|
||||
| "message.output_text.logprobs"
|
||||
| "reasoning.encrypted_content"
|
||||
|
||||
export type OpenAIResponsesIncludeOptions = Array<OpenAIResponsesIncludeValue> | undefined | null
|
||||
|
||||
export type OpenAIResponsesSystemMessage = {
|
||||
role: "system" | "developer"
|
||||
content: string
|
||||
}
|
||||
|
||||
export type OpenAIResponsesUserMessage = {
|
||||
role: "user"
|
||||
content: Array<
|
||||
| { type: "input_text"; text: string }
|
||||
| { type: "input_image"; image_url: string }
|
||||
| { type: "input_image"; file_id: string }
|
||||
| { type: "input_file"; file_url: string }
|
||||
| { type: "input_file"; filename: string; file_data: string }
|
||||
| { type: "input_file"; file_id: string }
|
||||
>
|
||||
}
|
||||
|
||||
export type OpenAIResponsesAssistantMessage = {
|
||||
role: "assistant"
|
||||
content: Array<{ type: "output_text"; text: string }>
|
||||
id?: string
|
||||
}
|
||||
|
||||
export type OpenAIResponsesFunctionCall = {
|
||||
type: "function_call"
|
||||
call_id: string
|
||||
name: string
|
||||
arguments: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
export type OpenAIResponsesFunctionCallOutput = {
|
||||
type: "function_call_output"
|
||||
call_id: string
|
||||
output: string
|
||||
}
|
||||
|
||||
export type OpenAIResponsesComputerCall = {
|
||||
type: "computer_call"
|
||||
id: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export type OpenAIResponsesLocalShellCall = {
|
||||
type: "local_shell_call"
|
||||
id: string
|
||||
call_id: string
|
||||
action: {
|
||||
type: "exec"
|
||||
command: string[]
|
||||
timeout_ms?: number
|
||||
user?: string
|
||||
working_directory?: string
|
||||
env?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenAIResponsesLocalShellCallOutput = {
|
||||
type: "local_shell_call_output"
|
||||
call_id: string
|
||||
output: string
|
||||
}
|
||||
|
||||
export type OpenAIResponsesItemReference = {
|
||||
type: "item_reference"
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter used to compare a specified attribute key to a given value using a defined comparison operation.
|
||||
*/
|
||||
export type OpenAIResponsesFileSearchToolComparisonFilter = {
|
||||
/**
|
||||
* The key to compare against the value.
|
||||
*/
|
||||
key: string
|
||||
|
||||
/**
|
||||
* Specifies the comparison operator: eq, ne, gt, gte, lt, lte.
|
||||
*/
|
||||
type: "eq" | "ne" | "gt" | "gte" | "lt" | "lte"
|
||||
|
||||
/**
|
||||
* The value to compare against the attribute key; supports string, number, or boolean types.
|
||||
*/
|
||||
value: string | number | boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine multiple filters using and or or.
|
||||
*/
|
||||
export type OpenAIResponsesFileSearchToolCompoundFilter = {
|
||||
/**
|
||||
* Type of operation: and or or.
|
||||
*/
|
||||
type: "and" | "or"
|
||||
|
||||
/**
|
||||
* Array of filters to combine. Items can be ComparisonFilter or CompoundFilter.
|
||||
*/
|
||||
filters: Array<OpenAIResponsesFileSearchToolComparisonFilter | OpenAIResponsesFileSearchToolCompoundFilter>
|
||||
}
|
||||
|
||||
export type OpenAIResponsesTool =
|
||||
| {
|
||||
type: "function"
|
||||
name: string
|
||||
description: string | undefined
|
||||
parameters: JSONSchema7
|
||||
strict: boolean | undefined
|
||||
}
|
||||
| {
|
||||
type: "web_search"
|
||||
filters: { allowed_domains: string[] | undefined } | undefined
|
||||
search_context_size: "low" | "medium" | "high" | undefined
|
||||
user_location:
|
||||
| {
|
||||
type: "approximate"
|
||||
city?: string
|
||||
country?: string
|
||||
region?: string
|
||||
timezone?: string
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
| {
|
||||
type: "web_search_preview"
|
||||
search_context_size: "low" | "medium" | "high" | undefined
|
||||
user_location:
|
||||
| {
|
||||
type: "approximate"
|
||||
city?: string
|
||||
country?: string
|
||||
region?: string
|
||||
timezone?: string
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
| {
|
||||
type: "code_interpreter"
|
||||
container: string | { type: "auto"; file_ids: string[] | undefined }
|
||||
}
|
||||
| {
|
||||
type: "file_search"
|
||||
vector_store_ids: string[]
|
||||
max_num_results: number | undefined
|
||||
ranking_options: { ranker?: string; score_threshold?: number } | undefined
|
||||
filters: OpenAIResponsesFileSearchToolComparisonFilter | OpenAIResponsesFileSearchToolCompoundFilter | undefined
|
||||
}
|
||||
| {
|
||||
type: "image_generation"
|
||||
background: "auto" | "opaque" | "transparent" | undefined
|
||||
input_fidelity: "low" | "high" | undefined
|
||||
input_image_mask:
|
||||
| {
|
||||
file_id: string | undefined
|
||||
image_url: string | undefined
|
||||
}
|
||||
| undefined
|
||||
model: string | undefined
|
||||
moderation: "auto" | undefined
|
||||
output_compression: number | undefined
|
||||
output_format: "png" | "jpeg" | "webp" | undefined
|
||||
partial_images: number | undefined
|
||||
quality: "auto" | "low" | "medium" | "high" | undefined
|
||||
size: "auto" | "1024x1024" | "1024x1536" | "1536x1024" | undefined
|
||||
}
|
||||
| {
|
||||
type: "local_shell"
|
||||
}
|
||||
|
||||
export type OpenAIResponsesReasoning = {
|
||||
type: "reasoning"
|
||||
id: string
|
||||
encrypted_content?: string | null
|
||||
summary: Array<{
|
||||
type: "summary_text"
|
||||
text: string
|
||||
}>
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
type LanguageModelV2CallOptions,
|
||||
type LanguageModelV2CallWarning,
|
||||
UnsupportedFunctionalityError,
|
||||
} from "@ai-sdk/provider"
|
||||
import { codeInterpreterArgsSchema } from "./tool/code-interpreter"
|
||||
import { fileSearchArgsSchema } from "./tool/file-search"
|
||||
import { webSearchArgsSchema } from "./tool/web-search"
|
||||
import { webSearchPreviewArgsSchema } from "./tool/web-search-preview"
|
||||
import { imageGenerationArgsSchema } from "./tool/image-generation"
|
||||
import type { OpenAIResponsesTool } from "./openai-responses-api-types"
|
||||
|
||||
export function prepareResponsesTools({
|
||||
tools,
|
||||
toolChoice,
|
||||
strictJsonSchema,
|
||||
}: {
|
||||
tools: LanguageModelV2CallOptions["tools"]
|
||||
toolChoice?: LanguageModelV2CallOptions["toolChoice"]
|
||||
strictJsonSchema: boolean
|
||||
}): {
|
||||
tools?: Array<OpenAIResponsesTool>
|
||||
toolChoice?:
|
||||
| "auto"
|
||||
| "none"
|
||||
| "required"
|
||||
| { type: "file_search" }
|
||||
| { type: "web_search_preview" }
|
||||
| { type: "web_search" }
|
||||
| { type: "function"; name: string }
|
||||
| { type: "code_interpreter" }
|
||||
| { type: "image_generation" }
|
||||
toolWarnings: LanguageModelV2CallWarning[]
|
||||
} {
|
||||
// when the tools array is empty, change it to undefined to prevent errors:
|
||||
tools = tools?.length ? tools : undefined
|
||||
|
||||
const toolWarnings: LanguageModelV2CallWarning[] = []
|
||||
|
||||
if (tools == null) {
|
||||
return { tools: undefined, toolChoice: undefined, toolWarnings }
|
||||
}
|
||||
|
||||
const openaiTools: Array<OpenAIResponsesTool> = []
|
||||
|
||||
for (const tool of tools) {
|
||||
switch (tool.type) {
|
||||
case "function":
|
||||
openaiTools.push({
|
||||
type: "function",
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.inputSchema,
|
||||
strict: strictJsonSchema,
|
||||
})
|
||||
break
|
||||
case "provider-defined": {
|
||||
switch (tool.id) {
|
||||
case "openai.file_search": {
|
||||
const args = fileSearchArgsSchema.parse(tool.args)
|
||||
|
||||
openaiTools.push({
|
||||
type: "file_search",
|
||||
vector_store_ids: args.vectorStoreIds,
|
||||
max_num_results: args.maxNumResults,
|
||||
ranking_options: args.ranking
|
||||
? {
|
||||
ranker: args.ranking.ranker,
|
||||
score_threshold: args.ranking.scoreThreshold,
|
||||
}
|
||||
: undefined,
|
||||
filters: args.filters,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case "openai.local_shell": {
|
||||
openaiTools.push({
|
||||
type: "local_shell",
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openai.web_search_preview": {
|
||||
const args = webSearchPreviewArgsSchema.parse(tool.args)
|
||||
openaiTools.push({
|
||||
type: "web_search_preview",
|
||||
search_context_size: args.searchContextSize,
|
||||
user_location: args.userLocation,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openai.web_search": {
|
||||
const args = webSearchArgsSchema.parse(tool.args)
|
||||
openaiTools.push({
|
||||
type: "web_search",
|
||||
filters: args.filters != null ? { allowed_domains: args.filters.allowedDomains } : undefined,
|
||||
search_context_size: args.searchContextSize,
|
||||
user_location: args.userLocation,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openai.code_interpreter": {
|
||||
const args = codeInterpreterArgsSchema.parse(tool.args)
|
||||
openaiTools.push({
|
||||
type: "code_interpreter",
|
||||
container:
|
||||
args.container == null
|
||||
? { type: "auto", file_ids: undefined }
|
||||
: typeof args.container === "string"
|
||||
? args.container
|
||||
: { type: "auto", file_ids: args.container.fileIds },
|
||||
})
|
||||
break
|
||||
}
|
||||
case "openai.image_generation": {
|
||||
const args = imageGenerationArgsSchema.parse(tool.args)
|
||||
openaiTools.push({
|
||||
type: "image_generation",
|
||||
background: args.background,
|
||||
input_fidelity: args.inputFidelity,
|
||||
input_image_mask: args.inputImageMask
|
||||
? {
|
||||
file_id: args.inputImageMask.fileId,
|
||||
image_url: args.inputImageMask.imageUrl,
|
||||
}
|
||||
: undefined,
|
||||
model: args.model,
|
||||
moderation: args.moderation,
|
||||
partial_images: args.partialImages,
|
||||
quality: args.quality,
|
||||
output_compression: args.outputCompression,
|
||||
output_format: args.outputFormat,
|
||||
size: args.size,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
toolWarnings.push({ type: "unsupported-tool", tool })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (toolChoice == null) {
|
||||
return { tools: openaiTools, toolChoice: undefined, toolWarnings }
|
||||
}
|
||||
|
||||
const type = toolChoice.type
|
||||
|
||||
switch (type) {
|
||||
case "auto":
|
||||
case "none":
|
||||
case "required":
|
||||
return { tools: openaiTools, toolChoice: type, toolWarnings }
|
||||
case "tool":
|
||||
return {
|
||||
tools: openaiTools,
|
||||
toolChoice:
|
||||
toolChoice.toolName === "code_interpreter" ||
|
||||
toolChoice.toolName === "file_search" ||
|
||||
toolChoice.toolName === "image_generation" ||
|
||||
toolChoice.toolName === "web_search_preview" ||
|
||||
toolChoice.toolName === "web_search"
|
||||
? { type: toolChoice.toolName }
|
||||
: { type: "function", name: toolChoice.toolName },
|
||||
toolWarnings,
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = type
|
||||
throw new UnsupportedFunctionalityError({
|
||||
functionality: `tool choice type: ${_exhaustiveCheck}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type OpenAIResponsesModelId = string
|
||||
@@ -0,0 +1,88 @@
|
||||
import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils"
|
||||
import { z } from "zod/v4"
|
||||
|
||||
export const codeInterpreterInputSchema = z.object({
|
||||
code: z.string().nullish(),
|
||||
containerId: z.string(),
|
||||
})
|
||||
|
||||
export const codeInterpreterOutputSchema = z.object({
|
||||
outputs: z
|
||||
.array(
|
||||
z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal("logs"), logs: z.string() }),
|
||||
z.object({ type: z.literal("image"), url: z.string() }),
|
||||
]),
|
||||
)
|
||||
.nullish(),
|
||||
})
|
||||
|
||||
export const codeInterpreterArgsSchema = z.object({
|
||||
container: z
|
||||
.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
fileIds: z.array(z.string()).optional(),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
|
||||
type CodeInterpreterArgs = {
|
||||
/**
|
||||
* The code interpreter container.
|
||||
* Can be a container ID
|
||||
* or an object that specifies uploaded file IDs to make available to your code.
|
||||
*/
|
||||
container?: string | { fileIds?: string[] }
|
||||
}
|
||||
|
||||
export const codeInterpreterToolFactory = createProviderDefinedToolFactoryWithOutputSchema<
|
||||
{
|
||||
/**
|
||||
* The code to run, or null if not available.
|
||||
*/
|
||||
code?: string | null
|
||||
|
||||
/**
|
||||
* The ID of the container used to run the code.
|
||||
*/
|
||||
containerId: string
|
||||
},
|
||||
{
|
||||
/**
|
||||
* The outputs generated by the code interpreter, such as logs or images.
|
||||
* Can be null if no outputs are available.
|
||||
*/
|
||||
outputs?: Array<
|
||||
| {
|
||||
type: "logs"
|
||||
|
||||
/**
|
||||
* The logs output from the code interpreter.
|
||||
*/
|
||||
logs: string
|
||||
}
|
||||
| {
|
||||
type: "image"
|
||||
|
||||
/**
|
||||
* The URL of the image output from the code interpreter.
|
||||
*/
|
||||
url: string
|
||||
}
|
||||
> | null
|
||||
},
|
||||
CodeInterpreterArgs
|
||||
>({
|
||||
id: "openai.code_interpreter",
|
||||
name: "code_interpreter",
|
||||
inputSchema: codeInterpreterInputSchema,
|
||||
outputSchema: codeInterpreterOutputSchema,
|
||||
})
|
||||
|
||||
export const codeInterpreter = (
|
||||
args: CodeInterpreterArgs = {}, // default
|
||||
) => {
|
||||
return codeInterpreterToolFactory(args)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils"
|
||||
import type {
|
||||
OpenAIResponsesFileSearchToolComparisonFilter,
|
||||
OpenAIResponsesFileSearchToolCompoundFilter,
|
||||
} from "../openai-responses-api-types"
|
||||
import { z } from "zod/v4"
|
||||
|
||||
const comparisonFilterSchema = z.object({
|
||||
key: z.string(),
|
||||
type: z.enum(["eq", "ne", "gt", "gte", "lt", "lte"]),
|
||||
value: z.union([z.string(), z.number(), z.boolean()]),
|
||||
})
|
||||
|
||||
const compoundFilterSchema: z.ZodType<any> = z.object({
|
||||
type: z.enum(["and", "or"]),
|
||||
filters: z.array(z.union([comparisonFilterSchema, z.lazy(() => compoundFilterSchema)])),
|
||||
})
|
||||
|
||||
export const fileSearchArgsSchema = z.object({
|
||||
vectorStoreIds: z.array(z.string()),
|
||||
maxNumResults: z.number().optional(),
|
||||
ranking: z
|
||||
.object({
|
||||
ranker: z.string().optional(),
|
||||
scoreThreshold: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
filters: z.union([comparisonFilterSchema, compoundFilterSchema]).optional(),
|
||||
})
|
||||
|
||||
export const fileSearchOutputSchema = z.object({
|
||||
queries: z.array(z.string()),
|
||||
results: z
|
||||
.array(
|
||||
z.object({
|
||||
attributes: z.record(z.string(), z.unknown()),
|
||||
fileId: z.string(),
|
||||
filename: z.string(),
|
||||
score: z.number(),
|
||||
text: z.string(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export const fileSearch = createProviderDefinedToolFactoryWithOutputSchema<
|
||||
{},
|
||||
{
|
||||
/**
|
||||
* The search query to execute.
|
||||
*/
|
||||
queries: string[]
|
||||
|
||||
/**
|
||||
* The results of the file search tool call.
|
||||
*/
|
||||
results:
|
||||
| null
|
||||
| {
|
||||
/**
|
||||
* Set of 16 key-value pairs that can be attached to an object.
|
||||
* This can be useful for storing additional information about the object
|
||||
* in a structured format, and querying for objects via API or the dashboard.
|
||||
* Keys are strings with a maximum length of 64 characters.
|
||||
* Values are strings with a maximum length of 512 characters, booleans, or numbers.
|
||||
*/
|
||||
attributes: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* The unique ID of the file.
|
||||
*/
|
||||
fileId: string
|
||||
|
||||
/**
|
||||
* The name of the file.
|
||||
*/
|
||||
filename: string
|
||||
|
||||
/**
|
||||
* The relevance score of the file - a value between 0 and 1.
|
||||
*/
|
||||
score: number
|
||||
|
||||
/**
|
||||
* The text that was retrieved from the file.
|
||||
*/
|
||||
text: string
|
||||
}[]
|
||||
},
|
||||
{
|
||||
/**
|
||||
* List of vector store IDs to search through.
|
||||
*/
|
||||
vectorStoreIds: string[]
|
||||
|
||||
/**
|
||||
* Maximum number of search results to return. Defaults to 10.
|
||||
*/
|
||||
maxNumResults?: number
|
||||
|
||||
/**
|
||||
* Ranking options for the search.
|
||||
*/
|
||||
ranking?: {
|
||||
/**
|
||||
* The ranker to use for the file search.
|
||||
*/
|
||||
ranker?: string
|
||||
|
||||
/**
|
||||
* The score threshold for the file search, a number between 0 and 1.
|
||||
* Numbers closer to 1 will attempt to return only the most relevant results,
|
||||
* but may return fewer results.
|
||||
*/
|
||||
scoreThreshold?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter to apply.
|
||||
*/
|
||||
filters?: OpenAIResponsesFileSearchToolComparisonFilter | OpenAIResponsesFileSearchToolCompoundFilter
|
||||
}
|
||||
>({
|
||||
id: "openai.file_search",
|
||||
name: "file_search",
|
||||
inputSchema: z.object({}),
|
||||
outputSchema: fileSearchOutputSchema,
|
||||
})
|
||||
@@ -0,0 +1,115 @@
|
||||
import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils"
|
||||
import { z } from "zod/v4"
|
||||
|
||||
export const imageGenerationArgsSchema = z
|
||||
.object({
|
||||
background: z.enum(["auto", "opaque", "transparent"]).optional(),
|
||||
inputFidelity: z.enum(["low", "high"]).optional(),
|
||||
inputImageMask: z
|
||||
.object({
|
||||
fileId: z.string().optional(),
|
||||
imageUrl: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
model: z.string().optional(),
|
||||
moderation: z.enum(["auto"]).optional(),
|
||||
outputCompression: z.number().int().min(0).max(100).optional(),
|
||||
outputFormat: z.enum(["png", "jpeg", "webp"]).optional(),
|
||||
partialImages: z.number().int().min(0).max(3).optional(),
|
||||
quality: z.enum(["auto", "low", "medium", "high"]).optional(),
|
||||
size: z.enum(["1024x1024", "1024x1536", "1536x1024", "auto"]).optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const imageGenerationOutputSchema = z.object({
|
||||
result: z.string(),
|
||||
})
|
||||
|
||||
type ImageGenerationArgs = {
|
||||
/**
|
||||
* Background type for the generated image. Default is 'auto'.
|
||||
*/
|
||||
background?: "auto" | "opaque" | "transparent"
|
||||
|
||||
/**
|
||||
* Input fidelity for the generated image. Default is 'low'.
|
||||
*/
|
||||
inputFidelity?: "low" | "high"
|
||||
|
||||
/**
|
||||
* Optional mask for inpainting.
|
||||
* Contains image_url (string, optional) and file_id (string, optional).
|
||||
*/
|
||||
inputImageMask?: {
|
||||
/**
|
||||
* File ID for the mask image.
|
||||
*/
|
||||
fileId?: string
|
||||
|
||||
/**
|
||||
* Base64-encoded mask image.
|
||||
*/
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The image generation model to use. Default: gpt-image-1.
|
||||
*/
|
||||
model?: string
|
||||
|
||||
/**
|
||||
* Moderation level for the generated image. Default: auto.
|
||||
*/
|
||||
moderation?: "auto"
|
||||
|
||||
/**
|
||||
* Compression level for the output image. Default: 100.
|
||||
*/
|
||||
outputCompression?: number
|
||||
|
||||
/**
|
||||
* The output format of the generated image. One of png, webp, or jpeg.
|
||||
* Default: png
|
||||
*/
|
||||
outputFormat?: "png" | "jpeg" | "webp"
|
||||
|
||||
/**
|
||||
* Number of partial images to generate in streaming mode, from 0 (default value) to 3.
|
||||
*/
|
||||
partialImages?: number
|
||||
|
||||
/**
|
||||
* The quality of the generated image.
|
||||
* One of low, medium, high, or auto. Default: auto.
|
||||
*/
|
||||
quality?: "auto" | "low" | "medium" | "high"
|
||||
|
||||
/**
|
||||
* The size of the generated image.
|
||||
* One of 1024x1024, 1024x1536, 1536x1024, or auto.
|
||||
* Default: auto.
|
||||
*/
|
||||
size?: "auto" | "1024x1024" | "1024x1536" | "1536x1024"
|
||||
}
|
||||
|
||||
const imageGenerationToolFactory = createProviderDefinedToolFactoryWithOutputSchema<
|
||||
{},
|
||||
{
|
||||
/**
|
||||
* The generated image encoded in base64.
|
||||
*/
|
||||
result: string
|
||||
},
|
||||
ImageGenerationArgs
|
||||
>({
|
||||
id: "openai.image_generation",
|
||||
name: "image_generation",
|
||||
inputSchema: z.object({}),
|
||||
outputSchema: imageGenerationOutputSchema,
|
||||
})
|
||||
|
||||
export const imageGeneration = (
|
||||
args: ImageGenerationArgs = {}, // default
|
||||
) => {
|
||||
return imageGenerationToolFactory(args)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils"
|
||||
import { z } from "zod/v4"
|
||||
|
||||
export const localShellInputSchema = z.object({
|
||||
action: z.object({
|
||||
type: z.literal("exec"),
|
||||
command: z.array(z.string()),
|
||||
timeoutMs: z.number().optional(),
|
||||
user: z.string().optional(),
|
||||
workingDirectory: z.string().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const localShellOutputSchema = z.object({
|
||||
output: z.string(),
|
||||
})
|
||||
|
||||
export const localShell = createProviderDefinedToolFactoryWithOutputSchema<
|
||||
{
|
||||
/**
|
||||
* Execute a shell command on the server.
|
||||
*/
|
||||
action: {
|
||||
type: "exec"
|
||||
|
||||
/**
|
||||
* The command to run.
|
||||
*/
|
||||
command: string[]
|
||||
|
||||
/**
|
||||
* Optional timeout in milliseconds for the command.
|
||||
*/
|
||||
timeoutMs?: number
|
||||
|
||||
/**
|
||||
* Optional user to run the command as.
|
||||
*/
|
||||
user?: string
|
||||
|
||||
/**
|
||||
* Optional working directory to run the command in.
|
||||
*/
|
||||
workingDirectory?: string
|
||||
|
||||
/**
|
||||
* Environment variables to set for the command.
|
||||
*/
|
||||
env?: Record<string, string>
|
||||
}
|
||||
},
|
||||
{
|
||||
/**
|
||||
* The output of local shell tool call.
|
||||
*/
|
||||
output: string
|
||||
},
|
||||
{}
|
||||
>({
|
||||
id: "openai.local_shell",
|
||||
name: "local_shell",
|
||||
inputSchema: localShellInputSchema,
|
||||
outputSchema: localShellOutputSchema,
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import { createProviderDefinedToolFactory } from "@ai-sdk/provider-utils"
|
||||
import { z } from "zod/v4"
|
||||
|
||||
// Args validation schema
|
||||
export const webSearchPreviewArgsSchema = z.object({
|
||||
/**
|
||||
* Search context size to use for the web search.
|
||||
* - high: Most comprehensive context, highest cost, slower response
|
||||
* - medium: Balanced context, cost, and latency (default)
|
||||
* - low: Least context, lowest cost, fastest response
|
||||
*/
|
||||
searchContextSize: z.enum(["low", "medium", "high"]).optional(),
|
||||
|
||||
/**
|
||||
* User location information to provide geographically relevant search results.
|
||||
*/
|
||||
userLocation: z
|
||||
.object({
|
||||
/**
|
||||
* Type of location (always 'approximate')
|
||||
*/
|
||||
type: z.literal("approximate"),
|
||||
/**
|
||||
* Two-letter ISO country code (e.g., 'US', 'GB')
|
||||
*/
|
||||
country: z.string().optional(),
|
||||
/**
|
||||
* City name (free text, e.g., 'Minneapolis')
|
||||
*/
|
||||
city: z.string().optional(),
|
||||
/**
|
||||
* Region name (free text, e.g., 'Minnesota')
|
||||
*/
|
||||
region: z.string().optional(),
|
||||
/**
|
||||
* IANA timezone (e.g., 'America/Chicago')
|
||||
*/
|
||||
timezone: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const webSearchPreview = createProviderDefinedToolFactory<
|
||||
{
|
||||
// Web search doesn't take input parameters - it's controlled by the prompt
|
||||
},
|
||||
{
|
||||
/**
|
||||
* Search context size to use for the web search.
|
||||
* - high: Most comprehensive context, highest cost, slower response
|
||||
* - medium: Balanced context, cost, and latency (default)
|
||||
* - low: Least context, lowest cost, fastest response
|
||||
*/
|
||||
searchContextSize?: "low" | "medium" | "high"
|
||||
|
||||
/**
|
||||
* User location information to provide geographically relevant search results.
|
||||
*/
|
||||
userLocation?: {
|
||||
/**
|
||||
* Type of location (always 'approximate')
|
||||
*/
|
||||
type: "approximate"
|
||||
/**
|
||||
* Two-letter ISO country code (e.g., 'US', 'GB')
|
||||
*/
|
||||
country?: string
|
||||
/**
|
||||
* City name (free text, e.g., 'Minneapolis')
|
||||
*/
|
||||
city?: string
|
||||
/**
|
||||
* Region name (free text, e.g., 'Minnesota')
|
||||
*/
|
||||
region?: string
|
||||
/**
|
||||
* IANA timezone (e.g., 'America/Chicago')
|
||||
*/
|
||||
timezone?: string
|
||||
}
|
||||
}
|
||||
>({
|
||||
id: "openai.web_search_preview",
|
||||
name: "web_search_preview",
|
||||
inputSchema: z.object({
|
||||
action: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("search"),
|
||||
query: z.string().nullish(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("open_page"),
|
||||
url: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("find"),
|
||||
url: z.string(),
|
||||
pattern: z.string(),
|
||||
}),
|
||||
])
|
||||
.nullish(),
|
||||
}),
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createProviderDefinedToolFactory } from "@ai-sdk/provider-utils"
|
||||
import { z } from "zod/v4"
|
||||
|
||||
export const webSearchArgsSchema = z.object({
|
||||
filters: z
|
||||
.object({
|
||||
allowedDomains: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
searchContextSize: z.enum(["low", "medium", "high"]).optional(),
|
||||
|
||||
userLocation: z
|
||||
.object({
|
||||
type: z.literal("approximate"),
|
||||
country: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
region: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const webSearchToolFactory = createProviderDefinedToolFactory<
|
||||
{
|
||||
// Web search doesn't take input parameters - it's controlled by the prompt
|
||||
},
|
||||
{
|
||||
/**
|
||||
* Filters for the search.
|
||||
*/
|
||||
filters?: {
|
||||
/**
|
||||
* Allowed domains for the search.
|
||||
* If not provided, all domains are allowed.
|
||||
* Subdomains of the provided domains are allowed as well.
|
||||
*/
|
||||
allowedDomains?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Search context size to use for the web search.
|
||||
* - high: Most comprehensive context, highest cost, slower response
|
||||
* - medium: Balanced context, cost, and latency (default)
|
||||
* - low: Least context, lowest cost, fastest response
|
||||
*/
|
||||
searchContextSize?: "low" | "medium" | "high"
|
||||
|
||||
/**
|
||||
* User location information to provide geographically relevant search results.
|
||||
*/
|
||||
userLocation?: {
|
||||
/**
|
||||
* Type of location (always 'approximate')
|
||||
*/
|
||||
type: "approximate"
|
||||
/**
|
||||
* Two-letter ISO country code (e.g., 'US', 'GB')
|
||||
*/
|
||||
country?: string
|
||||
/**
|
||||
* City name (free text, e.g., 'Minneapolis')
|
||||
*/
|
||||
city?: string
|
||||
/**
|
||||
* Region name (free text, e.g., 'Minnesota')
|
||||
*/
|
||||
region?: string
|
||||
/**
|
||||
* IANA timezone (e.g., 'America/Chicago')
|
||||
*/
|
||||
timezone?: string
|
||||
}
|
||||
}
|
||||
>({
|
||||
id: "openai.web_search",
|
||||
name: "web_search",
|
||||
inputSchema: z.object({
|
||||
action: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal("search"),
|
||||
query: z.string().nullish(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("open_page"),
|
||||
url: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("find"),
|
||||
url: z.string(),
|
||||
pattern: z.string(),
|
||||
}),
|
||||
])
|
||||
.nullish(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const webSearch = (
|
||||
args: Parameters<typeof webSearchToolFactory>[0] = {}, // default
|
||||
) => {
|
||||
return webSearchToolFactory(args)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ModelMessage } from "ai"
|
||||
import type { APICallError, ModelMessage } from "ai"
|
||||
import { STATUS_CODES } from "http"
|
||||
import { unique } from "remeda"
|
||||
import type { JSONSchema } from "zod/v4/core"
|
||||
|
||||
@@ -128,7 +129,13 @@ export namespace ProviderTransform {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function options(providerID: string, modelID: string, npm: string, sessionID: string): Record<string, any> {
|
||||
export function options(
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
npm: string,
|
||||
sessionID: string,
|
||||
providerOptions?: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
// switch to providerID later, for now use this
|
||||
@@ -138,7 +145,7 @@ export namespace ProviderTransform {
|
||||
}
|
||||
}
|
||||
|
||||
if (providerID === "openai") {
|
||||
if (providerID === "openai" || providerOptions?.setCacheKey) {
|
||||
result["promptCacheKey"] = sessionID
|
||||
}
|
||||
|
||||
@@ -248,7 +255,7 @@ export namespace ProviderTransform {
|
||||
return standardLimit
|
||||
}
|
||||
|
||||
export function schema(_providerID: string, _modelID: string, schema: JSONSchema.BaseSchema) {
|
||||
export function schema(providerID: string, modelID: string, schema: JSONSchema.BaseSchema) {
|
||||
/*
|
||||
if (["openai", "azure"].includes(providerID)) {
|
||||
if (schema.type === "object" && schema.properties) {
|
||||
@@ -265,19 +272,65 @@ export namespace ProviderTransform {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (providerID === "google") {
|
||||
}
|
||||
*/
|
||||
|
||||
// Convert integer enums to string enums for Google/Gemini
|
||||
if (providerID === "google" || modelID.includes("gemini")) {
|
||||
const convertIntEnumsToStrings = (obj: any): any => {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(convertIntEnumsToStrings)
|
||||
}
|
||||
|
||||
const result: any = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key === "enum" && Array.isArray(value)) {
|
||||
// Convert all enum values to strings
|
||||
result[key] = value.map((v) => String(v))
|
||||
// If we have integer type with enum, change type to string
|
||||
if (result.type === "integer" || result.type === "number") {
|
||||
result.type = "string"
|
||||
}
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
result[key] = convertIntEnumsToStrings(value)
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
schema = convertIntEnumsToStrings(schema)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
export function error(providerID: string, message: string) {
|
||||
export function error(providerID: string, error: APICallError) {
|
||||
let message = error.message
|
||||
if (providerID === "github-copilot" && message.includes("The requested model is not supported")) {
|
||||
message +=
|
||||
return (
|
||||
message +
|
||||
"\n\nMake sure the model is enabled in your copilot settings: https://github.com/settings/copilot/features"
|
||||
)
|
||||
}
|
||||
return message
|
||||
|
||||
if (!error.responseBody || (error.statusCode && message !== STATUS_CODES[error.statusCode])) {
|
||||
return message
|
||||
}
|
||||
|
||||
try {
|
||||
const body = JSON.parse(error.responseBody)
|
||||
// try to extract common error message fields
|
||||
const errMsg = body.message || body.error
|
||||
if (errMsg && typeof errMsg === "string") {
|
||||
return `${message}: ${errMsg}`
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return `${message}: ${error.responseBody}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { MessageV2 } from "../session/message-v2"
|
||||
import { TuiRoute } from "./tui"
|
||||
import { Permission } from "../permission"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Vcs } from "../project/vcs"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Auth } from "../auth"
|
||||
import { Command } from "../command"
|
||||
@@ -365,6 +366,29 @@ export namespace Server {
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/vcs",
|
||||
describeRoute({
|
||||
description: "Get VCS info for the current instance",
|
||||
operationId: "vcs.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "VCS info",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Vcs.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const branch = await Vcs.branch()
|
||||
return c.json({
|
||||
branch,
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/session",
|
||||
describeRoute({
|
||||
|
||||
@@ -178,7 +178,7 @@ export namespace SessionCompaction {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
|
||||
text: "Summarize our conversation above. This summary will be the only context available when the conversation continues, so preserve critical information including: what was accomplished, current work in progress, files involved, next steps, and any key user requests or constraints. Be concise but detailed enough that work can continue seamlessly.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -666,7 +666,7 @@ export namespace MessageV2 {
|
||||
}
|
||||
}
|
||||
|
||||
return convertToModelMessages(result)
|
||||
return convertToModelMessages(result.filter((msg) => msg.parts.length > 0))
|
||||
}
|
||||
|
||||
export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
|
||||
@@ -739,7 +739,7 @@ export namespace MessageV2 {
|
||||
{ cause: e },
|
||||
).toObject()
|
||||
case APICallError.isInstance(e):
|
||||
const message = ProviderTransform.error(ctx.providerID, e.message)
|
||||
const message = ProviderTransform.error(ctx.providerID, e)
|
||||
return new MessageV2.APIError(
|
||||
{
|
||||
message,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user