Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ece7816a60 | |||
| 9293cd3532 | |||
| 8213d56a7e | |||
| 254a5e07fd | |||
| bd3580f627 | |||
| 5d2907c40c | |||
| 07a4327027 | |||
| dff2d2f102 | |||
| 2307bcf714 | |||
| f85010bb04 | |||
| 16dc05ca0e | |||
| 057d75ac3a | |||
| fb566c1016 | |||
| 0ba2e4f4be | |||
| 9204d72e24 | |||
| c8df24ef28 | |||
| ca052fdec3 | |||
| b9ca267c29 | |||
| 51a6c41f35 | |||
| 7b3a87c40f | |||
| 789f06918d | |||
| 03ccad78c1 | |||
| 62619aa0b4 | |||
| d7fff24097 | |||
| 780439d6d2 | |||
| 9b995dffbd | |||
| 1ef3b60eba | |||
| a493a99de1 | |||
| 8560531a95 | |||
| f7f5c622b3 | |||
| fc34b0a06c | |||
| 5e91dba59b | |||
| 62ab2b0c91 | |||
| 26a71d29aa | |||
| b2c9c871c0 | |||
| 9e1bb9f53f | |||
| a8fae6eeb1 | |||
| 18822bf1ca | |||
| 96e6fda410 | |||
| 74ba72b2a7 | |||
| dade7f76ae | |||
| 0e8e5a6b7c | |||
| 8d8c8f35ce | |||
| 8397e3ead6 | |||
| b7b215da44 | |||
| ff00dec5ab | |||
| 4671824dd5 | |||
| 4192e0993f | |||
| e8c87fccdf | |||
| 670f8a0771 | |||
| 7d98484c51 | |||
| 3058eb76cf | |||
| a0770c1214 | |||
| 23bf0e700b | |||
| 34904d15cf | |||
| de0ef0b397 | |||
| adc747d21c | |||
| 3a68d7ace3 | |||
| 16682362ef | |||
| f5eda26794 | |||
| 0fd9b364f9 |
@@ -1,63 +0,0 @@
|
||||
name: Auto-label TUI Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Auto-label and assign issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title;
|
||||
const description = issue.body || '';
|
||||
|
||||
// Check for "opencode web" keyword
|
||||
const webPattern = /(opencode web)/i;
|
||||
const isWebRelated = webPattern.test(title) || webPattern.test(description);
|
||||
|
||||
// Check for version patterns like v1.0.x or 1.0.x
|
||||
const versionPattern = /[v]?1\.0\./i;
|
||||
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
||||
|
||||
// Check for "nix" keyword
|
||||
const nixPattern = /\bnix\b/i;
|
||||
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
|
||||
|
||||
const labels = [];
|
||||
|
||||
if (isWebRelated) {
|
||||
labels.push('web');
|
||||
|
||||
// Assign to adamdotdevin
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: ['adamdotdevin']
|
||||
});
|
||||
} else if (isVersionRelated) {
|
||||
// Only add opentui if NOT web-related
|
||||
labels.push('opentui');
|
||||
}
|
||||
|
||||
if (isNixRelated) {
|
||||
labels.push('nix');
|
||||
}
|
||||
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
@@ -165,6 +165,7 @@ jobs:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- name: Install tauri-cli from portable appimage branch
|
||||
@@ -197,3 +198,19 @@ jobs:
|
||||
releaseId: ${{ needs.publish.outputs.releaseId }}
|
||||
tagName: ${{ needs.publish.outputs.tagName }}
|
||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
releaseDraft: true
|
||||
|
||||
publish-release:
|
||||
needs:
|
||||
- publish
|
||||
- publish-tauri
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tagName }}
|
||||
|
||||
- run: gh release edit ${{ steps.publish.outputs.tagName }} --draft=false
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: release-github-action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "github/**"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./github/script/release
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Issue Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Triage issue
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
run: |
|
||||
opencode run --agent triage "The following issue was just opened, triage it:
|
||||
|
||||
Title: $ISSUE_TITLE
|
||||
|
||||
$ISSUE_BODY"
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
tools:
|
||||
"*": false
|
||||
"github-triage": true
|
||||
---
|
||||
|
||||
You are a triage agent responsible for triaging github issues.
|
||||
|
||||
Use your github-triage tool to triage issues.
|
||||
|
||||
## Labels
|
||||
|
||||
#### perf
|
||||
|
||||
Performance-related issues:
|
||||
|
||||
- Slow performance
|
||||
- High RAM usage
|
||||
- High CPU usage
|
||||
|
||||
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
|
||||
|
||||
#### desktop
|
||||
|
||||
Desktop app issues:
|
||||
|
||||
- `opencode web` command
|
||||
- The desktop app itself
|
||||
|
||||
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
|
||||
|
||||
#### nix
|
||||
|
||||
**Only** add if the issue explicitly mentions nix.
|
||||
|
||||
#### zen
|
||||
|
||||
**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers.
|
||||
|
||||
#### docs
|
||||
|
||||
Add if the issue requests better documentation or docs updates.
|
||||
|
||||
#### opentui
|
||||
|
||||
TUI issues potentially caused by our underlying TUI library:
|
||||
|
||||
- Keybindings not working
|
||||
- Scroll speed issues (too fast/slow/laggy)
|
||||
- Screen flickering
|
||||
- Crashes with opentui in the log
|
||||
|
||||
**Do not** add for general TUI bugs.
|
||||
|
||||
When assigning to people here are the following rules:
|
||||
|
||||
adamdotdev:
|
||||
ONLY assign adam if the issue will have the "desktop" label.
|
||||
|
||||
fwang:
|
||||
ONLY assign fwang if the issue will have the "zen" label.
|
||||
|
||||
jayair:
|
||||
ONLY assign jayair if the issue will have the "docs" label.
|
||||
|
||||
In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node.
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "*.txt" {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
@@ -11,4 +11,7 @@
|
||||
},
|
||||
},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { Octokit } from "@octokit/rest"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import DESCRIPTION from "./github-triage.txt"
|
||||
|
||||
function getIssueNumber(): number {
|
||||
const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10)
|
||||
if (!issue) throw new Error("ISSUE_NUMBER env var not set")
|
||||
return issue
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
assignee: tool.schema
|
||||
.enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"])
|
||||
.describe("The username of the assignee")
|
||||
.default("rekram1-node"),
|
||||
labels: tool.schema
|
||||
.array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs"]))
|
||||
.describe("The labels(s) to add to the issue")
|
||||
.default([]),
|
||||
},
|
||||
async execute(args) {
|
||||
const issue = getIssueNumber()
|
||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
|
||||
const owner = "sst"
|
||||
const repo = "opencode"
|
||||
|
||||
const results: string[] = []
|
||||
|
||||
if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) {
|
||||
throw new Error("Only desktop issues should be assigned to adamdotdevin")
|
||||
}
|
||||
|
||||
if (args.assignee === "fwang" && !args.labels.includes("zen")) {
|
||||
throw new Error("Only zen issues should be assigned to fwang")
|
||||
}
|
||||
|
||||
if (args.assignee === "kommander" && !args.labels.includes("opentui")) {
|
||||
throw new Error("Only opentui issues should be assigned to kommander")
|
||||
}
|
||||
|
||||
await octokit.rest.issues.addAssignees({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue,
|
||||
assignees: [args.assignee],
|
||||
})
|
||||
results.push(`Assigned @${args.assignee} to issue #${issue}`)
|
||||
|
||||
const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label))
|
||||
|
||||
if (labels.length > 0) {
|
||||
await octokit.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue,
|
||||
labels,
|
||||
})
|
||||
results.push(`Added labels: ${args.labels.join(", ")}`)
|
||||
}
|
||||
|
||||
return results.join("\n")
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
Use this tool to assign and/or label a Github issue.
|
||||
|
||||
You can assign the following users:
|
||||
- thdxr
|
||||
- adamdotdevin
|
||||
- fwang
|
||||
- jayair
|
||||
- kommander
|
||||
- rekram1-node
|
||||
|
||||
|
||||
You can use the following labels:
|
||||
- nix
|
||||
- opentui
|
||||
- perf
|
||||
- web
|
||||
- zen
|
||||
- docs
|
||||
|
||||
Always try to assign an issue, if in doubt, assign rekram1-node to it.
|
||||
|
||||
## Breakdown of responsibilities:
|
||||
|
||||
### thdxr
|
||||
|
||||
Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him.
|
||||
|
||||
This relates to OpenCode server primarily but has overlap with just about anything
|
||||
|
||||
### adamdotdevin
|
||||
|
||||
Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him.
|
||||
|
||||
|
||||
### fwang
|
||||
|
||||
Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue.
|
||||
|
||||
### jayair
|
||||
|
||||
Jay is responsible for documentation. If there is an issue relating to documentation assign him.
|
||||
|
||||
### kommander
|
||||
|
||||
Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about:
|
||||
- random characters on screen
|
||||
- keybinds not working on different terminals
|
||||
- general terminal stuff
|
||||
Then assign the issue to Him.
|
||||
|
||||
### rekram1-node
|
||||
|
||||
ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label.
|
||||
|
||||
Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things.
|
||||
If no one else makes sense to assign, assign rekram1-node to it.
|
||||
|
||||
Always assign to aiden if the issue mentions "acp", "zed", or model performance issues
|
||||
|
||||
## Breakdown of Labels:
|
||||
|
||||
### nix
|
||||
|
||||
Any issue that mentions nix, or nixos should have a nix label
|
||||
|
||||
### opentui
|
||||
|
||||
Anything relating to the TUI itself should have an opentui label
|
||||
|
||||
### perf
|
||||
|
||||
Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label
|
||||
|
||||
### desktop
|
||||
|
||||
Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related
|
||||
|
||||
### zen
|
||||
|
||||
Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label
|
||||
|
||||
### docs
|
||||
|
||||
Anything related to the documentation should have a docs label
|
||||
@@ -37,6 +37,22 @@ nix run nixpkgs#opencode # or github:sst/opencode for latest dev branc
|
||||
> [!TIP]
|
||||
> Remove versions older than 0.1.x before installing.
|
||||
|
||||
### Desktop App (BETA)
|
||||
|
||||
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
|
||||
|
||||
| Platform | Download |
|
||||
| --------------------- | ------------------------------------- |
|
||||
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||
| Linux | `.deb`, `.rpm`, or AppImage |
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
brew install --cask opencode-desktop
|
||||
```
|
||||
|
||||
#### Installation Directory
|
||||
|
||||
The install script respects the following priority order for the installation path:
|
||||
|
||||
@@ -171,3 +171,4 @@
|
||||
| 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) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"name": "opencode",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:",
|
||||
@@ -20,7 +21,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -48,7 +49,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -75,7 +76,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -99,7 +100,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -123,7 +124,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -170,7 +171,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -199,10 +200,10 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"jose": "6.0.11",
|
||||
},
|
||||
@@ -215,7 +216,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -238,7 +239,7 @@
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
@@ -307,7 +308,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -327,7 +328,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -338,7 +339,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -351,7 +352,7 @@
|
||||
},
|
||||
"packages/tauri": {
|
||||
"name": "@opencode-ai/tauri",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/desktop": "workspace:*",
|
||||
"@tauri-apps/api": "^2",
|
||||
@@ -376,7 +377,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -411,7 +412,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -422,7 +423,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -470,6 +471,7 @@
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.0.0-beta.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
@@ -1096,11 +1098,11 @@
|
||||
|
||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
|
||||
|
||||
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
|
||||
|
||||
"@octokit/plugin-retry": ["@octokit/plugin-retry@3.0.9", "", { "dependencies": { "@octokit/types": "^6.0.3", "bottleneck": "^2.15.3" } }, "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ=="],
|
||||
|
||||
@@ -1108,7 +1110,7 @@
|
||||
|
||||
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
|
||||
|
||||
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
"@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
@@ -4080,9 +4082,9 @@
|
||||
|
||||
"@octokit/oauth-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
||||
|
||||
"@octokit/plugin-retry/@octokit/types": ["@octokit/types@6.41.0", "", { "dependencies": { "@octokit/openapi-types": "^12.11.0" } }, "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg=="],
|
||||
|
||||
@@ -4094,6 +4096,8 @@
|
||||
|
||||
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
|
||||
"@opencode-ai/tauri/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
|
||||
@@ -4286,6 +4290,8 @@
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bpYruxVLhrTbVH6CCq48zMJNeHu6FmHtEedl9FXckEgcIEAi036idFhJlcRwC1jNCwlacbzb8dPD7OAH1EKJaQ=="],
|
||||
|
||||
"opencode/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||
|
||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||
@@ -4644,9 +4650,9 @@
|
||||
|
||||
"@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="],
|
||||
|
||||
@@ -4654,6 +4660,10 @@
|
||||
|
||||
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
|
||||
@@ -4868,6 +4878,10 @@
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||
@@ -5024,6 +5038,10 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
@@ -5044,6 +5062,10 @@
|
||||
|
||||
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
@@ -5138,10 +5160,18 @@
|
||||
|
||||
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765772535,
|
||||
"narHash": "sha256-aq+dQoaPONOSjtFIBnAXseDm9TUhIbe215TPmkfMYww=",
|
||||
"lastModified": 1765803225,
|
||||
"narHash": "sha256-xwaZV/UgJ04+ixbZZfoDE8IsOWjtvQZICh9aamzPnrg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "09b8fda8959d761445f12b55f380d90375a1d6bb",
|
||||
"rev": "ac9a217389ee622d4e1e727c4efcc9c4bc9089ba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,22 +240,23 @@ download_with_progress() {
|
||||
|
||||
download_and_install() {
|
||||
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||
mkdir -p opencodetmp && cd opencodetmp
|
||||
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
|
||||
# Fallback to standard curl on Windows or if custom progress fails
|
||||
curl -# -L -o "$filename" "$url"
|
||||
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
|
||||
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
|
||||
curl -# -L -o "$tmp_dir/$filename" "$url"
|
||||
fi
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
tar -xzf "$filename"
|
||||
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
|
||||
else
|
||||
unzip -q "$filename"
|
||||
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
|
||||
fi
|
||||
|
||||
mv opencode "$INSTALL_DIR"
|
||||
mv "$tmp_dir/opencode" "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/opencode"
|
||||
cd .. && rm -rf opencodetmp
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
check_version
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-PyoVOza+3WnwZbtpPF6uSN1zkyLsSG2VsgBfIMvIFAs="
|
||||
"nodeModules": "sha256-IkvFO/dANwC8MCOW8PqILqyxCa4IDiFZIIM3B4GMB+Q="
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.4",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -62,6 +63,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -93,6 +93,12 @@ export default function Download() {
|
||||
<span>[2]</span> OpenCode Desktop (Beta)
|
||||
</div>
|
||||
<div data-component="section-content">
|
||||
<button data-component="cli-row" onClick={handleCopyClick("brew install --cask opencode-desktop")}>
|
||||
<code>
|
||||
brew install --cask <strong>opencode-desktop</strong>
|
||||
</code>
|
||||
<CopyStatus />
|
||||
</button>
|
||||
<div data-component="download-row">
|
||||
<div data-component="download-info">
|
||||
<span data-slot="icon">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -5,7 +5,9 @@ import { MetaProvider } from "@solidjs/meta"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { Diff } from "@opencode-ai/ui/diff"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
@@ -39,41 +41,43 @@ export function App() {
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Router
|
||||
root={(props) => (
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<TerminalProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</MetaProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Router
|
||||
root={(props) => (
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<TerminalProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</MetaProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
|
||||
@@ -108,20 +108,18 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
|
||||
async function complete() {
|
||||
await globalSDK.client.global.dispose()
|
||||
setTimeout(() => {
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: `${provider().name} connected`,
|
||||
description: `${provider().name} models are now available to use.`,
|
||||
})
|
||||
dialog.replace(() => <DialogSelectModel provider={props.provider} />)
|
||||
}, 1000)
|
||||
dialog.close()
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: `${provider().name} connected`,
|
||||
description: `${provider().name} models are now available to use.`,
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (methods().length === 1) {
|
||||
dialog.replace(() => <DialogSelectProvider />)
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
return
|
||||
}
|
||||
if (store.authorization) {
|
||||
@@ -133,7 +131,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
setStore("methodIndex", undefined)
|
||||
return
|
||||
}
|
||||
dialog.replace(() => <DialogSelectProvider />)
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -352,7 +350,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
})
|
||||
if (result.error) {
|
||||
// TODO: show error
|
||||
dialog.clear()
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
await complete()
|
||||
|
||||
@@ -27,7 +27,7 @@ export function DialogSelectFile() {
|
||||
if (path) {
|
||||
tabs().open("file://" + path)
|
||||
}
|
||||
dialog.clear()
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
|
||||
@@ -42,7 +42,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
})
|
||||
dialog.clear()
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
@@ -75,7 +75,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
dialog.replace(() => <DialogConnectProvider provider={x.id} />)
|
||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
@@ -105,7 +105,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
||||
icon="dot-grid"
|
||||
onClick={() => {
|
||||
dialog.replace(() => <DialogSelectProvider />)
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}}
|
||||
>
|
||||
View all providers
|
||||
|
||||
@@ -28,7 +28,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
class="h-7 -my-1 text-14-medium"
|
||||
icon="plus-small"
|
||||
tabIndex={-1}
|
||||
onClick={() => dialog.replace(() => <DialogSelectProvider />)}
|
||||
onClick={() => dialog.show(() => <DialogSelectProvider />)}
|
||||
>
|
||||
Connect provider
|
||||
</Button>
|
||||
@@ -57,7 +57,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
})
|
||||
dialog.clear()
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
@@ -75,7 +75,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
||||
onClick={() => dialog.replace(() => <DialogManageModels />)}
|
||||
onClick={() => dialog.show(() => <DialogManageModels />)}
|
||||
>
|
||||
Manage models
|
||||
</Button>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
dialog.replace(() => <DialogConnectProvider provider={x.id} />)
|
||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
|
||||
@@ -864,9 +864,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
as="div"
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
dialog.replace(() =>
|
||||
providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />,
|
||||
)
|
||||
dialog.show(() => (providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />))
|
||||
}
|
||||
>
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
|
||||
@@ -128,7 +128,7 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
||||
groupBy={(x) => x.category ?? ""}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
dialog.clear()
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}}
|
||||
@@ -174,8 +174,8 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
const suspended = () => suspendCount() > 0
|
||||
|
||||
const showPalette = () => {
|
||||
if (dialog.stack.length === 0) {
|
||||
dialog.replace(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
||||
if (!dialog.active) {
|
||||
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
}
|
||||
|
||||
globalSDK.event.listen((e) => {
|
||||
console.log(e)
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
|
||||
|
||||
@@ -31,8 +31,16 @@ export type Notification = TurnCompleteNotification | ErrorNotification
|
||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||
name: "Notification",
|
||||
init: () => {
|
||||
const idlePlayer = makeAudioPlayer(idleSound)
|
||||
const errorPlayer = makeAudioPlayer(errorSound)
|
||||
let idlePlayer: ReturnType<typeof makeAudioPlayer> | undefined
|
||||
let errorPlayer: ReturnType<typeof makeAudioPlayer> | undefined
|
||||
|
||||
try {
|
||||
idlePlayer = makeAudioPlayer(idleSound)
|
||||
errorPlayer = makeAudioPlayer(errorSound)
|
||||
} catch (err) {
|
||||
console.log("Failed to load audio", err)
|
||||
}
|
||||
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
|
||||
@@ -45,13 +53,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
},
|
||||
)
|
||||
|
||||
// onMount(() => {
|
||||
// const daysToKeep = 7
|
||||
// // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now()))
|
||||
// })
|
||||
|
||||
globalSDK.event.listen((e) => {
|
||||
console.log(e)
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
const base = {
|
||||
@@ -59,14 +61,14 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
time: Date.now(),
|
||||
viewed: false,
|
||||
}
|
||||
switch (event?.type) {
|
||||
switch (event.type) {
|
||||
case "session.idle": {
|
||||
const sessionID = event.properties.sessionID
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const isChild = match.found && syncStore.session[match.index].parentID
|
||||
if (isChild) break
|
||||
idlePlayer.play()
|
||||
idlePlayer?.play()
|
||||
setStore("list", store.list.length, {
|
||||
...base,
|
||||
type: "turn-complete",
|
||||
@@ -82,7 +84,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
||||
const isChild = match.found && syncStore.session[match.index].parentID
|
||||
if (isChild) break
|
||||
}
|
||||
errorPlayer.play()
|
||||
errorPlayer?.play()
|
||||
setStore("list", store.list.length, {
|
||||
...base,
|
||||
type: "error",
|
||||
|
||||
@@ -217,7 +217,7 @@ export default function Layout(props: ParentProps) {
|
||||
])
|
||||
|
||||
function connectProvider() {
|
||||
dialog.replace(() => <DialogSelectProvider />)
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
|
||||
function navigateToProject(directory: string | undefined) {
|
||||
@@ -413,11 +413,11 @@ export default function Layout(props: ParentProps) {
|
||||
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
|
||||
const notifications = createMemo(() => notification.session.unseen(props.session.id))
|
||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||
const isWorking = createMemo(
|
||||
() =>
|
||||
props.session.id !== params.id &&
|
||||
globalSync.child(props.project.worktree)[0].session_status[props.session.id]?.type === "busy",
|
||||
)
|
||||
const isWorking = createMemo(() => {
|
||||
if (props.session.id === params.id) return false
|
||||
const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
|
||||
return status?.type === "busy" || status?.type === "retry"
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -710,7 +710,7 @@ export default function Layout(props: ParentProps) {
|
||||
<Match when={true}>
|
||||
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="plus"
|
||||
@@ -724,7 +724,7 @@ export default function Layout(props: ParentProps) {
|
||||
<Show when={platform.openDirectoryPickerDialog}>
|
||||
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
|
||||
<Button
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="folder-add-left"
|
||||
@@ -750,7 +750,7 @@ export default function Layout(props: ParentProps) {
|
||||
as={"a"}
|
||||
href="https://opencode.ai/desktop-feedback"
|
||||
target="_blank"
|
||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
icon="bubble-5"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect, on } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
@@ -11,7 +12,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
@@ -48,6 +49,7 @@ export default function Page() {
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const dialog = useDialog()
|
||||
const codeComponent = useCodeComponent()
|
||||
const command = useCommand()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
@@ -176,7 +178,7 @@ export default function Page() {
|
||||
category: "File",
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
onSelect: () => dialog.replace(() => <DialogSelectFile />),
|
||||
onSelect: () => dialog.show(() => <DialogSelectFile />),
|
||||
},
|
||||
// {
|
||||
// id: "theme.toggle",
|
||||
@@ -245,7 +247,7 @@ export default function Page() {
|
||||
category: "Model",
|
||||
keybind: "mod+'",
|
||||
slash: "model",
|
||||
onSelect: () => dialog.replace(() => <DialogSelectModel />),
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||
},
|
||||
{
|
||||
id: "agent.cycle",
|
||||
@@ -320,7 +322,7 @@ export default function Page() {
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((document.activeElement as HTMLElement)?.dataset?.component === "terminal") return
|
||||
if (dialog.stack.length > 0) return
|
||||
if (dialog.active) return
|
||||
|
||||
if (event.key === "PageUp" || event.key === "PageDown") {
|
||||
const scrollContainer = document.querySelector('[data-slot="session-turn-content"]') as HTMLElement
|
||||
@@ -613,7 +615,7 @@ export default function Page() {
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
onClick={() => dialog.replace(() => <DialogSelectFile />)}
|
||||
onClick={() => dialog.show(() => <DialogSelectFile />)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -764,7 +766,8 @@ export default function Page() {
|
||||
<Switch>
|
||||
<Match when={file()}>
|
||||
{(f) => (
|
||||
<Code
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
file={{
|
||||
name: f().path,
|
||||
contents: f().content?.content ?? "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
|
||||
import { Share } from "~/core/share"
|
||||
@@ -27,6 +28,7 @@ import { Meta } from "@solidjs/meta"
|
||||
import { Base64 } from "js-base64"
|
||||
|
||||
const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
|
||||
const ClientOnlyCode = clientOnly(() => import("@opencode-ai/ui/code").then((m) => ({ default: m.Code })))
|
||||
|
||||
const SessionDataMissingError = NamedError.create(
|
||||
"SessionDataMissingError",
|
||||
@@ -196,239 +198,254 @@ export default function () {
|
||||
<Meta property="og:image" content={ogImage()} />
|
||||
<Meta name="twitter:image" content={ogImage()} />
|
||||
<DiffComponentProvider component={ClientOnlyDiff}>
|
||||
<DataProvider data={data()} directory={info().directory}>
|
||||
{iife(() => {
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
})
|
||||
const messages = createMemo(() =>
|
||||
data().sessionID
|
||||
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
||||
(a, b) => a.time.created - b.time.created,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const firstUserMessage = createMemo(() => messages().at(0))
|
||||
const activeMessage = createMemo(
|
||||
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
||||
)
|
||||
function setActiveMessage(message: UserMessage | undefined) {
|
||||
if (message) {
|
||||
setStore("messageId", message.id)
|
||||
} else {
|
||||
setStore("messageId", undefined)
|
||||
<CodeComponentProvider component={ClientOnlyCode}>
|
||||
<DataProvider data={data()} directory={info().directory}>
|
||||
{iife(() => {
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
})
|
||||
const messages = createMemo(() =>
|
||||
data().sessionID
|
||||
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
||||
(a, b) => a.time.created - b.time.created,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const firstUserMessage = createMemo(() => messages().at(0))
|
||||
const activeMessage = createMemo(
|
||||
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
||||
)
|
||||
function setActiveMessage(message: UserMessage | undefined) {
|
||||
if (message) {
|
||||
setStore("messageId", message.id)
|
||||
} else {
|
||||
setStore("messageId", undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
||||
const diffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const splitDiffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
||||
const diffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const splitDiffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
|
||||
const title = () => (
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
|
||||
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
|
||||
<Mark class="shrink-0 w-3 my-0.5" />
|
||||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const turns = () => (
|
||||
<div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
||||
<div class="px-4">{title()}</div>
|
||||
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={message.id}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
container: "px-4",
|
||||
}}
|
||||
const title = () => (
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
|
||||
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
|
||||
<Mark class="shrink-0 w-3 my-0.5" />
|
||||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ProviderIcon
|
||||
id={provider() as IconName}
|
||||
class="size-3.5 shrink-0 text-icon-strong-base"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const wide = createMemo(() => diffs().length === 0)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
||||
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
||||
<div class="">
|
||||
<a href="https://opencode.ai">
|
||||
<Mark />
|
||||
</a>
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center">
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://github.com/sst/opencode"
|
||||
target="_blank"
|
||||
icon="github"
|
||||
variant="ghost"
|
||||
/>
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://opencode.ai/discord"
|
||||
target="_blank"
|
||||
icon="discord"
|
||||
variant="ghost"
|
||||
/>
|
||||
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const turns = () => (
|
||||
<div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
||||
<div class="px-4">{title()}</div>
|
||||
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={message.id}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</header>
|
||||
<div class="select-text flex flex-col flex-1 min-h-0">
|
||||
<div
|
||||
classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
|
||||
>
|
||||
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const wide = createMemo(() => diffs().length === 0)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
||||
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
||||
<div class="">
|
||||
<a href="https://opencode.ai">
|
||||
<Mark />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center">
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://github.com/sst/opencode"
|
||||
target="_blank"
|
||||
icon="github"
|
||||
variant="ghost"
|
||||
/>
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://opencode.ai/discord"
|
||||
target="_blank"
|
||||
icon="discord"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="select-text flex flex-col flex-1 min-h-0">
|
||||
<div
|
||||
classList={{
|
||||
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
|
||||
"mx-auto max-w-146": !wide(),
|
||||
"hidden w-full flex-1 min-h-0": true,
|
||||
"md:flex": wide(),
|
||||
"lg:flex": !wide(),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"w-full flex justify-start items-start min-w-0": true,
|
||||
"max-w-146 mx-auto px-6": wide(),
|
||||
"pr-6 pl-18": !wide() && messages().length > 1,
|
||||
"px-6": !wide() && messages().length === 1,
|
||||
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
|
||||
"mx-auto max-w-146": !wide(),
|
||||
}}
|
||||
>
|
||||
{title()}
|
||||
</div>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={wide()}
|
||||
/>
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||
classes={{
|
||||
root: "grow",
|
||||
content: "flex flex-col justify-between items-start",
|
||||
container:
|
||||
"w-full pb-20 " +
|
||||
(wide()
|
||||
? "max-w-146 mx-auto px-6"
|
||||
: messages().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
<div
|
||||
classList={{
|
||||
"w-full flex justify-start items-start min-w-0": true,
|
||||
"max-w-146 mx-auto px-6": wide(),
|
||||
"pr-6 pl-18": !wide() && messages().length > 1,
|
||||
"px-6": !wide() && messages().length === 1,
|
||||
}}
|
||||
>
|
||||
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</SessionTurn>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={diffs().length > 0}>
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||
<SessionReview
|
||||
class="@4xl:hidden"
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
<SessionReview
|
||||
split
|
||||
class="hidden @4xl:flex"
|
||||
diffs={splitDiffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
{title()}
|
||||
</div>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={wide()}
|
||||
/>
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||
classes={{
|
||||
root: "grow",
|
||||
content: "flex flex-col justify-between items-start",
|
||||
container:
|
||||
"w-full pb-20 " +
|
||||
(wide()
|
||||
? "max-w-146 mx-auto px-6"
|
||||
: messages().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
}}
|
||||
>
|
||||
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</SessionTurn>
|
||||
</div>
|
||||
</DiffComponentProvider>
|
||||
</Show>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={diffs().length > 0}>
|
||||
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||
Session
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
|
||||
{diffs().length} Files Changed
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="!overflow-hidden">
|
||||
{turns()}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
forceMount
|
||||
value="review"
|
||||
class="!overflow-hidden hidden data-[selected]:block"
|
||||
>
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</DiffComponentProvider>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
{turns()}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={diffs().length > 0}>
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||
<SessionReview
|
||||
class="@4xl:hidden"
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
<SessionReview
|
||||
split
|
||||
class="hidden @4xl:flex"
|
||||
diffs={splitDiffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DiffComponentProvider>
|
||||
</Show>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={diffs().length > 0}>
|
||||
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||
Session
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="review"
|
||||
class="w-1/2 !border-r-0"
|
||||
classes={{ button: "w-full" }}
|
||||
>
|
||||
{diffs().length} Files Changed
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="!overflow-hidden">
|
||||
{turns()}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
forceMount
|
||||
value="review"
|
||||
class="!overflow-hidden hidden data-[selected]:block"
|
||||
>
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</DiffComponentProvider>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div
|
||||
classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}
|
||||
>
|
||||
{turns()}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.161"
|
||||
version = "1.0.163"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.161/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.161/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.161/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.161/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.161/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"jose": "6.0.11"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -64,7 +64,7 @@
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
|
||||
@@ -128,6 +128,19 @@ const AGENT_USERNAME = "opencode-agent[bot]"
|
||||
const AGENT_REACTION = "eyes"
|
||||
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
|
||||
|
||||
// Parses GitHub remote URLs in various formats:
|
||||
// - https://github.com/owner/repo.git
|
||||
// - https://github.com/owner/repo
|
||||
// - git@github.com:owner/repo.git
|
||||
// - git@github.com:owner/repo
|
||||
// - ssh://git@github.com/owner/repo.git
|
||||
// - ssh://git@github.com/owner/repo
|
||||
export function parseGitHubRemote(url: string): { owner: string; repo: string } | null {
|
||||
const match = url.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/)
|
||||
if (!match) return null
|
||||
return { owner: match[1], repo: match[2] }
|
||||
}
|
||||
|
||||
export const GithubCommand = cmd({
|
||||
command: "github",
|
||||
describe: "manage GitHub agent",
|
||||
@@ -197,20 +210,12 @@ export const GithubInstallCommand = cmd({
|
||||
|
||||
// Get repo info
|
||||
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
|
||||
// match https or git pattern
|
||||
// ie. https://github.com/sst/opencode.git
|
||||
// ie. https://github.com/sst/opencode
|
||||
// ie. git@github.com:sst/opencode.git
|
||||
// ie. git@github.com:sst/opencode
|
||||
// ie. ssh://git@github.com/sst/opencode.git
|
||||
// ie. ssh://git@github.com/sst/opencode
|
||||
const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
|
||||
const parsed = parseGitHubRemote(info)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
const [, owner, repo] = parsed
|
||||
return { owner, repo, root: Instance.worktree }
|
||||
return { owner: parsed.owner, repo: parsed.repo, root: Instance.worktree }
|
||||
}
|
||||
|
||||
async function promptProvider() {
|
||||
|
||||
@@ -88,7 +88,9 @@ export const RunCommand = cmd({
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
let message = [...args.message, ...(args["--"] || [])].join(" ")
|
||||
let message = [...args.message, ...(args["--"] || [])]
|
||||
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
||||
.join(" ")
|
||||
|
||||
const fileParts: any[] = []
|
||||
if (args.file) {
|
||||
|
||||
@@ -357,7 +357,7 @@ export function Autocomplete(props: {
|
||||
|
||||
const options = createMemo(() => {
|
||||
const mixed: AutocompleteOption[] = (
|
||||
store.visible === "@" ? [...agents(), ...(files.loading ? files.latest || [] : files())] : [...commands()]
|
||||
store.visible === "@" ? [...agents(), ...(files() || [])] : [...commands()]
|
||||
).filter((x) => x.disabled !== true)
|
||||
const currentFilter = filter()
|
||||
if (!currentFilter) return mixed.slice(0, 10)
|
||||
|
||||
@@ -307,10 +307,9 @@ function Option(props: {
|
||||
fg={props.active ? fg : props.current ? theme.primary : theme.text}
|
||||
attributes={props.active ? TextAttributes.BOLD : undefined}
|
||||
overflow="hidden"
|
||||
wrapMode="word"
|
||||
paddingLeft={3}
|
||||
>
|
||||
{Locale.truncate(props.title, 62)}
|
||||
{Locale.truncate(props.title, 61)}
|
||||
<Show when={props.description}>
|
||||
<span style={{ fg: props.active ? fg : theme.textMuted }}> {props.description}</span>
|
||||
</Show>
|
||||
|
||||
@@ -5,7 +5,7 @@ import os from "os"
|
||||
import z from "zod"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { ModelsDev } from "../provider/models"
|
||||
import { mergeDeep, pipe } from "remeda"
|
||||
import { mergeDeep, pipe, unique } from "remeda"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import { lazy } from "../util/lazy"
|
||||
@@ -76,6 +76,13 @@ export namespace Config {
|
||||
stop: Instance.worktree,
|
||||
}),
|
||||
)),
|
||||
...(await Array.fromAsync(
|
||||
Filesystem.up({
|
||||
targets: [".opencode"],
|
||||
start: Global.Path.home,
|
||||
stop: Global.Path.home,
|
||||
}),
|
||||
)),
|
||||
]
|
||||
|
||||
if (Flag.OPENCODE_CONFIG_DIR) {
|
||||
@@ -84,7 +91,7 @@ export namespace Config {
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
for (const dir of directories) {
|
||||
for (const dir of unique(directories)) {
|
||||
await assertValid(dir)
|
||||
|
||||
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
||||
|
||||
@@ -5,11 +5,13 @@ import { Instance } from "../project/instance"
|
||||
import { Log } from "../util/log"
|
||||
import { FileIgnore } from "./ignore"
|
||||
import { Config } from "../config/config"
|
||||
import path from "path"
|
||||
// @ts-ignore
|
||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import type ParcelWatcher from "@parcel/watcher"
|
||||
import { $ } from "bun"
|
||||
import { Flag } from "@/flag/flag"
|
||||
|
||||
declare const OPENCODE_LIBC: string | undefined
|
||||
|
||||
@@ -57,17 +59,24 @@ export namespace FileWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
const subs = []
|
||||
const subs: ParcelWatcher.AsyncSubscription[] = []
|
||||
const cfgIgnores = cfg.watcher?.ignore ?? []
|
||||
|
||||
subs.push(
|
||||
await watcher().subscribe(Instance.directory, subscribe, {
|
||||
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
|
||||
backend,
|
||||
}),
|
||||
)
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
||||
subs.push(
|
||||
await watcher().subscribe(Instance.directory, subscribe, {
|
||||
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
|
||||
backend,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const vcsDir = await $`git rev-parse --git-dir`.quiet().nothrow().cwd(Instance.worktree).text()
|
||||
const vcsDir = await $`git rev-parse --git-dir`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(Instance.worktree)
|
||||
.text()
|
||||
.then((x) => path.resolve(Instance.worktree, x.trim()))
|
||||
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
||||
subs.push(
|
||||
await watcher().subscribe(vcsDir, subscribe, {
|
||||
@@ -86,6 +95,9 @@ export namespace FileWatcher {
|
||||
)
|
||||
|
||||
export function init() {
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) {
|
||||
return
|
||||
}
|
||||
state()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export namespace Flag {
|
||||
|
||||
// Experimental
|
||||
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
||||
export const OPENCODE_EXPERIMENTAL_FILEWATCHER = truthy("OPENCODE_EXPERIMENTAL_FILEWATCHER")
|
||||
export const OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER = truthy("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER")
|
||||
export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY =
|
||||
OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY")
|
||||
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
|
||||
|
||||
@@ -13,6 +13,8 @@ import { withTimeout } from "../util/timeout"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
const DIAGNOSTICS_DEBOUNCE_MS = 150
|
||||
|
||||
export namespace LSPClient {
|
||||
const log = Log.create({ service: "lsp.client" })
|
||||
|
||||
@@ -188,13 +190,18 @@ export namespace LSPClient {
|
||||
)
|
||||
log.info("waiting for diagnostics", { path: normalizedPath })
|
||||
let unsub: () => void
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
||||
return await withTimeout(
|
||||
new Promise<void>((resolve) => {
|
||||
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
|
||||
if (event.properties.path === normalizedPath && event.properties.serverID === result.serverID) {
|
||||
log.info("got diagnostics", { path: normalizedPath })
|
||||
unsub?.()
|
||||
resolve()
|
||||
// Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax)
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
log.info("got diagnostics", { path: normalizedPath })
|
||||
unsub?.()
|
||||
resolve()
|
||||
}, DIAGNOSTICS_DEBOUNCE_MS)
|
||||
}
|
||||
})
|
||||
}),
|
||||
@@ -202,6 +209,7 @@ export namespace LSPClient {
|
||||
)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
unsub?.()
|
||||
})
|
||||
},
|
||||
|
||||
@@ -127,8 +127,10 @@ export namespace Server {
|
||||
return streamSSE(c, async (stream) => {
|
||||
stream.writeSSE({
|
||||
data: JSON.stringify({
|
||||
type: "server.connected",
|
||||
properties: {},
|
||||
payload: {
|
||||
type: "server.connected",
|
||||
properties: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
async function handler(event: any) {
|
||||
@@ -137,8 +139,22 @@ export namespace Server {
|
||||
})
|
||||
}
|
||||
GlobalBus.on("event", handler)
|
||||
|
||||
// Send heartbeat every 30s to prevent WKWebView timeout (60s default)
|
||||
const heartbeat = setInterval(() => {
|
||||
stream.writeSSE({
|
||||
data: JSON.stringify({
|
||||
payload: {
|
||||
type: "server.heartbeat",
|
||||
properties: {},
|
||||
},
|
||||
}),
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
stream.onAbort(() => {
|
||||
clearInterval(heartbeat)
|
||||
GlobalBus.off("event", handler)
|
||||
resolve()
|
||||
log.info("global event disconnected")
|
||||
@@ -2468,8 +2484,20 @@ export namespace Server {
|
||||
stream.close()
|
||||
}
|
||||
})
|
||||
|
||||
// Send heartbeat every 30s to prevent WKWebView timeout (60s default)
|
||||
const heartbeat = setInterval(() => {
|
||||
stream.writeSSE({
|
||||
data: JSON.stringify({
|
||||
type: "server.heartbeat",
|
||||
properties: {},
|
||||
}),
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
stream.onAbort(() => {
|
||||
clearInterval(heartbeat)
|
||||
unsub()
|
||||
resolve()
|
||||
log.info("event disconnected")
|
||||
|
||||
@@ -60,11 +60,18 @@ export namespace LLM {
|
||||
.join("\n"),
|
||||
)
|
||||
|
||||
const header = system[0]
|
||||
const original = clone(system)
|
||||
await Plugin.trigger("experimental.chat.system.transform", {}, { system })
|
||||
if (system.length === 0) {
|
||||
system.push(...original)
|
||||
}
|
||||
// rejoin to maintain 2-part structure for caching if header unchanged
|
||||
if (system.length > 2 && system[0] === header) {
|
||||
const rest = system.slice(1)
|
||||
system.length = 0
|
||||
system.push(header, rest.join("\n"))
|
||||
}
|
||||
|
||||
const params = await Plugin.trigger(
|
||||
"chat.params",
|
||||
|
||||
@@ -683,6 +683,7 @@ export namespace SessionPrompt {
|
||||
tools: input.tools,
|
||||
agent: agent.name,
|
||||
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
|
||||
system: input.system,
|
||||
}
|
||||
|
||||
const parts = await Promise.all(
|
||||
@@ -1096,7 +1097,7 @@ export namespace SessionPrompt {
|
||||
`
|
||||
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
|
||||
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
|
||||
${input.command}
|
||||
eval ${JSON.stringify(input.command)}
|
||||
`,
|
||||
],
|
||||
},
|
||||
@@ -1105,8 +1106,9 @@ export namespace SessionPrompt {
|
||||
"-c",
|
||||
"-l",
|
||||
`
|
||||
shopt -s expand_aliases
|
||||
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
|
||||
${input.command}
|
||||
eval ${JSON.stringify(input.command)}
|
||||
`,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { test, expect } from "bun:test"
|
||||
import { parseGitHubRemote } from "../../src/cli/cmd/github"
|
||||
|
||||
test("parses https URL with .git suffix", () => {
|
||||
expect(parseGitHubRemote("https://github.com/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
|
||||
})
|
||||
|
||||
test("parses https URL without .git suffix", () => {
|
||||
expect(parseGitHubRemote("https://github.com/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
|
||||
})
|
||||
|
||||
test("parses git@ URL with .git suffix", () => {
|
||||
expect(parseGitHubRemote("git@github.com:sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
|
||||
})
|
||||
|
||||
test("parses git@ URL without .git suffix", () => {
|
||||
expect(parseGitHubRemote("git@github.com:sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
|
||||
})
|
||||
|
||||
test("parses ssh:// URL with .git suffix", () => {
|
||||
expect(parseGitHubRemote("ssh://git@github.com/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
|
||||
})
|
||||
|
||||
test("parses ssh:// URL without .git suffix", () => {
|
||||
expect(parseGitHubRemote("ssh://git@github.com/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
|
||||
})
|
||||
|
||||
test("parses http URL", () => {
|
||||
expect(parseGitHubRemote("http://github.com/owner/repo")).toEqual({ owner: "owner", repo: "repo" })
|
||||
})
|
||||
|
||||
test("parses URL with hyphenated owner and repo names", () => {
|
||||
expect(parseGitHubRemote("https://github.com/my-org/my-repo.git")).toEqual({ owner: "my-org", repo: "my-repo" })
|
||||
})
|
||||
|
||||
test("parses URL with underscores in names", () => {
|
||||
expect(parseGitHubRemote("git@github.com:my_org/my_repo.git")).toEqual({ owner: "my_org", repo: "my_repo" })
|
||||
})
|
||||
|
||||
test("parses URL with numbers in names", () => {
|
||||
expect(parseGitHubRemote("https://github.com/org123/repo456")).toEqual({ owner: "org123", repo: "repo456" })
|
||||
})
|
||||
|
||||
test("parses repos with dots in the name", () => {
|
||||
expect(parseGitHubRemote("https://github.com/socketio/socket.io.git")).toEqual({
|
||||
owner: "socketio",
|
||||
repo: "socket.io",
|
||||
})
|
||||
expect(parseGitHubRemote("https://github.com/vuejs/vue.js")).toEqual({
|
||||
owner: "vuejs",
|
||||
repo: "vue.js",
|
||||
})
|
||||
expect(parseGitHubRemote("git@github.com:mrdoob/three.js.git")).toEqual({
|
||||
owner: "mrdoob",
|
||||
repo: "three.js",
|
||||
})
|
||||
expect(parseGitHubRemote("https://github.com/jashkenas/backbone.git")).toEqual({
|
||||
owner: "jashkenas",
|
||||
repo: "backbone",
|
||||
})
|
||||
})
|
||||
|
||||
test("returns null for non-github URLs", () => {
|
||||
expect(parseGitHubRemote("https://gitlab.com/owner/repo.git")).toBeNull()
|
||||
expect(parseGitHubRemote("git@gitlab.com:owner/repo.git")).toBeNull()
|
||||
expect(parseGitHubRemote("https://bitbucket.org/owner/repo")).toBeNull()
|
||||
})
|
||||
|
||||
test("returns null for invalid URLs", () => {
|
||||
expect(parseGitHubRemote("not-a-url")).toBeNull()
|
||||
expect(parseGitHubRemote("")).toBeNull()
|
||||
expect(parseGitHubRemote("github.com")).toBeNull()
|
||||
expect(parseGitHubRemote("https://github.com/")).toBeNull()
|
||||
expect(parseGitHubRemote("https://github.com/owner")).toBeNull()
|
||||
})
|
||||
|
||||
test("returns null for URLs with extra path segments", () => {
|
||||
expect(parseGitHubRemote("https://github.com/owner/repo/tree/main")).toBeNull()
|
||||
expect(parseGitHubRemote("https://github.com/owner/repo/blob/main/file.ts")).toBeNull()
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -24,4 +24,4 @@
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -29,4 +29,4 @@
|
||||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/tauri",
|
||||
"private": true,
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import { $ } from "bun"
|
||||
|
||||
import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
|
||||
|
||||
@@ -9,7 +9,7 @@ const sidecarConfig = getCurrentSidecar()
|
||||
const dir = "src-tauri/target/opencode-binaries"
|
||||
|
||||
await $`mkdir -p ${dir}`
|
||||
await $`gh release download --pattern ${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} --repo sst/opencode --skip-existing --dir ${dir}`
|
||||
await $`gh release download ${Bun.env.OPENCODE_RELEASE_TAG} --pattern ${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} --repo sst/opencode --skip-existing --dir ${dir}`
|
||||
|
||||
if (sidecarConfig.assetExt === "tar.gz") {
|
||||
await $`tar -xvzf ${dir}/${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} -C ${dir}`
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use std::{
|
||||
net::{SocketAddr, TcpListener},
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, LogicalSize, Manager, Monitor, RunEvent, WebviewUrl, WebviewWindow};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
||||
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::net::TcpSocket;
|
||||
@@ -15,6 +13,28 @@ use tokio::net::TcpSocket;
|
||||
#[derive(Clone)]
|
||||
struct ServerState(Arc<Mutex<Option<CommandChild>>>);
|
||||
|
||||
#[tauri::command]
|
||||
fn kill_sidecar(app: AppHandle) {
|
||||
let Some(server_state) = app.try_state::<ServerState>() else {
|
||||
println!("Server not running");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(server_state) = server_state
|
||||
.0
|
||||
.lock()
|
||||
.expect("Failed to acquire mutex lock")
|
||||
.take()
|
||||
else {
|
||||
println!("Server state missing");
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = server_state.kill();
|
||||
|
||||
println!("Killed server");
|
||||
}
|
||||
|
||||
fn get_sidecar_port() -> u16 {
|
||||
option_env!("OPENCODE_PORT")
|
||||
.map(|s| s.to_string())
|
||||
@@ -29,41 +49,12 @@ fn get_sidecar_port() -> u16 {
|
||||
})
|
||||
}
|
||||
|
||||
fn find_and_kill_process_on_port(port: u16) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Find all listeners on the specified port
|
||||
let listeners = listeners::get_processes_by_port(port)?;
|
||||
|
||||
if listeners.is_empty() {
|
||||
println!("No processes found listening on port {}", port);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for listener in listeners {
|
||||
let pid = listener.pid;
|
||||
println!("Found process {} listening on port {}", pid, port);
|
||||
|
||||
// Kill the process using platform-appropriate command
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.output()?;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Command::new("kill")
|
||||
.args(["-9", &pid.to_string()])
|
||||
.output()?;
|
||||
}
|
||||
|
||||
println!("Killed process {}", pid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn get_user_shell() -> String {
|
||||
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
|
||||
}
|
||||
|
||||
fn spawn_sidecar(app: &AppHandle, port: u16) -> CommandChild {
|
||||
#[cfg(target_os = "windows")]
|
||||
let (mut rx, child) = app
|
||||
.shell()
|
||||
.sidecar("opencode-cli")
|
||||
@@ -74,6 +65,27 @@ fn spawn_sidecar(app: &AppHandle, port: u16) -> CommandChild {
|
||||
.spawn()
|
||||
.expect("Failed to spawn opencode");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let (mut rx, child) = {
|
||||
let sidecar_path = tauri::utils::platform::current_exe()
|
||||
.expect("Failed to get current exe")
|
||||
.parent()
|
||||
.expect("Failed to get parent dir")
|
||||
.join("opencode-cli");
|
||||
let shell = get_user_shell();
|
||||
app.shell()
|
||||
.command(&shell)
|
||||
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
||||
.env("OPENCODE_CLIENT", "desktop")
|
||||
.args([
|
||||
"-il",
|
||||
"-c",
|
||||
&format!("{} serve --port={}", sidecar_path.display(), port),
|
||||
])
|
||||
.spawn()
|
||||
.expect("Failed to spawn opencode")
|
||||
};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
@@ -116,6 +128,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![kill_sidecar])
|
||||
.setup(move |app| {
|
||||
let app = app.handle().clone();
|
||||
|
||||
@@ -124,28 +137,6 @@ pub fn run() {
|
||||
|
||||
let should_spawn_sidecar = !is_server_running(port).await;
|
||||
|
||||
// if server_running {
|
||||
// let res = app
|
||||
// .dialog()
|
||||
// .message(
|
||||
// "OpenCode Server is already running, would you like to restart it?",
|
||||
// )
|
||||
// .buttons(MessageDialogButtons::YesNo)
|
||||
// .blocking_show_with_result();
|
||||
|
||||
// match res {
|
||||
// MessageDialogResult::Yes => {
|
||||
// if let Err(e) = find_and_kill_process_on_port(port) {
|
||||
// eprintln!("Failed to kill process on port {}: {}", port, e);
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
// _ => false,
|
||||
// }
|
||||
// } else {
|
||||
// true
|
||||
// };
|
||||
|
||||
let child = if should_spawn_sidecar {
|
||||
let child = spawn_sidecar(&app, port);
|
||||
|
||||
@@ -218,24 +209,7 @@ pub fn run() {
|
||||
if let RunEvent::Exit = event {
|
||||
println!("Received Exit");
|
||||
|
||||
let Some(server_state) = app.try_state::<ServerState>() else {
|
||||
println!("Server not running");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(server_state) = server_state
|
||||
.0
|
||||
.lock()
|
||||
.expect("Failed to acquire mutex lock")
|
||||
.take()
|
||||
else {
|
||||
println!("Server state missing");
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = server_state.kill();
|
||||
|
||||
println!("Killed server");
|
||||
kill_sidecar(app.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// @refresh reload
|
||||
import { render } from "solid-js/web"
|
||||
import { App, PlatformProvider, Platform } from "@opencode-ai/desktop"
|
||||
import { runUpdater } from "./updater"
|
||||
import { onMount } from "solid-js"
|
||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||
import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||
|
||||
import { runUpdater, UPDATER_ENABLED } from "./updater"
|
||||
import { createMenu } from "./menu"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
throw new Error(
|
||||
@@ -48,9 +50,11 @@ const platform: Platform = {
|
||||
},
|
||||
}
|
||||
|
||||
createMenu()
|
||||
|
||||
render(() => {
|
||||
onMount(() => {
|
||||
if (window.__OPENCODE__?.updaterEnabled) runUpdater()
|
||||
if (UPDATER_ENABLED) runUpdater()
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/menu"
|
||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||
|
||||
import { runUpdater, UPDATER_ENABLED } from "./updater"
|
||||
|
||||
export async function createMenu() {
|
||||
if (ostype() !== "macos") return
|
||||
|
||||
const menu = await Menu.new({
|
||||
items: [
|
||||
await Submenu.new({
|
||||
text: "OpenCode",
|
||||
items: [
|
||||
await PredefinedMenuItem.new({
|
||||
item: { About: null },
|
||||
}),
|
||||
await MenuItem.new({
|
||||
enabled: UPDATER_ENABLED,
|
||||
action: () => runUpdater(),
|
||||
text: "Check For Updates...",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Separator",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Hide",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "HideOthers",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "ShowAll",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Separator",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Quit",
|
||||
}),
|
||||
].filter(Boolean),
|
||||
}),
|
||||
// await Submenu.new({
|
||||
// text: "File",
|
||||
// items: [
|
||||
// await MenuItem.new({
|
||||
// enabled: false,
|
||||
// text: "Open Project...",
|
||||
// }),
|
||||
// await PredefinedMenuItem.new({
|
||||
// item: "Separator"
|
||||
// }),
|
||||
// await MenuItem.new({
|
||||
// enabled: false,
|
||||
// text: "New Session",
|
||||
// }),
|
||||
// await PredefinedMenuItem.new({
|
||||
// item: "Separator"
|
||||
// }),
|
||||
// await MenuItem.new({
|
||||
// enabled: false,
|
||||
// text: "Close Project",
|
||||
// })
|
||||
// ]
|
||||
// }),
|
||||
await Submenu.new({
|
||||
text: "Edit",
|
||||
items: [
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Undo",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Redo",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Separator",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Cut",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Copy",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "Paste",
|
||||
}),
|
||||
await PredefinedMenuItem.new({
|
||||
item: "SelectAll",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
menu.setAsAppMenu()
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { check, DownloadEvent } from "@tauri-apps/plugin-updater"
|
||||
import { relaunch } from "@tauri-apps/plugin-process"
|
||||
import { ask, message } from "@tauri-apps/plugin-dialog"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
|
||||
export const UPDATER_ENABLED = window.__OPENCODE__?.updaterEnabled ?? false
|
||||
|
||||
export async function runUpdater(onDownloadEvent?: (progress: DownloadEvent) => void) {
|
||||
let update
|
||||
@@ -32,6 +35,7 @@ export async function runUpdater(onDownloadEvent?: (progress: DownloadEvent) =>
|
||||
return false
|
||||
}
|
||||
|
||||
await invoke("kill_sidecar")
|
||||
await relaunch()
|
||||
|
||||
return true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
||||
@@ -5,30 +5,69 @@
|
||||
color: var(--text-base);
|
||||
text-wrap: pretty;
|
||||
|
||||
/* text-12-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large); /* 166.667% */
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* text-12-regular */
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
|
||||
h1 {
|
||||
margin-top: 40px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-strong);
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 32px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-strong);
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 24px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-strong);
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
li {
|
||||
margin-bottom: 12px;
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
@@ -37,6 +76,14 @@
|
||||
border-color: var(--border-weaker-base);
|
||||
}
|
||||
|
||||
.shiki {
|
||||
font-size: 13px;
|
||||
background: var(--surface-raised-base) !important; /* temporary fix to test style */
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 0.5px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
@@ -51,7 +98,7 @@
|
||||
:not(pre) > code {
|
||||
font-family: var(--font-family-mono);
|
||||
font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||
font-size: 0.9em;
|
||||
font-size: 13px;
|
||||
/* background-color: var(--surface-base-strong); */
|
||||
/* padding: 0.15em 0.35em; */
|
||||
/* border-radius: var(--radius-sm); */
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
[data-component="user-message"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
@@ -77,6 +77,10 @@
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
background: var(--surface-inset-base);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 0.5px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
.text-text-strong {
|
||||
@@ -93,6 +97,7 @@
|
||||
|
||||
[data-component="markdown"] {
|
||||
margin-top: 32px;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +129,7 @@
|
||||
|
||||
[data-slot="message-part-tool-error-title"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
@@ -188,7 +193,7 @@
|
||||
[data-slot="message-part-title"] {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { useData } from "../context"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { useCodeComponent } from "../context/code"
|
||||
import { BasicTool } from "./basic-tool"
|
||||
import { GenericTool } from "./basic-tool"
|
||||
import { Card } from "./card"
|
||||
@@ -19,6 +20,7 @@ import { Checkbox } from "./checkbox"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Markdown } from "./markdown"
|
||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
|
||||
export interface MessageProps {
|
||||
message: MessageType
|
||||
@@ -488,9 +490,10 @@ ToolRegistry.register({
|
||||
ToolRegistry.register({
|
||||
name: "write",
|
||||
render(props) {
|
||||
console.log(props)
|
||||
const codeComponent = useCodeComponent()
|
||||
return (
|
||||
<BasicTool
|
||||
defaultOpen
|
||||
icon="code-lines"
|
||||
trigger={
|
||||
<div data-component="write-trigger">
|
||||
@@ -507,19 +510,19 @@ ToolRegistry.register({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* <Show when={props.input.content}> */}
|
||||
{/* <div data-component="write-content"> */}
|
||||
{/* <Code */}
|
||||
{/* file={{ */}
|
||||
{/* name: props.input.filePath, */}
|
||||
{/* contents: props.input.content, */}
|
||||
{/* cacheKey: checksum(props.input.content), */}
|
||||
{/* }} */}
|
||||
{/* overflow="scroll" */}
|
||||
{/* class="pb-40" */}
|
||||
{/* /> */}
|
||||
{/* </div> */}
|
||||
{/* </Show> */}
|
||||
<Show when={props.input.content}>
|
||||
<div data-component="write-content">
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
file={{
|
||||
name: props.input.filePath,
|
||||
contents: props.input.content,
|
||||
cacheKey: checksum(props.input.content),
|
||||
}}
|
||||
overflow="scroll"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -15,6 +15,30 @@
|
||||
color: var(--text-weak);
|
||||
transition: transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
&[data-variant="secondary"] {
|
||||
background-color: var(--button-secondary-hover);
|
||||
}
|
||||
&[data-variant="ghost"] {
|
||||
background-color: var(--surface-raised-base-active);
|
||||
}
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--icon-strong-active);
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-expanded]):focus {
|
||||
&[data-variant="secondary"] {
|
||||
background-color: var(--button-secondary-base);
|
||||
}
|
||||
&[data-variant="ghost"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
&[data-variant="primary"] {
|
||||
background-color: var(--icon-strong-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +61,6 @@
|
||||
}
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
min-height: 2rem;
|
||||
overflow-y: auto;
|
||||
max-height: 12rem;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -46,6 +46,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
|
||||
<Kobalte<T, { category: string; options: T[] }>
|
||||
{...others}
|
||||
data-component="select"
|
||||
placement="bottom-start"
|
||||
value={local.current}
|
||||
options={grouped()}
|
||||
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
[data-slot="session-review-title"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-strong);
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
gap: clamp(8px, calc(42px - var(--scroll-y) * 0.48), 42px);
|
||||
gap: 42px;
|
||||
/* gap: clamp(8px, calc(42px - var(--scroll-y) * 0.48), 42px); */
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
@@ -59,12 +60,12 @@
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-content"] {
|
||||
margin-top: -24px;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
[data-slot="session-turn-message-title"] {
|
||||
width: 100%;
|
||||
font-size: 14px; /* text-14-medium */
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: 500;
|
||||
color: var(--text-strong);
|
||||
overflow: hidden;
|
||||
@@ -104,17 +105,22 @@
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
|
||||
p {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-x-large);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-summary-title"] {
|
||||
font-size: 12px; /* text-12-medium */
|
||||
font-size: 13px; /* text-12-medium */
|
||||
font-weight: 500;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="session-turn-markdown"] {
|
||||
&[data-diffs="true"] {
|
||||
font-size: 14px; /* text-14-regular */
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&[data-fade="true"] > * {
|
||||
@@ -232,6 +238,10 @@
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
|
||||
[data-expandable="false"] {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="session-turn-file-info"] {
|
||||
@@ -317,9 +327,19 @@
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
[data-slot="session-turn-retry-message"] {
|
||||
font-weight: 500;
|
||||
color: var(--syntax-critical);
|
||||
}
|
||||
[data-slot="session-turn-retry-seconds"] {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
[data-slot="session-turn-retry-attempt"] {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="session-turn-details-text"] {
|
||||
font-size: 12px; /* text-12-medium */
|
||||
font-size: 13px; /* text-12-medium */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useData } from "../context"
|
||||
import { useDiffComponent } from "../context/diff"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { Typewriter } from "./typewriter"
|
||||
@@ -49,6 +49,29 @@ export function SessionTurn(
|
||||
},
|
||||
)
|
||||
const working = createMemo(() => status()?.type !== "idle")
|
||||
const retry = createMemo(() => {
|
||||
const s = status()
|
||||
if (s.type !== "retry") return
|
||||
return s
|
||||
})
|
||||
const [retrySeconds, setRetrySeconds] = createSignal(0)
|
||||
|
||||
createEffect(() => {
|
||||
const r = retry()
|
||||
if (!r) {
|
||||
setRetrySeconds(0)
|
||||
return
|
||||
}
|
||||
|
||||
const updateSeconds = () => {
|
||||
const next = r.next
|
||||
if (next) setRetrySeconds(Math.max(0, Math.round((next - Date.now()) / 1000)))
|
||||
}
|
||||
updateSeconds()
|
||||
|
||||
const timer = setInterval(updateSeconds, 1000)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
})
|
||||
|
||||
let scrollRef: HTMLDivElement | undefined
|
||||
const [state, setState] = createStore({
|
||||
@@ -57,17 +80,12 @@ export function SessionTurn(
|
||||
userScrolled: false,
|
||||
stickyHeaderHeight: 0,
|
||||
scrollY: 0,
|
||||
autoScrolling: false,
|
||||
})
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollRef) return
|
||||
if (state.autoScrolling) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef
|
||||
const scrollRoom = scrollHeight - clientHeight
|
||||
if (scrollRoom > 100) {
|
||||
setState("scrollY", scrollTop)
|
||||
}
|
||||
setState("scrollY", scrollTop)
|
||||
const atBottom = scrollHeight - scrollTop - clientHeight < 50
|
||||
if (!atBottom && working()) {
|
||||
setState("userScrolled", true)
|
||||
@@ -81,13 +99,9 @@ export function SessionTurn(
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (!scrollRef || state.userScrolled || !working() || state.autoScrolling) return
|
||||
setState("autoScrolling", true)
|
||||
if (!scrollRef || state.userScrolled || !working()) return
|
||||
requestAnimationFrame(() => {
|
||||
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "instant" })
|
||||
requestAnimationFrame(() => {
|
||||
setState("autoScrolling", false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -309,10 +323,12 @@ export function SessionTurn(
|
||||
{/* Trigger (sticky) */}
|
||||
<div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-expandable={assistantMessages().length > 0}
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (assistantMessages().length === 0) return
|
||||
const next = !store.stepsExpanded
|
||||
setStore("stepsExpanded", next)
|
||||
props.onStepsExpandedChange?.(next)
|
||||
@@ -322,17 +338,32 @@ export function SessionTurn(
|
||||
<Spinner />
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={retry()}>
|
||||
<span data-slot="session-turn-retry-message">
|
||||
{(() => {
|
||||
const r = retry()
|
||||
if (!r) return ""
|
||||
return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message
|
||||
})()}
|
||||
</span>
|
||||
<span data-slot="session-turn-retry-seconds">
|
||||
· retrying {retrySeconds() > 0 ? `in ${retrySeconds()}s ` : ""}
|
||||
</span>
|
||||
<span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span>
|
||||
</Match>
|
||||
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
|
||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||
</Switch>
|
||||
<span>·</span>
|
||||
<span>{store.duration}</span>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
<Show when={assistantMessages().length > 0}>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</Show>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Response */}
|
||||
<Show when={store.stepsExpanded}>
|
||||
<Show when={store.stepsExpanded && assistantMessages().length > 0}>
|
||||
<div data-slot="session-turn-collapsible-content-inner">
|
||||
<For each={assistantMessages()}>
|
||||
{(assistantMessage) => {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ValidComponent } from "solid-js"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
const ctx = createSimpleContext<ValidComponent, { component: ValidComponent }>({
|
||||
name: "CodeComponent",
|
||||
init: (props) => props.component,
|
||||
})
|
||||
|
||||
export const CodeComponentProvider = ctx.provider
|
||||
export const useCodeComponent = ctx.use
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
For,
|
||||
getOwner,
|
||||
Owner,
|
||||
ParentProps,
|
||||
@@ -13,74 +11,59 @@ import {
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
|
||||
type DialogElement = () => JSX.Element
|
||||
|
||||
const Context = createContext<ReturnType<typeof init>>()
|
||||
|
||||
function init() {
|
||||
const [store, setStore] = createSignal<
|
||||
{
|
||||
id: string
|
||||
element: DialogElement
|
||||
onClose?: () => void
|
||||
owner: Owner
|
||||
}[]
|
||||
>([])
|
||||
const [active, setActive] = createSignal<
|
||||
| {
|
||||
id: string
|
||||
element: DialogElement
|
||||
onClose?: () => void
|
||||
owner: Owner
|
||||
}
|
||||
| undefined
|
||||
>()
|
||||
|
||||
const result = {
|
||||
get stack() {
|
||||
return store()
|
||||
get active() {
|
||||
return active()
|
||||
},
|
||||
pop() {
|
||||
const current = store().at(-1)
|
||||
if (!current) return
|
||||
current?.onClose?.()
|
||||
setStore((stack) => {
|
||||
stack.pop()
|
||||
return [...stack]
|
||||
close() {
|
||||
active()?.onClose?.()
|
||||
setActive(undefined)
|
||||
},
|
||||
show(element: DialogElement, owner: Owner, onClose?: () => void) {
|
||||
active()?.onClose?.()
|
||||
const id = Math.random().toString(36).slice(2)
|
||||
setActive({
|
||||
id,
|
||||
element: () =>
|
||||
runWithOwner(owner, () => (
|
||||
<Show when={active()?.id === id}>
|
||||
<Kobalte
|
||||
modal
|
||||
open={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
console.log("closing")
|
||||
result.close()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Overlay data-component="dialog-overlay" />
|
||||
{element()}
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
</Show>
|
||||
)),
|
||||
onClose,
|
||||
owner,
|
||||
})
|
||||
},
|
||||
replace(element: DialogElement, owner: Owner, onClose?: () => void) {
|
||||
for (const item of store()) {
|
||||
item.onClose?.()
|
||||
}
|
||||
const id = Math.random().toString(36)
|
||||
setStore([
|
||||
{
|
||||
id,
|
||||
element: () =>
|
||||
runWithOwner(owner, () => (
|
||||
<Show when={result.stack.at(-1)?.id === id}>
|
||||
<Kobalte
|
||||
modal
|
||||
defaultOpen
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose?.()
|
||||
result.pop()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Overlay data-component="dialog-overlay" />
|
||||
{element()}
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
</Show>
|
||||
)),
|
||||
onClose,
|
||||
owner,
|
||||
},
|
||||
])
|
||||
},
|
||||
clear() {
|
||||
for (const item of store()) {
|
||||
item.onClose?.()
|
||||
}
|
||||
setStore([])
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -89,14 +72,12 @@ function init() {
|
||||
export function DialogProvider(props: ParentProps) {
|
||||
const ctx = init()
|
||||
createEffect(() => {
|
||||
console.log("store", ctx.stack.length)
|
||||
console.log("active", ctx.active)
|
||||
})
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{props.children}
|
||||
<div data-component="dialog-stack">
|
||||
<For each={ctx.stack}>{(item) => <>{item.element()}</>}</For>
|
||||
</div>
|
||||
<div data-component="dialog-stack">{ctx.active?.element?.()}</div>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
@@ -111,17 +92,14 @@ export function useDialog() {
|
||||
throw new Error("useDialog must be used within a DialogProvider")
|
||||
}
|
||||
return {
|
||||
get stack() {
|
||||
return ctx.stack
|
||||
get active() {
|
||||
return ctx.active
|
||||
},
|
||||
replace(element: DialogElement, onClose?: () => void) {
|
||||
ctx.replace(element, owner, onClose)
|
||||
show(element: DialogElement, onClose?: () => void) {
|
||||
ctx.show(element, owner, onClose)
|
||||
},
|
||||
pop() {
|
||||
ctx.pop()
|
||||
},
|
||||
clear() {
|
||||
ctx.clear()
|
||||
close() {
|
||||
ctx.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { createContext, useContext, type ParentProps, type ValidComponent } from "solid-js"
|
||||
import type { ValidComponent } from "solid-js"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
const DiffComponentContext = createContext<ValidComponent>()
|
||||
const ctx = createSimpleContext<ValidComponent, { component: ValidComponent }>({
|
||||
name: "DiffComponent",
|
||||
init: (props) => props.component,
|
||||
})
|
||||
|
||||
export function DiffComponentProvider(props: ParentProps<{ component: ValidComponent }>) {
|
||||
return <DiffComponentContext.Provider value={props.component}>{props.children}</DiffComponentContext.Provider>
|
||||
}
|
||||
|
||||
export function useDiffComponent() {
|
||||
const component = useContext(DiffComponentContext)
|
||||
if (!component) throw new Error("DiffComponentProvider must be used to provide a diff component")
|
||||
return component
|
||||
}
|
||||
export const DiffComponentProvider = ctx.provider
|
||||
export const useDiffComponent = ctx.use
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
--font-size-x-large: 20px;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--line-height-large: 20px;
|
||||
--line-height-x-large: 24px;
|
||||
--line-height-2x-large: 30px;
|
||||
--line-height-large: 150%;
|
||||
--line-height-x-large: 180%;
|
||||
--line-height-2x-large: 200%;
|
||||
--letter-spacing-normal: 0;
|
||||
--letter-spacing-tight: -0.1599999964237213;
|
||||
--letter-spacing-tightest: -0.3199999928474426;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -325,3 +325,4 @@ These environment variables enable experimental features that may change or be r
|
||||
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disable copy on select in TUI |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH` | number | Max output length for bash commands |
|
||||
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Default timeout for bash commands in ms |
|
||||
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Enable file watcher for entire dir |
|
||||
|
||||
@@ -23,7 +23,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
| [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-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
|
||||
| [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-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
|
||||
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
|
||||
| [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. |
|
||||
@@ -35,12 +35,13 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
|
||||
## Projects
|
||||
|
||||
| Name | Description |
|
||||
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| [kimaki](https://github.com/remorses/kimaki) | Discord bot to control OpenCode sessions, built on the SDK |
|
||||
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim plugin for editor-aware prompts, built on the API |
|
||||
| [portal](https://github.com/hosenur/portal) | Mobile-first web UI for OpenCode over Tailscale/VPN |
|
||||
| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Template for building OpenCode plugins |
|
||||
| Name | Description |
|
||||
| ---------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| [kimaki](https://github.com/remorses/kimaki) | Discord bot to control OpenCode sessions, built on the SDK |
|
||||
| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | Neovim plugin for editor-aware prompts, built on the API |
|
||||
| [portal](https://github.com/hosenur/portal) | Mobile-first web UI for OpenCode over Tailscale/VPN |
|
||||
| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | Template for building OpenCode plugins |
|
||||
| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | Neovim frontend for opencode - a terminal-based AI coding agent |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ if (!Script.preview) {
|
||||
await $`git cherry-pick HEAD..origin/dev`.nothrow()
|
||||
await $`git push origin HEAD --tags --no-verify --force-with-lease`
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
||||
await $`gh release create v${Script.version} --title "v${Script.version}" --notes ${notes.join("\n") || "No notable changes"} ./packages/opencode/dist/*.zip ./packages/opencode/dist/*.tar.gz`
|
||||
await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes ${notes.join("\n") || "No notable changes"} ./packages/opencode/dist/*.zip ./packages/opencode/dist/*.tar.gz`
|
||||
const release = await $`gh release view v${Script.version} --json id,tagName`.json()
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
await Bun.write(process.env.GITHUB_OUTPUT, `releaseId=${release.id}\ntagName=${release.tagName}\n`)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.161",
|
||||
"version": "1.0.163",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user