mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-16 17:25:22 +00:00
Compare commits
116 Commits
add-ignore
...
v1.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d187af9d2 | ||
|
|
7e14cc687a | ||
|
|
2f5b2b23d5 | ||
|
|
035baa4b38 | ||
|
|
9f38af44db | ||
|
|
7324b2260a | ||
|
|
166f169dbf | ||
|
|
9c55cb729b | ||
|
|
f2e65e40ea | ||
|
|
8b3ae08a55 | ||
|
|
555d7fcdde | ||
|
|
2410a6bc9e | ||
|
|
59ed8ccbd8 | ||
|
|
91ed101378 | ||
|
|
fb60f9c396 | ||
|
|
e93699b741 | ||
|
|
9ac00f55bc | ||
|
|
393cf78ca6 | ||
|
|
478fec61ab | ||
|
|
52ad134d55 | ||
|
|
3e09abbfda | ||
|
|
5450644c67 | ||
|
|
0c2ccf25dc | ||
|
|
65c7168492 | ||
|
|
c74c66e6b4 | ||
|
|
c545fa2a28 | ||
|
|
80235f325e | ||
|
|
88c306efd2 | ||
|
|
554572bc39 | ||
|
|
e5abe1e78b | ||
|
|
1d54f90330 | ||
|
|
5f10243e91 | ||
|
|
226a5c2000 | ||
|
|
f8442ad016 | ||
|
|
1e28d10610 | ||
|
|
7304ba616e | ||
|
|
cdd6ea514b | ||
|
|
24d9c1d18d | ||
|
|
5ca2f6c5a9 | ||
|
|
12ffb270fb | ||
|
|
dc25669b6e | ||
|
|
0f9130b649 | ||
|
|
a76570b5dd | ||
|
|
97977f6ad4 | ||
|
|
555a5ccb59 | ||
|
|
24dedb4f7b | ||
|
|
21dc3c24d9 | ||
|
|
e00621cb17 | ||
|
|
2d074f0472 | ||
|
|
f3cd3b8941 | ||
|
|
1f8dab50be | ||
|
|
29672e7b95 | ||
|
|
4f3ac709a4 | ||
|
|
8aa56dc01d | ||
|
|
d72d7ab510 | ||
|
|
5053822bd6 | ||
|
|
177b01a853 | ||
|
|
c9f907caec | ||
|
|
7ce0520f8d | ||
|
|
4486174e43 | ||
|
|
41cf45a16e | ||
|
|
3611260405 | ||
|
|
c3fd3c8656 | ||
|
|
4d7d28c30a | ||
|
|
96a00ffea9 | ||
|
|
02540b2464 | ||
|
|
5aa4fd0042 | ||
|
|
b934c22d8d | ||
|
|
72cef0d9e7 | ||
|
|
d3fd6d1a10 | ||
|
|
6b12a0084c | ||
|
|
a5a19197f5 | ||
|
|
74d0d2b942 | ||
|
|
235837d2d9 | ||
|
|
dcf37000e4 | ||
|
|
5944443a60 | ||
|
|
81e8d29ad2 | ||
|
|
8b6cf7081f | ||
|
|
0b4af95223 | ||
|
|
f6cc84747a | ||
|
|
586e7347bd | ||
|
|
69d4ef038b | ||
|
|
c7c1790da8 | ||
|
|
12eea69f2e | ||
|
|
308e8060dc | ||
|
|
5f93beed77 | ||
|
|
527553ada2 | ||
|
|
5c5e636030 | ||
|
|
da6df3d432 | ||
|
|
b9b0e3475c | ||
|
|
77fcefca0e | ||
|
|
47c670aea9 | ||
|
|
2b66b31d96 | ||
|
|
f991fbbde8 | ||
|
|
401b498c7d | ||
|
|
f2ec036027 | ||
|
|
a235aec9ab | ||
|
|
052de3c556 | ||
|
|
f6fe709f6e | ||
|
|
ff0bd84870 | ||
|
|
b4af8a65ec | ||
|
|
49c5c2b1df | ||
|
|
4956ee3ebd | ||
|
|
1261b7d333 | ||
|
|
a3f38e0533 | ||
|
|
681a257df6 | ||
|
|
586207adb4 | ||
|
|
a58dbb3b5c | ||
|
|
131d8e5778 | ||
|
|
0cf0294787 | ||
|
|
3c41e4e8f1 | ||
|
|
66bc046503 | ||
|
|
6e68ea034c | ||
|
|
c51fa7cb24 | ||
|
|
a4c67515c9 | ||
|
|
1d2d710fce |
4
.github/CODEOWNERS
vendored
Normal file
4
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# web + desktop packages
|
||||
packages/app/ @adamdotdevin
|
||||
packages/tauri/ @adamdotdevin
|
||||
packages/desktop/ @adamdotdevin
|
||||
8
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -11,6 +11,14 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: plugins
|
||||
attributes:
|
||||
label: Plugins
|
||||
description: What plugins are you using?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: opencode-version
|
||||
attributes:
|
||||
|
||||
4
.github/workflows/duplicate-issues.yml
vendored
4
.github/workflows/duplicate-issues.yml
vendored
@@ -28,8 +28,8 @@ jobs:
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
"bash": {
|
||||
"gh issue*": "allow",
|
||||
"*": "deny"
|
||||
"*": "deny",
|
||||
"gh issue*": "allow"
|
||||
},
|
||||
"webfetch": "deny"
|
||||
}
|
||||
|
||||
63
.github/workflows/duplicate-prs.yml
vendored
Normal file
63
.github/workflows/duplicate-prs.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Duplicate PR Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
if: |
|
||||
github.event.pull_request.user.login != 'actions-user' &&
|
||||
github.event.pull_request.user.login != 'opencode' &&
|
||||
github.event.pull_request.user.login != 'rekram1-node' &&
|
||||
github.event.pull_request.user.login != 'thdxr' &&
|
||||
github.event.pull_request.user.login != 'kommander' &&
|
||||
github.event.pull_request.user.login != 'jayair' &&
|
||||
github.event.pull_request.user.login != 'fwang' &&
|
||||
github.event.pull_request.user.login != 'adamdotdevin' &&
|
||||
github.event.pull_request.user.login != 'iamdavidhill' &&
|
||||
github.event.pull_request.user.login != 'opencode-agent[bot]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Build prompt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
{
|
||||
echo "Check for duplicate PRs related to this new PR:"
|
||||
echo ""
|
||||
echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)"
|
||||
echo ""
|
||||
echo "Description:"
|
||||
gh pr view "$PR_NUMBER" --json body --jq .body
|
||||
} > pr_info.txt
|
||||
|
||||
- name: Check for duplicate PRs
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates")
|
||||
|
||||
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
|
||||
|
||||
$COMMENT"
|
||||
35
.github/workflows/nix-desktop.yml
vendored
Normal file
35
.github/workflows/nix-desktop.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: nix desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "nix/**"
|
||||
- "packages/app/**"
|
||||
- "packages/desktop/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-desktop:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- blacksmith-4vcpu-ubuntu-2404
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v21
|
||||
|
||||
- name: Build desktop via flake
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix --version
|
||||
nix build .#desktop -L
|
||||
2
.github/workflows/opencode.yml
vendored
2
.github/workflows/opencode.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_PERMISSION: '{"bash": "deny"}'
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -31,7 +31,7 @@ permissions:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'sst/opencode'
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
||||
2
.github/workflows/review.yml
vendored
2
.github/workflows/review.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
|
||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||
run: |
|
||||
PR_BODY=$(jq -r .body pr_data.json)
|
||||
|
||||
24
.opencode/agent/duplicate-pr.md
Normal file
24
.opencode/agent/duplicate-pr.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
color: "#E67E22"
|
||||
tools:
|
||||
"*": false
|
||||
"github-pr-search": true
|
||||
---
|
||||
|
||||
You are a duplicate PR detection agent. When a PR is opened, your job is to search for potentially duplicate or related open PRs.
|
||||
|
||||
Use the github-pr-search tool to search for PRs that might be addressing the same issue or feature.
|
||||
|
||||
Search using keywords from the PR title and description. Try multiple searches with different relevant terms.
|
||||
|
||||
If you find potential duplicates:
|
||||
|
||||
- List them with their titles and URLs
|
||||
- Briefly explain why they might be related
|
||||
|
||||
If no duplicates are found, say so clearly.
|
||||
|
||||
Keep your response concise and actionable.
|
||||
@@ -3,7 +3,7 @@ description: "find issue(s) on github"
|
||||
model: opencode/claude-haiku-4-5
|
||||
---
|
||||
|
||||
Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
|
||||
Search through existing issues in anomalyco/opencode using the gh cli to find issues matching this query:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"permission": {
|
||||
"bash": {
|
||||
"ls foo": "ask",
|
||||
},
|
||||
},
|
||||
"mcp": {
|
||||
"context7": {
|
||||
"type": "remote",
|
||||
@@ -23,5 +18,6 @@
|
||||
},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
"github-pr-search": false,
|
||||
},
|
||||
}
|
||||
|
||||
52
.opencode/tool/github-pr-search.ts
Normal file
52
.opencode/tool/github-pr-search.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import DESCRIPTION from "./github-pr-search.txt"
|
||||
|
||||
async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
||||
const response = await fetch(`https://api.github.com${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
Accept: "application/vnd.github+json",
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
interface PR {
|
||||
title: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
query: tool.schema.string().describe("Search query for PR titles and descriptions"),
|
||||
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
|
||||
offset: tool.schema.number().describe("Number of results to skip for pagination").default(0),
|
||||
},
|
||||
async execute(args) {
|
||||
const owner = "anomalyco"
|
||||
const repo = "opencode"
|
||||
|
||||
const page = Math.floor(args.offset / args.limit) + 1
|
||||
const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:pr state:open`)
|
||||
const result = await githubFetch(
|
||||
`/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`,
|
||||
)
|
||||
|
||||
if (result.total_count === 0) {
|
||||
return `No PRs found matching "${args.query}"`
|
||||
}
|
||||
|
||||
const prs = result.items as PR[]
|
||||
const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n")
|
||||
|
||||
return `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}`
|
||||
},
|
||||
})
|
||||
10
.opencode/tool/github-pr-search.txt
Normal file
10
.opencode/tool/github-pr-search.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Use this tool to search GitHub pull requests by title and description.
|
||||
|
||||
This tool searches PRs in the sst/opencode repository and returns LLM-friendly results including:
|
||||
- PR number and title
|
||||
- Author
|
||||
- State (open/closed/merged)
|
||||
- Labels
|
||||
- Description snippet
|
||||
|
||||
Use the query parameter to search for keywords that might appear in PR titles or descriptions.
|
||||
@@ -40,7 +40,7 @@ export default tool({
|
||||
async execute(args) {
|
||||
const issue = getIssueNumber()
|
||||
// const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
|
||||
const owner = "sst"
|
||||
const owner = "anomalyco"
|
||||
const repo = "opencode"
|
||||
|
||||
const results: string[] = []
|
||||
|
||||
@@ -14,10 +14,10 @@ However, any UI or core product feature must go through a design review with the
|
||||
|
||||
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
|
||||
|
||||
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
|
||||
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
|
||||
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
|
||||
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
|
||||
- [`help wanted`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
|
||||
- [`good first issue`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
|
||||
- [`bug`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
|
||||
- [`perf`](https://github.com/anomalyco/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
|
||||
|
||||
> [!NOTE]
|
||||
> PRs that ignore these guardrails will likely be closed.
|
||||
@@ -67,8 +67,31 @@ Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
|
||||
- Core pieces:
|
||||
- `packages/opencode`: OpenCode core business logic & server.
|
||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||
- `packages/app`: The shared web UI components, written in SolidJS
|
||||
- `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
|
||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
### Running the Web App
|
||||
|
||||
To test UI changes during development, run the web app:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/app dev
|
||||
```
|
||||
|
||||
This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here.
|
||||
|
||||
### Running the Desktop App
|
||||
|
||||
The desktop app is a native Tauri application that wraps the web UI. To run it:
|
||||
|
||||
```bash
|
||||
bun run --cwd packages/desktop dev
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
|
||||
|
||||
> [!NOTE]
|
||||
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -31,7 +31,7 @@ choco install opencode # Windows
|
||||
brew install opencode # macOS and Linux
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # Any OS
|
||||
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
||||
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -39,7 +39,7 @@ nix run nixpkgs#opencode # or github:sst/opencode for latest dev branc
|
||||
|
||||
### 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).
|
||||
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
|
||||
|
||||
| Platform | Download |
|
||||
| --------------------- | ------------------------------------- |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
@@ -30,8 +30,8 @@ scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS 與 Linux
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g github:sst/opencode # 任何作業系統
|
||||
nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支
|
||||
mise use -g github:anomalyco/opencode # 任何作業系統
|
||||
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
@@ -39,7 +39,7 @@ nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最
|
||||
|
||||
### 桌面應用程式 (BETA)
|
||||
|
||||
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
|
||||
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
|
||||
|
||||
| 平台 | 下載連結 |
|
||||
| --------------------- | ------------------------------------- |
|
||||
|
||||
53
bun.lock
53
bun.lock
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -173,7 +173,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
@@ -201,7 +201,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -230,7 +230,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -246,7 +246,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -285,11 +285,12 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.67",
|
||||
"@opentui/solid": "0.1.67",
|
||||
"@opentui/core": "0.1.68",
|
||||
"@opentui/solid": "0.1.68",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/scheduled": "1.5.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
@@ -348,7 +349,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -368,7 +369,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -379,7 +380,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -392,7 +393,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -430,7 +431,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -441,7 +442,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1196,21 +1197,21 @@
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.67", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.67", "@opentui/core-darwin-x64": "0.1.67", "@opentui/core-linux-arm64": "0.1.67", "@opentui/core-linux-x64": "0.1.67", "@opentui/core-win32-arm64": "0.1.67", "@opentui/core-win32-x64": "0.1.67", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-zmfyA10QUbzT6ohacPoHmGiYzuJrDSCfQWRWrKtao0BrHj9bii73qWy3V/eR4ibVueoRREwxJs5GlBOSvK6IoA=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.68", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.68", "@opentui/core-darwin-x64": "0.1.68", "@opentui/core-linux-arm64": "0.1.68", "@opentui/core-linux-x64": "0.1.68", "@opentui/core-win32-arm64": "0.1.68", "@opentui/core-win32-x64": "0.1.68", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-SZz5qNO+2lJ8jDEoTSieyXH23t49myu6NetLex+xzqOf67XsU6QKlDcw5oMmc3zrKvETXhgbBvlSnbyJNQoBMg=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.67", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LtOcTlFD+kO7neItmkiF77H8cnjTYzBOZe8JQGwRSt9aaCke3UzMvLxmQnj4BP/kPC3hi9V6NRnFdptz0sJZIQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.68", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ipPX2gavBLVtw3d8L4ZPJDLlEwIjIRNdlNlxu07rqSEGSfxD5s29yc+33wLAlYXbmnJDajOqm0Dx6HnlY1Y9Fg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.67", "", { "os": "darwin", "cpu": "x64" }, "sha512-9i+awVWgpEVqZhFLaLq8usNGyCiyT5QxMLy6eH7JmRic79S34u23HfxiniGRtdYh3aqpm9SbLzo60v0nRIUkCA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.68", "", { "os": "darwin", "cpu": "x64" }, "sha512-9dW0S9HINnuVjvC9QLj+S+329H7qEBQQtyJ9WHpykemokiJ5k4rnuDkfws5FxgTHIf/ddoYYTyPoGCS7WN5gsQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.67", "", { "os": "linux", "cpu": "arm64" }, "sha512-WLjnTM3Ig//SRo0FUZYZJ5TITVbR6dKDVg6axU2D+sMoUzJMBP/Xo04q/TvZ3wP764Yca9l7oVMKWDxHlygyjQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.68", "", { "os": "linux", "cpu": "arm64" }, "sha512-/el6TbSQriBUfPhIa6SBfCCc7tjU98Bnhf2+w0zKwQFBjf3F3kmnI42++YxedMGFmL7bRt3EUawGOkQRZZzFAg=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.67", "", { "os": "linux", "cpu": "x64" }, "sha512-5UbZ/TqWi/DAmHIZL4NvhdpgTwglszRiddkRiQ8cT0IbnE4lutd4XxWUWcLKwsNT1YJv32TtcGWkuthluLiriQ=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.68", "", { "os": "linux", "cpu": "x64" }, "sha512-9NzVI3GZzmICoIu3YhWBdkEt0KvY27m++tu/MqW+xb6fnvN74jZkRWzlgjTdM70obL4eUGQdvU08sDHgZjsIJw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.67", "", { "os": "win32", "cpu": "arm64" }, "sha512-KNam5rObhN8/U9+GVVuvtAlGXp3MfdMHnw4W2P6YH7xp8HTsLvABUT91SJEyJ/ktVe9e1itLDG2fDHSoA5NbUg=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.68", "", { "os": "win32", "cpu": "arm64" }, "sha512-wrAeotyotOplUjQVBSxOGA8GCr9FWXSd6xCEo1PEGo/NjuAOtvHmKoENzyFEP0GzFsjvoUOyy2dZb987oFAn9A=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.67", "", { "os": "win32", "cpu": "x64" }, "sha512-740lkOw42zLNh9YfahXjCwV2DS/amH2uMDh3tCADDCLckrMhemIhqArXDiMlalDxDqYspoaZCpBsFVsG9dMS6A=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.68", "", { "os": "win32", "cpu": "x64" }, "sha512-w0yBjvzs/oMIwVdWICL4XlUrfsPoVXd4+RDqiuu+Xi/zD0UgANSTRY2asXca+gPe5zPHLsxvz1bAG0Z7uGtmyw=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.67", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.67", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-dVNq0+PJIdNb63D0T7vcbyVF/ZvLCihGvivTU50zDOzd0Sk5prbrIfpG8+DjMErFubXfdZQvdy/PqFdtw0rjtQ=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.68", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.68", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-S1oHvCQaY+gCQu2kiiksPIScP8i0FiDOlAlLjtfwcRlgeSjzT0wRwFkvoh4uVUPuAlyigox7vMCE3j04SYSGKg=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -1616,6 +1617,8 @@
|
||||
|
||||
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="],
|
||||
|
||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
|
||||
|
||||
"@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="],
|
||||
|
||||
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="],
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767242400,
|
||||
"narHash": "sha256-knFaYjeg7swqG1dljj1hOxfg39zrIy8pfGuicjm9s+o=",
|
||||
"lastModified": 1767364772,
|
||||
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c04833a1e584401bb63c1a63ddc51a71e6aa457a",
|
||||
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
20
flake.nix
20
flake.nix
@@ -66,10 +66,10 @@
|
||||
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
|
||||
hash = nodeModulesHash;
|
||||
};
|
||||
mkPackage = pkgs.callPackage ./nix/opencode.nix { };
|
||||
in
|
||||
{
|
||||
default = mkPackage {
|
||||
mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
|
||||
mkDesktop = pkgs.callPackage ./nix/desktop.nix { };
|
||||
|
||||
opencodePkg = mkOpencode {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
@@ -77,6 +77,18 @@
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
inherit mkNodeModules;
|
||||
};
|
||||
|
||||
desktopPkg = mkDesktop {
|
||||
inherit (packageJson) version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
mkNodeModules = mkNodeModules;
|
||||
opencode = opencodePkg;
|
||||
};
|
||||
in
|
||||
{
|
||||
default = opencodePkg;
|
||||
desktop = desktopPkg;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
||||
|
||||
## Support
|
||||
|
||||
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
|
||||
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/anomalyco/opencode/issues.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ runs:
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
|
||||
VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
|
||||
echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache opencode
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:"
|
||||
},
|
||||
|
||||
@@ -104,6 +104,7 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
new sst.Secret("ZEN_MODELS6"),
|
||||
new sst.Secret("ZEN_MODELS7"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
|
||||
10
install
10
install
@@ -147,8 +147,8 @@ INSTALL_DIR=$HOME/.opencode/bin
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
url="https://github.com/sst/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
@@ -157,14 +157,14 @@ if [ -z "$requested_version" ]; then
|
||||
else
|
||||
# Strip leading 'v' if present
|
||||
requested_version="${requested_version#v}"
|
||||
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
|
||||
url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
|
||||
specific_version=$requested_version
|
||||
|
||||
# Verify the release exists before downloading
|
||||
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/sst/opencode/releases/tag/v${requested_version}")
|
||||
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
|
||||
if [ "$http_status" = "404" ]; then
|
||||
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
|
||||
echo -e "${MUTED}Available releases: https://github.com/sst/opencode/releases${NC}"
|
||||
echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
145
nix/desktop.nix
Normal file
145
nix/desktop.nix
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
rustPlatform,
|
||||
bun,
|
||||
pkg-config,
|
||||
dbus ? null,
|
||||
openssl,
|
||||
glib ? null,
|
||||
gtk3 ? null,
|
||||
libsoup_3 ? null,
|
||||
webkitgtk_4_1 ? null,
|
||||
librsvg ? null,
|
||||
libappindicator-gtk3 ? null,
|
||||
cargo,
|
||||
rustc,
|
||||
makeBinaryWrapper,
|
||||
nodejs,
|
||||
jq,
|
||||
}:
|
||||
args:
|
||||
let
|
||||
scripts = args.scripts;
|
||||
mkModules =
|
||||
attrs:
|
||||
args.mkNodeModules (
|
||||
attrs
|
||||
// {
|
||||
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
|
||||
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
|
||||
}
|
||||
);
|
||||
in
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "opencode-desktop";
|
||||
version = args.version;
|
||||
|
||||
src = args.src;
|
||||
|
||||
# We need to set the root for cargo, but we also need access to the whole repo.
|
||||
postUnpack = ''
|
||||
# Update sourceRoot to point to the tauri app
|
||||
sourceRoot+=/packages/desktop/src-tauri
|
||||
'';
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ../packages/desktop/src-tauri/Cargo.lock;
|
||||
allowBuiltinFetchGit = true;
|
||||
};
|
||||
|
||||
node_modules = mkModules {
|
||||
version = version;
|
||||
src = src;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
bun
|
||||
makeBinaryWrapper
|
||||
cargo
|
||||
rustc
|
||||
nodejs
|
||||
jq
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
openssl
|
||||
]
|
||||
++ lib.optionals stdenv.isLinux [
|
||||
dbus
|
||||
glib
|
||||
gtk3
|
||||
libsoup_3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
libappindicator-gtk3
|
||||
];
|
||||
|
||||
preBuild = ''
|
||||
# Restore node_modules
|
||||
pushd ../../..
|
||||
|
||||
# Copy node_modules from the fixed-output derivation
|
||||
# We use cp -r --no-preserve=mode to ensure we can write to them if needed,
|
||||
# though we usually just read.
|
||||
cp -r ${node_modules}/node_modules .
|
||||
cp -r ${node_modules}/packages .
|
||||
|
||||
# Ensure node_modules is writable so patchShebangs can update script headers
|
||||
chmod -R u+w node_modules
|
||||
# Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo)
|
||||
chmod -R u+w packages
|
||||
# Patch shebangs so scripts can run
|
||||
patchShebangs node_modules
|
||||
|
||||
# Copy sidecar
|
||||
mkdir -p packages/desktop/src-tauri/sidecars
|
||||
targetTriple=${stdenv.hostPlatform.rust.rustcTarget}
|
||||
cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple
|
||||
|
||||
# Merge prod config into tauri.conf.json
|
||||
if ! jq -s '.[0] * .[1]' \
|
||||
packages/desktop/src-tauri/tauri.conf.json \
|
||||
packages/desktop/src-tauri/tauri.prod.conf.json \
|
||||
> packages/desktop/src-tauri/tauri.conf.json.tmp; then
|
||||
echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json
|
||||
|
||||
# Build the frontend
|
||||
cd packages/desktop
|
||||
|
||||
# The 'build' script runs 'bun run typecheck && vite build'.
|
||||
bun run build
|
||||
|
||||
popd
|
||||
'';
|
||||
|
||||
# Tauri bundles the assets during the rust build phase (which happens after preBuild).
|
||||
# It looks for them in the location specified in tauri.conf.json.
|
||||
|
||||
postInstall = lib.optionalString stdenv.isLinux ''
|
||||
# Wrap the binary to ensure it finds the libraries
|
||||
wrapProgram $out/bin/opencode-desktop \
|
||||
--prefix LD_LIBRARY_PATH : ${
|
||||
lib.makeLibraryPath [
|
||||
gtk3
|
||||
webkitgtk_4_1
|
||||
librsvg
|
||||
glib
|
||||
libsoup_3
|
||||
]
|
||||
}
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "OpenCode Desktop App";
|
||||
homepage = "https://opencode.ai";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ];
|
||||
mainProgram = "opencode-desktop";
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-uJDhOieOdMQLORyuOWtgtjLoMnNEQPrDcyij9TX0aTw="
|
||||
"nodeModules": "sha256-OJ3C4RMzfbbG1Fwa/5yru0rlISj+28UPITMNBEU5AeM="
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
It combines a TypeScript/JavaScript core with a Go-based TUI
|
||||
to provide an interactive AI coding experience.
|
||||
'';
|
||||
homepage = "https://github.com/sst/opencode";
|
||||
homepage = "https://github.com/anomalyco/opencode";
|
||||
license = lib.licenses.mit;
|
||||
platforms = [
|
||||
"aarch64-linux"
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sst/opencode"
|
||||
"url": "https://github.com/anomalyco/opencode"
|
||||
},
|
||||
"license": "MIT",
|
||||
"prettier": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Debugging
|
||||
|
||||
- To test the opencode app, use the playwrite mcp server, the app is already
|
||||
- To test the opencode app, use the playwright MCP server, the app is already
|
||||
running at http://localhost:3000
|
||||
- NEVER try to restart the app, or the server process, EVER.
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</head>
|
||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<div id="root" class="flex flex-col h-dvh"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
usePrompt,
|
||||
ImageAttachmentPart,
|
||||
AgentPart,
|
||||
FileAttachmentPart,
|
||||
} from "@/context/prompt"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -33,6 +34,12 @@ import { persisted } from "@/utils/persist"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
|
||||
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
||||
@@ -40,6 +47,8 @@ const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
||||
interface PromptInputProps {
|
||||
class?: string
|
||||
ref?: (el: HTMLDivElement) => void
|
||||
newSessionWorktree?: string
|
||||
onNewSessionWorktreeReset?: () => void
|
||||
}
|
||||
|
||||
const PLACEHOLDERS = [
|
||||
@@ -83,6 +92,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const globalSync = useGlobalSync()
|
||||
const platform = usePlatform()
|
||||
const local = useLocal()
|
||||
const files = useFile()
|
||||
const prompt = usePrompt()
|
||||
@@ -95,6 +106,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
let editorRef!: HTMLDivElement
|
||||
let fileInputRef!: HTMLInputElement
|
||||
let scrollRef!: HTMLDivElement
|
||||
let slashPopoverRef!: HTMLDivElement
|
||||
|
||||
const scrollCursorIntoView = () => {
|
||||
const container = scrollRef
|
||||
@@ -151,7 +163,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
imageAttachments: ImageAttachmentPart[]
|
||||
mode: "normal" | "shell"
|
||||
applyingHistory: boolean
|
||||
killBuffer: string
|
||||
}>({
|
||||
popover: null,
|
||||
historyIndex: -1,
|
||||
@@ -161,7 +172,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
imageAttachments: [],
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
killBuffer: "",
|
||||
})
|
||||
|
||||
const MAX_HISTORY = 100
|
||||
@@ -430,6 +440,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
active: slashActive,
|
||||
onInput: slashOnInput,
|
||||
onKeyDown: slashOnKeyDown,
|
||||
refetch: slashRefetch,
|
||||
} = useFilteredList<SlashCommand>({
|
||||
items: slashCommands,
|
||||
key: (x) => x?.id,
|
||||
@@ -437,32 +448,78 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onSelect: handleSlashSelect,
|
||||
})
|
||||
|
||||
const createPill = (part: FileAttachmentPart | AgentPart) => {
|
||||
const pill = document.createElement("span")
|
||||
pill.textContent = part.content
|
||||
pill.setAttribute("data-type", part.type)
|
||||
if (part.type === "file") pill.setAttribute("data-path", part.path)
|
||||
if (part.type === "agent") pill.setAttribute("data-name", part.name)
|
||||
pill.setAttribute("contenteditable", "false")
|
||||
pill.style.userSelect = "text"
|
||||
pill.style.cursor = "default"
|
||||
return pill
|
||||
}
|
||||
|
||||
const isNormalizedEditor = () =>
|
||||
Array.from(editorRef.childNodes).every((node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent ?? ""
|
||||
if (!text.includes("\u200B")) return true
|
||||
if (text !== "\u200B") return false
|
||||
|
||||
const prev = node.previousSibling
|
||||
const next = node.nextSibling
|
||||
const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
|
||||
const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR"
|
||||
if (!prevIsBr && !nextIsBr) return false
|
||||
if (nextIsBr && !prevIsBr && prev) return false
|
||||
return true
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return false
|
||||
const el = node as HTMLElement
|
||||
if (el.dataset.type === "file") return true
|
||||
if (el.dataset.type === "agent") return true
|
||||
return el.tagName === "BR"
|
||||
})
|
||||
|
||||
const renderEditor = (parts: Prompt) => {
|
||||
editorRef.innerHTML = ""
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") {
|
||||
editorRef.appendChild(createTextFragment(part.content))
|
||||
continue
|
||||
}
|
||||
if (part.type === "file" || part.type === "agent") {
|
||||
editorRef.appendChild(createPill(part))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => sync.data.command,
|
||||
() => slashRefetch(),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
// Auto-scroll active command into view when navigating with keyboard
|
||||
createEffect(() => {
|
||||
const activeId = slashActive()
|
||||
if (!activeId || !slashPopoverRef) return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`)
|
||||
element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => prompt.current(),
|
||||
(currentParts) => {
|
||||
const domParts = parseFromDOM()
|
||||
const normalized = Array.from(editorRef.childNodes).every((node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent ?? ""
|
||||
if (!text.includes("\u200B")) return true
|
||||
if (text !== "\u200B") return false
|
||||
|
||||
const prev = node.previousSibling
|
||||
const next = node.nextSibling
|
||||
const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
|
||||
const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR"
|
||||
if (!prevIsBr && !nextIsBr) return false
|
||||
if (nextIsBr && !prevIsBr && prev) return false
|
||||
return true
|
||||
}
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return false
|
||||
const el = node as HTMLElement
|
||||
if (el.dataset.type === "file") return true
|
||||
if (el.dataset.type === "agent") return true
|
||||
return el.tagName === "BR"
|
||||
})
|
||||
if (normalized && isPromptEqual(currentParts, domParts)) return
|
||||
if (isNormalizedEditor() && isPromptEqual(currentParts, domParts)) return
|
||||
|
||||
const selection = window.getSelection()
|
||||
let cursorPosition: number | null = null
|
||||
@@ -470,30 +527,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
cursorPosition = getCursorPosition(editorRef)
|
||||
}
|
||||
|
||||
editorRef.innerHTML = ""
|
||||
currentParts.forEach((part) => {
|
||||
if (part.type === "text") {
|
||||
editorRef.appendChild(createTextFragment(part.content))
|
||||
} else if (part.type === "file") {
|
||||
const pill = document.createElement("span")
|
||||
pill.textContent = part.content
|
||||
pill.setAttribute("data-type", "file")
|
||||
pill.setAttribute("data-path", part.path)
|
||||
pill.setAttribute("contenteditable", "false")
|
||||
pill.style.userSelect = "text"
|
||||
pill.style.cursor = "default"
|
||||
editorRef.appendChild(pill)
|
||||
} else if (part.type === "agent") {
|
||||
const pill = document.createElement("span")
|
||||
pill.textContent = part.content
|
||||
pill.setAttribute("data-type", "agent")
|
||||
pill.setAttribute("data-name", part.name)
|
||||
pill.setAttribute("contenteditable", "false")
|
||||
pill.style.userSelect = "text"
|
||||
pill.style.cursor = "default"
|
||||
editorRef.appendChild(pill)
|
||||
}
|
||||
})
|
||||
renderEditor(currentParts)
|
||||
|
||||
if (cursorPosition !== null) {
|
||||
setCursorPosition(editorRef, cursorPosition)
|
||||
@@ -671,40 +705,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const textBeforeCursor = rawText.substring(0, cursorPosition)
|
||||
const atMatch = textBeforeCursor.match(/@(\S*)$/)
|
||||
|
||||
if (part.type === "file") {
|
||||
const pill = document.createElement("span")
|
||||
pill.textContent = part.content
|
||||
pill.setAttribute("data-type", "file")
|
||||
pill.setAttribute("data-path", part.path)
|
||||
pill.setAttribute("contenteditable", "false")
|
||||
pill.style.userSelect = "text"
|
||||
pill.style.cursor = "default"
|
||||
|
||||
const gap = document.createTextNode(" ")
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
if (atMatch) {
|
||||
const start = atMatch.index ?? cursorPosition - atMatch[0].length
|
||||
setRangeEdge(range, "start", start)
|
||||
setRangeEdge(range, "end", cursorPosition)
|
||||
}
|
||||
|
||||
range.deleteContents()
|
||||
range.insertNode(gap)
|
||||
range.insertNode(pill)
|
||||
range.setStartAfter(gap)
|
||||
range.collapse(true)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
} else if (part.type === "agent") {
|
||||
const pill = document.createElement("span")
|
||||
pill.textContent = part.content
|
||||
pill.setAttribute("data-type", "agent")
|
||||
pill.setAttribute("data-name", part.name)
|
||||
pill.setAttribute("contenteditable", "false")
|
||||
pill.style.userSelect = "text"
|
||||
pill.style.cursor = "default"
|
||||
|
||||
if (part.type === "file" || part.type === "agent") {
|
||||
const pill = createPill(part)
|
||||
const gap = document.createTextNode(" ")
|
||||
const range = selection.getRangeAt(0)
|
||||
|
||||
@@ -750,77 +752,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
setStore("popover", null)
|
||||
}
|
||||
|
||||
const setSelectionOffsets = (start: number, end: number) => {
|
||||
const selection = window.getSelection()
|
||||
if (!selection) return false
|
||||
|
||||
const length = promptLength(prompt.current())
|
||||
const a = Math.max(0, Math.min(start, length))
|
||||
const b = Math.max(0, Math.min(end, length))
|
||||
const rangeStart = Math.min(a, b)
|
||||
const rangeEnd = Math.max(a, b)
|
||||
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editorRef)
|
||||
|
||||
const setEdge = (edge: "start" | "end", offset: number) => {
|
||||
let remaining = offset
|
||||
const nodes = Array.from(editorRef.childNodes)
|
||||
|
||||
for (const node of nodes) {
|
||||
const length = getNodeLength(node)
|
||||
const isText = node.nodeType === Node.TEXT_NODE
|
||||
const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
|
||||
const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
|
||||
|
||||
if (isText && remaining <= length) {
|
||||
if (edge === "start") range.setStart(node, remaining)
|
||||
if (edge === "end") range.setEnd(node, remaining)
|
||||
return
|
||||
}
|
||||
|
||||
if ((isFile || isBreak) && remaining <= length) {
|
||||
if (edge === "start" && remaining === 0) range.setStartBefore(node)
|
||||
if (edge === "start" && remaining > 0) range.setStartAfter(node)
|
||||
if (edge === "end" && remaining === 0) range.setEndBefore(node)
|
||||
if (edge === "end" && remaining > 0) range.setEndAfter(node)
|
||||
return
|
||||
}
|
||||
|
||||
remaining -= length
|
||||
}
|
||||
|
||||
const last = editorRef.lastChild
|
||||
if (!last) {
|
||||
if (edge === "start") range.setStart(editorRef, 0)
|
||||
if (edge === "end") range.setEnd(editorRef, 0)
|
||||
return
|
||||
}
|
||||
if (edge === "start") range.setStartAfter(last)
|
||||
if (edge === "end") range.setEndAfter(last)
|
||||
}
|
||||
|
||||
setEdge("start", rangeStart)
|
||||
setEdge("end", rangeEnd)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
return true
|
||||
}
|
||||
|
||||
const replaceOffsets = (start: number, end: number, content: string) => {
|
||||
if (!setSelectionOffsets(start, end)) return false
|
||||
addPart({ type: "text", content, start: 0, end: 0 })
|
||||
return true
|
||||
}
|
||||
|
||||
const killText = (start: number, end: number) => {
|
||||
if (start === end) return
|
||||
const current = prompt.current()
|
||||
if (!current.every((part) => part.type === "text")) return
|
||||
const text = current.map((part) => part.content).join("")
|
||||
setStore("killBuffer", text.slice(start, end))
|
||||
}
|
||||
|
||||
const abort = () =>
|
||||
sdk.client.session
|
||||
.abort({
|
||||
@@ -942,7 +873,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
|
||||
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
|
||||
const alt = event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey
|
||||
|
||||
if (ctrl && event.code === "KeyG") {
|
||||
if (store.popover) {
|
||||
@@ -957,148 +887,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (ctrl || alt) {
|
||||
const { collapsed, cursorPosition, textLength } = getCaretState()
|
||||
if (collapsed) {
|
||||
const current = prompt.current()
|
||||
const text = current.map((part) => ("content" in part ? part.content : "")).join("")
|
||||
|
||||
if (ctrl) {
|
||||
if (event.code === "KeyA") {
|
||||
const pos = text.lastIndexOf("\n", cursorPosition - 1) + 1
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyE") {
|
||||
const next = text.indexOf("\n", cursorPosition)
|
||||
const pos = next === -1 ? textLength : next
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyB") {
|
||||
const pos = Math.max(0, cursorPosition - 1)
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyF") {
|
||||
const pos = Math.min(textLength, cursorPosition + 1)
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyD") {
|
||||
if (store.mode === "shell" && cursorPosition === 0 && textLength === 0) {
|
||||
setStore("mode", "normal")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (cursorPosition >= textLength) return
|
||||
replaceOffsets(cursorPosition, cursorPosition + 1, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyK") {
|
||||
const next = text.indexOf("\n", cursorPosition)
|
||||
const lineEnd = next === -1 ? textLength : next
|
||||
const end = lineEnd === cursorPosition && lineEnd < textLength ? lineEnd + 1 : lineEnd
|
||||
if (end === cursorPosition) return
|
||||
killText(cursorPosition, end)
|
||||
replaceOffsets(cursorPosition, end, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyU") {
|
||||
const start = text.lastIndexOf("\n", cursorPosition - 1) + 1
|
||||
if (start === cursorPosition) return
|
||||
killText(start, cursorPosition)
|
||||
replaceOffsets(start, cursorPosition, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyW") {
|
||||
let start = cursorPosition
|
||||
while (start > 0 && /\s/.test(text[start - 1])) start -= 1
|
||||
while (start > 0 && !/\s/.test(text[start - 1])) start -= 1
|
||||
if (start === cursorPosition) return
|
||||
killText(start, cursorPosition)
|
||||
replaceOffsets(start, cursorPosition, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyY") {
|
||||
if (!store.killBuffer) return
|
||||
addPart({ type: "text", content: store.killBuffer, start: 0, end: 0 })
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyT") {
|
||||
if (!current.every((part) => part.type === "text")) return
|
||||
if (textLength < 2) return
|
||||
if (cursorPosition === 0) return
|
||||
|
||||
const atEnd = cursorPosition === textLength
|
||||
const first = atEnd ? cursorPosition - 2 : cursorPosition - 1
|
||||
const second = atEnd ? cursorPosition - 1 : cursorPosition
|
||||
|
||||
if (text[first] === "\n" || text[second] === "\n") return
|
||||
|
||||
replaceOffsets(first, second + 1, `${text[second]}${text[first]}`)
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (alt) {
|
||||
if (event.code === "KeyB") {
|
||||
let pos = cursorPosition
|
||||
while (pos > 0 && /\s/.test(text[pos - 1])) pos -= 1
|
||||
while (pos > 0 && !/\s/.test(text[pos - 1])) pos -= 1
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyF") {
|
||||
let pos = cursorPosition
|
||||
while (pos < textLength && /\s/.test(text[pos])) pos += 1
|
||||
while (pos < textLength && !/\s/.test(text[pos])) pos += 1
|
||||
setCursorPosition(editorRef, pos)
|
||||
event.preventDefault()
|
||||
queueScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.code === "KeyD") {
|
||||
let end = cursorPosition
|
||||
while (end < textLength && /\s/.test(text[end])) end += 1
|
||||
while (end < textLength && !/\s/.test(text[end])) end += 1
|
||||
if (end === cursorPosition) return
|
||||
killText(cursorPosition, end)
|
||||
replaceOffsets(cursorPosition, end, "")
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey) return
|
||||
const { collapsed } = getCaretState()
|
||||
@@ -1152,30 +940,169 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const handleSubmit = async (event: Event) => {
|
||||
event.preventDefault()
|
||||
|
||||
const currentPrompt = prompt.current()
|
||||
const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("")
|
||||
const hasImageAttachments = store.imageAttachments.length > 0
|
||||
if (text.trim().length === 0 && !hasImageAttachments) {
|
||||
const images = store.imageAttachments.slice()
|
||||
const mode = store.mode
|
||||
|
||||
if (text.trim().length === 0 && images.length === 0) {
|
||||
if (working()) abort()
|
||||
return
|
||||
}
|
||||
|
||||
addToHistory(currentPrompt, store.mode)
|
||||
const currentModel = local.model.current()
|
||||
const currentAgent = local.agent.current()
|
||||
if (!currentModel || !currentAgent) {
|
||||
showToast({
|
||||
title: "Select an agent and model",
|
||||
description: "Choose an agent and model before sending a prompt.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const errorMessage = (err: unknown) => {
|
||||
if (err && typeof err === "object" && "data" in err) {
|
||||
const data = (err as { data?: { message?: string } }).data
|
||||
if (data?.message) return data.message
|
||||
}
|
||||
if (err instanceof Error) return err.message
|
||||
return "Request failed"
|
||||
}
|
||||
|
||||
addToHistory(currentPrompt, mode)
|
||||
setStore("historyIndex", -1)
|
||||
setStore("savedPrompt", null)
|
||||
|
||||
let existing = info()
|
||||
if (!existing) {
|
||||
const created = await sdk.client.session.create()
|
||||
existing = created.data ?? undefined
|
||||
if (existing) navigate(existing.id)
|
||||
}
|
||||
if (!existing) return
|
||||
const projectDirectory = sdk.directory
|
||||
const isNewSession = !params.id
|
||||
const worktreeSelection = props.newSessionWorktree ?? "main"
|
||||
|
||||
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
|
||||
const fileAttachments = currentPrompt.filter(
|
||||
(part) => part.type === "file",
|
||||
) as import("@/context/prompt").FileAttachmentPart[]
|
||||
let sessionDirectory = projectDirectory
|
||||
let client = sdk.client
|
||||
|
||||
if (isNewSession) {
|
||||
if (worktreeSelection === "create") {
|
||||
const createdWorktree = await client.worktree
|
||||
.create({ directory: projectDirectory })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to create worktree",
|
||||
description: errorMessage(err),
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!createdWorktree?.directory) {
|
||||
showToast({
|
||||
title: "Failed to create worktree",
|
||||
description: "Request failed",
|
||||
})
|
||||
return
|
||||
}
|
||||
sessionDirectory = createdWorktree.directory
|
||||
}
|
||||
|
||||
if (worktreeSelection !== "main" && worktreeSelection !== "create") {
|
||||
sessionDirectory = worktreeSelection
|
||||
}
|
||||
|
||||
if (sessionDirectory !== projectDirectory) {
|
||||
client = createOpencodeClient({
|
||||
baseUrl: sdk.url,
|
||||
fetch: platform.fetch,
|
||||
directory: sessionDirectory,
|
||||
throwOnError: true,
|
||||
})
|
||||
globalSync.child(sessionDirectory)
|
||||
}
|
||||
|
||||
props.onNewSessionWorktreeReset?.()
|
||||
}
|
||||
|
||||
let session = info()
|
||||
if (!session && isNewSession) {
|
||||
session = await client.session.create().then((x) => x.data ?? undefined)
|
||||
if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||
}
|
||||
if (!session) return
|
||||
|
||||
const model = {
|
||||
modelID: currentModel.id,
|
||||
providerID: currentModel.provider.id,
|
||||
}
|
||||
const agent = currentAgent.name
|
||||
const variant = local.model.variant.current()
|
||||
|
||||
const clearInput = () => {
|
||||
prompt.reset()
|
||||
setStore("imageAttachments", [])
|
||||
setStore("mode", "normal")
|
||||
setStore("popover", null)
|
||||
}
|
||||
|
||||
const restoreInput = () => {
|
||||
prompt.set(currentPrompt, promptLength(currentPrompt))
|
||||
setStore("imageAttachments", images)
|
||||
setStore("mode", mode)
|
||||
setStore("popover", null)
|
||||
requestAnimationFrame(() => {
|
||||
editorRef.focus()
|
||||
setCursorPosition(editorRef, promptLength(currentPrompt))
|
||||
queueScroll()
|
||||
})
|
||||
}
|
||||
|
||||
if (mode === "shell") {
|
||||
clearInput()
|
||||
client.session
|
||||
.shell({
|
||||
sessionID: session.id,
|
||||
agent,
|
||||
model,
|
||||
command: text,
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to send shell command",
|
||||
description: errorMessage(err),
|
||||
})
|
||||
restoreInput()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (text.startsWith("/")) {
|
||||
const [cmdName, ...args] = text.split(" ")
|
||||
const commandName = cmdName.slice(1)
|
||||
const customCommand = sync.data.command.find((c) => c.name === commandName)
|
||||
if (customCommand) {
|
||||
clearInput()
|
||||
client.session
|
||||
.command({
|
||||
sessionID: session.id,
|
||||
command: commandName,
|
||||
arguments: args.join(" "),
|
||||
agent,
|
||||
model: `${model.providerID}/${model.modelID}`,
|
||||
variant,
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to send command",
|
||||
description: errorMessage(err),
|
||||
})
|
||||
restoreInput()
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const toAbsolutePath = (path: string) =>
|
||||
path.startsWith("/") ? path : (sessionDirectory + "/" + path).replace("//", "/")
|
||||
|
||||
const fileAttachments = currentPrompt.filter((part) => part.type === "file") as FileAttachmentPart[]
|
||||
const agentAttachments = currentPrompt.filter((part) => part.type === "agent") as AgentPart[]
|
||||
|
||||
const fileAttachmentParts = fileAttachments.map((attachment) => {
|
||||
@@ -1247,7 +1174,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
addContextFile(item.path, item.selection)
|
||||
}
|
||||
|
||||
const imageAttachmentParts = store.imageAttachments.map((attachment) => ({
|
||||
const imageAttachmentParts = images.map((attachment) => ({
|
||||
id: Identifier.ascending("part"),
|
||||
type: "file" as const,
|
||||
mime: attachment.mime,
|
||||
@@ -1255,60 +1182,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
filename: attachment.filename,
|
||||
}))
|
||||
|
||||
const isShellMode = store.mode === "shell"
|
||||
editorRef.innerHTML = ""
|
||||
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
|
||||
setStore("imageAttachments", [])
|
||||
setStore("mode", "normal")
|
||||
|
||||
const currentModel = local.model.current()
|
||||
const currentAgent = local.agent.current()
|
||||
if (!currentModel || !currentAgent) {
|
||||
console.warn("No agent or model available for prompt submission")
|
||||
return
|
||||
}
|
||||
const model = {
|
||||
modelID: currentModel.id,
|
||||
providerID: currentModel.provider.id,
|
||||
}
|
||||
const agent = currentAgent.name
|
||||
const variant = local.model.variant.current()
|
||||
|
||||
if (isShellMode) {
|
||||
sdk.client.session
|
||||
.shell({
|
||||
sessionID: existing.id,
|
||||
agent,
|
||||
model,
|
||||
command: text,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to send shell command", e)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (text.startsWith("/")) {
|
||||
const [cmdName, ...args] = text.split(" ")
|
||||
const commandName = cmdName.slice(1)
|
||||
const customCommand = sync.data.command.find((c) => c.name === commandName)
|
||||
if (customCommand) {
|
||||
sdk.client.session
|
||||
.command({
|
||||
sessionID: existing.id,
|
||||
command: commandName,
|
||||
arguments: args.join(" "),
|
||||
agent,
|
||||
model: `${model.providerID}/${model.modelID}`,
|
||||
variant,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to send command", e)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
const textPart = {
|
||||
id: Identifier.ascending("part"),
|
||||
@@ -1322,31 +1195,74 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
...agentAttachmentParts,
|
||||
...imageAttachmentParts,
|
||||
]
|
||||
|
||||
const optimisticParts = requestParts.map((part) => ({
|
||||
...part,
|
||||
sessionID: existing.id,
|
||||
sessionID: session.id,
|
||||
messageID,
|
||||
}))
|
||||
})) as unknown as Part[]
|
||||
|
||||
sync.session.addOptimisticMessage({
|
||||
sessionID: existing.id,
|
||||
messageID,
|
||||
parts: optimisticParts,
|
||||
const optimisticMessage: Message = {
|
||||
id: messageID,
|
||||
sessionID: session.id,
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
sdk.client.session
|
||||
const setSyncStore = sessionDirectory === projectDirectory ? sync.set : globalSync.child(sessionDirectory)[1]
|
||||
|
||||
const addOptimisticMessage = () => {
|
||||
setSyncStore(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
if (!messages) {
|
||||
draft.message[session.id] = [optimisticMessage]
|
||||
} else {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
messages.splice(result.index, 0, optimisticMessage)
|
||||
}
|
||||
draft.part[messageID] = optimisticParts
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const removeOptimisticMessage = () => {
|
||||
setSyncStore(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[session.id]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
}
|
||||
delete draft.part[messageID]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
clearInput()
|
||||
addOptimisticMessage()
|
||||
|
||||
client.session
|
||||
.prompt({
|
||||
sessionID: existing.id,
|
||||
sessionID: session.id,
|
||||
agent,
|
||||
model,
|
||||
messageID,
|
||||
parts: requestParts,
|
||||
variant,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to send prompt", e)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: "Failed to send prompt",
|
||||
description: errorMessage(err),
|
||||
})
|
||||
removeOptimisticMessage()
|
||||
restoreInput()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1354,6 +1270,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
|
||||
<Show when={store.popover}>
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (store.popover === "slash") slashPopoverRef = el
|
||||
}}
|
||||
class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10
|
||||
overflow-auto no-scrollbar flex flex-col p-2 rounded-md
|
||||
border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
|
||||
@@ -1412,6 +1331,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<For each={slashFlat()}>
|
||||
{(cmd) => (
|
||||
<button
|
||||
data-slash-id={cmd.id}
|
||||
classList={{
|
||||
"w-full flex items-center justify-between gap-4 rounded-md px-2 py-1": true,
|
||||
"bg-surface-raised-base-hover": slashActive() === cmd.id,
|
||||
@@ -1665,7 +1585,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={ACCEPTED_IMAGE_TYPES.join(",")}
|
||||
accept={ACCEPTED_FILE_TYPES.join(",")}
|
||||
class="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.currentTarget.files?.[0]
|
||||
@@ -1676,7 +1596,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="flex items-center gap-2">
|
||||
<SessionContextUsage />
|
||||
<Show when={store.mode === "normal"}>
|
||||
<Tooltip placement="top" value="Attach image">
|
||||
<Tooltip placement="top" value="Attach file">
|
||||
<Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
|
||||
<Icon name="photo" class="size-4.5" />
|
||||
</Button>
|
||||
|
||||
@@ -305,13 +305,19 @@ export function SessionContextTab(props: SessionContextTabProps) {
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
|
||||
const restoreScroll = () => {
|
||||
const restoreScroll = (retries = 0) => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
const s = props.view()?.scroll("context")
|
||||
if (!s) return
|
||||
|
||||
// Wait for content to be scrollable - content may not have rendered yet
|
||||
if (el.scrollHeight <= el.clientHeight && retries < 10) {
|
||||
requestAnimationFrame(() => restoreScroll(retries + 1))
|
||||
return
|
||||
}
|
||||
|
||||
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
||||
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMemo, createResource, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, Show } from "solid-js"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useCommand } from "@/context/command"
|
||||
@@ -7,7 +7,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
@@ -31,10 +31,11 @@ export function SessionHeader() {
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
|
||||
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
|
||||
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const branch = createMemo(() => sync.data.vcs?.branch)
|
||||
|
||||
function navigateToProject(directory: string) {
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
@@ -46,7 +47,7 @@ export function SessionHeader() {
|
||||
}
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
|
||||
<button
|
||||
type="button"
|
||||
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
|
||||
@@ -60,12 +61,8 @@ export function SessionHeader() {
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={sync.directory}
|
||||
label={(x) => {
|
||||
const name = getFilename(x)
|
||||
const b = x === sync.directory ? branch() : undefined
|
||||
return b ? `${name}:${b}` : name
|
||||
}}
|
||||
current={sync.project?.worktree ?? projectDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
@@ -191,7 +188,7 @@ export function SessionHeader() {
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: sync.directory })
|
||||
.share({ sessionID: session.id, directory: projectDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
import { Show } from "solid-js"
|
||||
import { Show, createMemo } from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
|
||||
export function NewSessionView() {
|
||||
const MAIN_WORKTREE = "main"
|
||||
const CREATE_WORKTREE = "create"
|
||||
|
||||
interface NewSessionViewProps {
|
||||
worktree: string
|
||||
onWorktreeChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function NewSessionView(props: NewSessionViewProps) {
|
||||
const sync = useSync()
|
||||
|
||||
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
|
||||
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
|
||||
const current = createMemo(() => {
|
||||
const selection = props.worktree
|
||||
if (options().includes(selection)) return selection
|
||||
return MAIN_WORKTREE
|
||||
})
|
||||
|
||||
const label = (value: string) => {
|
||||
if (value === MAIN_WORKTREE) {
|
||||
const branch = sync.data.vcs?.branch
|
||||
if (branch) return `Current branch (${branch})`
|
||||
return "Main branch"
|
||||
}
|
||||
|
||||
if (value === CREATE_WORKTREE) return "Create new worktree"
|
||||
|
||||
return getFilename(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
|
||||
<div class="text-20-medium text-text-weaker">New session</div>
|
||||
@@ -17,6 +46,21 @@ export function NewSessionView() {
|
||||
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center gap-1">
|
||||
<Icon name="branch" size="small" />
|
||||
<Select
|
||||
options={options()}
|
||||
current={current()}
|
||||
value={(x) => x}
|
||||
label={label}
|
||||
onSelect={(value) => {
|
||||
props.onWorktreeChange(value ?? MAIN_WORKTREE)
|
||||
}}
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
class="text-12-medium"
|
||||
/>
|
||||
</div>
|
||||
<Show when={sync.project}>
|
||||
{(project) => (
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
|
||||
@@ -31,7 +31,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
||||
const platform = usePlatform()
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: server.url,
|
||||
signal: AbortSignal.timeout(1000 * 60 * 10),
|
||||
fetch: platform.fetch,
|
||||
throwOnError: true,
|
||||
})
|
||||
|
||||
@@ -90,7 +90,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
}
|
||||
|
||||
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||
const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
const [childStore] = globalSync.child(project.worktree)
|
||||
const projectID = childStore.project
|
||||
const metadata = projectID
|
||||
? globalSync.data.project.find((x) => x.id === projectID)
|
||||
: globalSync.data.project.find((x) => x.worktree === project.worktree)
|
||||
return [
|
||||
{
|
||||
...project,
|
||||
|
||||
@@ -160,6 +160,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
),
|
||||
)
|
||||
|
||||
const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
|
||||
|
||||
const userVisibilityMap = createMemo(() => {
|
||||
const map = new Map<string, "show" | "hide">()
|
||||
for (const item of store.user) {
|
||||
map.set(`${item.providerID}:${item.modelID}`, item.visibility)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
available().map((m) => ({
|
||||
...m,
|
||||
@@ -264,12 +274,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
},
|
||||
visible(model: ModelKey) {
|
||||
const user = store.user.find((x) => x.modelID === model.modelID && x.providerID === model.providerID)
|
||||
return (
|
||||
user?.visibility !== "hide" &&
|
||||
(latest().find((x) => x.modelID === model.modelID && x.providerID === model.providerID) ||
|
||||
user?.visibility === "show")
|
||||
)
|
||||
const key = `${model.providerID}:${model.modelID}`
|
||||
const visibility = userVisibilityMap().get(key)
|
||||
return visibility !== "hide" && (latestSet().has(key) || visibility === "show")
|
||||
},
|
||||
setVisibility(model: ModelKey, visible: boolean) {
|
||||
updateVisibility(model, visible ? "show" : "hide")
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
|
||||
|
||||
export type Platform = {
|
||||
/** Platform discriminator */
|
||||
platform: "web" | "tauri"
|
||||
platform: "web" | "desktop"
|
||||
|
||||
/** App version */
|
||||
version?: string
|
||||
|
||||
@@ -11,7 +11,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
const globalSDK = useGlobalSDK()
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
signal: AbortSignal.timeout(1000 * 60 * 10),
|
||||
fetch: platform.fetch,
|
||||
directory: props.directory,
|
||||
throwOnError: true,
|
||||
|
||||
@@ -11,8 +11,7 @@ export function normalizeServerUrl(input: string) {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) return
|
||||
const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`
|
||||
const cleaned = withProtocol.replace(/\/+$/, "")
|
||||
return cleaned.replace(/^(https?:\/\/[^/]+).*/, "$1")
|
||||
return withProtocol.replace(/\/+$/, "")
|
||||
}
|
||||
|
||||
export function serverDisplayName(url: string) {
|
||||
@@ -100,7 +99,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: url,
|
||||
fetch: platform.fetch,
|
||||
signal: AbortSignal.timeout(2000),
|
||||
signal: AbortSignal.timeout(3000),
|
||||
})
|
||||
return sdk.global
|
||||
.health()
|
||||
|
||||
@@ -53,8 +53,8 @@ export default function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="mx-auto mt-55">
|
||||
<Logo class="w-xl opacity-12" />
|
||||
<div class="mx-auto mt-55 w-full md:w-auto px-4">
|
||||
<Logo class="md:w-xl opacity-12" />
|
||||
<Button
|
||||
size="large"
|
||||
variant="ghost"
|
||||
|
||||
@@ -172,9 +172,9 @@ export default function Layout(props: ParentProps) {
|
||||
const perm = e.details.properties
|
||||
if (permission.autoResponds(perm)) return
|
||||
|
||||
const sessionKey = `${directory}:${perm.sessionID}`
|
||||
const [store] = globalSync.child(directory)
|
||||
const session = store.session.find((s) => s.id === perm.sessionID)
|
||||
const sessionKey = `${directory}:${perm.sessionID}`
|
||||
|
||||
const sessionTitle = session?.title ?? "New session"
|
||||
const projectName = getFilename(directory)
|
||||
@@ -665,14 +665,13 @@ export default function Layout(props: ParentProps) {
|
||||
<>
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full pr-2 py-1 rounded-md cursor-default transition-colors
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors
|
||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||
style={{ "padding-left": "16px" }}
|
||||
>
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
|
||||
<A
|
||||
href={`${props.slug}/session/${props.session.id}`}
|
||||
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
|
||||
class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
||||
<span
|
||||
@@ -740,10 +739,17 @@ export default function Layout(props: ParentProps) {
|
||||
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||
const sortable = createSortable(props.project.worktree)
|
||||
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const defaultWorktree = createMemo(() => base64Encode(props.project.worktree))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
||||
const sessions = createMemo(() => store.session.toSorted(sortSessions))
|
||||
const stores = createMemo(() =>
|
||||
[props.project.worktree, ...(props.project.sandboxes ?? [])].map((dir) => globalSync.child(dir)[0]),
|
||||
)
|
||||
const sessions = createMemo(() =>
|
||||
stores()
|
||||
.flatMap((store) => store.session.filter((session) => session.directory === store.path.directory))
|
||||
.toSorted(sortSessions),
|
||||
)
|
||||
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
||||
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
|
||||
const loadMoreSessions = async () => {
|
||||
@@ -799,7 +805,7 @@ export default function Layout(props: ParentProps) {
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
<TooltipKeybind placement="top" title="New session" keybind={command.keybind("session.new")}>
|
||||
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
|
||||
<IconButton as={A} href={`${defaultWorktree()}/session`} icon="plus-small" variant="ghost" />
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -807,7 +813,12 @@ export default function Layout(props: ParentProps) {
|
||||
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
|
||||
<For each={rootSessions()}>
|
||||
{(session) => (
|
||||
<SessionItem session={session} slug={slug()} project={props.project} mobile={props.mobile} />
|
||||
<SessionItem
|
||||
session={session}
|
||||
slug={base64Encode(session.directory)}
|
||||
project={props.project}
|
||||
mobile={props.mobile}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<Show when={rootSessions().length === 0}>
|
||||
@@ -819,7 +830,7 @@ export default function Layout(props: ParentProps) {
|
||||
<div class="flex-1 min-w-0">
|
||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
|
||||
<A
|
||||
href={`${slug()}/session`}
|
||||
href={`${defaultWorktree()}/session`}
|
||||
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
|
||||
>
|
||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||
@@ -875,76 +886,85 @@ export default function Layout(props: ParentProps) {
|
||||
const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
|
||||
const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
|
||||
return (
|
||||
<>
|
||||
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
|
||||
<div class="flex flex-col self-stretch h-full items-center justify-between overflow-hidden min-h-0">
|
||||
<div class="flex flex-col items-start self-stretch gap-4 min-h-0">
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<A href="/" class="shrink-0 h-8 flex items-center justify-start px-2" data-tauri-drag-region>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
</Show>
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<TooltipKeybind
|
||||
class="shrink-0"
|
||||
placement="right"
|
||||
title="Toggle sidebar"
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
inactive={expanded()}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
|
||||
onClick={layout.sidebar.toggle}
|
||||
>
|
||||
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
|
||||
size="small"
|
||||
class="group-hover/sidebar-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/sidebar-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
|
||||
size="small"
|
||||
class="hidden group-active/sidebar-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
<Show when={layout.sidebar.opened()}>
|
||||
<div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
|
||||
Toggle sidebar
|
||||
</div>
|
||||
</Show>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!sidebarProps.mobile) scrollContainerRef = el
|
||||
classList={{
|
||||
"border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0": true,
|
||||
"justify-start": expanded(),
|
||||
}}
|
||||
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
|
||||
>
|
||||
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
||||
<For each={layout.projects.list()}>
|
||||
{(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<A href="/" class="shrink-0 h-8 flex items-center justify-start px-2 w-full" data-tauri-drag-region>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<ProjectDragOverlay />
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</Show>
|
||||
<div class="flex flex-col items-start self-stretch gap-4 px-2 overflow-hidden min-h-0">
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<TooltipKeybind
|
||||
class="shrink-0"
|
||||
placement="right"
|
||||
title="Toggle sidebar"
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
inactive={expanded()}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="large"
|
||||
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
|
||||
onClick={layout.sidebar.toggle}
|
||||
>
|
||||
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
|
||||
size="small"
|
||||
class="group-hover/sidebar-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/sidebar-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
|
||||
size="small"
|
||||
class="hidden group-active/sidebar-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
<Show when={layout.sidebar.opened()}>
|
||||
<div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
|
||||
Toggle sidebar
|
||||
</div>
|
||||
</Show>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<DragDropProvider
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
collisionDetector={closestCenter}
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragXAxis />
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!sidebarProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
|
||||
>
|
||||
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
||||
<For each={layout.projects.list()}>
|
||||
{(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<ProjectDragOverlay />
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
||||
<Switch>
|
||||
@@ -1017,7 +1037,7 @@ export default function Layout(props: ParentProps) {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1065,12 +1085,21 @@ export default function Layout(props: ParentProps) {
|
||||
/>
|
||||
<div
|
||||
classList={{
|
||||
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pt-12 pb-5 transition-transform duration-200 ease-out": true,
|
||||
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pb-5 transition-transform duration-200 ease-out": true,
|
||||
"translate-x-0": layout.mobileSidebar.opened(),
|
||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0">
|
||||
<A
|
||||
href="/"
|
||||
class="shrink-0 h-8 flex items-center justify-start px-2 w-full"
|
||||
onClick={() => layout.mobileSidebar.hide()}
|
||||
>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
</div>
|
||||
<SidebarContent mobile />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on, createRenderEffect, batch } from "solid-js"
|
||||
import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on, batch } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -24,7 +24,7 @@ import { useSync } from "@/context/sync"
|
||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { Terminal } from "@/components/terminal"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { checksum, base64Decode } from "@opencode-ai/util/encode"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
SortableTerminalTab,
|
||||
NewSessionView,
|
||||
} from "@/components/session"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
if (a === b) return true
|
||||
@@ -73,13 +74,19 @@ function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
|
||||
const restoreScroll = () => {
|
||||
const restoreScroll = (retries = 0) => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
const s = props.view().scroll("review")
|
||||
if (!s) return
|
||||
|
||||
// Wait for content to be scrollable - content may not have rendered yet
|
||||
if (el.scrollHeight <= el.clientHeight && retries < 10) {
|
||||
requestAnimationFrame(() => restoreScroll(retries + 1))
|
||||
return
|
||||
}
|
||||
|
||||
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
||||
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
||||
}
|
||||
@@ -147,6 +154,7 @@ export default function Page() {
|
||||
const dialog = useDialog()
|
||||
const codeComponent = useCodeComponent()
|
||||
const command = useCommand()
|
||||
const platform = usePlatform()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
@@ -218,20 +226,12 @@ export default function Page() {
|
||||
return sync.data.message[id] !== undefined
|
||||
})
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
const userMessages = createMemo(
|
||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
)
|
||||
const visibleUserMessages = createMemo(
|
||||
() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
)
|
||||
const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages)
|
||||
const visibleUserMessages = createMemo(() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
}, emptyUserMessages)
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
|
||||
|
||||
createEffect(
|
||||
@@ -249,13 +249,10 @@ export default function Page() {
|
||||
const [store, setStore] = createStore({
|
||||
activeDraggable: undefined as string | undefined,
|
||||
activeTerminalDraggable: undefined as string | undefined,
|
||||
userInteracted: false,
|
||||
stepsExpanded: true,
|
||||
mobileStepsExpanded: {} as Record<string, boolean>,
|
||||
expanded: {} as Record<string, boolean>,
|
||||
messageId: undefined as string | undefined,
|
||||
mobileTab: "session" as "session" | "review",
|
||||
ignoreScrollSpy: false,
|
||||
initialScrollDone: !params.id,
|
||||
newSessionWorktree: "main",
|
||||
})
|
||||
|
||||
const activeMessage = createMemo(() => {
|
||||
@@ -316,47 +313,24 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => params.id,
|
||||
(id) => {
|
||||
const status = sync.data.session_status[id ?? ""] ?? idle
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", status.type !== "idle")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => status().type,
|
||||
(type) => {
|
||||
if (type !== "idle") return
|
||||
batch(() => {
|
||||
setStore("userInteracted", false)
|
||||
setStore("stepsExpanded", false)
|
||||
})
|
||||
() => params.id,
|
||||
() => {
|
||||
setStore("messageId", undefined)
|
||||
setStore("expanded", {})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
|
||||
|
||||
createRenderEffect((prev) => {
|
||||
const isWorking = working()
|
||||
if (!prev && isWorking) {
|
||||
setStore("stepsExpanded", true)
|
||||
}
|
||||
if (prev && !isWorking && !store.userInteracted) {
|
||||
setStore("stepsExpanded", false)
|
||||
}
|
||||
return isWorking
|
||||
}, working())
|
||||
createEffect(() => {
|
||||
const id = lastUserMessage()?.id
|
||||
if (!id) return
|
||||
setStore("expanded", id, status().type !== "idle")
|
||||
})
|
||||
|
||||
command.register(() => [
|
||||
{
|
||||
@@ -405,12 +379,16 @@ export default function Page() {
|
||||
{
|
||||
id: "steps.toggle",
|
||||
title: "Toggle steps",
|
||||
description: "Show or hide the steps",
|
||||
description: "Show or hide steps for the current message",
|
||||
category: "View",
|
||||
keybind: "mod+e",
|
||||
slash: "steps",
|
||||
disabled: !params.id,
|
||||
onSelect: () => setStore("stepsExpanded", (x) => !x),
|
||||
onSelect: () => {
|
||||
const msg = activeMessage()
|
||||
if (!msg) return
|
||||
setStore("expanded", msg.id, (open: boolean | undefined) => !open)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "message.previous",
|
||||
@@ -555,6 +533,31 @@ export default function Page() {
|
||||
setActiveMessage(priorMsg)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "session.compact",
|
||||
title: "Compact session",
|
||||
description: "Summarize the session to reduce context size",
|
||||
category: "Session",
|
||||
slash: "compact",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const model = local.model.current()
|
||||
if (!model) {
|
||||
showToast({
|
||||
title: "No model selected",
|
||||
description: "Connect a provider to summarize this session",
|
||||
})
|
||||
return
|
||||
}
|
||||
await sdk.client.session.summarize({
|
||||
sessionID,
|
||||
modelID: model.id,
|
||||
providerID: model.provider.id,
|
||||
})
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -655,204 +658,76 @@ export default function Page() {
|
||||
const isWorking = createMemo(() => status().type !== "idle")
|
||||
const autoScroll = createAutoScroll({
|
||||
working: isWorking,
|
||||
onUserInteracted: () => setStore("userInteracted", true),
|
||||
})
|
||||
|
||||
let scrollContainer: HTMLDivElement | undefined
|
||||
let initialScrollFrame: number | undefined
|
||||
let initialScrollTarget: string | undefined
|
||||
|
||||
const cancelInitialScroll = () => {
|
||||
if (initialScrollFrame === undefined) return
|
||||
cancelAnimationFrame(initialScrollFrame)
|
||||
initialScrollFrame = undefined
|
||||
}
|
||||
|
||||
const ensureInitialScroll = () => {
|
||||
cancelInitialScroll()
|
||||
initialScrollFrame = requestAnimationFrame(() => {
|
||||
initialScrollFrame = undefined
|
||||
if (!params.id) {
|
||||
initialScrollTarget = undefined
|
||||
setStore("initialScrollDone", true)
|
||||
return
|
||||
}
|
||||
const msgs = visibleUserMessages()
|
||||
if (msgs.length === 0) {
|
||||
if (!messagesReady()) {
|
||||
ensureInitialScroll()
|
||||
return
|
||||
}
|
||||
initialScrollTarget = undefined
|
||||
setStore("initialScrollDone", true)
|
||||
return
|
||||
}
|
||||
const last = msgs[msgs.length - 1]
|
||||
const el = messageRefs.get(last.id)
|
||||
if (!el || !scrollContainer) {
|
||||
ensureInitialScroll()
|
||||
return
|
||||
}
|
||||
scrollToMessage(last, "auto")
|
||||
initialScrollTarget = last.id
|
||||
setStore("initialScrollDone", true)
|
||||
})
|
||||
}
|
||||
|
||||
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
||||
scrollContainer = el
|
||||
autoScroll.scrollRef(el)
|
||||
}
|
||||
|
||||
const messageRefs = new Map<string, HTMLDivElement>()
|
||||
let scrollTimer: number | undefined
|
||||
|
||||
createEffect(() => {
|
||||
const msgs = visibleUserMessages()
|
||||
if (msgs.length === 0) {
|
||||
messageRefs.clear()
|
||||
return
|
||||
}
|
||||
const ids = new Set(msgs.map((m) => m.id))
|
||||
for (const id of messageRefs.keys()) {
|
||||
if (ids.has(id)) continue
|
||||
messageRefs.delete(id)
|
||||
}
|
||||
})
|
||||
|
||||
let scrollSpyIndex = 0
|
||||
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
setStore("ignoreScrollSpy", true)
|
||||
setActiveMessage(message)
|
||||
|
||||
const msgs = visibleUserMessages()
|
||||
const idx = msgs.findIndex((m) => m.id === message.id)
|
||||
if (idx >= 0) scrollSpyIndex = idx
|
||||
|
||||
const el = messageRefs.get(message.id)
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior, block: "start" })
|
||||
}
|
||||
|
||||
if (scrollTimer !== undefined) window.clearTimeout(scrollTimer)
|
||||
scrollTimer = window.setTimeout(() => setStore("ignoreScrollSpy", false), 1000)
|
||||
}
|
||||
|
||||
let scrollSpyFrame: number | undefined
|
||||
let scrollSpyTarget: HTMLDivElement | undefined
|
||||
|
||||
const anchor = (id: string) => `message-${id}`
|
||||
|
||||
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
||||
autoScroll.scrollRef(el)
|
||||
}
|
||||
|
||||
const updateHash = (id: string) => {
|
||||
window.history.replaceState(null, "", `#${anchor(id)}`)
|
||||
}
|
||||
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
setActiveMessage(message)
|
||||
|
||||
const el = document.getElementById(anchor(message.id))
|
||||
if (el) el.scrollIntoView({ behavior, block: "start" })
|
||||
updateHash(message.id)
|
||||
}
|
||||
|
||||
const getActiveMessageId = (container: HTMLDivElement) => {
|
||||
const cutoff = container.scrollTop + 100
|
||||
const nodes = container.querySelectorAll<HTMLElement>("[data-message-id]")
|
||||
let id: string | undefined
|
||||
|
||||
for (const node of nodes) {
|
||||
const next = node.dataset.messageId
|
||||
if (!next) continue
|
||||
if (node.offsetTop > cutoff) break
|
||||
id = next
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
const scheduleScrollSpy = (container: HTMLDivElement) => {
|
||||
if (store.ignoreScrollSpy) return
|
||||
scrollSpyTarget = container
|
||||
if (scrollSpyFrame !== undefined) return
|
||||
|
||||
scrollSpyFrame = requestAnimationFrame(() => {
|
||||
scrollSpyFrame = undefined
|
||||
|
||||
const target = scrollSpyTarget
|
||||
scrollSpyTarget = undefined
|
||||
if (!target) return
|
||||
if (store.ignoreScrollSpy) return
|
||||
|
||||
const msgs = visibleUserMessages()
|
||||
const scrollTop = target.scrollTop
|
||||
const threshold = 100
|
||||
const cutoff = scrollTop + threshold
|
||||
const id = getActiveMessageId(target)
|
||||
if (!id) return
|
||||
if (id === store.messageId) return
|
||||
|
||||
if (msgs.length === 0) return
|
||||
|
||||
if (scrollSpyIndex >= msgs.length) scrollSpyIndex = msgs.length - 1
|
||||
if (scrollSpyIndex < 0) scrollSpyIndex = 0
|
||||
|
||||
while (scrollSpyIndex + 1 < msgs.length) {
|
||||
const next = msgs[scrollSpyIndex + 1]
|
||||
if (!next) break
|
||||
|
||||
const el = messageRefs.get(next.id)
|
||||
if (!el) break
|
||||
if (el.offsetTop <= cutoff) {
|
||||
scrollSpyIndex += 1
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
while (scrollSpyIndex > 0) {
|
||||
const cur = msgs[scrollSpyIndex]
|
||||
if (!cur) break
|
||||
|
||||
const el = messageRefs.get(cur.id)
|
||||
if (!el) break
|
||||
if (el.offsetTop > cutoff) {
|
||||
scrollSpyIndex -= 1
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const msg = msgs[scrollSpyIndex]
|
||||
if (!msg) return
|
||||
if (msg.id === activeMessage()?.id) return
|
||||
|
||||
setActiveMessage(msg)
|
||||
setStore("messageId", id)
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => params.id,
|
||||
(id) => {
|
||||
cancelInitialScroll()
|
||||
if (scrollTimer !== undefined) window.clearTimeout(scrollTimer)
|
||||
scrollTimer = undefined
|
||||
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
|
||||
scrollSpyFrame = undefined
|
||||
scrollSpyTarget = undefined
|
||||
messageRefs.clear()
|
||||
scrollSpyIndex = 0
|
||||
initialScrollTarget = undefined
|
||||
setStore("initialScrollDone", !id)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const msgs = visibleUserMessages()
|
||||
const target = msgs.at(-1)?.id
|
||||
const sessionID = params.id
|
||||
const ready = messagesReady()
|
||||
if (!sessionID || !ready) return
|
||||
|
||||
if (!params.id) {
|
||||
setStore("initialScrollDone", true)
|
||||
initialScrollTarget = undefined
|
||||
return
|
||||
}
|
||||
|
||||
if (!ready) {
|
||||
setStore("initialScrollDone", false)
|
||||
ensureInitialScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.initialScrollDone) {
|
||||
ensureInitialScroll()
|
||||
return
|
||||
}
|
||||
|
||||
if (!initialScrollTarget && target) {
|
||||
setStore("initialScrollDone", false)
|
||||
ensureInitialScroll()
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const msgs = visibleUserMessages()
|
||||
if (msgs.length === 0) return
|
||||
requestAnimationFrame(() => {
|
||||
if (!scrollContainer) return
|
||||
if (!isDesktop()) return
|
||||
// Manually trigger spy once to set initial active message based on scroll position
|
||||
scheduleScrollSpy(scrollContainer)
|
||||
const id = window.location.hash.slice(1)
|
||||
const hashTarget = id ? document.getElementById(id) : undefined
|
||||
if (hashTarget) {
|
||||
hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
|
||||
return
|
||||
}
|
||||
autoScroll.forceScrollToBottom()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -862,8 +737,6 @@ export default function Page() {
|
||||
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
cancelInitialScroll()
|
||||
if (scrollTimer !== undefined) window.clearTimeout(scrollTimer)
|
||||
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
|
||||
})
|
||||
|
||||
@@ -944,13 +817,10 @@ export default function Page() {
|
||||
}}
|
||||
onClick={autoScroll.handleInteraction}
|
||||
class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
|
||||
classList={{
|
||||
"opacity-0 pointer-events-none": !store.initialScrollDone,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={autoScroll.contentRef}
|
||||
class="flex flex-col gap-45 items-start justify-start pb-32 md:pb-40 transition-[margin]"
|
||||
class="flex flex-col gap-32 items-start justify-start pb-32 md:pb-40 transition-[margin]"
|
||||
classList={{
|
||||
"mt-0.5": !showTabs(),
|
||||
"mt-0": showTabs(),
|
||||
@@ -959,16 +829,24 @@ export default function Page() {
|
||||
<For each={visibleUserMessages()}>
|
||||
{(message) => (
|
||||
<div
|
||||
ref={(el) => messageRefs.set(message.id, el)}
|
||||
class="min-w-0 w-full max-w-full last:min-h-[80vh]"
|
||||
id={anchor(message.id)}
|
||||
data-message-id={message.id}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"last:min-h-[calc(100vh-13.5rem)] md:last:min-h-[calc(100vh-14.5rem)]":
|
||||
platform.platform !== "desktop",
|
||||
"last:min-h-[calc(100vh-15rem)] md:last:min-h-[calc(100vh-16rem)]":
|
||||
platform.platform === "desktop",
|
||||
}}
|
||||
>
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
|
||||
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
|
||||
onUserInteracted={() => setStore("userInteracted", true)}
|
||||
stepsExpanded={store.expanded[message.id] ?? false}
|
||||
onStepsExpandedToggle={() =>
|
||||
setStore("expanded", message.id, (open: boolean | undefined) => !open)
|
||||
}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
@@ -992,7 +870,10 @@ export default function Page() {
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<NewSessionView />
|
||||
<NewSessionView
|
||||
worktree={store.newSessionWorktree}
|
||||
onWorktreeChange={(value) => setStore("newSessionWorktree", value)}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
@@ -1009,6 +890,8 @@ export default function Page() {
|
||||
ref={(el) => {
|
||||
inputRef = el
|
||||
}}
|
||||
newSessionWorktree={store.newSessionWorktree}
|
||||
onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1128,6 +1011,35 @@ export default function Page() {
|
||||
})
|
||||
const contents = createMemo(() => state()?.content?.content ?? "")
|
||||
const cacheKey = createMemo(() => checksum(contents()))
|
||||
const isImage = createMemo(() => {
|
||||
const c = state()?.content
|
||||
return (
|
||||
c?.encoding === "base64" && c?.mimeType?.startsWith("image/") && c?.mimeType !== "image/svg+xml"
|
||||
)
|
||||
})
|
||||
const isSvg = createMemo(() => {
|
||||
const c = state()?.content
|
||||
return c?.mimeType === "image/svg+xml"
|
||||
})
|
||||
const svgContent = createMemo(() => {
|
||||
if (!isSvg()) return
|
||||
const c = state()?.content
|
||||
if (!c) return
|
||||
if (c.encoding === "base64") return base64Decode(c.content)
|
||||
return c.content
|
||||
})
|
||||
const svgPreviewUrl = createMemo(() => {
|
||||
if (!isSvg()) return
|
||||
const c = state()?.content
|
||||
if (!c) return
|
||||
if (c.encoding === "base64") return `data:image/svg+xml;base64,${c.content}`
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(c.content)}`
|
||||
})
|
||||
const imageDataUrl = createMemo(() => {
|
||||
if (!isImage()) return
|
||||
const c = state()?.content
|
||||
return `data:${c?.mimeType};base64,${c?.content}`
|
||||
})
|
||||
const selectedLines = createMemo(() => {
|
||||
const p = path()
|
||||
if (!p) return null
|
||||
@@ -1145,13 +1057,19 @@ export default function Page() {
|
||||
return `L${sel.startLine}-${sel.endLine}`
|
||||
})
|
||||
|
||||
const restoreScroll = () => {
|
||||
const restoreScroll = (retries = 0) => {
|
||||
const el = scroll
|
||||
if (!el) return
|
||||
|
||||
const s = view()?.scroll(tab)
|
||||
if (!s) return
|
||||
|
||||
// Wait for content to be scrollable - content may not have rendered yet
|
||||
if (el.scrollHeight <= el.clientHeight && retries < 10) {
|
||||
requestAnimationFrame(() => restoreScroll(retries + 1))
|
||||
return
|
||||
}
|
||||
|
||||
if (el.scrollTop !== s.y) el.scrollTop = s.y
|
||||
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
|
||||
}
|
||||
@@ -1196,6 +1114,17 @@ export default function Page() {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => tabs().active() === tab,
|
||||
(active) => {
|
||||
if (!active) return
|
||||
if (!state()?.loaded) return
|
||||
requestAnimationFrame(restoreScroll)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (scrollFrame === undefined) return
|
||||
cancelAnimationFrame(scrollFrame)
|
||||
@@ -1230,6 +1159,37 @@ export default function Page() {
|
||||
)}
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={state()?.loaded && isImage()}>
|
||||
<div class="px-6 py-4 pb-40">
|
||||
<img src={imageDataUrl()} alt={path()} class="max-w-full" />
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isSvg()}>
|
||||
<div class="flex flex-col gap-4 px-6 py-4">
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
file={{
|
||||
name: path() ?? "",
|
||||
contents: svgContent() ?? "",
|
||||
cacheKey: cacheKey(),
|
||||
}}
|
||||
enableLineSelection
|
||||
selectedLines={selectedLines()}
|
||||
onLineSelected={(range: SelectedLineRange | null) => {
|
||||
const p = path()
|
||||
if (!p) return
|
||||
file.setSelectedLines(p, range)
|
||||
}}
|
||||
overflow="scroll"
|
||||
class="select-text"
|
||||
/>
|
||||
<Show when={svgPreviewUrl()}>
|
||||
<div class="flex justify-center pb-40">
|
||||
<img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded}>
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vite dev --host 0.0.0.0",
|
||||
|
||||
@@ -7,7 +7,7 @@ export const config = {
|
||||
|
||||
// GitHub
|
||||
github: {
|
||||
repoUrl: "https://github.com/sst/opencode",
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "45K",
|
||||
full: "45,000",
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function NotFound() {
|
||||
<a href="/docs">Docs</a>
|
||||
</div>
|
||||
<div data-slot="action">
|
||||
<a href="https://github.com/sst/opencode">GitHub</a>
|
||||
<a href="https://github.com/anomalyco/opencode">GitHub</a>
|
||||
</div>
|
||||
<div data-slot="action">
|
||||
<a href="/discord">Discord</a>
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function GET({ params: { platform } }: APIEvent) {
|
||||
const assetName = assetNames[platform]
|
||||
if (!assetName) return new Response("Not Found", { status: 404 })
|
||||
|
||||
const resp = await fetch(`https://github.com/sst/opencode/releases/latest/download/${assetName}`, {
|
||||
const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, {
|
||||
cf: {
|
||||
// in case gh releases has rate limits
|
||||
cacheTtl: 60 * 60 * 24,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export async function GET() {
|
||||
const response = await fetch(
|
||||
"https://raw.githubusercontent.com/sst/opencode/refs/heads/dev/packages/sdk/openapi.json",
|
||||
"https://raw.githubusercontent.com/anomalyco/opencode/refs/heads/dev/packages/sdk/openapi.json",
|
||||
)
|
||||
const json = await response.json()
|
||||
return json
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function Home() {
|
||||
<a href="https://x.com/opencode">X.com</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href="https://github.com/sst/opencode">GitHub</a>
|
||||
<a href="https://github.com/anomalyco/opencode">GitHub</a>
|
||||
</div>
|
||||
<div data-slot="cell">
|
||||
<a href="https://opencode.ai/discord">Discord</a>
|
||||
|
||||
1
packages/console/core/migrations/0040_broken_gamora.sql
Normal file
1
packages/console/core/migrations/0040_broken_gamora.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE INDEX `usage_time_created` ON `usage` (`workspace_id`,`time_created`);
|
||||
1059
packages/console/core/migrations/meta/0040_snapshot.json
Normal file
1059
packages/console/core/migrations/meta/0040_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -281,6 +281,13 @@
|
||||
"when": 1766946179892,
|
||||
"tag": "0039_striped_forge",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 40,
|
||||
"version": "5",
|
||||
"when": 1767584617316,
|
||||
"tag": "0040_broken_gamora",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
|
||||
@@ -18,15 +18,17 @@ const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!value7) throw new Error("ZEN_MODELS7 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||
@@ -35,3 +37,4 @@ await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${value7} --stage ${stage}`
|
||||
|
||||
@@ -18,14 +18,16 @@ const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!value6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!value7) throw new Error("ZEN_MODELS7 not found")
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||
@@ -34,3 +36,4 @@ await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${value6}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${value7}`
|
||||
|
||||
@@ -16,18 +16,24 @@ const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=
|
||||
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
|
||||
const oldValue7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
|
||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
|
||||
if (!oldValue6) throw new Error("ZEN_MODELS6 not found")
|
||||
if (!oldValue7) throw new Error("ZEN_MODELS7 not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `models-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(
|
||||
JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6), null, 2),
|
||||
JSON.stringify(
|
||||
JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6 + oldValue7),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
console.log("tempFile", tempFile.name)
|
||||
|
||||
@@ -37,13 +43,14 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
ZenData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
const chunk = Math.ceil(newValue.length / 6)
|
||||
const chunk = Math.ceil(newValue.length / 7)
|
||||
const newValue1 = newValue.slice(0, chunk)
|
||||
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
|
||||
const newValue5 = newValue.slice(chunk * 4, chunk * 5)
|
||||
const newValue6 = newValue.slice(chunk * 5)
|
||||
const newValue6 = newValue.slice(chunk * 5, chunk * 6)
|
||||
const newValue7 = newValue.slice(chunk * 6)
|
||||
|
||||
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||
@@ -51,3 +58,4 @@ await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
|
||||
await $`bun sst secret set ZEN_MODELS6 ${newValue6}`
|
||||
await $`bun sst secret set ZEN_MODELS7 ${newValue7}`
|
||||
|
||||
@@ -73,7 +73,8 @@ export namespace ZenData {
|
||||
Resource.ZEN_MODELS3.value +
|
||||
Resource.ZEN_MODELS4.value +
|
||||
Resource.ZEN_MODELS5.value +
|
||||
Resource.ZEN_MODELS6.value,
|
||||
Resource.ZEN_MODELS6.value +
|
||||
Resource.ZEN_MODELS7.value,
|
||||
)
|
||||
return ModelsSchema.parse(json)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bigint, boolean, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { bigint, boolean, index, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
@@ -55,5 +55,5 @@ export const UsageTable = mysqlTable(
|
||||
cost: bigint("cost", { mode: "number" }).notNull(),
|
||||
keyID: ulid("key_id"),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table)],
|
||||
(table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],
|
||||
)
|
||||
|
||||
4
packages/console/core/sst-env.d.ts
vendored
4
packages/console/core/sst-env.d.ts
vendored
@@ -122,6 +122,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS7": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
|
||||
4
packages/console/function/sst-env.d.ts
vendored
4
packages/console/function/sst-env.d.ts
vendored
@@ -122,6 +122,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS7": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -17,5 +17,6 @@
|
||||
"scripts": {
|
||||
"dev": "email preview emails/templates"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-resource",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cloudflare/workers-types": "catalog:"
|
||||
},
|
||||
|
||||
4
packages/console/resource/sst-env.d.ts
vendored
4
packages/console/resource/sst-env.d.ts
vendored
@@ -122,6 +122,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS7": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||
<script id="oc-theme-preload-script">
|
||||
;(function () {
|
||||
var themeId = localStorage.getItem("opencode-theme-id")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
"predev": "bun ./scripts/predev.ts",
|
||||
|
||||
127
packages/desktop/src-tauri/release/appstream.metainfo.xml
Normal file
127
packages/desktop/src-tauri/release/appstream.metainfo.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>ai.opencode.opencode</id>
|
||||
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>MIT</project_license>
|
||||
|
||||
<name>OpenCode</name>
|
||||
<summary>Open source AI coding agent</summary>
|
||||
|
||||
<developer id="ly.anoma">
|
||||
<name>Anomaly Innovations Inc.</name>
|
||||
</developer>
|
||||
|
||||
<description>
|
||||
<p>
|
||||
OpenCode is an open source agent that helps you write and run code with any AI model.
|
||||
</p>
|
||||
</description>
|
||||
|
||||
<launchable type="desktop-id">ai.opencode.opencode.desktop</launchable>
|
||||
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<url type="bugtracker">https://github.com/anomalyco/opencode/issues</url>
|
||||
<url type="homepage">https://opencode.ai</url>
|
||||
<url type="vcs-browser">https://github.com/anomalyco/opencode</url>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://opencode.ai/docs/_astro/screenshot.Bs5D4atL_ZvsvFu.webp</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="1.0.223" date="2026-01-01">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.223</url>
|
||||
</release>
|
||||
<release version="1.0.222" date="2026-01-01">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.222</url>
|
||||
</release>
|
||||
<release version="1.0.221" date="2025-12-31">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.221</url>
|
||||
</release>
|
||||
<release version="1.0.220" date="2025-12-31">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.220</url>
|
||||
</release>
|
||||
<release version="1.0.219" date="2025-12-31">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.219</url>
|
||||
</release>
|
||||
<release version="1.0.218" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.218</url>
|
||||
</release>
|
||||
<release version="1.0.217" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.217</url>
|
||||
</release>
|
||||
<release version="1.0.216" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.216</url>
|
||||
</release>
|
||||
<release version="1.0.215" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.215</url>
|
||||
</release>
|
||||
<release version="1.0.214" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.214</url>
|
||||
</release>
|
||||
<release version="1.0.213" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.213</url>
|
||||
</release>
|
||||
<release version="1.0.212" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.212</url>
|
||||
</release>
|
||||
<release version="1.0.211" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.211</url>
|
||||
</release>
|
||||
<release version="1.0.210" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.210</url>
|
||||
</release>
|
||||
<release version="1.0.209" date="2025-12-30">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.209</url>
|
||||
</release>
|
||||
<release version="1.0.208" date="2025-12-29">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.208</url>
|
||||
</release>
|
||||
<release version="1.0.207" date="2025-12-29">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.207</url>
|
||||
</release>
|
||||
<release version="1.0.206" date="2025-12-28">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.206</url>
|
||||
</release>
|
||||
<release version="1.0.205" date="2025-12-28">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.205</url>
|
||||
</release>
|
||||
<release version="1.0.204" date="2025-12-27">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.204</url>
|
||||
</release>
|
||||
<release version="1.0.203" date="2025-12-26">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.203</url>
|
||||
</release>
|
||||
<release version="1.0.202" date="2025-12-26">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.202</url>
|
||||
</release>
|
||||
<release version="1.0.201" date="2025-12-25">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.201</url>
|
||||
</release>
|
||||
<release version="1.0.200" date="2025-12-25">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.200</url>
|
||||
</release>
|
||||
<release version="1.0.199" date="2025-12-25">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.199</url>
|
||||
</release>
|
||||
<release version="1.0.198" date="2025-12-24">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.198</url>
|
||||
</release>
|
||||
<release version="1.0.195" date="2025-12-24">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.195</url>
|
||||
</release>
|
||||
<release version="1.0.194" date="2025-12-24">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.194</url>
|
||||
</release>
|
||||
<release version="1.0.193" date="2025-12-23">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.193</url>
|
||||
</release>
|
||||
<release version="1.0.191" date="2025-12-23">
|
||||
<url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.191</url>
|
||||
</release>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -15,12 +15,19 @@
|
||||
"nsis": {
|
||||
"installerIcon": "icons/prod/icon.ico"
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"files": {
|
||||
"/usr/share/metainfo/ai.opencode.opencode.metainfo.xml": "release/appstream.metainfo.xml"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYwMDM5Nzg5OUMzOUExMDQKUldRRW9UbWNpWmNEOENYT01CV0lhOXR1UFhpaXJsK1Z3aU9lZnNtNzE0TDROWVMwVW9XQnFOelkK",
|
||||
"endpoints": ["https://github.com/sst/opencode/releases/latest/download/latest.json"]
|
||||
"endpoints": ["https://github.com/anomalyco/opencode/releases/latest/download/latest.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
let update: Update | null = null
|
||||
|
||||
const platform: Platform = {
|
||||
platform: "tauri",
|
||||
platform: "desktop",
|
||||
version: pkg.version,
|
||||
|
||||
async openDirectoryPickerDialog(opts) {
|
||||
@@ -198,7 +198,7 @@ render(() => {
|
||||
return (
|
||||
<PlatformProvider value={platform}>
|
||||
{ostype() === "macos" && (
|
||||
<div class="bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
|
||||
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
|
||||
)}
|
||||
<App />
|
||||
</PlatformProvider>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -328,7 +328,7 @@ export default function () {
|
||||
<div class="flex gap-3 items-center">
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://github.com/sst/opencode"
|
||||
href="https://github.com/anomalyco/opencode"
|
||||
target="_blank"
|
||||
icon="github"
|
||||
variant="ghost"
|
||||
|
||||
4
packages/enterprise/sst-env.d.ts
vendored
4
packages/enterprise/sst-env.d.ts
vendored
@@ -122,6 +122,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS7": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.224"
|
||||
version = "1.1.2"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
|
||||
[agent_servers.opencode]
|
||||
name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.224/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/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.224/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.224/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/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.224/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/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.224/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
|
||||
4
packages/function/sst-env.d.ts
vendored
4
packages/function/sst-env.d.ts
vendored
@@ -122,6 +122,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS7": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Build/Test Commands
|
||||
|
||||
- **Install**: `bun install`
|
||||
- **Run**: `bun run index.ts`
|
||||
- **Run**: `bun run --conditions=browser ./src/index.ts`
|
||||
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
||||
- **Test**: `bun test` (runs all tests)
|
||||
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
||||
@@ -24,4 +24,4 @@
|
||||
- **Validation**: All inputs validated with Zod schemas
|
||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||
- **Storage**: Use `Storage` namespace for persistence
|
||||
- **API Client**: Go TUI communicates with TypeScript server via stainless SDK. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, ask the user to generate a new client SDK to proceed with client-side changes.
|
||||
- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.224",
|
||||
"version": "1.1.2",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
@@ -25,6 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.4",
|
||||
"@octokit/webhooks-types": "7.6.1",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
@@ -38,12 +40,11 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
"zod-to-json-schema": "3.24.5",
|
||||
"@opencode-ai/script": "workspace:*"
|
||||
"zod-to-json-schema": "3.24.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
@@ -80,11 +81,12 @@
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||
"@opentui/core": "0.1.67",
|
||||
"@opentui/solid": "0.1.67",
|
||||
"@opentui/core": "0.1.68",
|
||||
"@opentui/solid": "0.1.68",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/scheduled": "1.5.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"ai": "catalog:",
|
||||
|
||||
@@ -22,17 +22,17 @@ if (!Script.preview) {
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
"pkgdesc='The AI coding agent built for the terminal.'",
|
||||
"url='https://github.com/sst/opencode'",
|
||||
"url='https://github.com/anomalyco/opencode'",
|
||||
"arch=('aarch64' 'x86_64')",
|
||||
"license=('MIT')",
|
||||
"provides=('opencode')",
|
||||
"conflicts=('opencode')",
|
||||
"depends=('ripgrep')",
|
||||
"",
|
||||
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
|
||||
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
|
||||
`sha256sums_aarch64=('${arm64Sha}')`,
|
||||
|
||||
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
|
||||
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
|
||||
`sha256sums_x86_64=('${x64Sha}')`,
|
||||
"",
|
||||
"package() {",
|
||||
@@ -52,7 +52,7 @@ if (!Script.preview) {
|
||||
"options=('!debug' '!strip')",
|
||||
"pkgrel=1",
|
||||
"pkgdesc='The AI coding agent built for the terminal.'",
|
||||
"url='https://github.com/sst/opencode'",
|
||||
"url='https://github.com/anomalyco/opencode'",
|
||||
"arch=('aarch64' 'x86_64')",
|
||||
"license=('MIT')",
|
||||
"provides=('opencode')",
|
||||
@@ -60,7 +60,7 @@ if (!Script.preview) {
|
||||
"depends=('ripgrep')",
|
||||
"makedepends=('git' 'bun' 'go')",
|
||||
"",
|
||||
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
|
||||
`source=("opencode-\${pkgver}.tar.gz::https://github.com/anomalyco/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
|
||||
`sha256sums=('SKIP')`,
|
||||
"",
|
||||
"build() {",
|
||||
@@ -133,14 +133,14 @@ if (!Script.preview) {
|
||||
"# This file was generated by GoReleaser. DO NOT EDIT.",
|
||||
"class Opencode < Formula",
|
||||
` desc "The AI coding agent built for the terminal."`,
|
||||
` homepage "https://github.com/sst/opencode"`,
|
||||
` homepage "https://github.com/anomalyco/opencode"`,
|
||||
` version "${Script.version.split("-")[0]}"`,
|
||||
"",
|
||||
` depends_on "ripgrep"`,
|
||||
"",
|
||||
" on_macos do",
|
||||
" if Hardware::CPU.intel?",
|
||||
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
|
||||
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
|
||||
` sha256 "${macX64Sha}"`,
|
||||
"",
|
||||
" def install",
|
||||
@@ -148,7 +148,7 @@ if (!Script.preview) {
|
||||
" end",
|
||||
" end",
|
||||
" if Hardware::CPU.arm?",
|
||||
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
|
||||
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
|
||||
` sha256 "${macArm64Sha}"`,
|
||||
"",
|
||||
" def install",
|
||||
@@ -159,14 +159,14 @@ if (!Script.preview) {
|
||||
"",
|
||||
" on_linux do",
|
||||
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
|
||||
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
|
||||
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
|
||||
` sha256 "${x64Sha}"`,
|
||||
" def install",
|
||||
' bin.install "opencode"',
|
||||
" end",
|
||||
" end",
|
||||
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
|
||||
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
|
||||
` url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
|
||||
` sha256 "${arm64Sha}"`,
|
||||
" def install",
|
||||
' bin.install "opencode"',
|
||||
|
||||
@@ -62,7 +62,7 @@ if (!Script.preview) {
|
||||
}
|
||||
}
|
||||
|
||||
const image = "ghcr.io/sst/opencode"
|
||||
const image = "ghcr.io/anomalyco/opencode"
|
||||
const platforms = "linux/amd64,linux/arm64"
|
||||
const tags = [`${image}:${Script.version}`, `${image}:latest`]
|
||||
const tagFlags = tags.flatMap((t) => ["-t", t])
|
||||
|
||||
@@ -34,13 +34,9 @@ import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2
|
||||
export namespace ACP {
|
||||
const log = Log.create({ service: "acp-agent" })
|
||||
|
||||
export async function init({ sdk }: { sdk: OpencodeClient }) {
|
||||
const model = await defaultModel({ sdk })
|
||||
export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
|
||||
return {
|
||||
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
|
||||
if (!fullConfig.defaultModel) {
|
||||
fullConfig.defaultModel = model
|
||||
}
|
||||
return new Agent(connection, fullConfig)
|
||||
},
|
||||
}
|
||||
@@ -988,8 +984,10 @@ export namespace ACP {
|
||||
const configured = config.defaultModel
|
||||
if (configured) return configured
|
||||
|
||||
const model = await sdk.config
|
||||
.get({ directory: cwd }, { throwOnError: true })
|
||||
const directory = cwd ?? process.cwd()
|
||||
|
||||
const specified = await sdk.config
|
||||
.get({ directory }, { throwOnError: true })
|
||||
.then((resp) => {
|
||||
const cfg = resp.data
|
||||
if (!cfg || !cfg.model) return undefined
|
||||
@@ -1004,7 +1002,47 @@ export namespace ACP {
|
||||
return undefined
|
||||
})
|
||||
|
||||
return model ?? { providerID: "opencode", modelID: "big-pickle" }
|
||||
const providers = await sdk.config
|
||||
.providers({ directory }, { throwOnError: true })
|
||||
.then((x) => x.data?.providers ?? [])
|
||||
.catch((error) => {
|
||||
log.error("failed to list providers for default model", { error })
|
||||
return []
|
||||
})
|
||||
|
||||
if (specified && providers.length) {
|
||||
const provider = providers.find((p) => p.id === specified.providerID)
|
||||
if (provider && provider.models[specified.modelID]) return specified
|
||||
}
|
||||
|
||||
if (specified && !providers.length) return specified
|
||||
|
||||
const opencodeProvider = providers.find((p) => p.id === "opencode")
|
||||
if (opencodeProvider) {
|
||||
if (opencodeProvider.models["big-pickle"]) {
|
||||
return { providerID: "opencode", modelID: "big-pickle" }
|
||||
}
|
||||
const [best] = Provider.sort(Object.values(opencodeProvider.models))
|
||||
if (best) {
|
||||
return {
|
||||
providerID: best.providerID,
|
||||
modelID: best.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const models = providers.flatMap((p) => Object.values(p.models))
|
||||
const [best] = Provider.sort(models)
|
||||
if (best) {
|
||||
return {
|
||||
providerID: best.providerID,
|
||||
modelID: best.id,
|
||||
}
|
||||
}
|
||||
|
||||
if (specified) return specified
|
||||
|
||||
return { providerID: "opencode", modelID: "big-pickle" }
|
||||
}
|
||||
|
||||
function parseUri(
|
||||
|
||||
@@ -47,6 +47,13 @@ export namespace Agent {
|
||||
"*": "allow",
|
||||
doom_loop: "ask",
|
||||
external_directory: "ask",
|
||||
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
||||
read: {
|
||||
"*": "allow",
|
||||
"*.env": "deny",
|
||||
"*.env.*": "deny",
|
||||
"*.env.example": "allow",
|
||||
},
|
||||
})
|
||||
const user = PermissionNext.fromConfig(cfg.permission ?? {})
|
||||
|
||||
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest${envStr}
|
||||
uses: anomalyco/opencode/github@latest${envStr}
|
||||
with:
|
||||
model: ${provider}/${model}`,
|
||||
)
|
||||
@@ -994,12 +994,16 @@ export const GithubRunCommand = cmd({
|
||||
|
||||
console.log("Configuring git...")
|
||||
const config = "http.https://github.com/.extraheader"
|
||||
const ret = await $`git config --local --get ${config}`
|
||||
gitConfig = ret.stdout.toString().trim()
|
||||
// actions/checkout@v6 no longer stores credentials in .git/config,
|
||||
// so this may not exist - use nothrow() to handle gracefully
|
||||
const ret = await $`git config --local --get ${config}`.nothrow()
|
||||
if (ret.exitCode === 0) {
|
||||
gitConfig = ret.stdout.toString().trim()
|
||||
await $`git config --local --unset-all ${config}`
|
||||
}
|
||||
|
||||
const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
|
||||
|
||||
await $`git config --local --unset-all ${config}`
|
||||
await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
|
||||
await $`git config --global user.name "${AGENT_USERNAME}"`
|
||||
await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
|
||||
@@ -1233,17 +1237,55 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
||||
|
||||
async function createPR(base: string, branch: string, title: string, body: string) {
|
||||
console.log("Creating pull request...")
|
||||
const pr = await octoRest.rest.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
head: branch,
|
||||
base,
|
||||
title,
|
||||
body,
|
||||
})
|
||||
|
||||
// Check if an open PR already exists for this head→base combination
|
||||
// This handles the case where the agent created a PR via gh pr create during its run
|
||||
try {
|
||||
const existing = await withRetry(() =>
|
||||
octoRest.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
head: `${owner}:${branch}`,
|
||||
base,
|
||||
state: "open",
|
||||
}),
|
||||
)
|
||||
|
||||
if (existing.data.length > 0) {
|
||||
console.log(`PR #${existing.data[0].number} already exists for branch ${branch}`)
|
||||
return existing.data[0].number
|
||||
}
|
||||
} catch (e) {
|
||||
// If the check fails, proceed to create - we'll get a clear error if a PR already exists
|
||||
console.log(`Failed to check for existing PR: ${e}`)
|
||||
}
|
||||
|
||||
const pr = await withRetry(() =>
|
||||
octoRest.rest.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
head: branch,
|
||||
base,
|
||||
title,
|
||||
body,
|
||||
}),
|
||||
)
|
||||
return pr.data.number
|
||||
}
|
||||
|
||||
async function withRetry<T>(fn: () => Promise<T>, retries = 1, delayMs = 5000): Promise<T> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (e) {
|
||||
if (retries > 0) {
|
||||
console.log(`Retrying after ${delayMs}ms...`)
|
||||
await Bun.sleep(delayMs)
|
||||
return withRetry(fn, retries - 1, delayMs)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function footer(opts?: { image?: boolean }) {
|
||||
const image = (() => {
|
||||
if (!shareId) return ""
|
||||
|
||||
@@ -36,6 +36,18 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
|
||||
}
|
||||
}
|
||||
|
||||
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
|
||||
|
||||
type McpConfigured = Config.Mcp
|
||||
function isMcpConfigured(config: McpEntry): config is McpConfigured {
|
||||
return typeof config === "object" && config !== null && "type" in config
|
||||
}
|
||||
|
||||
type McpRemote = Extract<McpConfigured, { type: "remote" }>
|
||||
function isMcpRemote(config: McpEntry): config is McpRemote {
|
||||
return isMcpConfigured(config) && config.type === "remote"
|
||||
}
|
||||
|
||||
export const McpCommand = cmd({
|
||||
command: "mcp",
|
||||
builder: (yargs) =>
|
||||
@@ -64,15 +76,19 @@ export const McpListCommand = cmd({
|
||||
const mcpServers = config.mcp ?? {}
|
||||
const statuses = await MCP.status()
|
||||
|
||||
if (Object.keys(mcpServers).length === 0) {
|
||||
const servers = Object.entries(mcpServers).filter((entry): entry is [string, McpConfigured] =>
|
||||
isMcpConfigured(entry[1]),
|
||||
)
|
||||
|
||||
if (servers.length === 0) {
|
||||
prompts.log.warn("No MCP servers configured")
|
||||
prompts.outro("Add servers with: opencode mcp add")
|
||||
return
|
||||
}
|
||||
|
||||
for (const [name, serverConfig] of Object.entries(mcpServers)) {
|
||||
for (const [name, serverConfig] of servers) {
|
||||
const status = statuses[name]
|
||||
const hasOAuth = serverConfig.type === "remote" && !!serverConfig.oauth
|
||||
const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
|
||||
const hasStoredTokens = await MCP.hasStoredTokens(name)
|
||||
|
||||
let statusIcon: string
|
||||
@@ -110,7 +126,7 @@ export const McpListCommand = cmd({
|
||||
)
|
||||
}
|
||||
|
||||
prompts.outro(`${Object.keys(mcpServers).length} server(s)`)
|
||||
prompts.outro(`${servers.length} server(s)`)
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -138,7 +154,7 @@ export const McpAuthCommand = cmd({
|
||||
|
||||
// Get OAuth-capable servers (remote servers with oauth not explicitly disabled)
|
||||
const oauthServers = Object.entries(mcpServers).filter(
|
||||
([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
|
||||
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
||||
)
|
||||
|
||||
if (oauthServers.length === 0) {
|
||||
@@ -163,7 +179,7 @@ export const McpAuthCommand = cmd({
|
||||
const authStatus = await MCP.getAuthStatus(name)
|
||||
const icon = getAuthStatusIcon(authStatus)
|
||||
const statusText = getAuthStatusText(authStatus)
|
||||
const url = cfg.type === "remote" ? cfg.url : ""
|
||||
const url = cfg.url
|
||||
return {
|
||||
label: `${icon} ${name} (${statusText})`,
|
||||
value: name,
|
||||
@@ -187,8 +203,8 @@ export const McpAuthCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
if (serverConfig.type !== "remote" || serverConfig.oauth === false) {
|
||||
prompts.log.error(`MCP server ${serverName} does not support OAuth (oauth is disabled)`)
|
||||
if (!isMcpRemote(serverConfig) || serverConfig.oauth === false) {
|
||||
prompts.log.error(`MCP server ${serverName} is not an OAuth-capable remote server`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
@@ -263,7 +279,7 @@ export const McpAuthListCommand = cmd({
|
||||
|
||||
// Get OAuth-capable servers
|
||||
const oauthServers = Object.entries(mcpServers).filter(
|
||||
([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
|
||||
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
||||
)
|
||||
|
||||
if (oauthServers.length === 0) {
|
||||
@@ -276,7 +292,7 @@ export const McpAuthListCommand = cmd({
|
||||
const authStatus = await MCP.getAuthStatus(name)
|
||||
const icon = getAuthStatusIcon(authStatus)
|
||||
const statusText = getAuthStatusText(authStatus)
|
||||
const url = serverConfig.type === "remote" ? serverConfig.url : ""
|
||||
const url = serverConfig.url
|
||||
|
||||
prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`)
|
||||
}
|
||||
@@ -506,7 +522,7 @@ export const McpDebugCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
if (serverConfig.type !== "remote") {
|
||||
if (!isMcpRemote(serverConfig)) {
|
||||
prompts.log.error(`MCP server ${serverName} is not a remote server`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
|
||||
@@ -87,6 +87,10 @@ export const RunCommand = cmd({
|
||||
type: "number",
|
||||
describe: "port for the local server (defaults to random port if no value provided)",
|
||||
})
|
||||
.option("variant", {
|
||||
type: "string",
|
||||
describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
let message = [...args.message, ...(args["--"] || [])]
|
||||
@@ -254,6 +258,7 @@ export const RunCommand = cmd({
|
||||
model: args.model,
|
||||
command: args.command,
|
||||
arguments: message,
|
||||
variant: args.variant,
|
||||
})
|
||||
} else {
|
||||
const modelParam = args.model ? Provider.parseModel(args.model) : undefined
|
||||
@@ -261,6 +266,7 @@ export const RunCommand = cmd({
|
||||
sessionID,
|
||||
agent: resolvedAgent,
|
||||
model: modelParam,
|
||||
variant: args.variant,
|
||||
parts: [...fileParts, { type: "text", text: message }],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
})
|
||||
}
|
||||
|
||||
export function tui(input: { url: string; args: Args; onExit?: () => Promise<void> }) {
|
||||
export function tui(input: { url: string; args: Args; directory?: string; onExit?: () => Promise<void> }) {
|
||||
// promise to prevent immediate exit
|
||||
return new Promise<void>(async (resolve) => {
|
||||
const mode = await getTerminalBackgroundColor()
|
||||
@@ -116,7 +116,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
|
||||
<KVProvider>
|
||||
<ToastProvider>
|
||||
<RouteProvider>
|
||||
<SDKProvider url={input.url}>
|
||||
<SDKProvider url={input.url} directory={input.directory}>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
@@ -412,6 +412,7 @@ function App() {
|
||||
{
|
||||
title: "Switch theme",
|
||||
value: "theme.switch",
|
||||
keybind: "theme_list",
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogThemeList />)
|
||||
},
|
||||
@@ -549,6 +550,13 @@ function App() {
|
||||
})
|
||||
})
|
||||
|
||||
sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
|
||||
route.navigate({
|
||||
type: "session",
|
||||
sessionID: evt.properties.sessionID,
|
||||
})
|
||||
})
|
||||
|
||||
sdk.event.on(SessionApi.Event.Deleted.type, (evt) => {
|
||||
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
|
||||
route.navigate({ type: "home" })
|
||||
@@ -648,7 +656,7 @@ function ErrorComponent(props: {
|
||||
})
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
|
||||
const issueURL = new URL("https://github.com/anomalyco/opencode/issues/new?template=bug-report.yml")
|
||||
|
||||
// Choose safe fallback colors per mode since theme context may not be available
|
||||
const isLight = props.mode === "light"
|
||||
|
||||
@@ -22,9 +22,11 @@ export const AttachCommand = cmd({
|
||||
}),
|
||||
handler: async (args) => {
|
||||
if (args.dir) process.chdir(args.dir)
|
||||
const directory = process.cwd()
|
||||
await tui({
|
||||
url: args.url,
|
||||
args: { sessionID: args.session },
|
||||
directory,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -17,7 +17,6 @@ const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
}
|
||||
|
||||
export function createDialogProviderOptions() {
|
||||
|
||||
@@ -2,13 +2,14 @@ import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
|
||||
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import { useKV } from "../context/kv"
|
||||
import { createDebouncedSignal } from "../util/signal"
|
||||
import "opentui-spinner/solid"
|
||||
|
||||
export function DialogSessionList() {
|
||||
@@ -20,6 +21,13 @@ export function DialogSessionList() {
|
||||
const kv = useKV()
|
||||
|
||||
const [toDelete, setToDelete] = createSignal<string>()
|
||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
||||
|
||||
const [searchResults] = createResource(search, async (query) => {
|
||||
if (!query) return undefined
|
||||
const result = await sdk.client.session.list({ search: query, limit: 30 })
|
||||
return result.data ?? []
|
||||
})
|
||||
|
||||
const deleteKeybind = "ctrl+d"
|
||||
|
||||
@@ -27,9 +35,11 @@ export function DialogSessionList() {
|
||||
|
||||
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
|
||||
const sessions = createMemo(() => searchResults() ?? sync.data.session)
|
||||
|
||||
const options = createMemo(() => {
|
||||
const today = new Date().toDateString()
|
||||
return sync.data.session
|
||||
return sessions()
|
||||
.filter((x) => x.parentID === undefined)
|
||||
.toSorted((a, b) => b.time.updated - a.time.updated)
|
||||
.map((x) => {
|
||||
@@ -54,11 +64,6 @@ export function DialogSessionList() {
|
||||
) : undefined,
|
||||
}
|
||||
})
|
||||
.slice(0, 150)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
console.log("session count", sync.data.session.length)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -69,7 +74,9 @@ export function DialogSessionList() {
|
||||
<DialogSelect
|
||||
title="Sessions"
|
||||
options={options()}
|
||||
skipFilter={true}
|
||||
current={currentSessionID()}
|
||||
onFilter={setSearch}
|
||||
onMove={() => {
|
||||
setToDelete(undefined)
|
||||
}}
|
||||
|
||||
@@ -231,6 +231,40 @@ export function Autocomplete(props: {
|
||||
},
|
||||
)
|
||||
|
||||
const mcpResources = createMemo(() => {
|
||||
if (!store.visible || store.visible === "/") return []
|
||||
|
||||
const options: AutocompleteOption[] = []
|
||||
const width = props.anchor().width - 4
|
||||
|
||||
for (const res of Object.values(sync.data.mcp_resource)) {
|
||||
options.push({
|
||||
display: Locale.truncateMiddle(`${res.name} (${res.uri})`, width),
|
||||
description: res.description,
|
||||
onSelect: () => {
|
||||
insertPart(res.name, {
|
||||
type: "file",
|
||||
mime: res.mimeType ?? "text/plain",
|
||||
filename: res.name,
|
||||
url: res.uri,
|
||||
source: {
|
||||
type: "resource",
|
||||
text: {
|
||||
start: 0,
|
||||
end: 0,
|
||||
value: "",
|
||||
},
|
||||
clientName: res.client,
|
||||
uri: res.uri,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const agents = createMemo(() => {
|
||||
const agents = sync.data.agent
|
||||
return agents
|
||||
@@ -416,7 +450,7 @@ export function Autocomplete(props: {
|
||||
const commandsValue = commands()
|
||||
|
||||
const mixed: AutocompleteOption[] = (
|
||||
store.visible === "@" ? [...agentsValue, ...(filesValue || [])] : [...commandsValue]
|
||||
store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue]
|
||||
).filter((x) => x.disabled !== true)
|
||||
|
||||
const currentFilter = filter()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, type KeyBinding } from "@opentui/core"
|
||||
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core"
|
||||
import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js"
|
||||
import "opentui-spinner/solid"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
@@ -10,7 +10,6 @@ import { useSync } from "@tui/context/sync"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { usePromptHistory, type PromptInfo } from "./history"
|
||||
import { usePromptStash } from "./stash"
|
||||
import { DialogStash } from "../dialog-stash"
|
||||
@@ -30,6 +29,7 @@ import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
|
||||
import { DialogAlert } from "../../ui/dialog-alert"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv"
|
||||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
@@ -53,61 +53,6 @@ export type PromptRef = {
|
||||
|
||||
const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
|
||||
|
||||
const TEXTAREA_ACTIONS = [
|
||||
"submit",
|
||||
"newline",
|
||||
"move-left",
|
||||
"move-right",
|
||||
"move-up",
|
||||
"move-down",
|
||||
"select-left",
|
||||
"select-right",
|
||||
"select-up",
|
||||
"select-down",
|
||||
"line-home",
|
||||
"line-end",
|
||||
"select-line-home",
|
||||
"select-line-end",
|
||||
"visual-line-home",
|
||||
"visual-line-end",
|
||||
"select-visual-line-home",
|
||||
"select-visual-line-end",
|
||||
"buffer-home",
|
||||
"buffer-end",
|
||||
"select-buffer-home",
|
||||
"select-buffer-end",
|
||||
"delete-line",
|
||||
"delete-to-line-end",
|
||||
"delete-to-line-start",
|
||||
"backspace",
|
||||
"delete",
|
||||
"undo",
|
||||
"redo",
|
||||
"word-forward",
|
||||
"word-backward",
|
||||
"select-word-forward",
|
||||
"select-word-backward",
|
||||
"delete-word-forward",
|
||||
"delete-word-backward",
|
||||
] as const
|
||||
|
||||
function mapTextareaKeybindings(
|
||||
keybinds: Record<string, Keybind.Info[]>,
|
||||
action: (typeof TEXTAREA_ACTIONS)[number],
|
||||
): KeyBinding[] {
|
||||
const configKey = `input_${action.replace(/-/g, "_")}`
|
||||
const bindings = keybinds[configKey]
|
||||
if (!bindings) return []
|
||||
return bindings.map((binding) => ({
|
||||
name: binding.name,
|
||||
ctrl: binding.ctrl || undefined,
|
||||
meta: binding.meta || undefined,
|
||||
shift: binding.shift || undefined,
|
||||
super: binding.super || undefined,
|
||||
action,
|
||||
}))
|
||||
}
|
||||
|
||||
export function Prompt(props: PromptProps) {
|
||||
let input: TextareaRenderable
|
||||
let anchor: BoxRenderable
|
||||
@@ -139,15 +84,7 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const textareaKeybindings = createMemo(() => {
|
||||
const keybinds = keybind.all
|
||||
|
||||
return [
|
||||
{ name: "return", action: "submit" },
|
||||
{ name: "return", meta: true, action: "newline" },
|
||||
...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)),
|
||||
] satisfies KeyBinding[]
|
||||
})
|
||||
const textareaKeybindings = useTextareaKeybindings()
|
||||
|
||||
const fileStyleId = syntax().getStyleId("extmark.file")!
|
||||
const agentStyleId = syntax().getStyleId("extmark.agent")!
|
||||
@@ -812,7 +749,7 @@ export function Prompt(props: PromptProps) {
|
||||
>
|
||||
<box
|
||||
paddingLeft={2}
|
||||
paddingRight={1}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
flexShrink={0}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import type { KeyBinding } from "@opentui/core"
|
||||
import { useKeybind } from "../context/keybind"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
|
||||
const TEXTAREA_ACTIONS = [
|
||||
"submit",
|
||||
"newline",
|
||||
"move-left",
|
||||
"move-right",
|
||||
"move-up",
|
||||
"move-down",
|
||||
"select-left",
|
||||
"select-right",
|
||||
"select-up",
|
||||
"select-down",
|
||||
"line-home",
|
||||
"line-end",
|
||||
"select-line-home",
|
||||
"select-line-end",
|
||||
"visual-line-home",
|
||||
"visual-line-end",
|
||||
"select-visual-line-home",
|
||||
"select-visual-line-end",
|
||||
"buffer-home",
|
||||
"buffer-end",
|
||||
"select-buffer-home",
|
||||
"select-buffer-end",
|
||||
"delete-line",
|
||||
"delete-to-line-end",
|
||||
"delete-to-line-start",
|
||||
"backspace",
|
||||
"delete",
|
||||
"undo",
|
||||
"redo",
|
||||
"word-forward",
|
||||
"word-backward",
|
||||
"select-word-forward",
|
||||
"select-word-backward",
|
||||
"delete-word-forward",
|
||||
"delete-word-backward",
|
||||
] as const
|
||||
|
||||
function mapTextareaKeybindings(
|
||||
keybinds: Record<string, Keybind.Info[]>,
|
||||
action: (typeof TEXTAREA_ACTIONS)[number],
|
||||
): KeyBinding[] {
|
||||
const configKey = `input_${action.replace(/-/g, "_")}`
|
||||
const bindings = keybinds[configKey]
|
||||
if (!bindings) return []
|
||||
return bindings.map((binding) => ({
|
||||
name: binding.name,
|
||||
ctrl: binding.ctrl || undefined,
|
||||
meta: binding.meta || undefined,
|
||||
shift: binding.shift || undefined,
|
||||
super: binding.super || undefined,
|
||||
action,
|
||||
}))
|
||||
}
|
||||
|
||||
export function useTextareaKeybindings() {
|
||||
const keybind = useKeybind()
|
||||
|
||||
return createMemo(() => {
|
||||
const keybinds = keybind.all
|
||||
|
||||
return [
|
||||
{ name: "return", action: "submit" },
|
||||
{ name: "return", meta: true, action: "newline" },
|
||||
...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)),
|
||||
] satisfies KeyBinding[]
|
||||
})
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export const TIPS = [
|
||||
"Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info.",
|
||||
"Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling.",
|
||||
"Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight}).",
|
||||
"Run {highlight}docker run -it --rm ghcr.io/sst/opencode{/highlight} for containerized use.",
|
||||
"Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use.",
|
||||
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models.",
|
||||
"Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing.",
|
||||
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs.",
|
||||
|
||||
@@ -5,11 +5,12 @@ import { batch, onCleanup, onMount } from "solid-js"
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
name: "SDK",
|
||||
init: (props: { url: string }) => {
|
||||
init: (props: { url: string; directory?: string }) => {
|
||||
const abort = new AbortController()
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: props.url,
|
||||
signal: abort.signal,
|
||||
directory: props.directory,
|
||||
})
|
||||
|
||||
const emitter = createGlobalEmitter<{
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
PermissionRequest,
|
||||
LspStatus,
|
||||
McpStatus,
|
||||
McpResource,
|
||||
FormatterStatus,
|
||||
SessionStatus,
|
||||
ProviderListResponse,
|
||||
@@ -62,6 +63,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
mcp: {
|
||||
[key: string]: McpStatus
|
||||
}
|
||||
mcp_resource: {
|
||||
[key: string]: McpResource
|
||||
}
|
||||
formatter: FormatterStatus[]
|
||||
vcs: VcsInfo | undefined
|
||||
path: Path
|
||||
@@ -87,6 +91,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
part: {},
|
||||
lsp: [],
|
||||
mcp: {},
|
||||
mcp_resource: {},
|
||||
formatter: [],
|
||||
vcs: undefined,
|
||||
path: { state: "", config: "", worktree: "", directory: "" },
|
||||
@@ -264,8 +269,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
|
||||
async function bootstrap() {
|
||||
console.log("bootstrapping")
|
||||
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||
const sessionListPromise = sdk.client.session
|
||||
.list()
|
||||
.list({ start: start })
|
||||
.then((x) => setStore("session", reconcile((x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))))
|
||||
|
||||
// blocking - include session.list when continuing a session
|
||||
@@ -295,6 +301,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
|
||||
sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
|
||||
sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))),
|
||||
sdk.client.session.status().then((x) => {
|
||||
setStore("session_status", reconcile(x.data!))
|
||||
|
||||
@@ -407,25 +407,45 @@ async function getCustomThemes() {
|
||||
function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
|
||||
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
|
||||
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
|
||||
const palette = colors.palette.filter((x) => x !== null).map((x) => RGBA.fromHex(x))
|
||||
const isDark = mode == "dark"
|
||||
|
||||
const col = (i: number) => {
|
||||
const value = colors.palette[i]
|
||||
if (value) return RGBA.fromHex(value)
|
||||
return ansiToRgba(i)
|
||||
}
|
||||
|
||||
const tint = (base: RGBA, overlay: RGBA, alpha: number) => {
|
||||
const r = base.r + (overlay.r - base.r) * alpha
|
||||
const g = base.g + (overlay.g - base.g) * alpha
|
||||
const b = base.b + (overlay.b - base.b) * alpha
|
||||
return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
|
||||
}
|
||||
|
||||
// Generate gray scale based on terminal background
|
||||
const grays = generateGrayScale(bg, isDark)
|
||||
const textMuted = generateMutedTextColor(bg, isDark)
|
||||
|
||||
// ANSI color references
|
||||
const ansiColors = {
|
||||
black: palette[0],
|
||||
red: palette[1],
|
||||
green: palette[2],
|
||||
yellow: palette[3],
|
||||
blue: palette[4],
|
||||
magenta: palette[5],
|
||||
cyan: palette[6],
|
||||
white: palette[7],
|
||||
black: col(0),
|
||||
red: col(1),
|
||||
green: col(2),
|
||||
yellow: col(3),
|
||||
blue: col(4),
|
||||
magenta: col(5),
|
||||
cyan: col(6),
|
||||
white: col(7),
|
||||
redBright: col(9),
|
||||
greenBright: col(10),
|
||||
}
|
||||
|
||||
const diffAlpha = isDark ? 0.22 : 0.14
|
||||
const diffAddedBg = tint(bg, ansiColors.green, diffAlpha)
|
||||
const diffRemovedBg = tint(bg, ansiColors.red, diffAlpha)
|
||||
const diffAddedLineNumberBg = tint(grays[3], ansiColors.green, diffAlpha)
|
||||
const diffRemovedLineNumberBg = tint(grays[3], ansiColors.red, diffAlpha)
|
||||
|
||||
return {
|
||||
theme: {
|
||||
// Primary colors using ANSI
|
||||
@@ -460,14 +480,14 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
||||
diffRemoved: ansiColors.red,
|
||||
diffContext: grays[7],
|
||||
diffHunkHeader: grays[7],
|
||||
diffHighlightAdded: ansiColors.green,
|
||||
diffHighlightRemoved: ansiColors.red,
|
||||
diffAddedBg: grays[2],
|
||||
diffRemovedBg: grays[2],
|
||||
diffHighlightAdded: ansiColors.greenBright,
|
||||
diffHighlightRemoved: ansiColors.redBright,
|
||||
diffAddedBg,
|
||||
diffRemovedBg,
|
||||
diffContextBg: grays[1],
|
||||
diffLineNumber: grays[6],
|
||||
diffAddedLineNumberBg: grays[3],
|
||||
diffRemovedLineNumberBg: grays[3],
|
||||
diffAddedLineNumberBg,
|
||||
diffRemovedLineNumberBg,
|
||||
|
||||
// Markdown colors
|
||||
markdownText: fg,
|
||||
|
||||
@@ -37,4 +37,10 @@ export const TuiEvent = {
|
||||
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
|
||||
}),
|
||||
),
|
||||
SessionSelect: BusEvent.define(
|
||||
"tui.session.select",
|
||||
z.object({
|
||||
sessionID: z.string().regex(/^ses/).describe("Session ID to navigate to"),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export function Home() {
|
||||
<span style={{ fg: theme.error }}>⊙ </span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<span style={{ fg: theme.success }}>⊙ </span>
|
||||
<span style={{ fg: connectedMcpCount() > 0 ? theme.success : theme.textMuted }}>⊙ </span>
|
||||
</Match>
|
||||
</Switch>
|
||||
{connectedMcpCount()} MCP
|
||||
|
||||
@@ -64,7 +64,7 @@ export function Footer() {
|
||||
</text>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
<span style={{ fg: theme.success }}>•</span> {lsp().length} LSP
|
||||
<span style={{ fg: lsp().length > 0 ? theme.success : theme.textMuted }}>•</span> {lsp().length} LSP
|
||||
</text>
|
||||
<Show when={mcp()}>
|
||||
<text fg={theme.text}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user