Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e2ac94a91 | ||
|
|
52d7475dbf | ||
|
|
49d9f99924 | ||
|
|
1f9e195cd8 | ||
|
|
539d6baa8c | ||
|
|
f6fc693c1f | ||
|
|
50d8396c9a | ||
|
|
22dd70b75b | ||
|
|
b4f8de0c0a | ||
|
|
768e0553bd | ||
|
|
f3e79235fb | ||
|
|
eacf3ad361 | ||
|
|
72062d22a0 | ||
|
|
9f90f0e8ed | ||
|
|
c74bc323b6 | ||
|
|
c7b825a42a | ||
|
|
b1a613b3b9 | ||
|
|
958f1edfef | ||
|
|
2bb299d741 |
@@ -70,20 +70,23 @@ jobs:
|
||||
}
|
||||
|
||||
// Step 1: Check title format
|
||||
const validPrefixes = ['feat:', 'fix:', 'docs:', 'chore:', 'refactor:', 'test:'];
|
||||
const hasValidTitle = validPrefixes.some(prefix => title.startsWith(prefix));
|
||||
// Matches: feat:, feat(scope):, feat (scope):, etc.
|
||||
const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/;
|
||||
const hasValidTitle = titlePattern.test(title);
|
||||
|
||||
if (!hasValidTitle) {
|
||||
await addLabel('needs:title');
|
||||
await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format.
|
||||
|
||||
Please update it to start with one of:
|
||||
- \`feat:\` new feature
|
||||
- \`fix:\` bug fix
|
||||
- \`docs:\` documentation changes
|
||||
- \`chore:\` maintenance tasks
|
||||
- \`refactor:\` code refactoring
|
||||
- \`test:\` adding or updating tests
|
||||
- \`feat:\` or \`feat(scope):\` new feature
|
||||
- \`fix:\` or \`fix(scope):\` bug fix
|
||||
- \`docs:\` or \`docs(scope):\` documentation changes
|
||||
- \`chore:\` or \`chore(scope):\` maintenance tasks
|
||||
- \`refactor:\` or \`refactor(scope):\` code refactoring
|
||||
- \`test:\` or \`test(scope):\` adding or updating tests
|
||||
|
||||
Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`).
|
||||
|
||||
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`);
|
||||
return;
|
||||
@@ -92,7 +95,7 @@ jobs:
|
||||
await removeLabel('needs:title');
|
||||
|
||||
// Step 2: Check for linked issue (skip for docs/refactor PRs)
|
||||
const skipIssueCheck = title.startsWith('docs:') || title.startsWith('refactor:');
|
||||
const skipIssueCheck = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
|
||||
if (skipIssueCheck) {
|
||||
await removeLabel('needs:issue');
|
||||
console.log('Skipping issue check for docs/refactor PR');
|
||||
|
||||
@@ -177,8 +177,22 @@ jobs:
|
||||
cargo tauri --version
|
||||
|
||||
- name: Build and upload artifacts
|
||||
timeout-minutes: 20
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
uses: Wandalen/wretry.action@v3
|
||||
timeout-minutes: 60
|
||||
with:
|
||||
attempt_limit: 3
|
||||
attempt_delay: 10000
|
||||
action: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
with: |
|
||||
projectPath: packages/desktop
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
|
||||
updaterJsonPreferNsis: true
|
||||
releaseId: ${{ needs.publish.outputs.release }}
|
||||
tagName: ${{ needs.publish.outputs.tag }}
|
||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
releaseDraft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
||||
@@ -190,16 +204,6 @@ jobs:
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
with:
|
||||
projectPath: packages/desktop
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
|
||||
updaterJsonPreferNsis: true
|
||||
releaseId: ${{ needs.publish.outputs.release }}
|
||||
tagName: ${{ needs.publish.outputs.tag }}
|
||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
releaseDraft: true
|
||||
|
||||
publish-release:
|
||||
needs:
|
||||
|
||||
@@ -192,11 +192,19 @@ PR titles should follow conventional commit standards:
|
||||
- `refactor:` code refactoring without changing behavior
|
||||
- `test:` adding or updating tests
|
||||
|
||||
You can optionally include a scope to indicate which package is affected:
|
||||
|
||||
- `feat(app):` feature in the app package
|
||||
- `fix(desktop):` bug fix in the desktop package
|
||||
- `chore(opencode):` maintenance in the opencode package
|
||||
|
||||
Examples:
|
||||
|
||||
- `docs: update contributing guidelines`
|
||||
- `fix: resolve crash on startup`
|
||||
- `feat: add dark mode support`
|
||||
- `feat(app): add dark mode support`
|
||||
- `fix(desktop): resolve crash on startup`
|
||||
- `chore: bump dependency versions`
|
||||
|
||||
### Style Preferences
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -173,7 +173,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -202,7 +202,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -231,7 +231,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -247,7 +247,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -286,8 +286,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.69",
|
||||
"@opentui/solid": "0.1.69",
|
||||
"@opentui/core": "0.1.70",
|
||||
"@opentui/solid": "0.1.70",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -350,7 +350,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -370,7 +370,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -381,7 +381,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -394,7 +394,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -433,7 +433,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -444,7 +444,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1201,21 +1201,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.69", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.69", "@opentui/core-darwin-x64": "0.1.69", "@opentui/core-linux-arm64": "0.1.69", "@opentui/core-linux-x64": "0.1.69", "@opentui/core-win32-arm64": "0.1.69", "@opentui/core-win32-x64": "0.1.69", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-BcEFnAuMq4vgfb+zxOP/l+NO1AS3fVHkYjn+E8Wpmaxr0AzWNTi2NPAMtQf+Wqufxo0NYh0gY4c9B6n8OxTjGw=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.70", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.70", "@opentui/core-darwin-x64": "0.1.70", "@opentui/core-linux-arm64": "0.1.70", "@opentui/core-linux-x64": "0.1.70", "@opentui/core-win32-arm64": "0.1.70", "@opentui/core-win32-x64": "0.1.70", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-6cPAlbCnaiUUtQtvZNpkr0Xv8AdVAgJuy2VAwIsDN1pIv0zMpa0ZG+mr7afCGygw1eeDRveefrjfgFAB1r0SVw=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.69", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d9RPAh84O2XIyMw+7+X0fEyi+4KH5sPk9AxLze8GHRBGOzkRunqagFCLBrN5VFs2e2nbhIYtjMszo7gcpWyh7g=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.70", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rM8EnvW1tOAXWnp2Iy2M82I+ViSmRwUagx3v1/ni6N8GCcw/3mE0C6eB3sVlYNXVMwBEgiKpWFn85RCe4+qXQw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.69", "", { "os": "darwin", "cpu": "x64" }, "sha512-41K9zkL2IG0ahL+8Gd+e9ulMrnJF6lArPzG7grjWzo+FWEZwvw0WLCO1/Gn5K85G8Yx7gQXkZOUaw1BmHjxoRw=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.70", "", { "os": "darwin", "cpu": "x64" }, "sha512-XdBgW+em8J+YGSUpaKF8/NxPjikJygK3dIkeMAw5xQ2lt7jXKxeM5MMmN/V4MfK3pLMtO56rLJlXaLH/h50uQA=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.69", "", { "os": "linux", "cpu": "arm64" }, "sha512-IcUjwjuIpX3BBG1a9kjMqWrHYCFHAVfjh5nIRozWZZoqaczLzJb3nJeF2eg8aDeIoGhXvERWB1r1gmqPW8u3vQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.70", "", { "os": "linux", "cpu": "arm64" }, "sha512-oSVWNMSOx0Na0M0LCqtWCxeh4SuLSK5lg8ZwVzsEoimIAxh0snp9nRUo/Qi8yD9BP0DSDmXuM/B3ONtzFaf0dw=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.69", "", { "os": "linux", "cpu": "x64" }, "sha512-5S9vqEIq7q+MEdp4cT0HLegBWu0pWLcletHZL80bsLbJt9OT8en3sQmL5bvas9sIuyeBFru9bfCmrQ/gnVTTiA=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.70", "", { "os": "linux", "cpu": "x64" }, "sha512-WUrhukefMghcZ7sAjkxEy50vA6ii0X21xh7m8c4omXyYYfQXyDs25pNExB8cwoCrZEaC8RTlF4lRSNPIXsZKhA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.69", "", { "os": "win32", "cpu": "arm64" }, "sha512-eSKcGwbcnJJPtrTFJI7STZ7inSYeedHS0swwjZhh9SADAruEz08intamunOslffv5+mnlvRp7UBGK35cMjbv/w=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.70", "", { "os": "win32", "cpu": "arm64" }, "sha512-p1K2VJXGmZqSV7mR61v7KJpT1Zth7DS99wEtaqqfK68OWt33K2XxLmGO0KD142R2JLfXu32NnRmBHxmVx8IjBA=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.69", "", { "os": "win32", "cpu": "x64" }, "sha512-OjG/0jqYXURqbbUwNgSPrBA6yuKF3OOFh8JSG7VvzoYHJFJRmwVWY0fztWv/hgGHe354ti37c7JDJBQ44HOCdA=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.70", "", { "os": "win32", "cpu": "x64" }, "sha512-G6b8te1twMeDhjg1oZa0IcUjhOJZFCSdlQt+q5gu5vVtjCrIwAn9o7m5EwNMPakc31pDWUZ7v0ktgv0Xw1AQVA=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.69", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.69", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-ls589N8P9gvcNW8uF+Il4xisF5Uouk0RRmSaLdzmItNJSW5J9Y0nPtMELta6hBp0yIRAurWUO1wtkKXVF+eaxg=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.70", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.70", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-8Cw/w4Of2OJhsFhcp/Wdj8cJRVaGvVsIiUoYiFtyToM01J4en0bg/vnbeZteyuZWeEtA4iz1/rSEQf7Dp+2FIQ=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-rNGq0yjL5ZHYVg+zyV4nFPug4gqhKhyOnfebaufyd34="
|
||||
"nodeModules": "sha256-KjBAaI9Kv6huOmPvUbtyYsMhbScI91w1lOZyXpIWqI0="
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -276,7 +276,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
visible(model: ModelKey) {
|
||||
const key = `${model.providerID}:${model.modelID}`
|
||||
const visibility = userVisibilityMap().get(key)
|
||||
return visibility !== "hide" && (latestSet().has(key) || visibility === "show")
|
||||
if (visibility === "hide") return false
|
||||
if (visibility === "show") return true
|
||||
if (latestSet().has(key)) return true
|
||||
// For models without valid release_date (e.g. custom models), show by default
|
||||
const m = find(model)
|
||||
if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
|
||||
return false
|
||||
},
|
||||
setVisibility(model: ModelKey, visible: boolean) {
|
||||
updateVisibility(model, visible ? "show" : "hide")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.1.6"
|
||||
version = "1.1.7"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.7/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -81,8 +81,8 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.69",
|
||||
"@opentui/solid": "0.1.69",
|
||||
"@opentui/core": "0.1.70",
|
||||
"@opentui/solid": "0.1.70",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
|
||||
@@ -167,6 +167,8 @@ export namespace ACP {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: part.callID,
|
||||
status: "in_progress",
|
||||
kind: toToolKind(part.tool),
|
||||
title: part.tool,
|
||||
locations: toLocations(part.tool, part.state.input),
|
||||
rawInput: part.state.input,
|
||||
},
|
||||
@@ -242,6 +244,7 @@ export namespace ACP {
|
||||
kind,
|
||||
content,
|
||||
title: part.state.title,
|
||||
rawInput: part.state.input,
|
||||
rawOutput: {
|
||||
output: part.state.output,
|
||||
metadata: part.state.metadata,
|
||||
@@ -260,6 +263,9 @@ export namespace ACP {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: part.callID,
|
||||
status: "failed",
|
||||
kind: toToolKind(part.tool),
|
||||
title: part.tool,
|
||||
rawInput: part.state.input,
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
@@ -491,6 +497,8 @@ export namespace ACP {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: part.callID,
|
||||
status: "in_progress",
|
||||
kind: toToolKind(part.tool),
|
||||
title: part.tool,
|
||||
locations: toLocations(part.tool, part.state.input),
|
||||
rawInput: part.state.input,
|
||||
},
|
||||
@@ -566,6 +574,7 @@ export namespace ACP {
|
||||
kind,
|
||||
content,
|
||||
title: part.state.title,
|
||||
rawInput: part.state.input,
|
||||
rawOutput: {
|
||||
output: part.state.output,
|
||||
metadata: part.state.metadata,
|
||||
@@ -584,6 +593,9 @@ export namespace ACP {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: part.callID,
|
||||
status: "failed",
|
||||
kind: toToolKind(part.tool),
|
||||
title: part.tool,
|
||||
rawInput: part.state.input,
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
|
||||
@@ -653,9 +653,17 @@ function ErrorComponent(props: {
|
||||
mode?: "dark" | "light"
|
||||
}) {
|
||||
const term = useTerminalDimensions()
|
||||
const renderer = useRenderer()
|
||||
|
||||
const handleExit = async () => {
|
||||
renderer.setTerminalTitle("")
|
||||
renderer.destroy()
|
||||
props.onExit()
|
||||
}
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
props.onExit()
|
||||
handleExit()
|
||||
}
|
||||
})
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
@@ -708,7 +716,7 @@ function ErrorComponent(props: {
|
||||
<box onMouseUp={props.reset} backgroundColor={colors.primary} padding={1}>
|
||||
<text fg={colors.bg}>Reset TUI</text>
|
||||
</box>
|
||||
<box onMouseUp={props.onExit} backgroundColor={colors.primary} padding={1}>
|
||||
<box onMouseUp={handleExit} backgroundColor={colors.primary} padding={1}>
|
||||
<text fg={colors.bg}>Exit</text>
|
||||
</box>
|
||||
</box>
|
||||
|
||||
@@ -53,6 +53,7 @@ export type AutocompleteRef = {
|
||||
|
||||
export type AutocompleteOption = {
|
||||
display: string
|
||||
value?: string
|
||||
aliases?: string[]
|
||||
disabled?: boolean
|
||||
description?: string
|
||||
@@ -221,6 +222,7 @@ export function Autocomplete(props: {
|
||||
const isDir = item.endsWith("/")
|
||||
return {
|
||||
display: Locale.truncateMiddle(filename, width),
|
||||
value: filename,
|
||||
isDirectory: isDir,
|
||||
path: item,
|
||||
onSelect: () => {
|
||||
@@ -259,8 +261,10 @@ export function Autocomplete(props: {
|
||||
const width = props.anchor().width - 4
|
||||
|
||||
for (const res of Object.values(sync.data.mcp_resource)) {
|
||||
const text = `${res.name} (${res.uri})`
|
||||
options.push({
|
||||
display: Locale.truncateMiddle(`${res.name} (${res.uri})`, width),
|
||||
display: Locale.truncateMiddle(text, width),
|
||||
value: text,
|
||||
description: res.description,
|
||||
onSelect: () => {
|
||||
insertPart(res.name, {
|
||||
@@ -485,7 +489,11 @@ export function Autocomplete(props: {
|
||||
}
|
||||
|
||||
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
|
||||
keys: [(obj) => removeLineRange(obj.display.trimEnd()), "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||
keys: [
|
||||
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
||||
"description",
|
||||
(obj) => obj.aliases?.join(" ") ?? "",
|
||||
],
|
||||
limit: 10,
|
||||
scoreFn: (objResults) => {
|
||||
const displayResult = objResults[0]
|
||||
|
||||
@@ -1303,8 +1303,16 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
|
||||
// Pending messages moved to individual tool pending functions
|
||||
|
||||
function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMessage }) {
|
||||
const ctx = use()
|
||||
const sync = useSync()
|
||||
|
||||
// Hide tool if showDetails is false and tool completed successfully
|
||||
const shouldHide = createMemo(() => {
|
||||
if (ctx.showDetails()) return false
|
||||
if (props.part.state.status !== "completed") return false
|
||||
return true
|
||||
})
|
||||
|
||||
const toolprops = {
|
||||
get metadata() {
|
||||
return props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
|
||||
@@ -1329,53 +1337,55 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={props.part.tool === "bash"}>
|
||||
<Bash {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "glob"}>
|
||||
<Glob {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "read"}>
|
||||
<Read {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "grep"}>
|
||||
<Grep {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "list"}>
|
||||
<List {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "webfetch"}>
|
||||
<WebFetch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "codesearch"}>
|
||||
<CodeSearch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "websearch"}>
|
||||
<WebSearch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "write"}>
|
||||
<Write {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "edit"}>
|
||||
<Edit {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "task"}>
|
||||
<Task {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "patch"}>
|
||||
<Patch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "todowrite"}>
|
||||
<TodoWrite {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "question"}>
|
||||
<Question {...toolprops} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<GenericTool {...toolprops} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={!shouldHide()}>
|
||||
<Switch>
|
||||
<Match when={props.part.tool === "bash"}>
|
||||
<Bash {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "glob"}>
|
||||
<Glob {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "read"}>
|
||||
<Read {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "grep"}>
|
||||
<Grep {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "list"}>
|
||||
<List {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "webfetch"}>
|
||||
<WebFetch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "codesearch"}>
|
||||
<CodeSearch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "websearch"}>
|
||||
<WebSearch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "write"}>
|
||||
<Write {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "edit"}>
|
||||
<Edit {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "task"}>
|
||||
<Task {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "patch"}>
|
||||
<Patch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "todowrite"}>
|
||||
<TodoWrite {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.tool === "question"}>
|
||||
<Question {...toolprops} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<GenericTool {...toolprops} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1830,6 +1840,12 @@ function TodoWrite(props: ToolProps<typeof TodoWriteTool>) {
|
||||
function Question(props: ToolProps<typeof QuestionTool>) {
|
||||
const { theme } = useTheme()
|
||||
const count = createMemo(() => props.input.questions?.length ?? 0)
|
||||
|
||||
function format(answer?: string[]) {
|
||||
if (!answer?.length) return "(no answer)"
|
||||
return answer.join(", ")
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={props.metadata.answers}>
|
||||
@@ -1839,7 +1855,7 @@ function Question(props: ToolProps<typeof QuestionTool>) {
|
||||
{(q, i) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.textMuted}>{q.question}</text>
|
||||
<text fg={theme.text}>{props.metadata.answers?.[i()] || "(no answer)"}</text>
|
||||
<text fg={theme.text}>{format(props.metadata.answers?.[i()])}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useKeyboard } from "@opentui/solid"
|
||||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { SplitBorder } from "../../component/border"
|
||||
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
||||
@@ -17,11 +17,11 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
const bindings = useTextareaKeybindings()
|
||||
|
||||
const questions = createMemo(() => props.request.questions)
|
||||
const single = createMemo(() => questions().length === 1)
|
||||
const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single)
|
||||
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
|
||||
const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single select)
|
||||
const [store, setStore] = createStore({
|
||||
tab: 0,
|
||||
answers: [] as string[],
|
||||
answers: [] as QuestionAnswer[],
|
||||
custom: [] as string[],
|
||||
selected: 0,
|
||||
editing: false,
|
||||
@@ -34,10 +34,15 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
const options = createMemo(() => question()?.options ?? [])
|
||||
const other = createMemo(() => store.selected === options().length)
|
||||
const input = createMemo(() => store.custom[store.tab] ?? "")
|
||||
const multi = createMemo(() => question()?.multiple === true)
|
||||
const customPicked = createMemo(() => {
|
||||
const value = input()
|
||||
if (!value) return false
|
||||
return store.answers[store.tab]?.includes(value) ?? false
|
||||
})
|
||||
|
||||
function submit() {
|
||||
// Fill in empty answers with empty strings
|
||||
const answers = questions().map((_, i) => store.answers[i] ?? "")
|
||||
const answers = questions().map((_, i) => store.answers[i] ?? [])
|
||||
sdk.client.question.reply({
|
||||
requestID: props.request.id,
|
||||
answers,
|
||||
@@ -52,7 +57,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
|
||||
function pick(answer: string, custom: boolean = false) {
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = answer
|
||||
answers[store.tab] = [answer]
|
||||
setStore("answers", answers)
|
||||
if (custom) {
|
||||
const inputs = [...store.custom]
|
||||
@@ -62,7 +67,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
if (single()) {
|
||||
sdk.client.question.reply({
|
||||
requestID: props.request.id,
|
||||
answers: [answer],
|
||||
answers: [[answer]],
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -70,6 +75,17 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
setStore("selected", 0)
|
||||
}
|
||||
|
||||
function toggle(answer: string) {
|
||||
const existing = store.answers[store.tab] ?? []
|
||||
const next = [...existing]
|
||||
const index = next.indexOf(answer)
|
||||
if (index === -1) next.push(answer)
|
||||
if (index !== -1) next.splice(index, 1)
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = next
|
||||
setStore("answers", answers)
|
||||
}
|
||||
|
||||
const dialog = useDialog()
|
||||
|
||||
useKeyboard((evt) => {
|
||||
@@ -82,11 +98,49 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
}
|
||||
if (evt.name === "return") {
|
||||
evt.preventDefault()
|
||||
const text = textarea?.plainText?.trim()
|
||||
if (text) {
|
||||
pick(text, true)
|
||||
const text = textarea?.plainText?.trim() ?? ""
|
||||
const prev = store.custom[store.tab]
|
||||
|
||||
if (!text) {
|
||||
if (prev) {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = ""
|
||||
setStore("custom", inputs)
|
||||
}
|
||||
|
||||
const answers = [...store.answers]
|
||||
if (prev) {
|
||||
answers[store.tab] = (answers[store.tab] ?? []).filter((x) => x !== prev)
|
||||
}
|
||||
if (!prev) {
|
||||
answers[store.tab] = []
|
||||
}
|
||||
setStore("answers", answers)
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
|
||||
if (multi()) {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = text
|
||||
setStore("custom", inputs)
|
||||
|
||||
const existing = store.answers[store.tab] ?? []
|
||||
const next = [...existing]
|
||||
if (prev) {
|
||||
const index = next.indexOf(prev)
|
||||
if (index !== -1) next.splice(index, 1)
|
||||
}
|
||||
if (!next.includes(text)) next.push(text)
|
||||
const answers = [...store.answers]
|
||||
answers[store.tab] = next
|
||||
setStore("answers", answers)
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
|
||||
pick(text, true)
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
// Let textarea handle all other keys
|
||||
@@ -133,13 +187,25 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
if (evt.name === "return") {
|
||||
evt.preventDefault()
|
||||
if (other()) {
|
||||
setStore("editing", true)
|
||||
} else {
|
||||
const opt = opts[store.selected]
|
||||
if (opt) {
|
||||
pick(opt.label)
|
||||
if (!multi()) {
|
||||
setStore("editing", true)
|
||||
return
|
||||
}
|
||||
const value = input()
|
||||
if (value && customPicked()) {
|
||||
toggle(value)
|
||||
return
|
||||
}
|
||||
setStore("editing", true)
|
||||
return
|
||||
}
|
||||
const opt = opts[store.selected]
|
||||
if (!opt) return
|
||||
if (multi()) {
|
||||
toggle(opt.label)
|
||||
return
|
||||
}
|
||||
pick(opt.label)
|
||||
}
|
||||
|
||||
if (evt.name === "escape" || keybind.match("app_exit", evt)) {
|
||||
@@ -162,7 +228,9 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const isActive = () => index() === store.tab
|
||||
const isAnswered = () => store.answers[index()] !== undefined
|
||||
const isAnswered = () => {
|
||||
return (store.answers[index()]?.length ?? 0) > 0
|
||||
}
|
||||
return (
|
||||
<box
|
||||
paddingLeft={1}
|
||||
@@ -185,13 +253,16 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
<Show when={!confirm()}>
|
||||
<box paddingLeft={1} gap={1}>
|
||||
<box>
|
||||
<text fg={theme.text}>{question()?.question}</text>
|
||||
<text fg={theme.text}>
|
||||
{question()?.question}
|
||||
{multi() ? " (select all that apply)" : ""}
|
||||
</text>
|
||||
</box>
|
||||
<box>
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const active = () => i() === store.selected
|
||||
const picked = () => store.answers[store.tab] === opt.label
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<box>
|
||||
<box flexDirection="row" gap={1}>
|
||||
@@ -212,17 +283,18 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
<box>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||
<text fg={other() ? theme.secondary : input() ? theme.success : theme.text}>
|
||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||
{options().length + 1}. Type your own answer
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.success}>{input() ? "✓" : ""}</text>
|
||||
<text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
|
||||
</box>
|
||||
<Show when={store.editing}>
|
||||
<box paddingLeft={3}>
|
||||
<textarea
|
||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||
focused
|
||||
initialValue={input()}
|
||||
placeholder="Type your own answer"
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
@@ -247,11 +319,12 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
</box>
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const answer = () => store.answers[index()]
|
||||
const value = () => store.answers[index()]?.join(", ") ?? ""
|
||||
const answered = () => Boolean(value())
|
||||
return (
|
||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>{q.header}:</text>
|
||||
<text fg={answer() ? theme.text : theme.error}>{answer() ?? "(not answered)"}</text>
|
||||
<text fg={answered() ? theme.text : theme.error}>{answered() ? value() : "(not answered)"}</text>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
@@ -279,8 +352,12 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>{confirm() ? "submit" : single() ? "submit" : "confirm"}</span>
|
||||
enter{" "}
|
||||
<span style={{ fg: theme.textMuted }}>
|
||||
{confirm() ? "submit" : multi() ? "toggle" : single() ? "submit" : "confirm"}
|
||||
</span>
|
||||
</text>
|
||||
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>dismiss</span>
|
||||
</text>
|
||||
|
||||
@@ -62,6 +62,7 @@ function init() {
|
||||
current.onClose?.()
|
||||
setStore("stack", store.stack.slice(0, -1))
|
||||
evt.preventDefault()
|
||||
evt.stopPropagation()
|
||||
refocus()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -348,7 +348,7 @@ export const rustfmt: Info = {
|
||||
}
|
||||
|
||||
export const cargofmt: Info = {
|
||||
name: "cargo fmt",
|
||||
name: "cargofmt",
|
||||
command: ["cargo", "fmt", "--", "$FILE"],
|
||||
extensions: [".rs"],
|
||||
async enabled() {
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import z from "zod"
|
||||
import { Log } from "../util/log"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Wildcard } from "../util/wildcard"
|
||||
|
||||
export namespace Permission {
|
||||
const log = Log.create({ service: "permission" })
|
||||
|
||||
function toKeys(pattern: Info["pattern"], type: string): string[] {
|
||||
return pattern === undefined ? [type] : Array.isArray(pattern) ? pattern : [pattern]
|
||||
}
|
||||
|
||||
function covered(keys: string[], approved: Record<string, boolean>): boolean {
|
||||
const pats = Object.keys(approved)
|
||||
return keys.every((k) => pats.some((p) => Wildcard.match(k, p)))
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
pattern: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
callID: z.string().optional(),
|
||||
message: z.string(),
|
||||
metadata: z.record(z.string(), z.any()),
|
||||
time: z.object({
|
||||
created: z.number(),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
ref: "Permission",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Event = {
|
||||
Updated: BusEvent.define("permission.updated", Info),
|
||||
Replied: BusEvent.define(
|
||||
"permission.replied",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
permissionID: z.string(),
|
||||
response: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
() => {
|
||||
const pending: {
|
||||
[sessionID: string]: {
|
||||
[permissionID: string]: {
|
||||
info: Info
|
||||
resolve: () => void
|
||||
reject: (e: any) => void
|
||||
}
|
||||
}
|
||||
} = {}
|
||||
|
||||
const approved: {
|
||||
[sessionID: string]: {
|
||||
[permissionID: string]: boolean
|
||||
}
|
||||
} = {}
|
||||
|
||||
return {
|
||||
pending,
|
||||
approved,
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
for (const pending of Object.values(state.pending)) {
|
||||
for (const item of Object.values(pending)) {
|
||||
item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export function pending() {
|
||||
return state().pending
|
||||
}
|
||||
|
||||
export function list() {
|
||||
const { pending } = state()
|
||||
const result: Info[] = []
|
||||
for (const items of Object.values(pending)) {
|
||||
for (const item of Object.values(items)) {
|
||||
result.push(item.info)
|
||||
}
|
||||
}
|
||||
return result.sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
|
||||
export async function ask(input: {
|
||||
type: Info["type"]
|
||||
message: Info["message"]
|
||||
pattern?: Info["pattern"]
|
||||
callID?: Info["callID"]
|
||||
sessionID: Info["sessionID"]
|
||||
messageID: Info["messageID"]
|
||||
metadata: Info["metadata"]
|
||||
}) {
|
||||
const { pending, approved } = state()
|
||||
log.info("asking", {
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
toolCallID: input.callID,
|
||||
pattern: input.pattern,
|
||||
})
|
||||
const approvedForSession = approved[input.sessionID] || {}
|
||||
const keys = toKeys(input.pattern, input.type)
|
||||
if (covered(keys, approvedForSession)) return
|
||||
const info: Info = {
|
||||
id: Identifier.ascending("permission"),
|
||||
type: input.type,
|
||||
pattern: input.pattern,
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
callID: input.callID,
|
||||
message: input.message,
|
||||
metadata: input.metadata,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
}
|
||||
|
||||
switch (
|
||||
await Plugin.trigger("permission.ask", info, {
|
||||
status: "ask",
|
||||
}).then((x) => x.status)
|
||||
) {
|
||||
case "deny":
|
||||
throw new RejectedError(info.sessionID, info.id, info.callID, info.metadata)
|
||||
case "allow":
|
||||
return
|
||||
}
|
||||
|
||||
pending[input.sessionID] = pending[input.sessionID] || {}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
pending[input.sessionID][info.id] = {
|
||||
info,
|
||||
resolve,
|
||||
reject,
|
||||
}
|
||||
Bus.publish(Event.Updated, info)
|
||||
})
|
||||
}
|
||||
|
||||
export const Response = z.enum(["once", "always", "reject"])
|
||||
export type Response = z.infer<typeof Response>
|
||||
|
||||
export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) {
|
||||
log.info("response", input)
|
||||
const { pending, approved } = state()
|
||||
const match = pending[input.sessionID]?.[input.permissionID]
|
||||
if (!match) return
|
||||
delete pending[input.sessionID][input.permissionID]
|
||||
Bus.publish(Event.Replied, {
|
||||
sessionID: input.sessionID,
|
||||
permissionID: input.permissionID,
|
||||
response: input.response,
|
||||
})
|
||||
if (input.response === "reject") {
|
||||
match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata))
|
||||
return
|
||||
}
|
||||
match.resolve()
|
||||
if (input.response === "always") {
|
||||
approved[input.sessionID] = approved[input.sessionID] || {}
|
||||
const approveKeys = toKeys(match.info.pattern, match.info.type)
|
||||
for (const k of approveKeys) {
|
||||
approved[input.sessionID][k] = true
|
||||
}
|
||||
const items = pending[input.sessionID]
|
||||
if (!items) return
|
||||
for (const item of Object.values(items)) {
|
||||
const itemKeys = toKeys(item.info.pattern, item.info.type)
|
||||
if (covered(itemKeys, approved[input.sessionID])) {
|
||||
respond({
|
||||
sessionID: item.info.sessionID,
|
||||
permissionID: item.info.id,
|
||||
response: input.response,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RejectedError extends Error {
|
||||
constructor(
|
||||
public readonly sessionID: string,
|
||||
public readonly permissionID: string,
|
||||
public readonly toolCallID?: string,
|
||||
public readonly metadata?: Record<string, any>,
|
||||
public readonly reason?: string,
|
||||
) {
|
||||
super(
|
||||
reason !== undefined
|
||||
? reason
|
||||
: `The user rejected permission to use this specific tool call. You may try again with different parameters.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,28 +120,57 @@ export namespace PermissionNext {
|
||||
async (input) => {
|
||||
const s = await state()
|
||||
const { ruleset, ...request } = input
|
||||
for (const pattern of request.patterns ?? []) {
|
||||
const ask = (request.patterns ?? []).reduce((ask, pattern) => {
|
||||
const rule = evaluate(request.permission, pattern, ruleset, s.approved)
|
||||
log.info("evaluated", { permission: request.permission, pattern, action: rule })
|
||||
if (rule.action === "deny")
|
||||
if (rule.action === "deny") {
|
||||
throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission)))
|
||||
if (rule.action === "ask") {
|
||||
const id = input.id ?? Identifier.ascending("permission")
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const info: Request = {
|
||||
id,
|
||||
...request,
|
||||
}
|
||||
return ask || rule.action === "ask"
|
||||
}, false)
|
||||
|
||||
if (!ask) return
|
||||
|
||||
const id = input.id ?? Identifier.ascending("permission")
|
||||
const info: Request = {
|
||||
id,
|
||||
...request,
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
s.pending[id] = {
|
||||
info,
|
||||
resolve,
|
||||
reject,
|
||||
}
|
||||
|
||||
void import("@/plugin")
|
||||
.then((plugin) =>
|
||||
plugin.Plugin.trigger("permission.ask", info, {
|
||||
status: "ask" as "ask" | "allow" | "deny",
|
||||
}),
|
||||
)
|
||||
.then((result) => {
|
||||
const existing = s.pending[id]
|
||||
if (!existing) return
|
||||
if (result.status === "deny") {
|
||||
delete s.pending[id]
|
||||
existing.reject(new RejectedError())
|
||||
return
|
||||
}
|
||||
s.pending[id] = {
|
||||
info,
|
||||
resolve,
|
||||
reject,
|
||||
if (result.status === "allow") {
|
||||
delete s.pending[id]
|
||||
existing.resolve()
|
||||
return
|
||||
}
|
||||
Bus.publish(Event.Asked, info)
|
||||
})
|
||||
}
|
||||
if (rule.action === "allow") continue
|
||||
}
|
||||
.catch((error) => {
|
||||
log.error("permission.ask plugin failed", { error })
|
||||
if (!s.pending[id]) return
|
||||
Bus.publish(Event.Asked, info)
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export namespace Question {
|
||||
question: z.string().describe("Complete question"),
|
||||
header: z.string().max(12).describe("Very short label (max 12 chars)"),
|
||||
options: z.array(Option).describe("Available choices"),
|
||||
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
|
||||
})
|
||||
.meta({
|
||||
ref: "QuestionInfo",
|
||||
@@ -46,8 +47,15 @@ export namespace Question {
|
||||
})
|
||||
export type Request = z.infer<typeof Request>
|
||||
|
||||
export const Answer = z.array(z.string()).meta({
|
||||
ref: "QuestionAnswer",
|
||||
})
|
||||
export type Answer = z.infer<typeof Answer>
|
||||
|
||||
export const Reply = z.object({
|
||||
answers: z.array(z.string()).describe("User answers in order of questions"),
|
||||
answers: z
|
||||
.array(Answer)
|
||||
.describe("User answers in order of questions (each answer is an array of selected labels)"),
|
||||
})
|
||||
export type Reply = z.infer<typeof Reply>
|
||||
|
||||
@@ -58,7 +66,7 @@ export namespace Question {
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
requestID: z.string(),
|
||||
answers: z.array(z.string()),
|
||||
answers: z.array(Answer),
|
||||
}),
|
||||
),
|
||||
Rejected: BusEvent.define(
|
||||
@@ -75,7 +83,7 @@ export namespace Question {
|
||||
string,
|
||||
{
|
||||
info: Request
|
||||
resolve: (answers: string[]) => void
|
||||
resolve: (answers: Answer[]) => void
|
||||
reject: (e: any) => void
|
||||
}
|
||||
> = {}
|
||||
@@ -89,13 +97,13 @@ export namespace Question {
|
||||
sessionID: string
|
||||
questions: Info[]
|
||||
tool?: { messageID: string; callID: string }
|
||||
}): Promise<string[]> {
|
||||
}): Promise<Answer[]> {
|
||||
const s = await state()
|
||||
const id = Identifier.ascending("question")
|
||||
|
||||
log.info("asking", { id, questions: input.questions.length })
|
||||
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
return new Promise<Answer[]>((resolve, reject) => {
|
||||
const info: Request = {
|
||||
id,
|
||||
sessionID: input.sessionID,
|
||||
@@ -111,7 +119,7 @@ export namespace Question {
|
||||
})
|
||||
}
|
||||
|
||||
export async function reply(input: { requestID: string; answers: string[] }): Promise<void> {
|
||||
export async function reply(input: { requestID: string; answers: Answer[] }): Promise<void> {
|
||||
const s = await state()
|
||||
const existing = s.pending[input.requestID]
|
||||
if (!existing) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export const QuestionRoute = new Hono()
|
||||
requestID: z.string(),
|
||||
}),
|
||||
),
|
||||
validator("json", z.object({ answers: z.array(z.string()) })),
|
||||
validator("json", Question.Reply),
|
||||
async (c) => {
|
||||
const params = c.req.valid("param")
|
||||
const json = c.req.valid("json")
|
||||
|
||||
@@ -15,7 +15,12 @@ export const QuestionTool = Tool.define("question", {
|
||||
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||
})
|
||||
|
||||
const formatted = params.questions.map((q, i) => `"${q.question}"="${answers[i] ?? "Unanswered"}"`).join(", ")
|
||||
function format(answer: Question.Answer | undefined) {
|
||||
if (!answer?.length) return "Unanswered"
|
||||
return answer.join(", ")
|
||||
}
|
||||
|
||||
const formatted = params.questions.map((q, i) => `"${q.question}"="${format(answers[i])}"`).join(", ")
|
||||
|
||||
return {
|
||||
title: `Asked ${params.questions.length} question${params.questions.length > 1 ? "s" : ""}`,
|
||||
|
||||
@@ -6,4 +6,5 @@ Use this tool when you need to ask the user questions during execution. This all
|
||||
|
||||
Usage notes:
|
||||
- Users will always be able to select "Other" to provide custom text input
|
||||
- Answers are returned as arrays of labels; set `multiple: true` to allow selecting more than one
|
||||
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
|
||||
|
||||
@@ -82,11 +82,11 @@ test("reply - resolves the pending ask with answers", async () => {
|
||||
|
||||
await Question.reply({
|
||||
requestID,
|
||||
answers: ["Option 1"],
|
||||
answers: [["Option 1"]],
|
||||
})
|
||||
|
||||
const answers = await askPromise
|
||||
expect(answers).toEqual(["Option 1"])
|
||||
expect(answers).toEqual([["Option 1"]])
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -115,7 +115,7 @@ test("reply - removes from pending list", async () => {
|
||||
|
||||
await Question.reply({
|
||||
requestID: pending[0].id,
|
||||
answers: ["Option 1"],
|
||||
answers: [["Option 1"]],
|
||||
})
|
||||
|
||||
const pendingAfter = await Question.list()
|
||||
@@ -131,7 +131,7 @@ test("reply - does nothing for unknown requestID", async () => {
|
||||
fn: async () => {
|
||||
await Question.reply({
|
||||
requestID: "que_unknown",
|
||||
answers: ["Option 1"],
|
||||
answers: [["Option 1"]],
|
||||
})
|
||||
// Should not throw
|
||||
},
|
||||
@@ -244,11 +244,11 @@ test("ask - handles multiple questions", async () => {
|
||||
|
||||
await Question.reply({
|
||||
requestID: pending[0].id,
|
||||
answers: ["Build", "Dev"],
|
||||
answers: [["Build"], ["Dev"]],
|
||||
})
|
||||
|
||||
const answers = await askPromise
|
||||
expect(answers).toEqual(["Build", "Dev"])
|
||||
expect(answers).toEqual([["Build"], ["Dev"]])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -84,6 +84,7 @@ import type {
|
||||
PtyRemoveResponses,
|
||||
PtyUpdateErrors,
|
||||
PtyUpdateResponses,
|
||||
QuestionAnswer,
|
||||
QuestionListResponses,
|
||||
QuestionRejectErrors,
|
||||
QuestionRejectResponses,
|
||||
@@ -1815,7 +1816,7 @@ export class Question extends HeyApiClient {
|
||||
parameters: {
|
||||
requestID: string
|
||||
directory?: string
|
||||
answers?: Array<string>
|
||||
answers?: Array<QuestionAnswer>
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
|
||||
@@ -541,6 +541,10 @@ export type QuestionInfo = {
|
||||
* Available choices
|
||||
*/
|
||||
options: Array<QuestionOption>
|
||||
/**
|
||||
* Allow selecting multiple choices
|
||||
*/
|
||||
multiple?: boolean
|
||||
}
|
||||
|
||||
export type QuestionRequest = {
|
||||
@@ -561,12 +565,14 @@ export type EventQuestionAsked = {
|
||||
properties: QuestionRequest
|
||||
}
|
||||
|
||||
export type QuestionAnswer = Array<string>
|
||||
|
||||
export type EventQuestionReplied = {
|
||||
type: "question.replied"
|
||||
properties: {
|
||||
sessionID: string
|
||||
requestID: string
|
||||
answers: Array<string>
|
||||
answers: Array<QuestionAnswer>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3630,7 +3636,10 @@ export type QuestionListResponse = QuestionListResponses[keyof QuestionListRespo
|
||||
|
||||
export type QuestionReplyData = {
|
||||
body?: {
|
||||
answers: Array<string>
|
||||
/**
|
||||
* User answers in order of questions (each answer is an array of selected labels)
|
||||
*/
|
||||
answers: Array<QuestionAnswer>
|
||||
}
|
||||
path: {
|
||||
requestID: string
|
||||
|
||||
@@ -3254,9 +3254,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answers": {
|
||||
"description": "User answers in order of questions (each answer is an array of selected labels)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/QuestionAnswer"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7117,6 +7118,10 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/QuestionOption"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"description": "Allow selecting multiple choices",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["question", "header", "options"]
|
||||
@@ -7167,6 +7172,12 @@
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
},
|
||||
"QuestionAnswer": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Event.question.replied": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7186,7 +7197,7 @@
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/QuestionAnswer"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -22,6 +22,7 @@ OpenCode comes with several built-in formatters for popular languages and framew
|
||||
| ktlint | .kt, .kts | `ktlint` command available |
|
||||
| ruff | .py, .pyi | `ruff` command available with config |
|
||||
| rustfmt | .rs | `rustfmt` command available |
|
||||
| cargofmt | .rs | `cargo fmt` command available |
|
||||
| uv | .py, .pyi | `uv` command available |
|
||||
| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` command available |
|
||||
| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available |
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { $ } from "bun"
|
||||
import { tmpdir } from "os"
|
||||
import { join } from "path"
|
||||
|
||||
const FORK_REPO = "sst/zed-extensions"
|
||||
const FORK_REPO = "anomalyco/zed-extensions"
|
||||
const UPSTREAM_REPO = "zed-industries/extensions"
|
||||
const EXTENSION_NAME = "opencode"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.1.6",
|
||||
"version": "1.1.7",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user