Merge branch 'dev' into llm-centralization
This commit is contained in:
commit
a5914f4d7c
56 changed files with 772 additions and 279 deletions
6
.github/workflows/publish.yml
vendored
6
.github/workflows/publish.yml
vendored
|
|
@ -64,6 +64,12 @@ jobs:
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
|
|
|
||||||
2
STATS.md
2
STATS.md
|
|
@ -168,3 +168,5 @@
|
||||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||||
|
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||||
|
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||||
|
|
|
||||||
37
bun.lock
37
bun.lock
|
|
@ -20,7 +20,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
|
@ -133,6 +133,7 @@
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/audio": "1.4.2",
|
"@solid-primitives/audio": "1.4.2",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
"@solid-primitives/media": "2.3.3",
|
||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solid-primitives/scroll": "2.1.3",
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
|
|
@ -169,7 +170,7 @@
|
||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
|
|
@ -198,7 +199,7 @@
|
||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
|
|
@ -214,7 +215,7 @@
|
||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
|
|
@ -306,7 +307,7 @@
|
||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
|
|
@ -326,7 +327,7 @@
|
||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.88.1",
|
"@hey-api/openapi-ts": "0.88.1",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
|
|
@ -337,7 +338,7 @@
|
||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
|
|
@ -350,7 +351,7 @@
|
||||||
},
|
},
|
||||||
"packages/tauri": {
|
"packages/tauri": {
|
||||||
"name": "@opencode-ai/tauri",
|
"name": "@opencode-ai/tauri",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/desktop": "workspace:*",
|
"@opencode-ai/desktop": "workspace:*",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
|
@ -375,7 +376,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
|
@ -410,7 +411,7 @@
|
||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
@ -421,7 +422,7 @@
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|
@ -477,7 +478,7 @@
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@tsconfig/node22": "22.0.2",
|
"@tsconfig/node22": "22.0.2",
|
||||||
"@types/bun": "1.3.3",
|
"@types/bun": "1.3.4",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
|
|
@ -1703,7 +1704,7 @@
|
||||||
|
|
||||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
||||||
|
|
||||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
|
|
@ -2009,7 +2010,7 @@
|
||||||
|
|
||||||
"bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
|
"bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||||
|
|
||||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ inputs:
|
||||||
description: "Custom prompt to override the default prompt"
|
description: "Custom prompt to override the default prompt"
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
use_github_token:
|
||||||
|
description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -51,3 +56,4 @@ runs:
|
||||||
MODEL: ${{ inputs.model }}
|
MODEL: ${{ inputs.model }}
|
||||||
SHARE: ${{ inputs.share }}
|
SHARE: ${{ inputs.share }}
|
||||||
PROMPT: ${{ inputs.prompt }}
|
PROMPT: ${{ inputs.prompt }}
|
||||||
|
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"nodeModules": "sha256-nWSAnQEm/t1ESZe23dr4JnIOJQ0JLN0w4NVoMJajbVQ="
|
"nodeModules": "sha256-lgPsYtNJT7a+mDk5cTiEJLlBnTMTjxZCl8bw5WxcuaM="
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"description": "AI-powered development tool",
|
"description": "AI-powered development tool",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.3",
|
"packageManager": "bun@1.3.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||||
"typecheck": "bun turbo typecheck",
|
"typecheck": "bun turbo typecheck",
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/bun": "1.3.3",
|
"@types/bun": "1.3.4",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/audio": "1.4.2",
|
"@solid-primitives/audio": "1.4.2",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
"@solid-primitives/media": "2.3.3",
|
||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solid-primitives/scroll": "2.1.3",
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,7 @@
|
||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import {
|
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
|
||||||
createEffect,
|
|
||||||
on,
|
|
||||||
Component,
|
|
||||||
Show,
|
|
||||||
For,
|
|
||||||
onMount,
|
|
||||||
onCleanup,
|
|
||||||
Switch,
|
|
||||||
Match,
|
|
||||||
createSignal,
|
|
||||||
createMemo,
|
|
||||||
} from "solid-js"
|
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
|
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
|
||||||
|
|
@ -81,22 +70,85 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
|
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
popoverIsOpen: boolean
|
popoverIsOpen: boolean
|
||||||
|
historyIndex: number
|
||||||
|
savedPrompt: Prompt | null
|
||||||
|
placeholder: number
|
||||||
}>({
|
}>({
|
||||||
popoverIsOpen: false,
|
popoverIsOpen: false,
|
||||||
|
historyIndex: -1,
|
||||||
|
savedPrompt: null,
|
||||||
|
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length))
|
const MAX_HISTORY = 100
|
||||||
|
const [history, setHistory] = makePersisted(
|
||||||
|
createStore<{
|
||||||
|
entries: Prompt[]
|
||||||
|
}>({
|
||||||
|
entries: [],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "prompt-history.v1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
const clonePromptParts = (prompt: Prompt): Prompt =>
|
||||||
const interval = setInterval(() => {
|
prompt.map((part) =>
|
||||||
setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length)
|
part.type === "text"
|
||||||
}, 6500)
|
? { ...part }
|
||||||
onCleanup(() => clearInterval(interval))
|
: {
|
||||||
})
|
...part,
|
||||||
|
selection: part.selection ? { ...part.selection } : undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const promptLength = (prompt: Prompt) => prompt.reduce((len, part) => len + part.content.length, 0)
|
||||||
|
|
||||||
|
const applyHistoryPrompt = (prompt: Prompt, position: "start" | "end") => {
|
||||||
|
const length = position === "start" ? 0 : promptLength(prompt)
|
||||||
|
session.prompt.set(prompt, length)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
editorRef.focus()
|
||||||
|
setCursorPosition(editorRef, length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCaretLineState = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return { collapsed: false, onFirstLine: false, onLastLine: false }
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
const rect = range.getBoundingClientRect()
|
||||||
|
const editorRect = editorRef.getBoundingClientRect()
|
||||||
|
const style = window.getComputedStyle(editorRef)
|
||||||
|
const paddingTop = parseFloat(style.paddingTop) || 0
|
||||||
|
const paddingBottom = parseFloat(style.paddingBottom) || 0
|
||||||
|
let lineHeight = parseFloat(style.lineHeight)
|
||||||
|
if (!Number.isFinite(lineHeight)) lineHeight = parseFloat(style.fontSize) || 16
|
||||||
|
const scrollTop = editorRef.scrollTop
|
||||||
|
let relativeTop = rect.top - editorRect.top - paddingTop + scrollTop
|
||||||
|
if (!Number.isFinite(relativeTop)) relativeTop = scrollTop
|
||||||
|
relativeTop = Math.max(0, relativeTop)
|
||||||
|
let caretHeight = rect.height
|
||||||
|
if (!caretHeight || !Number.isFinite(caretHeight)) caretHeight = lineHeight
|
||||||
|
const relativeBottom = relativeTop + caretHeight
|
||||||
|
const contentHeight = Math.max(caretHeight, editorRef.scrollHeight - paddingTop - paddingBottom)
|
||||||
|
const threshold = Math.max(2, lineHeight / 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
collapsed: selection.isCollapsed,
|
||||||
|
onFirstLine: relativeTop <= threshold,
|
||||||
|
onLastLine: contentHeight - relativeBottom <= threshold,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
session.id
|
session.id
|
||||||
editorRef.focus()
|
editorRef.focus()
|
||||||
|
if (session.id) return
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
|
||||||
|
}, 6500)
|
||||||
|
onCleanup(() => clearInterval(interval))
|
||||||
})
|
})
|
||||||
|
|
||||||
const isFocused = createFocusSignal(() => editorRef)
|
const isFocused = createFocusSignal(() => editorRef)
|
||||||
|
|
@ -129,17 +181,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
|
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
|
const { flat, active, onInput, onKeyDown } = useFilteredList<string>({
|
||||||
items: local.file.searchFilesAndDirectories,
|
items: local.file.searchFilesAndDirectories,
|
||||||
key: (x) => x,
|
key: (x) => x,
|
||||||
onSelect: handleFileSelect,
|
onSelect: handleFileSelect,
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
local.model.recent()
|
|
||||||
refetch()
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => session.prompt.current(),
|
() => session.prompt.current(),
|
||||||
|
|
@ -221,6 +268,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
setStore("popoverIsOpen", false)
|
setStore("popoverIsOpen", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (store.historyIndex >= 0) {
|
||||||
|
setStore("historyIndex", -1)
|
||||||
|
setStore("savedPrompt", null)
|
||||||
|
}
|
||||||
|
|
||||||
session.prompt.set(rawParts, cursorPosition)
|
session.prompt.set(rawParts, cursorPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,12 +348,100 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
sessionID: session.id!,
|
sessionID: session.id!,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const addToHistory = (prompt: Prompt) => {
|
||||||
|
const text = prompt
|
||||||
|
.map((p) => p.content)
|
||||||
|
.join("")
|
||||||
|
.trim()
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
const entry = clonePromptParts(prompt)
|
||||||
|
const lastEntry = history.entries[0]
|
||||||
|
if (lastEntry) {
|
||||||
|
const lastText = lastEntry.map((p) => p.content).join("")
|
||||||
|
if (lastText === text) return
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateHistory = (direction: "up" | "down") => {
|
||||||
|
const entries = history.entries
|
||||||
|
const current = store.historyIndex
|
||||||
|
|
||||||
|
if (direction === "up") {
|
||||||
|
if (entries.length === 0) return false
|
||||||
|
if (current === -1) {
|
||||||
|
setStore("savedPrompt", clonePromptParts(session.prompt.current()))
|
||||||
|
setStore("historyIndex", 0)
|
||||||
|
applyHistoryPrompt(entries[0], "start")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (current < entries.length - 1) {
|
||||||
|
const next = current + 1
|
||||||
|
setStore("historyIndex", next)
|
||||||
|
applyHistoryPrompt(entries[next], "start")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current > 0) {
|
||||||
|
const next = current - 1
|
||||||
|
setStore("historyIndex", next)
|
||||||
|
applyHistoryPrompt(entries[next], "end")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (current === 0) {
|
||||||
|
setStore("historyIndex", -1)
|
||||||
|
const saved = store.savedPrompt
|
||||||
|
if (saved) {
|
||||||
|
applyHistoryPrompt(saved, "end")
|
||||||
|
setStore("savedPrompt", null)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
applyHistoryPrompt(DEFAULT_PROMPT, "end")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (store.popoverIsOpen && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
|
if (store.popoverIsOpen && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
|
||||||
onKeyDown(event)
|
onKeyDown(event)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||||
|
const { collapsed, onFirstLine, onLastLine } = getCaretLineState()
|
||||||
|
if (!collapsed) return
|
||||||
|
const cursorPos = getCursorPosition(editorRef)
|
||||||
|
const textLength = promptLength(session.prompt.current())
|
||||||
|
const inHistory = store.historyIndex >= 0
|
||||||
|
const isStart = cursorPos === 0
|
||||||
|
const isEnd = cursorPos === textLength
|
||||||
|
const atAbsoluteStart = onFirstLine && isStart
|
||||||
|
const atAbsoluteEnd = onLastLine && isEnd
|
||||||
|
const allowUp = (inHistory && isEnd) || atAbsoluteStart
|
||||||
|
const allowDown = (inHistory && isStart) || atAbsoluteEnd
|
||||||
|
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
if (!allowUp) return
|
||||||
|
if (navigateHistory("up")) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowDown) return
|
||||||
|
if (navigateHistory("down")) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
handleSubmit(event)
|
handleSubmit(event)
|
||||||
}
|
}
|
||||||
|
|
@ -323,6 +463,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addToHistory(prompt)
|
||||||
|
setStore("historyIndex", -1)
|
||||||
|
setStore("savedPrompt", null)
|
||||||
|
|
||||||
let existing = session.info()
|
let existing = session.info()
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
const created = await sdk.client.session.create()
|
const created = await sdk.client.session.create()
|
||||||
|
|
@ -461,7 +605,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
/>
|
/>
|
||||||
<Show when={!session.prompt.dirty()}>
|
<Show when={!session.prompt.dirty()}>
|
||||||
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
|
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
|
||||||
Ask anything... "{PLACEHOLDERS[placeholder()]}"
|
Ask anything... "{PLACEHOLDERS[store.placeholder]}"
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -507,6 +651,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
items={models}
|
items={models}
|
||||||
current={local.model.current()}
|
current={local.model.current()}
|
||||||
filterKeys={["provider.name", "name", "id"]}
|
filterKeys={["provider.name", "name", "id"]}
|
||||||
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
|
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
|
||||||
groupBy={(x) => x.provider.name}
|
groupBy={(x) => x.provider.name}
|
||||||
sortGroupsBy={(a, b) => {
|
sortGroupsBy={(a, b) => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||||
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
|
import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { SerializeAddon } from "@/addons/serialize"
|
import { SerializeAddon } from "@/addons/serialize"
|
||||||
import { LocalPTY } from "@/context/session"
|
import { LocalPTY } from "@/context/session"
|
||||||
|
import { usePrefersDark } from "@solid-primitives/media"
|
||||||
|
|
||||||
export interface TerminalProps extends ComponentProps<"div"> {
|
export interface TerminalProps extends ComponentProps<"div"> {
|
||||||
pty: LocalPTY
|
pty: LocalPTY
|
||||||
|
|
@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
let serializeAddon: SerializeAddon
|
let serializeAddon: SerializeAddon
|
||||||
let fitAddon: FitAddon
|
let fitAddon: FitAddon
|
||||||
let handleResize: () => void
|
let handleResize: () => void
|
||||||
|
const prefersDark = usePrefersDark()
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
ghostty = await Ghostty.load()
|
ghostty = await Ghostty.load()
|
||||||
|
|
@ -31,10 +33,17 @@ export const Terminal = (props: TerminalProps) => {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: "TX-02, monospace",
|
fontFamily: "TX-02, monospace",
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
theme: {
|
theme: prefersDark()
|
||||||
background: "#191515",
|
? {
|
||||||
foreground: "#d4d4d4",
|
background: "#191515",
|
||||||
},
|
foreground: "#d4d4d4",
|
||||||
|
cursor: "#d4d4d4",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
background: "#fcfcfc",
|
||||||
|
foreground: "#211e1e",
|
||||||
|
cursor: "#211e1e",
|
||||||
|
},
|
||||||
scrollback: 10_000,
|
scrollback: 10_000,
|
||||||
ghostty,
|
ghostty,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -189,11 +189,13 @@ export default function Layout(props: ParentProps) {
|
||||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||||
const name = createMemo(() => getFilename(props.project.worktree))
|
const name = createMemo(() => getFilename(props.project.worktree))
|
||||||
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
||||||
|
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative size-6 shrink-0">
|
<div class="relative size-6 shrink-0">
|
||||||
<Avatar
|
<Avatar
|
||||||
fallback={name()}
|
fallback={name()}
|
||||||
src={props.project.icon?.url}
|
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
|
||||||
{...getAvatarColors(props.project.icon?.color)}
|
{...getAvatarColors(props.project.icon?.color)}
|
||||||
class={`size-full ${props.class ?? ""}`}
|
class={`size-full ${props.class ?? ""}`}
|
||||||
style={
|
style={
|
||||||
|
|
@ -318,22 +320,20 @@ export default function Layout(props: ParentProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<A
|
<div
|
||||||
href={`${slug()}/session/${session.id}`}
|
class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
|
||||||
class="group/session focus:outline-none cursor-default"
|
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||||
>
|
>
|
||||||
<Tooltip placement="right" value={session.title}>
|
<Tooltip placement="right" value={session.title} gutter={10}>
|
||||||
<div
|
<A
|
||||||
class="relative w-full pl-4 pr-1 py-1 rounded-md
|
href={`${slug()}/session/${session.id}`}
|
||||||
group-[.active]/session:bg-surface-raised-base-hover
|
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
|
||||||
group-hover/session:bg-surface-raised-base-hover
|
|
||||||
group-focus/session:bg-surface-raised-base-hover"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
||||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||||
{session.title}
|
{session.title}
|
||||||
</span>
|
</span>
|
||||||
<div class="shrink-0 group-hover/session:hidden mr-1">
|
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={hasError()}>
|
<Match when={hasError()}>
|
||||||
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
|
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
|
||||||
|
|
@ -358,12 +358,6 @@ export default function Layout(props: ParentProps) {
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden group-hover/session:flex group-active/session:flex text-text-base gap-1">
|
|
||||||
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
|
|
||||||
<Tooltip placement="right" value="Archive session">
|
|
||||||
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Show when={session.summary?.files}>
|
<Show when={session.summary?.files}>
|
||||||
<div class="flex justify-between items-center self-stretch">
|
<div class="flex justify-between items-center self-stretch">
|
||||||
|
|
@ -371,29 +365,40 @@ export default function Layout(props: ParentProps) {
|
||||||
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
|
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</A>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</A>
|
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
|
||||||
|
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
|
||||||
|
<Tooltip placement="right" value="Archive session">
|
||||||
|
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
<Show when={sessions().length === 0}>
|
<Show when={sessions().length === 0}>
|
||||||
<A href={`${slug()}/session`} class="group/session focus:outline-none cursor-default">
|
<div
|
||||||
<Tooltip placement="right" value="New session">
|
class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
|
||||||
<div
|
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||||
class="relative w-full pl-4 pr-1 py-1 rounded-md
|
>
|
||||||
group-[.active]/session:bg-surface-raised-base-hover
|
<div class="flex items-center self-stretch w-full">
|
||||||
group-hover/session:bg-surface-raised-base-hover
|
<div class="flex-1 min-w-0">
|
||||||
group-focus/session:bg-surface-raised-base-hover"
|
<Tooltip placement="right" value="New session">
|
||||||
>
|
<A
|
||||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
href={`${slug()}/session`}
|
||||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
|
||||||
New session
|
>
|
||||||
</span>
|
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||||
</div>
|
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||||
|
New session
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</A>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</div>
|
||||||
</A>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</nav>
|
</nav>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.0.152"
|
version = "1.0.153"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/sst/opencode"
|
repository = "https://github.com/sst/opencode"
|
||||||
|
|
@ -11,26 +11,26 @@ name = "OpenCode"
|
||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-windows-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
FROM alpine
|
FROM alpine AS base
|
||||||
|
|
||||||
# Disable the runtime transpiler cache by default inside Docker containers.
|
# Disable the runtime transpiler cache by default inside Docker containers.
|
||||||
# On ephemeral containers, the cache is not useful
|
# On ephemeral containers, the cache is not useful
|
||||||
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
|
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
|
||||||
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
|
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
|
||||||
RUN apk add libgcc libstdc++ ripgrep
|
RUN apk add libgcc libstdc++ ripgrep
|
||||||
ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
|
||||||
|
FROM base AS build-amd64
|
||||||
|
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||||
|
|
||||||
|
FROM base AS build-arm64
|
||||||
|
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
FROM build-${TARGETARCH}
|
||||||
RUN opencode --version
|
RUN opencode --version
|
||||||
ENTRYPOINT ["opencode"]
|
ENTRYPOINT ["opencode"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,9 @@ for (const item of targets) {
|
||||||
compile: {
|
compile: {
|
||||||
autoloadBunfig: false,
|
autoloadBunfig: false,
|
||||||
autoloadDotenv: false,
|
autoloadDotenv: false,
|
||||||
|
//@ts-ignore (bun types aren't up to date)
|
||||||
|
autoloadTsconfig: true,
|
||||||
|
autoloadPackageJson: true,
|
||||||
target: name.replace(pkg.name, "bun") as any,
|
target: name.replace(pkg.name, "bun") as any,
|
||||||
outfile: `dist/${name}/bin/opencode`,
|
outfile: `dist/${name}/bin/opencode`,
|
||||||
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
|
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
|
||||||
|
|
|
||||||
|
|
@ -244,8 +244,8 @@ if (!Script.preview) {
|
||||||
await $`cd ./dist/homebrew-tap && git push`
|
await $`cd ./dist/homebrew-tap && git push`
|
||||||
|
|
||||||
const image = "ghcr.io/sst/opencode"
|
const image = "ghcr.io/sst/opencode"
|
||||||
await $`docker build -t ${image}:${Script.version} .`
|
const platforms = "linux/amd64,linux/arm64"
|
||||||
await $`docker push ${image}:${Script.version}`
|
const tags = [`${image}:${Script.version}`, `${image}:latest`]
|
||||||
await $`docker tag ${image}:${Script.version} ${image}:latest`
|
const tagFlags = tags.flatMap((t) => ["-t", t])
|
||||||
await $`docker push ${image}:latest`
|
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,47 +85,16 @@ export namespace BunProc {
|
||||||
version,
|
version,
|
||||||
})
|
})
|
||||||
|
|
||||||
const total = 3
|
await BunProc.run(args, {
|
||||||
const wait = 500
|
cwd: Global.Path.cache,
|
||||||
|
}).catch((e) => {
|
||||||
const runInstall = async (count: number = 1): Promise<void> => {
|
throw new InstallFailedError(
|
||||||
log.info("bun install attempt", {
|
{ pkg, version },
|
||||||
pkg,
|
{
|
||||||
version,
|
cause: e,
|
||||||
attempt: count,
|
},
|
||||||
total,
|
)
|
||||||
})
|
})
|
||||||
await BunProc.run(args, {
|
|
||||||
cwd: Global.Path.cache,
|
|
||||||
}).catch(async (error) => {
|
|
||||||
log.warn("bun install failed", {
|
|
||||||
pkg,
|
|
||||||
version,
|
|
||||||
attempt: count,
|
|
||||||
total,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
if (count >= total) {
|
|
||||||
throw new InstallFailedError(
|
|
||||||
{ pkg, version },
|
|
||||||
{
|
|
||||||
cause: error,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const delay = wait * count
|
|
||||||
log.info("bun install retrying", {
|
|
||||||
pkg,
|
|
||||||
version,
|
|
||||||
next: count + 1,
|
|
||||||
delay,
|
|
||||||
})
|
|
||||||
await Bun.sleep(delay)
|
|
||||||
return runInstall(count + 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await runInstall()
|
|
||||||
|
|
||||||
// Resolve actual version from installed package when using "latest"
|
// Resolve actual version from installed package when using "latest"
|
||||||
// This ensures subsequent starts use the cached version until explicitly updated
|
// This ensures subsequent starts use the cached version until explicitly updated
|
||||||
|
|
|
||||||
|
|
@ -411,17 +411,30 @@ export const GithubRunCommand = cmd({
|
||||||
let exitCode = 0
|
let exitCode = 0
|
||||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||||
const triggerCommentId = payload.comment.id
|
const triggerCommentId = payload.comment.id
|
||||||
|
const useGithubToken = normalizeUseGithubToken()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const actionToken = isMock ? args.token! : await getOidcToken()
|
if (useGithubToken) {
|
||||||
appToken = await exchangeForAppToken(actionToken)
|
const githubToken = process.env["GITHUB_TOKEN"]
|
||||||
|
if (!githubToken) {
|
||||||
|
throw new Error(
|
||||||
|
"GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
appToken = githubToken
|
||||||
|
} else {
|
||||||
|
const actionToken = isMock ? args.token! : await getOidcToken()
|
||||||
|
appToken = await exchangeForAppToken(actionToken)
|
||||||
|
}
|
||||||
octoRest = new Octokit({ auth: appToken })
|
octoRest = new Octokit({ auth: appToken })
|
||||||
octoGraph = graphql.defaults({
|
octoGraph = graphql.defaults({
|
||||||
headers: { authorization: `token ${appToken}` },
|
headers: { authorization: `token ${appToken}` },
|
||||||
})
|
})
|
||||||
|
|
||||||
const { userPrompt, promptFiles } = await getUserPrompt()
|
const { userPrompt, promptFiles } = await getUserPrompt()
|
||||||
await configureGit(appToken)
|
if (!useGithubToken) {
|
||||||
|
await configureGit(appToken)
|
||||||
|
}
|
||||||
await assertPermissions()
|
await assertPermissions()
|
||||||
|
|
||||||
await addReaction()
|
await addReaction()
|
||||||
|
|
@ -514,8 +527,10 @@ export const GithubRunCommand = cmd({
|
||||||
// Also output the clean error message for the action to capture
|
// Also output the clean error message for the action to capture
|
||||||
//core.setOutput("prepare_error", e.message);
|
//core.setOutput("prepare_error", e.message);
|
||||||
} finally {
|
} finally {
|
||||||
await restoreGitConfig()
|
if (!useGithubToken) {
|
||||||
await revokeAppToken()
|
await restoreGitConfig()
|
||||||
|
await revokeAppToken()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
process.exit(exitCode)
|
process.exit(exitCode)
|
||||||
|
|
||||||
|
|
@ -544,6 +559,14 @@ export const GithubRunCommand = cmd({
|
||||||
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
|
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeUseGithubToken() {
|
||||||
|
const value = process.env["USE_GITHUB_TOKEN"]
|
||||||
|
if (!value) return false
|
||||||
|
if (value === "true") return true
|
||||||
|
if (value === "false") return false
|
||||||
|
throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
|
||||||
|
}
|
||||||
|
|
||||||
function isIssueCommentEvent(
|
function isIssueCommentEvent(
|
||||||
event: IssueCommentEvent | PullRequestReviewCommentEvent,
|
event: IssueCommentEvent | PullRequestReviewCommentEvent,
|
||||||
): event is IssueCommentEvent {
|
): event is IssueCommentEvent {
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ function App() {
|
||||||
let continued = false
|
let continued = false
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (continued || sync.status !== "complete" || !args.continue) return
|
if (continued || sync.status !== "complete" || !args.continue) return
|
||||||
const match = sync.data.session.at(0)?.id
|
const match = sync.data.session.find((x) => x.parentID === undefined)?.id
|
||||||
if (match) {
|
if (match) {
|
||||||
continued = true
|
continued = true
|
||||||
route.navigate({ type: "session", sessionID: match })
|
route.navigate({ type: "session", sessionID: match })
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,17 @@ export const AttachCommand = cmd({
|
||||||
.option("dir", {
|
.option("dir", {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "directory to run in",
|
description: "directory to run in",
|
||||||
|
})
|
||||||
|
.option("session", {
|
||||||
|
alias: ["s"],
|
||||||
|
type: "string",
|
||||||
|
describe: "session id to continue",
|
||||||
}),
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
if (args.dir) process.chdir(args.dir)
|
if (args.dir) process.chdir(args.dir)
|
||||||
await tui({
|
await tui({
|
||||||
url: args.url,
|
url: args.url,
|
||||||
args: {},
|
args: { sessionID: args.session },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -705,8 +705,8 @@ export function Prompt(props: PromptProps) {
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||||
textColor={theme.text}
|
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||||
focusedTextColor={theme.text}
|
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
||||||
minHeight={1}
|
minHeight={1}
|
||||||
maxHeight={6}
|
maxHeight={6}
|
||||||
onContentChange={() => {
|
onContentChange={() => {
|
||||||
|
|
@ -854,7 +854,7 @@ export function Prompt(props: PromptProps) {
|
||||||
</text>
|
</text>
|
||||||
<Show when={store.mode === "normal"}>
|
<Show when={store.mode === "normal"}>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text flexShrink={0} fg={theme.text}>
|
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
||||||
{local.model.parsed().model}
|
{local.model.parsed().model}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||||
|
|
@ -869,25 +869,17 @@ export function Prompt(props: PromptProps) {
|
||||||
borderColor={highlight()}
|
borderColor={highlight()}
|
||||||
customBorderChars={{
|
customBorderChars={{
|
||||||
...EmptyBorder,
|
...EmptyBorder,
|
||||||
// when the background is transparent, don't draw the vertical line
|
vertical: "╹",
|
||||||
vertical: theme.background.a != 0 ? "╹" : " ",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<box
|
<box
|
||||||
height={1}
|
height={1}
|
||||||
border={["bottom"]}
|
border={["bottom"]}
|
||||||
borderColor={theme.backgroundElement}
|
borderColor={theme.backgroundElement}
|
||||||
customBorderChars={
|
customBorderChars={{
|
||||||
theme.background.a != 0
|
...EmptyBorder,
|
||||||
? {
|
horizontal: "▀",
|
||||||
...EmptyBorder,
|
}}
|
||||||
horizontal: "▀",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
...EmptyBorder,
|
|
||||||
horizontal: " ",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,10 @@ export function Sidebar(props: { sessionID: string }) {
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<text fg={theme.text}>{directory()}</text>
|
<text>
|
||||||
|
<span style={{ fg: theme.textMuted }}>{directory().split("/").slice(0, -1).join("/")}/</span>
|
||||||
|
<span style={{ fg: theme.text }}>{directory().split("/").at(-1)}</span>
|
||||||
|
</text>
|
||||||
<text fg={theme.textMuted}>
|
<text fg={theme.textMuted}>
|
||||||
<span style={{ fg: theme.success }}>•</span> <b>Open</b>
|
<span style={{ fg: theme.success }}>•</span> <b>Open</b>
|
||||||
<span style={{ fg: theme.text }}>
|
<span style={{ fg: theme.text }}>
|
||||||
|
|
|
||||||
|
|
@ -275,3 +275,21 @@ export const terraform: Info = {
|
||||||
return Bun.which("terraform") !== null
|
return Bun.which("terraform") !== null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const latexindent: Info = {
|
||||||
|
name: "latexindent",
|
||||||
|
command: ["latexindent", "-w", "-s", "$FILE"],
|
||||||
|
extensions: [".tex"],
|
||||||
|
async enabled() {
|
||||||
|
return Bun.which("latexindent") !== null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gleam: Info = {
|
||||||
|
name: "gleam",
|
||||||
|
command: ["gleam", "format", "$FILE"],
|
||||||
|
extensions: [".gleam"],
|
||||||
|
async enabled() {
|
||||||
|
return Bun.which("gleam") !== null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
||||||
".gitrebase": "git-rebase",
|
".gitrebase": "git-rebase",
|
||||||
".go": "go",
|
".go": "go",
|
||||||
".groovy": "groovy",
|
".groovy": "groovy",
|
||||||
|
".gleam": "gleam",
|
||||||
".hbs": "handlebars",
|
".hbs": "handlebars",
|
||||||
".handlebars": "handlebars",
|
".handlebars": "handlebars",
|
||||||
".hs": "haskell",
|
".hs": "haskell",
|
||||||
|
|
|
||||||
|
|
@ -1386,4 +1386,145 @@ export namespace LSPServer {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TexLab: Info = {
|
||||||
|
id: "texlab",
|
||||||
|
extensions: [".tex", ".bib"],
|
||||||
|
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
|
||||||
|
async spawn(root) {
|
||||||
|
let bin = Bun.which("texlab", {
|
||||||
|
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!bin) {
|
||||||
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
log.info("downloading texlab from GitHub releases")
|
||||||
|
|
||||||
|
const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
|
||||||
|
if (!response.ok) {
|
||||||
|
log.error("Failed to fetch texlab release info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const release = (await response.json()) as {
|
||||||
|
tag_name?: string
|
||||||
|
assets?: { name?: string; browser_download_url?: string }[]
|
||||||
|
}
|
||||||
|
const version = release.tag_name?.replace("v", "")
|
||||||
|
if (!version) {
|
||||||
|
log.error("texlab release did not include a version tag")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = process.platform
|
||||||
|
const arch = process.arch
|
||||||
|
|
||||||
|
const texArch = arch === "arm64" ? "aarch64" : "x86_64"
|
||||||
|
const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
|
||||||
|
const ext = platform === "win32" ? "zip" : "tar.gz"
|
||||||
|
const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
|
||||||
|
|
||||||
|
const assets = release.assets ?? []
|
||||||
|
const asset = assets.find((a) => a.name === assetName)
|
||||||
|
if (!asset?.browser_download_url) {
|
||||||
|
log.error(`Could not find asset ${assetName} in texlab release`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadResponse = await fetch(asset.browser_download_url)
|
||||||
|
if (!downloadResponse.ok) {
|
||||||
|
log.error("Failed to download texlab")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempPath = path.join(Global.Path.bin, assetName)
|
||||||
|
await Bun.file(tempPath).write(downloadResponse)
|
||||||
|
|
||||||
|
if (ext === "zip") {
|
||||||
|
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||||
|
}
|
||||||
|
if (ext === "tar.gz") {
|
||||||
|
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.rm(tempPath, { force: true })
|
||||||
|
|
||||||
|
bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
|
||||||
|
|
||||||
|
if (!(await Bun.file(bin).exists())) {
|
||||||
|
log.error("Failed to extract texlab binary")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform !== "win32") {
|
||||||
|
await $`chmod +x ${bin}`.nothrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("installed texlab", { bin })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
process: spawn(bin, {
|
||||||
|
cwd: root,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DockerfileLS: Info = {
|
||||||
|
id: "dockerfile",
|
||||||
|
extensions: [".dockerfile", "Dockerfile"],
|
||||||
|
root: async () => Instance.directory,
|
||||||
|
async spawn(root) {
|
||||||
|
let binary = Bun.which("docker-langserver")
|
||||||
|
const args: string[] = []
|
||||||
|
if (!binary) {
|
||||||
|
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
|
||||||
|
if (!(await Bun.file(js).exists())) {
|
||||||
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
|
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
||||||
|
cwd: Global.Path.bin,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
stdin: "pipe",
|
||||||
|
}).exited
|
||||||
|
}
|
||||||
|
binary = BunProc.which()
|
||||||
|
args.push("run", js)
|
||||||
|
}
|
||||||
|
args.push("--stdio")
|
||||||
|
const proc = spawn(binary, args, {
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
process: proc,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Gleam: Info = {
|
||||||
|
id: "gleam",
|
||||||
|
extensions: [".gleam"],
|
||||||
|
root: NearestRoot(["gleam.toml"]),
|
||||||
|
async spawn(root) {
|
||||||
|
const gleam = Bun.which("gleam")
|
||||||
|
if (!gleam) {
|
||||||
|
log.info("gleam not found, please install gleam first")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
process: spawn(gleam, ["lsp"], {
|
||||||
|
cwd: root,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,14 +199,29 @@ export namespace ProviderTransform {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function temperature(model: Provider.Model) {
|
export function temperature(model: Provider.Model) {
|
||||||
if (model.api.id.toLowerCase().includes("qwen")) return 0.55
|
const id = model.id.toLowerCase()
|
||||||
if (model.api.id.toLowerCase().includes("claude")) return undefined
|
if (id.includes("qwen")) return 0.55
|
||||||
if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
|
if (id.includes("claude")) return undefined
|
||||||
return 0
|
if (id.includes("gemini-3-pro")) return 1.0
|
||||||
|
if (id.includes("glm-4.6")) return 1.0
|
||||||
|
if (id.includes("minimax-m2")) return 1.0
|
||||||
|
// if (id.includes("kimi-k2")) {
|
||||||
|
// if (id.includes("thinking")) return 1.0
|
||||||
|
// return 0.6
|
||||||
|
// }
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function topP(model: Provider.Model) {
|
export function topP(model: Provider.Model) {
|
||||||
if (model.api.id.toLowerCase().includes("qwen")) return 1
|
const id = model.id.toLowerCase()
|
||||||
|
if (id.includes("qwen")) return 1
|
||||||
|
if (id.includes("minimax-m2")) return 0.95
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function topK(model: Provider.Model) {
|
||||||
|
const id = model.id.toLowerCase()
|
||||||
|
if (id.includes("minimax-m2")) return 40
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type { MessageV2 } from "./message-v2"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
import { ToolRegistry } from "@/tool/registry"
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
|
import { Flag } from "@/flag/flag"
|
||||||
|
|
||||||
export namespace LLM {
|
export namespace LLM {
|
||||||
const log = Log.create({ service: "llm" })
|
const log = Log.create({ service: "llm" })
|
||||||
|
|
@ -133,6 +134,7 @@ export namespace LLM {
|
||||||
"x-opencode-project": Instance.project.id,
|
"x-opencode-project": Instance.project.id,
|
||||||
"x-opencode-session": input.sessionID,
|
"x-opencode-session": input.sessionID,
|
||||||
"x-opencode-request": input.user.id,
|
"x-opencode-request": input.user.id,
|
||||||
|
"x-opencode-client": Flag.OPENCODE_CLIENT,
|
||||||
}
|
}
|
||||||
: undefined),
|
: undefined),
|
||||||
...input.model.headers,
|
...input.model.headers,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,15 @@ export namespace SessionRetry {
|
||||||
if (json.code === "Some resource has been exhausted") {
|
if (json.code === "Some resource has been exhausted") {
|
||||||
return "Provider is overloaded"
|
return "Provider is overloaded"
|
||||||
}
|
}
|
||||||
|
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
|
||||||
|
return "Rate Limited"
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
json.error?.message?.includes("no_kv_space") ||
|
||||||
|
(json.type === "error" && json.error?.type === "server_error")
|
||||||
|
) {
|
||||||
|
return "Provider Server Error"
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import { Instance } from "../project/instance"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
|
|
||||||
|
const MAX_DIAGNOSTICS_PER_FILE = 20
|
||||||
|
|
||||||
function normalizeLineEndings(text: string): string {
|
function normalizeLineEndings(text: string): string {
|
||||||
return text.replaceAll("\r\n", "\n")
|
return text.replaceAll("\r\n", "\n")
|
||||||
}
|
}
|
||||||
|
|
@ -141,10 +143,11 @@ export const EditTool = Tool.define("edit", {
|
||||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||||
if (issues.length === 0) continue
|
if (issues.length === 0) continue
|
||||||
if (file === filePath) {
|
if (file === filePath) {
|
||||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues
|
const errors = issues.filter((item) => item.severity === 1)
|
||||||
.filter((item) => item.severity === 1)
|
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
|
||||||
.map(LSP.Diagnostic.pretty)
|
const suffix =
|
||||||
.join("\n")}\n</file_diagnostics>\n`
|
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
|
||||||
|
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import { Filesystem } from "../util/filesystem"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
|
|
||||||
|
const MAX_DIAGNOSTICS_PER_FILE = 20
|
||||||
|
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
|
||||||
|
|
||||||
export const WriteTool = Tool.define("write", {
|
export const WriteTool = Tool.define("write", {
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
|
|
@ -77,13 +80,20 @@ export const WriteTool = Tool.define("write", {
|
||||||
let output = ""
|
let output = ""
|
||||||
await LSP.touchFile(filepath, true)
|
await LSP.touchFile(filepath, true)
|
||||||
const diagnostics = await LSP.diagnostics()
|
const diagnostics = await LSP.diagnostics()
|
||||||
|
let projectDiagnosticsCount = 0
|
||||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||||
if (issues.length === 0) continue
|
if (issues.length === 0) continue
|
||||||
|
const sorted = issues.toSorted((a, b) => (a.severity ?? 4) - (b.severity ?? 4))
|
||||||
|
const limited = sorted.slice(0, MAX_DIAGNOSTICS_PER_FILE)
|
||||||
|
const suffix =
|
||||||
|
issues.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${issues.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
|
||||||
if (file === filepath) {
|
if (file === filepath) {
|
||||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
|
if (projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
|
||||||
|
projectDiagnosticsCount++
|
||||||
|
output += `\n<project_diagnostics>\n${file}\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</project_diagnostics>\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|
@ -24,4 +24,4 @@
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:"
|
"@typescript/native-preview": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|
@ -29,4 +29,4 @@
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"directory": "dist"
|
"directory": "dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/tauri",
|
"name": "@opencode-ai/tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo -b",
|
"typecheck": "tsgo -b",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "OpenCode",
|
"productName": "OpenCode",
|
||||||
"mainBinaryName": "OpenCode Desktop",
|
"mainBinaryName": "OpenCode",
|
||||||
"version": "../package.json",
|
"version": "../package.json",
|
||||||
"identifier": "ai.opencode.desktop",
|
"identifier": "ai.opencode.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["deb", "rpm", "dmg", "nsis"],
|
"targets": ["deb", "rpm", "dmg", "nsis", "appimage"],
|
||||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||||
"externalBin": ["sidecars/opencode"],
|
"externalBin": ["sidecars/opencode"],
|
||||||
"createUpdaterArtifacts": true,
|
"createUpdaterArtifacts": true,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/components/*.tsx",
|
"./*": "./src/components/*.tsx",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ export type TriggerTitle = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTriggerTitle = (val: any): val is TriggerTitle => {
|
const isTriggerTitle = (val: any): val is TriggerTitle => {
|
||||||
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
|
return (
|
||||||
|
typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BasicToolProps {
|
export interface BasicToolProps {
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@
|
||||||
padding: 0 12px 0 8px;
|
padding: 0 12px 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
gap: 4px;
|
gap: 8px;
|
||||||
|
|
||||||
/* text-14-medium */
|
/* text-14-medium */
|
||||||
font-family: var(--font-family-sans);
|
font-family: var(--font-family-sans);
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,6 @@ ToolRegistry.register({
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
<BasicTool
|
<BasicTool
|
||||||
defaultOpen
|
|
||||||
icon="console"
|
icon="console"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "Shell",
|
title: "Shell",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
[data-component="session-turn"] {
|
[data-component="session-turn"] {
|
||||||
/* flex: 1; */
|
/* flex: 1; */
|
||||||
|
--scroll-y: 0px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -26,18 +27,27 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 32px;
|
gap: clamp(8px, calc(42px - var(--scroll-y) * 0.48), 42px);
|
||||||
|
overflow-anchor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-sticky-header"] {
|
[data-slot="session-turn-sticky-title"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: var(--background-stronger);
|
background-color: var(--background-stronger);
|
||||||
|
z-index: 21;
|
||||||
|
/* padding-bottom: clamp(0px, calc(8px - var(--scroll-y) * 0.16), 8px); */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-turn-response-trigger"] {
|
||||||
|
position: sticky;
|
||||||
|
top: 32px;
|
||||||
|
background-color: var(--background-stronger);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: flex;
|
width: calc(100% + 9px);
|
||||||
flex-direction: column;
|
margin-left: -9px;
|
||||||
gap: 8px;
|
padding-left: 9px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,13 +59,8 @@
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* [data-slot="session-turn-message-content"] { */
|
[data-slot="session-turn-message-content"] {
|
||||||
/* } */
|
margin-top: -24px;
|
||||||
|
|
||||||
[data-slot="session-turn-response-trigger"] {
|
|
||||||
width: calc(100% + 9px);
|
|
||||||
margin-left: -9px;
|
|
||||||
padding-left: 9px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-message-title"] {
|
[data-slot="session-turn-message-title"] {
|
||||||
|
|
@ -292,6 +297,7 @@
|
||||||
[data-slot="session-turn-collapsible"] {
|
[data-slot="session-turn-collapsible"] {
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
/* margin-top: clamp(8px, calc(24px - var(--scroll-y) * 0.32), 24px); */
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-collapsible-trigger-content"] {
|
[data-slot="session-turn-collapsible-trigger-content"] {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,7 @@ import { useData } from "../context"
|
||||||
import { useDiffComponent } from "../context/diff"
|
import { useDiffComponent } from "../context/diff"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
import {
|
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
Match,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
ParentProps,
|
|
||||||
Show,
|
|
||||||
Switch,
|
|
||||||
} from "solid-js"
|
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { Typewriter } from "./typewriter"
|
import { Typewriter } from "./typewriter"
|
||||||
|
|
@ -60,45 +49,77 @@ export function SessionTurn(
|
||||||
const working = createMemo(() => status()?.type !== "idle")
|
const working = createMemo(() => status()?.type !== "idle")
|
||||||
|
|
||||||
let scrollRef: HTMLDivElement | undefined
|
let scrollRef: HTMLDivElement | undefined
|
||||||
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
const [state, setState] = createStore({
|
||||||
const [stickyHeaderRef, setStickyHeaderRef] = createSignal<HTMLDivElement>()
|
contentRef: undefined as HTMLDivElement | undefined,
|
||||||
const [userScrolled, setUserScrolled] = createSignal(false)
|
stickyTitleRef: undefined as HTMLDivElement | undefined,
|
||||||
const [stickyHeaderHeight, setStickyHeaderHeight] = createSignal(0)
|
stickyTriggerRef: undefined as HTMLDivElement | undefined,
|
||||||
|
userScrolled: false,
|
||||||
|
stickyHeaderHeight: 0,
|
||||||
|
scrollY: 0,
|
||||||
|
autoScrolling: false,
|
||||||
|
})
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (!scrollRef) return
|
if (!scrollRef) return
|
||||||
|
setState("scrollY", scrollRef.scrollTop)
|
||||||
|
if (state.autoScrolling) return
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef
|
||||||
const atBottom = scrollHeight - scrollTop - clientHeight < 50
|
const atBottom = scrollHeight - scrollTop - clientHeight < 50
|
||||||
if (!atBottom && working()) {
|
if (!atBottom && working()) {
|
||||||
setUserScrolled(true)
|
setState("userScrolled", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInteraction() {
|
function handleInteraction() {
|
||||||
if (working()) {
|
if (working()) {
|
||||||
setUserScrolled(true)
|
setState("userScrolled", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
if (!scrollRef || state.userScrolled || !working() || state.autoScrolling) return
|
||||||
|
setState("autoScrolling", true)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "auto" })
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setState("autoScrolling", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!working()) {
|
if (!working()) {
|
||||||
setUserScrolled(false)
|
setState("userScrolled", false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createResizeObserver(contentRef, () => {
|
createResizeObserver(
|
||||||
if (!scrollRef || userScrolled() || !working()) return
|
() => state.contentRef,
|
||||||
scrollRef.scrollTop = scrollRef.scrollHeight
|
() => {
|
||||||
})
|
scrollToBottom()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
createResizeObserver(stickyHeaderRef, ({ height }) => {
|
createResizeObserver(
|
||||||
setStickyHeaderHeight(height + 8)
|
() => state.stickyTitleRef,
|
||||||
})
|
({ height }) => {
|
||||||
|
const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
|
||||||
|
setState("stickyHeaderHeight", height + triggerHeight + 8)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
createResizeObserver(
|
||||||
|
() => state.stickyTriggerRef,
|
||||||
|
({ height }) => {
|
||||||
|
const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
|
||||||
|
setState("stickyHeaderHeight", titleHeight + height + 8)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="session-turn" class={props.classes?.root}>
|
<div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${state.scrollY}px` }}>
|
||||||
<div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
|
<div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
|
||||||
<div ref={setContentRef} onClick={handleInteraction}>
|
<div ref={(el) => setState("contentRef", el)} onClick={handleInteraction}>
|
||||||
<Show when={message()}>
|
<Show when={message()}>
|
||||||
{(message) => {
|
{(message) => {
|
||||||
const assistantMessages = createMemo(() => {
|
const assistantMessages = createMemo(() => {
|
||||||
|
|
@ -175,6 +196,9 @@ export function SessionTurn(
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else if (last.type === "reasoning") {
|
} else if (last.type === "reasoning") {
|
||||||
|
const text = last.text ?? ""
|
||||||
|
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
|
||||||
|
if (match) return `Thinking · ${match[1].trim()}`
|
||||||
return "Thinking"
|
return "Thinking"
|
||||||
} else if (last.type === "text") {
|
} else if (last.type === "text") {
|
||||||
return "Gathering thoughts"
|
return "Gathering thoughts"
|
||||||
|
|
@ -237,7 +261,7 @@ export function SessionTurn(
|
||||||
|
|
||||||
createEffect((prev) => {
|
createEffect((prev) => {
|
||||||
const isWorking = working()
|
const isWorking = working()
|
||||||
if (prev && !isWorking && !userScrolled()) {
|
if (prev && !isWorking && !state.userScrolled) {
|
||||||
setStore("stepsExpanded", false)
|
setStore("stepsExpanded", false)
|
||||||
}
|
}
|
||||||
return isWorking
|
return isWorking
|
||||||
|
|
@ -248,10 +272,10 @@ export function SessionTurn(
|
||||||
data-message={message().id}
|
data-message={message().id}
|
||||||
data-slot="session-turn-message-container"
|
data-slot="session-turn-message-container"
|
||||||
class={props.classes?.container}
|
class={props.classes?.container}
|
||||||
style={{ "--sticky-header-height": `${stickyHeaderHeight()}px` }}
|
style={{ "--sticky-header-height": `${state.stickyHeaderHeight}px` }}
|
||||||
>
|
>
|
||||||
{/* Sticky Header */}
|
{/* Title (sticky) */}
|
||||||
<div ref={setStickyHeaderRef} data-slot="session-turn-sticky-header">
|
<div ref={(el) => setState("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
|
||||||
<div data-slot="session-turn-message-header">
|
<div data-slot="session-turn-message-header">
|
||||||
<div data-slot="session-turn-message-title">
|
<div data-slot="session-turn-message-title">
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
@ -264,29 +288,31 @@ export function SessionTurn(
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="session-turn-message-content">
|
</div>
|
||||||
<Message message={message()} parts={parts()} />
|
{/* User Message */}
|
||||||
</div>
|
<div data-slot="session-turn-message-content">
|
||||||
<div data-slot="session-turn-response-trigger">
|
<Message message={message()} parts={parts()} />
|
||||||
<Button
|
</div>
|
||||||
data-slot="session-turn-collapsible-trigger-content"
|
{/* Trigger (sticky) */}
|
||||||
variant="ghost"
|
<div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
||||||
size="small"
|
<Button
|
||||||
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
data-slot="session-turn-collapsible-trigger-content"
|
||||||
>
|
variant="ghost"
|
||||||
<Show when={working()}>
|
size="small"
|
||||||
<Spinner />
|
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
||||||
</Show>
|
>
|
||||||
<Switch>
|
<Show when={working()}>
|
||||||
<Match when={working()}>{store.status ?? "Considering next steps..."}</Match>
|
<Spinner />
|
||||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
</Show>
|
||||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
<Switch>
|
||||||
</Switch>
|
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
|
||||||
<span>·</span>
|
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||||
<span>{store.duration}</span>
|
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
</Switch>
|
||||||
</Button>
|
<span>·</span>
|
||||||
</div>
|
<span>{store.duration}</span>
|
||||||
|
<Icon name="chevron-grabber-vertical" size="small" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* Response */}
|
{/* Response */}
|
||||||
<Show when={store.stepsExpanded}>
|
<Show when={store.stepsExpanded}>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
@keyframes pulse-opacity {
|
@keyframes pulse-opacity {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.3;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export default defineConfig({
|
||||||
"",
|
"",
|
||||||
"config",
|
"config",
|
||||||
"providers",
|
"providers",
|
||||||
|
"network",
|
||||||
"enterprise",
|
"enterprise",
|
||||||
"troubleshooting",
|
"troubleshooting",
|
||||||
"1-0",
|
"1-0",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,25 @@ You can also bind a keyboard shortcut by editing your `keymap.json`:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### JetBrains IDEs
|
||||||
|
|
||||||
|
Add to your [JetBrains IDE](https://www.jetbrains.com/) acp.json according to the [documentation](https://www.jetbrains.com/help/ai-assistant/acp.html):
|
||||||
|
|
||||||
|
```json title="acp.json"
|
||||||
|
{
|
||||||
|
"agent_servers": {
|
||||||
|
"OpenCode": {
|
||||||
|
"command": "/absolute/path/bin/opencode",
|
||||||
|
"args": ["acp"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To open it, use the new 'OpenCode' agent in the AI Chat agent selector.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Avante.nvim
|
### Avante.nvim
|
||||||
|
|
||||||
Add to your [Avante.nvim](https://github.com/yetone/avante.nvim) configuration:
|
Add to your [Avante.nvim](https://github.com/yetone/avante.nvim) configuration:
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,21 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||||
|
|
||||||
## Plugins
|
## Plugins
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
||||||
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
|
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
|
||||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
||||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
||||||
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
|
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
|
||||||
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
|
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
|
||||||
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
|
| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-goggle-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
|
||||||
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
|
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
|
||||||
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
|
| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
|
||||||
|
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
|
||||||
|
| [opencode-wakatime](https://github.com/angrister/opencode-wakatime) | Track OpenCode usage with Wakatime |
|
||||||
|
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
|
||||||
|
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ OpenCode comes with several built-in formatters for popular languages and framew
|
||||||
| dart | .dart | `dart` command available |
|
| dart | .dart | `dart` command available |
|
||||||
| ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file |
|
| ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file |
|
||||||
| terraform | .tf, .tfvars | `terraform` command available |
|
| terraform | .tf, .tfvars | `terraform` command available |
|
||||||
|
| gleam | .gleam | `gleam` command available |
|
||||||
|
|
||||||
So if your project has `prettier` in your `package.json`, OpenCode will automatically use it.
|
So if your project has `prettier` in your `package.json`, OpenCode will automatically use it.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
|
||||||
| ocaml-lsp | .ml, .mli | `ocamllsp` command available |
|
| ocaml-lsp | .ml, .mli | `ocamllsp` command available |
|
||||||
| terraform | .tf, .tfvars | Auto-installs from GitHub releases |
|
| terraform | .tf, .tfvars | Auto-installs from GitHub releases |
|
||||||
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
|
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
|
||||||
|
| gleam | .gleam | `gleam` command available |
|
||||||
|
|
||||||
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
|
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
|
||||||
|
|
||||||
|
|
|
||||||
57
packages/web/src/content/docs/network.mdx
Normal file
57
packages/web/src/content/docs/network.mdx
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
title: Network
|
||||||
|
description: Configure proxies and custom certificates.
|
||||||
|
---
|
||||||
|
|
||||||
|
OpenCode supports standard proxy environment variables and custom certificates for enterprise network environments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proxy
|
||||||
|
|
||||||
|
OpenCode respects standard proxy environment variables.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTPS proxy (recommended)
|
||||||
|
export HTTPS_PROXY=https://proxy.example.com:8080
|
||||||
|
|
||||||
|
# HTTP proxy (if HTTPS not available)
|
||||||
|
export HTTP_PROXY=http://proxy.example.com:8080
|
||||||
|
|
||||||
|
# Bypass proxy for local server (required)
|
||||||
|
export NO_PROXY=localhost,127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
The TUI communicates with a local HTTP server. You must bypass the proxy for this connection to prevent routing loops.
|
||||||
|
:::
|
||||||
|
|
||||||
|
You can configure the server's port and hostname using [CLI flags](/docs/cli#run).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Authenticate
|
||||||
|
|
||||||
|
If your proxy requires basic authentication, include credentials in the URL.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export HTTPS_PROXY=http://username:password@proxy.example.com:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
Avoid hardcoding passwords. Use environment variables or secure credential storage.
|
||||||
|
:::
|
||||||
|
|
||||||
|
For proxies requiring advanced authentication like NTLM or Kerberos, consider using an LLM Gateway that supports your authentication method.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom certificates
|
||||||
|
|
||||||
|
If your enterprise uses custom CAs for HTTPS connections, configure OpenCode to trust them.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
This works for both proxy connections and direct API access.
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"displayName": "opencode",
|
"displayName": "opencode",
|
||||||
"description": "opencode for VS Code",
|
"description": "opencode for VS Code",
|
||||||
"version": "1.0.152",
|
"version": "1.0.153",
|
||||||
"publisher": "sst-dev",
|
"publisher": "sst-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue