mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Compare commits
121 commits
github-v1.
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43e92b4932 | ||
|
|
83397ebde2 | ||
|
|
fde74a72bb | ||
|
|
10ee8e5b3d | ||
|
|
96d3f1fe7c | ||
|
|
1a2b656c4d | ||
|
|
161e9287a8 | ||
|
|
968543af39 | ||
|
|
5af35117db | ||
|
|
eab177f5e7 | ||
|
|
279dc04b3c | ||
|
|
cbc5903aa1 | ||
|
|
81c3c63895 | ||
|
|
b76bd4141d | ||
|
|
794fe8f381 | ||
|
|
a4eebf9f08 | ||
|
|
680a63e3de | ||
|
|
3a54ab68d1 | ||
|
|
44fd0eee64 | ||
|
|
ac371d2987 | ||
|
|
a7baa5ce18 | ||
|
|
b129f809b9 | ||
|
|
92c0ab51e2 | ||
|
|
b25418e68b | ||
|
|
046e351140 | ||
|
|
b9029afa22 | ||
|
|
b229aeec0b | ||
|
|
c9140c6bab | ||
|
|
38551bda38 | ||
|
|
cd16d31510 | ||
|
|
54ba1af5d6 | ||
| fe3144ce5b | |||
|
|
a1c0bae3af | ||
|
|
85f8655dfd | ||
|
|
9b6c9f64f7 | ||
|
|
1aae1c795d | ||
|
|
526c723e62 | ||
|
|
6011200128 | ||
|
|
740fcd243c | ||
|
|
e4d8a117c4 | ||
|
|
8c4a816cf6 | ||
|
|
5605fc3f38 | ||
|
|
009b096004 | ||
|
|
64f898601b | ||
|
|
224e5466c1 | ||
|
|
87b5b34280 | ||
|
|
855fd07d22 | ||
|
|
f9be2bab3a | ||
|
|
25f1643e8e | ||
|
|
e015bea462 | ||
|
|
7dc55ac3ca | ||
|
|
cd8ecf9722 | ||
|
|
eb021a5f92 | ||
|
|
7f5e30834f | ||
|
|
750a936ae1 | ||
|
|
8dfef670b3 | ||
|
|
1b1b73b5b3 | ||
|
|
6baee0791f | ||
|
|
291b65977c | ||
|
|
90f232d7f1 | ||
|
|
af214d35cb | ||
|
|
3f0afd7cf6 | ||
|
|
0545c5da2d | ||
|
|
4a32fa6f02 | ||
|
|
29c99ed4ab | ||
|
|
753abbe164 | ||
|
|
8e01f6cc13 | ||
|
|
33c0b125cb | ||
|
|
dab2e54df8 | ||
|
|
60db171b44 | ||
|
|
c6e9a5c800 | ||
|
|
2c16b9fa61 | ||
|
|
240ad31edd | ||
|
|
a97631f769 | ||
|
|
dbaac79039 | ||
|
|
a05915ddc8 | ||
|
|
eebbd73346 | ||
|
|
d4c981495a | ||
|
|
653c206688 | ||
|
|
580f46b589 | ||
|
|
986d12fd20 | ||
|
|
d04a72a4ad | ||
|
|
5fd873a35a | ||
|
|
a9fbd786b3 | ||
|
|
abde984b3e | ||
|
|
a95aa037a3 | ||
|
|
11a92b24c2 | ||
|
|
f9c10c62d8 | ||
|
|
6339f39871 | ||
|
|
68b09b30a1 | ||
|
|
92ade2a320 | ||
|
|
cb1a1fb26c | ||
|
|
af5ebabd03 | ||
|
|
fe2626a4ea | ||
|
|
45447e3336 | ||
|
|
7a3e82ec5d | ||
|
|
345f4801e8 | ||
|
|
ac4b8d62e3 | ||
|
|
236ce7a8c0 | ||
|
|
8bdc0c8f79 | ||
|
|
04650f01fe | ||
|
|
02d4594abf | ||
|
|
c1894b4e3d | ||
|
|
2062247e72 | ||
|
|
8785bec29c | ||
|
|
d4b7f75ce3 | ||
|
|
4f73d58031 | ||
|
|
b906f2de88 | ||
|
|
4035afe5c8 | ||
|
|
8fe0715928 | ||
|
|
cb8af962cd | ||
|
|
c333ffa38b | ||
|
|
3456f4ed80 | ||
|
|
2536e9f45b | ||
|
|
9188bc542c | ||
|
|
cbaba10994 | ||
|
|
85d3604309 | ||
|
|
507ba644cf | ||
|
|
3d6f62746a | ||
|
|
2f48c8c05f | ||
|
|
4828fd1eac |
378 changed files with 5192 additions and 2329 deletions
69
.github/workflows/docs-update.yml
vendored
Normal file
69
.github/workflows/docs-update.yml
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
name: Docs Update
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 */12 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-docs:
|
||||||
|
if: github.repository == 'sst/opencode'
|
||||||
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch full history to access commits
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- name: Get recent commits
|
||||||
|
id: commits
|
||||||
|
run: |
|
||||||
|
COMMITS=$(git log --since="4 hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "")
|
||||||
|
if [ -z "$COMMITS" ]; then
|
||||||
|
echo "No commits in the last 4 hours"
|
||||||
|
echo "has_commits=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_commits=true" >> $GITHUB_OUTPUT
|
||||||
|
{
|
||||||
|
echo "list<<EOF"
|
||||||
|
echo "$COMMITS"
|
||||||
|
echo "EOF"
|
||||||
|
} >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run opencode
|
||||||
|
if: steps.commits.outputs.has_commits == 'true'
|
||||||
|
uses: sst/opencode/github@latest
|
||||||
|
env:
|
||||||
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
with:
|
||||||
|
model: opencode/gpt-5.2
|
||||||
|
agent: docs
|
||||||
|
prompt: |
|
||||||
|
Review the following commits from the last 4 hours and identify any new features that may need documentation.
|
||||||
|
|
||||||
|
<recent_commits>
|
||||||
|
${{ steps.commits.outputs.list }}
|
||||||
|
</recent_commits>
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. For each commit that looks like a new feature or significant change:
|
||||||
|
- Read the changed files to understand what was added
|
||||||
|
- Check if the feature is already documented in packages/web/src/content/docs/*
|
||||||
|
2. If you find undocumented features:
|
||||||
|
- Update the relevant documentation files in packages/web/src/content/docs/*
|
||||||
|
- Follow the existing documentation style and structure
|
||||||
|
- Make sure to document the feature clearly with examples where appropriate
|
||||||
|
3. If all new features are already documented, report that no updates are needed
|
||||||
|
4. If you are creating a new documentation file be sure to update packages/web/astro.config.mjs too.
|
||||||
|
|
||||||
|
Focus on user-facing features and API changes. Skip internal refactors, bug fixes, and test updates unless they affect user-facing behavior.
|
||||||
|
Don't feel the need to document every little thing. It is perfectly okay to make 0 changes at all.
|
||||||
|
Try to keep documentation only for large features or changes that already have a good spot to be documented.
|
||||||
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
|
|
@ -79,6 +79,12 @@ jobs:
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||||
NPM_CONFIG_PROVENANCE: false
|
NPM_CONFIG_PROVENANCE: false
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: opencode-cli
|
||||||
|
path: packages/opencode/dist
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
release: ${{ steps.publish.outputs.release }}
|
release: ${{ steps.publish.outputs.release }}
|
||||||
tag: ${{ steps.publish.outputs.tag }}
|
tag: ${{ steps.publish.outputs.tag }}
|
||||||
|
|
@ -99,6 +105,8 @@ jobs:
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
- host: blacksmith-4vcpu-ubuntu-2404
|
- host: blacksmith-4vcpu-ubuntu-2404
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
|
- host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
runs-on: ${{ matrix.settings.host }}
|
runs-on: ${{ matrix.settings.host }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
@ -143,13 +151,12 @@ jobs:
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: packages/tauri/src-tauri
|
workspaces: packages/desktop/src-tauri
|
||||||
shared-key: ${{ matrix.settings.target }}
|
shared-key: ${{ matrix.settings.target }}
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
if: inputs.bump || inputs.version
|
|
||||||
run: |
|
run: |
|
||||||
cd packages/tauri
|
cd packages/desktop
|
||||||
bun ./scripts/prepare.ts
|
bun ./scripts/prepare.ts
|
||||||
env:
|
env:
|
||||||
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
|
||||||
|
|
@ -159,6 +166,7 @@ jobs:
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
RUST_TARGET: ${{ matrix.settings.target }}
|
RUST_TARGET: ${{ matrix.settings.target }}
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
|
||||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||||
- name: Install tauri-cli from portable appimage branch
|
- name: Install tauri-cli from portable appimage branch
|
||||||
|
|
@ -183,10 +191,10 @@ jobs:
|
||||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||||
with:
|
with:
|
||||||
projectPath: packages/tauri
|
projectPath: packages/desktop
|
||||||
uploadWorkflowArtifacts: true
|
uploadWorkflowArtifacts: true
|
||||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||||
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
|
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
|
||||||
updaterJsonPreferNsis: true
|
updaterJsonPreferNsis: true
|
||||||
releaseId: ${{ needs.publish.outputs.release }}
|
releaseId: ${{ needs.publish.outputs.release }}
|
||||||
tagName: ${{ needs.publish.outputs.tag }}
|
tagName: ${{ needs.publish.outputs.tag }}
|
||||||
|
|
|
||||||
3
.github/workflows/stats.yml
vendored
3
.github/workflows/stats.yml
vendored
|
|
@ -5,8 +5,11 @@ on:
|
||||||
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||||
workflow_dispatch: # Allow manual trigger
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stats:
|
stats:
|
||||||
|
if: github.repository == 'sst/opencode'
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
|
||||||
6
.opencode/skill/test-skill/SKILL.md
Normal file
6
.opencode/skill/test-skill/SKILL.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
name: test-skill
|
||||||
|
description: use this when asked to test skill
|
||||||
|
---
|
||||||
|
|
||||||
|
woah this is a test skill
|
||||||
29
AGENTS.md
29
AGENTS.md
|
|
@ -4,31 +4,4 @@
|
||||||
|
|
||||||
## Tool Calling
|
## Tool Calling
|
||||||
|
|
||||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
|
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||||
|
|
||||||
json
|
|
||||||
{
|
|
||||||
"recipient_name": "multi_tool_use.parallel",
|
|
||||||
"parameters": {
|
|
||||||
"tool_uses": [
|
|
||||||
{
|
|
||||||
"recipient_name": "functions.read",
|
|
||||||
"parameters": {
|
|
||||||
"filePath": "path/to/file.tsx"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient_name": "functions.read",
|
|
||||||
"parameters": {
|
|
||||||
"filePath": "path/to/file.ts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"recipient_name": "functions.read",
|
|
||||||
"parameters": {
|
|
||||||
"filePath": "path/to/file.md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
1
STATS.md
1
STATS.md
|
|
@ -177,3 +177,4 @@
|
||||||
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
|
||||||
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
|
||||||
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
|
||||||
|
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||||
|
|
|
||||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766125104,
|
"lastModified": 1766314097,
|
||||||
"narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
|
"narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
|
"rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,12 @@ new sst.cloudflare.x.Astro("Web", {
|
||||||
VITE_API_URL: api.url.apply((url) => url!),
|
VITE_API_URL: api.url.apply((url) => url!),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
new sst.cloudflare.StaticSite("App", {
|
||||||
|
domain: "app." + domain,
|
||||||
|
path: "packages/app",
|
||||||
|
build: {
|
||||||
|
command: "bun turbo build",
|
||||||
|
output: "./dist",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||||
////////////////
|
////////////////
|
||||||
|
|
||||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||||
|
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
||||||
|
|
||||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
@ -136,6 +137,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||||
path: "packages/console/app",
|
path: "packages/console/app",
|
||||||
link: [
|
link: [
|
||||||
bucket,
|
bucket,
|
||||||
|
bucketNew,
|
||||||
database,
|
database,
|
||||||
AUTH_API_URL,
|
AUTH_API_URL,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { domain } from "./stage"
|
||||||
|
|
||||||
new sst.cloudflare.StaticSite("Desktop", {
|
new sst.cloudflare.StaticSite("Desktop", {
|
||||||
domain: "desktop." + domain,
|
domain: "desktop." + domain,
|
||||||
path: "packages/desktop",
|
path: "packages/app",
|
||||||
build: {
|
build: {
|
||||||
command: "bun turbo build",
|
command: "bun turbo build",
|
||||||
output: "./dist",
|
output: "./dist",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"nodeModules": "sha256-cpXmqJQJeFj3eED/aOb4YLUdkZFV//7u4f0STBxzUhk="
|
"nodeModules": "sha256-QlQblkUq49DOdvNNMNAzHHAfHxR6cZNmJtyzc4rD168="
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
package.json
13
package.json
|
|
@ -31,7 +31,7 @@
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/diffs": "1.0.0-beta.3",
|
"@pierre/diffs": "1.0.2",
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"zod": "4.1.8",
|
"zod": "4.1.8",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
|
"shiki": "3.20.0",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"virtua": "0.42.3",
|
"virtua": "0.42.3",
|
||||||
|
|
@ -56,6 +57,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@actions/artifact": "5.0.1",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
|
|
@ -63,7 +65,15 @@
|
||||||
"turbo": "2.5.6"
|
"turbo": "2.5.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/cerebras": "1.0.33",
|
||||||
|
"@ai-sdk/cohere": "2.0.21",
|
||||||
|
"@ai-sdk/deepinfra": "1.0.30",
|
||||||
|
"@ai-sdk/gateway": "2.0.23",
|
||||||
|
"@ai-sdk/groq": "2.0.33",
|
||||||
|
"@ai-sdk/perplexity": "2.0.22",
|
||||||
|
"@ai-sdk/togetherai": "1.0.30",
|
||||||
"@aws-sdk/client-s3": "3.933.0",
|
"@aws-sdk/client-s3": "3.933.0",
|
||||||
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
|
|
@ -80,7 +90,6 @@
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
"sharp",
|
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"web-tree-sitter"
|
"web-tree-sitter"
|
||||||
|
|
|
||||||
1
packages/app/.gitignore
vendored
Normal file
1
packages/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
src/assets/theme.css
|
||||||
34
packages/app/README.md
Normal file
34
packages/app/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
||||||
|
|
||||||
|
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm run dev` or `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.<br>
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<meta property="og:image" content="/social-share.png" />
|
<meta property="og:image" content="/social-share.png" />
|
||||||
<meta property="twitter:image" content="/social-share.png" />
|
<meta property="twitter:image" content="/social-share.png" />
|
||||||
</head>
|
</head>
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
|
||||||
<script>
|
<script>
|
||||||
;(function () {
|
;(function () {
|
||||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||||
|
|
@ -23,6 +23,6 @@
|
||||||
</script>
|
</script>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<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-screen"></div>
|
||||||
<script src="/src/index.tsx" type="module"></script>
|
<script src="/src/entry.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
62
packages/app/package.json
Normal file
62
packages/app/package.json
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"name": "@opencode-ai/app",
|
||||||
|
"version": "1.0.191",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./vite": "./vite.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsgo -b",
|
||||||
|
"start": "vite",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@happy-dom/global-registrator": "20.0.11",
|
||||||
|
"@tailwindcss/vite": "catalog:",
|
||||||
|
"@tsconfig/bun": "1.0.9",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@types/luxon": "catalog:",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"vite": "catalog:",
|
||||||
|
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||||
|
"vite-plugin-solid": "catalog:"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kobalte/core": "catalog:",
|
||||||
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@opencode-ai/util": "workspace:*",
|
||||||
|
"@shikijs/transformers": "3.9.2",
|
||||||
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
|
"@solid-primitives/audio": "1.4.2",
|
||||||
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
"@solid-primitives/media": "2.3.3",
|
||||||
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
|
"@solid-primitives/scroll": "2.1.3",
|
||||||
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solid-primitives/websocket": "1.3.1",
|
||||||
|
"@solidjs/meta": "catalog:",
|
||||||
|
"@solidjs/router": "catalog:",
|
||||||
|
"@thisbeyond/solid-dnd": "0.7.5",
|
||||||
|
"diff": "catalog:",
|
||||||
|
"fuzzysort": "catalog:",
|
||||||
|
"ghostty-web": "0.3.0",
|
||||||
|
"luxon": "catalog:",
|
||||||
|
"marked": "16.2.0",
|
||||||
|
"marked-shiki": "1.2.1",
|
||||||
|
"remeda": "catalog:",
|
||||||
|
"shiki": "catalog:",
|
||||||
|
"solid-js": "catalog:",
|
||||||
|
"solid-list": "catalog:",
|
||||||
|
"tailwindcss": "catalog:",
|
||||||
|
"virtua": "catalog:",
|
||||||
|
"zod": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import Home from "@/pages/home"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
import Session from "@/pages/session"
|
import Session from "@/pages/session"
|
||||||
import { ErrorPage } from "./pages/error"
|
import { ErrorPage } from "./pages/error"
|
||||||
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
@ -28,14 +29,17 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
|
const url = iife(() => {
|
||||||
const port = window.__OPENCODE__?.port ?? import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
|
const param = new URLSearchParams(document.location.search).get("url")
|
||||||
|
if (param) return param
|
||||||
|
|
||||||
const url =
|
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
|
||||||
new URLSearchParams(document.location.search).get("url") ||
|
if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
|
||||||
(location.hostname.includes("opencode.ai") || location.hostname.includes("localhost")
|
if (import.meta.env.DEV)
|
||||||
? `http://${host}:${port}`
|
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||||
: "/")
|
|
||||||
|
return "http://localhost:4096"
|
||||||
|
})
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
|
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||||
|
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||||
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
import { usePlatform } from "@/context/platform"
|
|
||||||
import { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
||||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
||||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
|
||||||
import { Link } from "@/components/link"
|
import { Link } from "@/components/link"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
import { DialogSelectModel } from "./dialog-select-model"
|
import { DialogSelectModel } from "./dialog-select-model"
|
||||||
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
|
|
||||||
export function DialogConnectProvider(props: { provider: string }) {
|
export function DialogConnectProvider(props: { provider: string }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
@ -154,7 +154,9 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||||
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<List
|
<List
|
||||||
ref={(ref) => (listRef = ref)}
|
ref={(ref) => {
|
||||||
|
listRef = ref
|
||||||
|
}}
|
||||||
items={methods}
|
items={methods}
|
||||||
key={(m) => m?.label}
|
key={(m) => m?.label}
|
||||||
onSelect={async (method, index) => {
|
onSelect={async (method, index) => {
|
||||||
|
|
@ -163,7 +165,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="w-full flex items-center gap-x-4">
|
<div class="w-full flex items-center gap-x-2">
|
||||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||||
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -175,7 +177,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "pending"}>
|
<Match when={store.state === "pending"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
<div class="flex items-center gap-x-4">
|
<div class="flex items-center gap-x-2">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<span>Authorization in progress...</span>
|
<span>Authorization in progress...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -183,7 +185,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.state === "error"}>
|
<Match when={store.state === "error"}>
|
||||||
<div class="text-14-regular text-text-base">
|
<div class="text-14-regular text-text-base">
|
||||||
<div class="flex items-center gap-x-4">
|
<div class="flex items-center gap-x-2">
|
||||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||||
<span>Authorization failed: {store.error}</span>
|
<span>Authorization failed: {store.error}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { Component } from "solid-js"
|
|
||||||
import { useLocal } from "@/context/local"
|
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { Switch } from "@opencode-ai/ui/switch"
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
import type { Component } from "solid-js"
|
||||||
|
import { useLocal } from "@/context/local"
|
||||||
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
|
|
||||||
export const DialogManageModels: Component = () => {
|
export const DialogManageModels: Component = () => {
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
return (
|
return (
|
||||||
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
|
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
|
||||||
<List
|
<List
|
||||||
class="px-2.5"
|
|
||||||
search={{ placeholder: "Search models", autofocus: true }}
|
search={{ placeholder: "Search models", autofocus: true }}
|
||||||
emptyMessage="No model results"
|
emptyMessage="No model results"
|
||||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||||
|
|
@ -27,16 +26,24 @@ export const DialogManageModels: Component = () => {
|
||||||
}}
|
}}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (!x) return
|
if (!x) return
|
||||||
const visible = local.model.visible({ modelID: x.id, providerID: x.provider.id })
|
const visible = local.model.visible({
|
||||||
|
modelID: x.id,
|
||||||
|
providerID: x.provider.id,
|
||||||
|
})
|
||||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
|
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="w-full flex items-center justify-between gap-x-2.5">
|
<div class="w-full flex items-center justify-between gap-x-3">
|
||||||
<span>{i.name}</span>
|
<span>{i.name}</span>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
|
checked={
|
||||||
|
!!local.model.visible({
|
||||||
|
modelID: i.id,
|
||||||
|
providerID: i.provider.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
||||||
}}
|
}}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useLocal } from "@/context/local"
|
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
|
||||||
import { List } from "@opencode-ai/ui/list"
|
|
||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
|
||||||
import { useLayout } from "@/context/layout"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
|
import { List } from "@opencode-ai/ui/list"
|
||||||
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useLocal } from "@/context/local"
|
||||||
|
|
||||||
export function DialogSelectFile() {
|
export function DialogSelectFile() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
|
|
@ -18,7 +18,6 @@ export function DialogSelectFile() {
|
||||||
return (
|
return (
|
||||||
<Dialog title="Select file">
|
<Dialog title="Select file">
|
||||||
<List
|
<List
|
||||||
class="px-2.5"
|
|
||||||
search={{ placeholder: "Search files", autofocus: true }}
|
search={{ placeholder: "Search files", autofocus: true }}
|
||||||
emptyMessage="No files found"
|
emptyMessage="No files found"
|
||||||
items={local.file.searchFiles}
|
items={local.file.searchFiles}
|
||||||
|
|
@ -32,7 +31,7 @@ export function DialogSelectFile() {
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="w-full flex items-center justify-between rounded-md">
|
<div class="w-full flex items-center justify-between rounded-md">
|
||||||
<div class="flex items-center gap-x-2 grow min-w-0">
|
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||||
<div class="flex items-center text-14-regular">
|
<div class="flex items-center text-14-regular">
|
||||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { Component, onCleanup, onMount, Show } from "solid-js"
|
|
||||||
import { useLocal } from "@/context/local"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
||||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Tag } from "@opencode-ai/ui/tag"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||||
|
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
import { Tag } from "@opencode-ai/ui/tag"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { type Component, onCleanup, onMount, Show } from "solid-js"
|
||||||
|
import { useLocal } from "@/context/local"
|
||||||
|
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
|
|
||||||
export const DialogSelectModelUnpaid: Component = () => {
|
export const DialogSelectModelUnpaid: Component = () => {
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
|
|
@ -64,7 +64,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||||
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
|
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<List
|
<List
|
||||||
class="w-full"
|
class="w-full px-0"
|
||||||
key={(x) => x?.id}
|
key={(x) => x?.id}
|
||||||
items={providers.popular}
|
items={providers.popular}
|
||||||
activeIcon="plus-small"
|
activeIcon="plus-small"
|
||||||
|
|
@ -79,17 +79,8 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="w-full flex items-center gap-x-4">
|
<div class="w-full flex items-center gap-x-3">
|
||||||
<ProviderIcon
|
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||||
data-slot="list-item-extra-icon"
|
|
||||||
id={i.id as IconName}
|
|
||||||
// TODO: clean this up after we update icon in models.dev
|
|
||||||
classList={{
|
|
||||||
"text-icon-weak-base": true,
|
|
||||||
"size-4 mx-0.5": i.id === "opencode",
|
|
||||||
"size-5": i.id !== "opencode",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span>{i.name}</span>
|
<span>{i.name}</span>
|
||||||
<Show when={i.id === "opencode"}>
|
<Show when={i.id === "opencode"}>
|
||||||
<Tag>Recommended</Tag>
|
<Tag>Recommended</Tag>
|
||||||
|
|
@ -35,7 +35,6 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<List
|
<List
|
||||||
class="px-2.5"
|
|
||||||
search={{ placeholder: "Search models", autofocus: true }}
|
search={{ placeholder: "Search models", autofocus: true }}
|
||||||
emptyMessage="No model results"
|
emptyMessage="No model results"
|
||||||
key={(x) => `${x.provider.id}:${x.id}`}
|
key={(x) => `${x.provider.id}:${x.id}`}
|
||||||
|
|
@ -61,7 +60,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="w-full flex items-center gap-x-2.5">
|
<div class="w-full flex items-center gap-x-3">
|
||||||
<span>{i.name}</span>
|
<span>{i.name}</span>
|
||||||
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
||||||
<Tag>Free</Tag>
|
<Tag>Free</Tag>
|
||||||
|
|
@ -15,7 +15,6 @@ export const DialogSelectProvider: Component = () => {
|
||||||
return (
|
return (
|
||||||
<Dialog title="Connect provider">
|
<Dialog title="Connect provider">
|
||||||
<List
|
<List
|
||||||
class="px-2.5"
|
|
||||||
search={{ placeholder: "Search providers", autofocus: true }}
|
search={{ placeholder: "Search providers", autofocus: true }}
|
||||||
activeIcon="plus-small"
|
activeIcon="plus-small"
|
||||||
key={(x) => x?.id}
|
key={(x) => x?.id}
|
||||||
|
|
@ -38,17 +37,8 @@ export const DialogSelectProvider: Component = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="px-1.25 w-full flex items-center gap-x-4">
|
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||||
<ProviderIcon
|
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||||
data-slot="list-item-extra-icon"
|
|
||||||
id={i.id as IconName}
|
|
||||||
// TODO: clean this up after we update icon in models.dev
|
|
||||||
classList={{
|
|
||||||
"text-icon-weak-base": true,
|
|
||||||
"size-4 mx-0.5": i.id === "opencode",
|
|
||||||
"size-5": i.id !== "opencode",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span>{i.name}</span>
|
<span>{i.name}</span>
|
||||||
<Show when={i.id === "opencode"}>
|
<Show when={i.id === "opencode"}>
|
||||||
<Tag>Recommended</Tag>
|
<Tag>Recommended</Tag>
|
||||||
|
|
@ -20,6 +20,7 @@ import { iife } from "@opencode-ai/util/iife"
|
||||||
export function Header(props: {
|
export function Header(props: {
|
||||||
navigateToProject: (directory: string) => void
|
navigateToProject: (directory: string) => void
|
||||||
navigateToSession: (session: Session | undefined) => void
|
navigateToSession: (session: Session | undefined) => void
|
||||||
|
onMobileMenuToggle?: () => void
|
||||||
}) {
|
}) {
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
@ -29,11 +30,19 @@ export function Header(props: {
|
||||||
|
|
||||||
return (
|
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" data-tauri-drag-region>
|
||||||
|
<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"
|
||||||
|
onClick={props.onMobileMenuToggle}
|
||||||
|
>
|
||||||
|
<Icon name="menu" size="small" />
|
||||||
|
</button>
|
||||||
<A
|
<A
|
||||||
href="/"
|
href="/"
|
||||||
classList={{
|
classList={{
|
||||||
|
"hidden xl:flex": true,
|
||||||
"w-12 shrink-0 px-4 py-3.5": true,
|
"w-12 shrink-0 px-4 py-3.5": true,
|
||||||
"flex items-center justify-start self-stretch": true,
|
"items-center justify-start self-stretch": true,
|
||||||
"border-r border-border-weak-base": true,
|
"border-r border-border-weak-base": true,
|
||||||
}}
|
}}
|
||||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||||
|
|
@ -46,30 +55,32 @@ export function Header(props: {
|
||||||
{(directory) => {
|
{(directory) => {
|
||||||
const currentDirectory = createMemo(() => base64Decode(directory()))
|
const currentDirectory = createMemo(() => base64Decode(directory()))
|
||||||
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
||||||
const sessions = createMemo(() => store().session ?? [])
|
const sessions = createMemo(() => (store().session ?? []).filter((s) => !s.parentID))
|
||||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||||
const shareEnabled = createMemo(() => store().config.share !== "disabled")
|
const shareEnabled = createMemo(() => store().config.share !== "disabled")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<Select
|
<div class="hidden xl:flex items-center gap-2">
|
||||||
options={layout.projects.list().map((project) => project.worktree)}
|
<Select
|
||||||
current={currentDirectory()}
|
options={layout.projects.list().map((project) => project.worktree)}
|
||||||
label={(x) => getFilename(x)}
|
current={currentDirectory()}
|
||||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
label={(x) => getFilename(x)}
|
||||||
class="text-14-regular text-text-base"
|
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||||
variant="ghost"
|
class="text-14-regular text-text-base"
|
||||||
>
|
variant="ghost"
|
||||||
{/* @ts-ignore */}
|
>
|
||||||
{(i) => (
|
{/* @ts-ignore */}
|
||||||
<div class="flex items-center gap-2">
|
{(i) => (
|
||||||
<Icon name="folder" size="small" />
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-text-strong">{getFilename(i)}</div>
|
<Icon name="folder" size="small" />
|
||||||
</div>
|
<div class="text-text-strong">{getFilename(i)}</div>
|
||||||
)}
|
</div>
|
||||||
</Select>
|
)}
|
||||||
<div class="text-text-weaker">/</div>
|
</Select>
|
||||||
|
<div class="text-text-weaker">/</div>
|
||||||
|
</div>
|
||||||
<Select
|
<Select
|
||||||
options={sessions()}
|
options={sessions()}
|
||||||
current={currentSession()}
|
current={currentSession()}
|
||||||
|
|
@ -77,12 +88,13 @@ export function Header(props: {
|
||||||
label={(x) => x.title}
|
label={(x) => x.title}
|
||||||
value={(x) => x.id}
|
value={(x) => x.id}
|
||||||
onSelect={props.navigateToSession}
|
onSelect={props.navigateToSession}
|
||||||
class="text-14-regular text-text-base max-w-md"
|
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show when={currentSession()}>
|
<Show when={currentSession()}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
class="hidden xl:block"
|
||||||
value={
|
value={
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>New session</span>
|
<span>New session</span>
|
||||||
|
|
@ -98,7 +110,36 @@ export function Header(props: {
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
class="shrink-0"
|
class="hidden md:block shrink-0"
|
||||||
|
value={
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>Toggle review</span>
|
||||||
|
<span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
|
||||||
|
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||||
|
<Icon
|
||||||
|
size="small"
|
||||||
|
name={layout.review.opened() ? "layout-right-full" : "layout-right"}
|
||||||
|
class="group-hover/review-toggle:hidden"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
size="small"
|
||||||
|
name="layout-right-partial"
|
||||||
|
class="hidden group-hover/review-toggle:inline-block"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
size="small"
|
||||||
|
name={layout.review.opened() ? "layout-right" : "layout-right-full"}
|
||||||
|
class="hidden group-active/review-toggle:inline-block"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
class="hidden md:block shrink-0"
|
||||||
value={
|
value={
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>Toggle terminal</span>
|
<span>Toggle terminal</span>
|
||||||
|
|
@ -22,6 +22,7 @@ import { useProviders } from "@/hooks/use-providers"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { persisted } from "@/utils/persist"
|
import { persisted } from "@/utils/persist"
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
|
|
||||||
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||||
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
||||||
|
|
@ -972,7 +973,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
|
<Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
|
||||||
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
|
<div class="absolute top-0 inset-x-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
|
||||||
{store.mode === "shell"
|
{store.mode === "shell"
|
||||||
? "Enter shell command..."
|
? "Enter shell command..."
|
||||||
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||||
|
|
@ -1026,12 +1027,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{local.model.current()?.name ?? "Select model"}
|
{local.model.current()?.name ?? "Select model"}
|
||||||
<span class="ml-0.5 text-text-weak text-12-regular">{local.model.current()?.provider.name}</span>
|
<span class="hidden md:block ml-0.5 text-text-weak text-12-regular">
|
||||||
|
{local.model.current()?.provider.name}
|
||||||
|
</span>
|
||||||
<Icon name="chevron-down" size="small" />
|
<Icon name="chevron-down" size="small" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
<SessionContextUsage />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 absolute right-2 bottom-2">
|
<div class="flex items-center gap-1 absolute right-2 bottom-2">
|
||||||
<input
|
<input
|
||||||
64
packages/app/src/components/session-context-usage.tsx
Normal file
64
packages/app/src/components/session-context-usage.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { createMemo, Show } from "solid-js"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { useParams } from "@solidjs/router"
|
||||||
|
import { AssistantMessage } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
export function SessionContextUsage() {
|
||||||
|
const sync = useSync()
|
||||||
|
const params = useParams()
|
||||||
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
|
|
||||||
|
const cost = createMemo(() => {
|
||||||
|
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
|
||||||
|
return new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
}).format(total)
|
||||||
|
})
|
||||||
|
|
||||||
|
const context = createMemo(() => {
|
||||||
|
const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
|
||||||
|
if (!last) return
|
||||||
|
const total =
|
||||||
|
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||||
|
const model = sync.data.provider.all.find((x) => x.id === last.providerID)?.models[last.modelID]
|
||||||
|
return {
|
||||||
|
tokens: total.toLocaleString(),
|
||||||
|
percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={context?.()}>
|
||||||
|
{(ctx) => (
|
||||||
|
<Tooltip
|
||||||
|
openDelay={300}
|
||||||
|
value={
|
||||||
|
<div class="flex flex-col gap-1 p-2">
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-text-weaker">Tokens</span>
|
||||||
|
<span class="text-text-strong">{ctx().tokens}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-text-weaker">Usage</span>
|
||||||
|
<span class="text-text-strong">{ctx().percentage ?? 0}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span class="text-text-weaker">Cost</span>
|
||||||
|
<span class="text-text-strong">{cost()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span>
|
||||||
|
<ProgressCircle size={16} strokeWidth={2} percentage={ctx().percentage ?? 0} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -119,7 +119,6 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
||||||
return (
|
return (
|
||||||
<Dialog title="Commands">
|
<Dialog title="Commands">
|
||||||
<List
|
<List
|
||||||
class="px-2.5"
|
|
||||||
search={{ placeholder: "Search commands", autofocus: true }}
|
search={{ placeholder: "Search commands", autofocus: true }}
|
||||||
emptyMessage="No commands found"
|
emptyMessage="No commands found"
|
||||||
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
||||||
|
|
@ -295,6 +295,15 @@ function createGlobalSync() {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
const health = await globalSDK.client.global.health().then((x) => x.data)
|
||||||
|
if (!health?.healthy) {
|
||||||
|
setGlobalStore(
|
||||||
|
"error",
|
||||||
|
new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
retry(() =>
|
retry(() =>
|
||||||
globalSDK.client.path.get().then((x) => {
|
globalSDK.client.path.get().then((x) => {
|
||||||
|
|
@ -46,6 +46,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
opened: false,
|
opened: false,
|
||||||
height: 280,
|
height: 280,
|
||||||
},
|
},
|
||||||
|
review: {
|
||||||
|
opened: true,
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
width: 600,
|
width: 600,
|
||||||
},
|
},
|
||||||
|
|
@ -108,10 +111,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
|
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
|
||||||
},
|
},
|
||||||
expand(directory: string) {
|
expand(directory: string) {
|
||||||
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
|
const index = store.projects.findIndex((x) => x.worktree === directory)
|
||||||
|
if (index !== -1) setStore("projects", index, "expanded", true)
|
||||||
},
|
},
|
||||||
collapse(directory: string) {
|
collapse(directory: string) {
|
||||||
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
|
const index = store.projects.findIndex((x) => x.worktree === directory)
|
||||||
|
if (index !== -1) setStore("projects", index, "expanded", false)
|
||||||
},
|
},
|
||||||
move(directory: string, toIndex: number) {
|
move(directory: string, toIndex: number) {
|
||||||
setStore("projects", (projects) => {
|
setStore("projects", (projects) => {
|
||||||
|
|
@ -156,6 +161,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
setStore("terminal", "height", height)
|
setStore("terminal", "height", height)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
review: {
|
||||||
|
opened: createMemo(() => store.review?.opened ?? true),
|
||||||
|
open() {
|
||||||
|
setStore("review", "opened", true)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
setStore("review", "opened", false)
|
||||||
|
},
|
||||||
|
toggle() {
|
||||||
|
setStore("review", "opened", (x) => !x)
|
||||||
|
},
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
width: createMemo(() => store.session?.width ?? 600),
|
width: createMemo(() => store.session?.width ?? 600),
|
||||||
resize(width: number) {
|
resize(width: number) {
|
||||||
|
|
@ -62,27 +62,49 @@ function formatInitError(error: InitError): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatErrorChain(error: unknown, depth = 0): string {
|
function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): string {
|
||||||
if (!error) return "Unknown error"
|
if (!error) return "Unknown error"
|
||||||
|
|
||||||
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
|
||||||
|
|
||||||
if (isInitError(error)) {
|
if (isInitError(error)) {
|
||||||
return indent + formatInitError(error)
|
const message = formatInitError(error)
|
||||||
|
if (depth > 0 && parentMessage === message) return ""
|
||||||
|
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||||
|
return indent + message
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
const parts = [indent + `${error.name}: ${error.message}`]
|
const isDuplicate = depth > 0 && parentMessage === error.message
|
||||||
if (error.stack) {
|
const parts: string[] = []
|
||||||
parts.push(error.stack)
|
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
// Stack already includes error name and message, so prefer it
|
||||||
|
parts.push(indent + (error.stack ?? `${error.name}: ${error.message}`))
|
||||||
|
} else if (error.stack) {
|
||||||
|
// Duplicate message - only show the stack trace lines (skip message)
|
||||||
|
const trace = error.stack.split("\n").slice(1).join("\n").trim()
|
||||||
|
if (trace) {
|
||||||
|
parts.push(trace)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.cause) {
|
if (error.cause) {
|
||||||
parts.push(formatErrorChain(error.cause, depth + 1))
|
const causeResult = formatErrorChain(error.cause, depth + 1, error.message)
|
||||||
|
if (causeResult) {
|
||||||
|
parts.push(causeResult)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join("\n\n")
|
return parts.join("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof error === "string") return indent + error
|
if (typeof error === "string") {
|
||||||
|
if (depth > 0 && parentMessage === error) return ""
|
||||||
|
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||||
|
return indent + error
|
||||||
|
}
|
||||||
|
|
||||||
|
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
|
||||||
return indent + JSON.stringify(error, null, 2)
|
return indent + JSON.stringify(error, null, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
import { createEffect, createMemo, For, Match, onMount, ParentProps, Show, Switch, type JSX } from "solid-js"
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
Match,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
ParentProps,
|
||||||
|
Show,
|
||||||
|
Switch,
|
||||||
|
type JSX,
|
||||||
|
} from "solid-js"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
|
||||||
|
|
@ -42,9 +54,29 @@ export default function Layout(props: ParentProps) {
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
lastSession: {} as { [directory: string]: string },
|
lastSession: {} as { [directory: string]: string },
|
||||||
activeDraggable: undefined as string | undefined,
|
activeDraggable: undefined as string | undefined,
|
||||||
|
mobileSidebarOpen: false,
|
||||||
|
mobileProjectsExpanded: {} as Record<string, boolean>,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const mobileSidebar = {
|
||||||
|
open: () => store.mobileSidebarOpen,
|
||||||
|
show: () => setStore("mobileSidebarOpen", true),
|
||||||
|
hide: () => setStore("mobileSidebarOpen", false),
|
||||||
|
toggle: () => setStore("mobileSidebarOpen", (x) => !x),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mobileProjects = {
|
||||||
|
expanded: (directory: string) => store.mobileProjectsExpanded[directory] ?? true,
|
||||||
|
expand: (directory: string) => setStore("mobileProjectsExpanded", directory, true),
|
||||||
|
collapse: (directory: string) => setStore("mobileProjectsExpanded", directory, false),
|
||||||
|
}
|
||||||
|
|
||||||
let scrollContainerRef: HTMLDivElement | undefined
|
let scrollContainerRef: HTMLDivElement | undefined
|
||||||
|
const xlQuery = window.matchMedia("(min-width: 1280px)")
|
||||||
|
const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
|
||||||
|
const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches)
|
||||||
|
xlQuery.addEventListener("change", handleViewportChange)
|
||||||
|
onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange))
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
@ -259,11 +291,13 @@ export default function Layout(props: ParentProps) {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const lastSession = store.lastSession[directory]
|
const lastSession = store.lastSession[directory]
|
||||||
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
||||||
|
mobileSidebar.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToSession(session: Session | undefined) {
|
function navigateToSession(session: Session | undefined) {
|
||||||
if (!session) return
|
if (!session) return
|
||||||
navigate(`/${params.dir}/session/${session?.id}`)
|
navigate(`/${params.dir}/session/${session?.id}`)
|
||||||
|
mobileSidebar.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openProject(directory: string, navigate = true) {
|
function openProject(directory: string, navigate = true) {
|
||||||
|
|
@ -302,8 +336,12 @@ export default function Layout(props: ParentProps) {
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
|
if (isLargeViewport()) {
|
||||||
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
|
||||||
|
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
|
||||||
|
} else {
|
||||||
|
document.documentElement.style.setProperty("--dialog-left-margin", "0px")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getDraggableId(event: unknown): string | undefined {
|
function getDraggableId(event: unknown): string | undefined {
|
||||||
|
|
@ -419,6 +457,7 @@ export default function Layout(props: ParentProps) {
|
||||||
project: LocalProject
|
project: LocalProject
|
||||||
depth?: number
|
depth?: number
|
||||||
childrenMap: Map<string, Session[]>
|
childrenMap: Map<string, Session[]>
|
||||||
|
mobile?: boolean
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const depth = props.depth ?? 0
|
const depth = props.depth ?? 0
|
||||||
|
|
@ -439,7 +478,7 @@ export default function Layout(props: ParentProps) {
|
||||||
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
|
||||||
style={{ "padding-left": `${16 + depth * 12}px` }}
|
style={{ "padding-left": `${16 + depth * 12}px` }}
|
||||||
>
|
>
|
||||||
<Tooltip placement="right" value={props.session.title} gutter={10}>
|
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
|
||||||
<A
|
<A
|
||||||
href={`${props.slug}/session/${props.session.id}`}
|
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"
|
||||||
|
|
@ -486,7 +525,7 @@ export default function Layout(props: ParentProps) {
|
||||||
</A>
|
</A>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
|
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
|
||||||
<Tooltip placement="right" value="Archive session">
|
<Tooltip placement={props.mobile ? "bottom" : "right"} value="Archive session">
|
||||||
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
|
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -499,6 +538,7 @@ export default function Layout(props: ParentProps) {
|
||||||
project={props.project}
|
project={props.project}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
childrenMap={props.childrenMap}
|
childrenMap={props.childrenMap}
|
||||||
|
mobile={props.mobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -506,8 +546,9 @@ export default function Layout(props: ParentProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortableProject = (props: { project: LocalProject }): JSX.Element => {
|
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
|
||||||
const sortable = createSortable(props.project.worktree)
|
const sortable = createSortable(props.project.worktree)
|
||||||
|
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
|
||||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||||
const name = createMemo(() => getFilename(props.project.worktree))
|
const name = createMemo(() => getFilename(props.project.worktree))
|
||||||
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
||||||
|
|
@ -531,21 +572,24 @@ export default function Layout(props: ParentProps) {
|
||||||
setProjectStore("limit", (limit) => limit + 5)
|
setProjectStore("limit", (limit) => limit + 5)
|
||||||
await globalSync.project.loadSessions(props.project.worktree)
|
await globalSync.project.loadSessions(props.project.worktree)
|
||||||
}
|
}
|
||||||
|
const isExpanded = createMemo(() =>
|
||||||
|
props.mobile ? mobileProjects.expanded(props.project.worktree) : props.project.expanded,
|
||||||
|
)
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
if (open) layout.projects.expand(props.project.worktree)
|
if (props.mobile) {
|
||||||
else layout.projects.collapse(props.project.worktree)
|
if (open) mobileProjects.expand(props.project.worktree)
|
||||||
|
else mobileProjects.collapse(props.project.worktree)
|
||||||
|
} else {
|
||||||
|
if (open) layout.projects.expand(props.project.worktree)
|
||||||
|
else layout.projects.collapse(props.project.worktree)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={layout.sidebar.opened()}>
|
<Match when={showExpanded()}>
|
||||||
<Collapsible
|
<Collapsible variant="ghost" open={isExpanded()} class="gap-2 shrink-0" onOpenChange={handleOpenChange}>
|
||||||
variant="ghost"
|
|
||||||
open={props.project.expanded}
|
|
||||||
class="gap-2 shrink-0"
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
as={"div"}
|
as={"div"}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -556,7 +600,7 @@ export default function Layout(props: ParentProps) {
|
||||||
project={props.project}
|
project={props.project}
|
||||||
class="group-hover/session:hidden"
|
class="group-hover/session:hidden"
|
||||||
expandable
|
expandable
|
||||||
notify={!props.project.expanded}
|
notify={!isExpanded()}
|
||||||
/>
|
/>
|
||||||
<span class="truncate text-14-medium text-text-strong">{name()}</span>
|
<span class="truncate text-14-medium text-text-strong">{name()}</span>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
|
|
@ -585,6 +629,7 @@ export default function Layout(props: ParentProps) {
|
||||||
slug={slug()}
|
slug={slug()}
|
||||||
project={props.project}
|
project={props.project}
|
||||||
childrenMap={childSessionsByParent()}
|
childrenMap={childSessionsByParent()}
|
||||||
|
mobile={props.mobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -595,7 +640,7 @@ export default function Layout(props: ParentProps) {
|
||||||
>
|
>
|
||||||
<div class="flex items-center self-stretch w-full">
|
<div class="flex items-center self-stretch w-full">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<Tooltip placement="right" value="New session">
|
<Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
|
||||||
<A
|
<A
|
||||||
href={`${slug()}/session`}
|
href={`${slug()}/session`}
|
||||||
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
|
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
|
||||||
|
|
@ -650,30 +695,12 @@ export default function Layout(props: ParentProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
|
||||||
<div class="relative flex-1 min-h-0 flex flex-col">
|
const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
|
||||||
<Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
|
return (
|
||||||
<div class="flex-1 min-h-0 flex">
|
<>
|
||||||
<div
|
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
|
||||||
classList={{
|
<Show when={!sidebarProps.mobile}>
|
||||||
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
|
|
||||||
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
|
|
||||||
"border-r border-border-weak-base contain-strict": true,
|
|
||||||
}}
|
|
||||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
|
||||||
>
|
|
||||||
<Show when={layout.sidebar.opened()}>
|
|
||||||
<ResizeHandle
|
|
||||||
direction="horizontal"
|
|
||||||
size={layout.sidebar.width()}
|
|
||||||
min={150}
|
|
||||||
max={window.innerWidth * 0.3}
|
|
||||||
collapseThreshold={80}
|
|
||||||
onResize={layout.sidebar.resize}
|
|
||||||
onCollapse={layout.sidebar.close}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
placement="right"
|
placement="right"
|
||||||
|
|
@ -683,7 +710,7 @@ export default function Layout(props: ParentProps) {
|
||||||
<span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
|
<span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
inactive={layout.sidebar.opened()}
|
inactive={expanded()}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -715,110 +742,160 @@ export default function Layout(props: ParentProps) {
|
||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DragDropProvider
|
</Show>
|
||||||
onDragStart={handleDragStart}
|
<DragDropProvider
|
||||||
onDragEnd={handleDragEnd}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={handleDragOver}
|
onDragEnd={handleDragEnd}
|
||||||
collisionDetector={closestCenter}
|
onDragOver={handleDragOver}
|
||||||
|
collisionDetector={closestCenter}
|
||||||
|
>
|
||||||
|
<DragDropSensors />
|
||||||
|
<ConstrainDragXAxis />
|
||||||
|
<div
|
||||||
|
ref={sidebarProps.mobile ? undefined : scrollContainerRef}
|
||||||
|
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
|
||||||
>
|
>
|
||||||
<DragDropSensors />
|
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
||||||
<ConstrainDragXAxis />
|
<For each={layout.projects.list()}>
|
||||||
<div
|
{(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
|
||||||
ref={scrollContainerRef}
|
</For>
|
||||||
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
|
</SortableProvider>
|
||||||
>
|
</div>
|
||||||
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
<DragOverlay>
|
||||||
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
|
<ProjectDragOverlay />
|
||||||
</SortableProvider>
|
</DragOverlay>
|
||||||
</div>
|
</DragDropProvider>
|
||||||
<DragOverlay>
|
</div>
|
||||||
<ProjectDragOverlay />
|
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
||||||
</DragOverlay>
|
<Switch>
|
||||||
</DragDropProvider>
|
<Match when={!providers.paid().length && expanded()}>
|
||||||
</div>
|
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
||||||
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
<div class="p-3 flex flex-col gap-2">
|
||||||
<Switch>
|
<div class="text-12-medium text-text-strong">Getting started</div>
|
||||||
<Match when={!providers.paid().length && layout.sidebar.opened()}>
|
<div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
|
||||||
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
|
<div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
|
||||||
<div class="p-3 flex flex-col gap-2">
|
|
||||||
<div class="text-12-medium text-text-strong">Getting started</div>
|
|
||||||
<div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
|
|
||||||
<div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
|
|
||||||
</div>
|
|
||||||
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
|
|
||||||
<Button
|
|
||||||
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
|
|
||||||
size="large"
|
|
||||||
icon="plus"
|
|
||||||
onClick={connectProvider}
|
|
||||||
>
|
|
||||||
<Show when={layout.sidebar.opened()}>Connect provider</Show>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
|
||||||
<Match when={true}>
|
|
||||||
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
|
|
||||||
<Button
|
<Button
|
||||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
|
||||||
variant="ghost"
|
|
||||||
size="large"
|
size="large"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
onClick={connectProvider}
|
onClick={connectProvider}
|
||||||
>
|
>
|
||||||
<Show when={layout.sidebar.opened()}>Connect provider</Show>
|
Connect provider
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Match>
|
</div>
|
||||||
</Switch>
|
</Match>
|
||||||
<Show when={platform.openDirectoryPickerDialog}>
|
<Match when={true}>
|
||||||
<Tooltip
|
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
|
||||||
placement="right"
|
|
||||||
value={
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span>Open project</span>
|
|
||||||
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
inactive={layout.sidebar.opened()}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
icon="folder-add-left"
|
icon="plus"
|
||||||
onClick={chooseProject}
|
onClick={connectProvider}
|
||||||
>
|
>
|
||||||
<Show when={layout.sidebar.opened()}>Open project</Show>
|
<Show when={expanded()}>Connect provider</Show>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Match>
|
||||||
{/* <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}> */}
|
</Switch>
|
||||||
{/* <Button */}
|
<Show when={platform.openDirectoryPickerDialog}>
|
||||||
{/* disabled */}
|
<Tooltip
|
||||||
{/* class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2" */}
|
placement="right"
|
||||||
{/* variant="ghost" */}
|
value={
|
||||||
{/* size="large" */}
|
<div class="flex items-center gap-2">
|
||||||
{/* icon="settings-gear" */}
|
<span>Open project</span>
|
||||||
{/* > */}
|
<Show when={!sidebarProps.mobile}>
|
||||||
{/* <Show when={layout.sidebar.opened()}>Settings</Show> */}
|
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
|
||||||
{/* </Button> */}
|
</Show>
|
||||||
{/* </Tooltip> */}
|
</div>
|
||||||
<Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
|
}
|
||||||
|
inactive={expanded()}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
as={"a"}
|
|
||||||
href="https://opencode.ai/desktop-feedback"
|
|
||||||
target="_blank"
|
|
||||||
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
icon="bubble-5"
|
icon="folder-add-left"
|
||||||
|
onClick={chooseProject}
|
||||||
>
|
>
|
||||||
<Show when={layout.sidebar.opened()}>Share feedback</Show>
|
<Show when={expanded()}>Open project</Show>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Show>
|
||||||
|
<Tooltip placement="right" value="Share feedback" inactive={expanded()}>
|
||||||
|
<Button
|
||||||
|
as={"a"}
|
||||||
|
href="https://opencode.ai/desktop-feedback"
|
||||||
|
target="_blank"
|
||||||
|
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
|
||||||
|
variant="ghost"
|
||||||
|
size="large"
|
||||||
|
icon="bubble-5"
|
||||||
|
>
|
||||||
|
<Show when={expanded()}>Share feedback</Show>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="relative flex-1 min-h-0 flex flex-col">
|
||||||
|
<Header
|
||||||
|
navigateToProject={navigateToProject}
|
||||||
|
navigateToSession={navigateToSession}
|
||||||
|
onMobileMenuToggle={mobileSidebar.toggle}
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-h-0 flex">
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"hidden xl:flex": true,
|
||||||
|
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
|
||||||
|
"flex-col gap-5.5 items-start self-stretch justify-between": true,
|
||||||
|
"border-r border-border-weak-base contain-strict": true,
|
||||||
|
}}
|
||||||
|
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||||
|
>
|
||||||
|
<Show when={layout.sidebar.opened()}>
|
||||||
|
<ResizeHandle
|
||||||
|
direction="horizontal"
|
||||||
|
size={layout.sidebar.width()}
|
||||||
|
min={150}
|
||||||
|
max={window.innerWidth * 0.3}
|
||||||
|
collapseThreshold={80}
|
||||||
|
onResize={layout.sidebar.resize}
|
||||||
|
onCollapse={layout.sidebar.close}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<SidebarContent />
|
||||||
|
</div>
|
||||||
|
<div class="xl:hidden">
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
|
||||||
|
"opacity-100 pointer-events-auto": mobileSidebar.open(),
|
||||||
|
"opacity-0 pointer-events-none": !mobileSidebar.open(),
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) mobileSidebar.hide()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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,
|
||||||
|
"translate-x-0": mobileSidebar.open(),
|
||||||
|
"-translate-x-full": !mobileSidebar.open(),
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<SidebarContent mobile />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
|
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
|
||||||
</div>
|
</div>
|
||||||
<Toast.Region />
|
<Toast.Region />
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
batch,
|
batch,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
|
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { useLocal, type LocalFile } from "@/context/local"
|
import { useLocal, type LocalFile } from "@/context/local"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
|
@ -26,6 +27,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
|
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||||
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
|
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
import {
|
import {
|
||||||
|
|
@ -70,7 +72,6 @@ export default function Page() {
|
||||||
|
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||||
|
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
|
|
@ -79,7 +80,6 @@ export default function Page() {
|
||||||
.filter((m) => m.role === "user")
|
.filter((m) => m.role === "user")
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
)
|
)
|
||||||
// Visible user messages excludes reverted messages (those >= revertMessageID)
|
|
||||||
const visibleUserMessages = createMemo(() => {
|
const visibleUserMessages = createMemo(() => {
|
||||||
const revert = revertMessageID()
|
const revert = revertMessageID()
|
||||||
if (!revert) return userMessages()
|
if (!revert) return userMessages()
|
||||||
|
|
@ -87,15 +87,24 @@ export default function Page() {
|
||||||
})
|
})
|
||||||
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
|
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
|
||||||
|
|
||||||
const [messageStore, setMessageStore] = createStore<{ messageId?: string }>({})
|
const [store, setStore] = createStore({
|
||||||
|
clickTimer: undefined as number | undefined,
|
||||||
|
activeDraggable: undefined as string | undefined,
|
||||||
|
activeTerminalDraggable: undefined as string | undefined,
|
||||||
|
userInteracted: false,
|
||||||
|
stepsExpanded: true,
|
||||||
|
mobileStepsExpanded: {} as Record<string, boolean>,
|
||||||
|
messageId: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
const activeMessage = createMemo(() => {
|
const activeMessage = createMemo(() => {
|
||||||
if (!messageStore.messageId) return lastUserMessage()
|
if (!store.messageId) return lastUserMessage()
|
||||||
// If the stored message is no longer visible (e.g., was reverted), fall back to last visible
|
// If the stored message is no longer visible (e.g., was reverted), fall back to last visible
|
||||||
const found = visibleUserMessages()?.find((m) => m.id === messageStore.messageId)
|
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
|
||||||
return found ?? lastUserMessage()
|
return found ?? lastUserMessage()
|
||||||
})
|
})
|
||||||
const setActiveMessage = (message: UserMessage | undefined) => {
|
const setActiveMessage = (message: UserMessage | undefined) => {
|
||||||
setMessageStore("messageId", message?.id)
|
setStore("messageId", message?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateMessageByOffset(offset: number) {
|
function navigateMessageByOffset(offset: number) {
|
||||||
|
|
@ -119,13 +128,6 @@ export default function Page() {
|
||||||
|
|
||||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
clickTimer: undefined as number | undefined,
|
|
||||||
activeDraggable: undefined as string | undefined,
|
|
||||||
activeTerminalDraggable: undefined as string | undefined,
|
|
||||||
userInteracted: false,
|
|
||||||
stepsExpanded: true,
|
|
||||||
})
|
|
||||||
let inputRef!: HTMLDivElement
|
let inputRef!: HTMLDivElement
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -146,7 +148,7 @@ export default function Page() {
|
||||||
() => visibleUserMessages().at(-1)?.id,
|
() => visibleUserMessages().at(-1)?.id,
|
||||||
(lastId, prevLastId) => {
|
(lastId, prevLastId) => {
|
||||||
if (lastId && prevLastId && lastId > prevLastId) {
|
if (lastId && prevLastId && lastId > prevLastId) {
|
||||||
setMessageStore("messageId", undefined)
|
setStore("messageId", undefined)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
|
|
@ -219,6 +221,15 @@ export default function Page() {
|
||||||
slash: "terminal",
|
slash: "terminal",
|
||||||
onSelect: () => layout.terminal.toggle(),
|
onSelect: () => layout.terminal.toggle(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "review.toggle",
|
||||||
|
title: "Toggle review",
|
||||||
|
description: "Show or hide the review panel",
|
||||||
|
category: "View",
|
||||||
|
keybind: "mod+b",
|
||||||
|
slash: "review",
|
||||||
|
onSelect: () => layout.review.toggle(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "terminal.new",
|
id: "terminal.new",
|
||||||
title: "New terminal",
|
title: "New terminal",
|
||||||
|
|
@ -531,74 +542,163 @@ export default function Page() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showTabs = createMemo(() => diffs().length > 0 || tabs().all().length > 0)
|
const showTabs = createMemo(() => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0))
|
||||||
|
|
||||||
|
const mobileWorking = createMemo(() => status().type !== "idle")
|
||||||
|
const mobileAutoScroll = createAutoScroll({
|
||||||
|
working: mobileWorking,
|
||||||
|
onUserInteracted: () => setStore("userInteracted", true),
|
||||||
|
})
|
||||||
|
|
||||||
|
const MobileTurns = () => (
|
||||||
|
<div
|
||||||
|
ref={mobileAutoScroll.scrollRef}
|
||||||
|
onScroll={mobileAutoScroll.handleScroll}
|
||||||
|
onClick={mobileAutoScroll.handleInteraction}
|
||||||
|
class="relative mt-2 min-w-0 w-full h-full overflow-y-auto no-scrollbar pb-12"
|
||||||
|
>
|
||||||
|
<div ref={mobileAutoScroll.contentRef} class="flex flex-col gap-45 items-start justify-start mt-4">
|
||||||
|
<For each={visibleUserMessages()}>
|
||||||
|
{(message) => (
|
||||||
|
<SessionTurn
|
||||||
|
sessionID={params.id!}
|
||||||
|
messageID={message.id}
|
||||||
|
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
|
||||||
|
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
|
||||||
|
onUserInteracted={() => setStore("userInteracted", true)}
|
||||||
|
classes={{
|
||||||
|
root: "min-w-0 w-full relative",
|
||||||
|
content:
|
||||||
|
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||||
|
container: "px-4",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const NewSessionView = () => (
|
||||||
|
<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>
|
||||||
|
<div class="flex justify-center items-center gap-3">
|
||||||
|
<Icon name="folder" size="small" />
|
||||||
|
<div class="text-12-medium text-text-weak">
|
||||||
|
{getDirectory(sync.data.path.directory)}
|
||||||
|
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={sync.project}>
|
||||||
|
{(project) => (
|
||||||
|
<div class="flex justify-center items-center gap-3">
|
||||||
|
<Icon name="pencil-line" size="small" />
|
||||||
|
<div class="text-12-medium text-text-weak">
|
||||||
|
Last modified
|
||||||
|
<span class="text-text-strong">
|
||||||
|
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const DesktopSessionContent = () => (
|
||||||
|
<Switch>
|
||||||
|
<Match when={params.id}>
|
||||||
|
<div class="flex items-start justify-start h-full min-h-0">
|
||||||
|
<SessionMessageRail
|
||||||
|
messages={visibleUserMessages()}
|
||||||
|
current={activeMessage()}
|
||||||
|
onMessageSelect={setActiveMessage}
|
||||||
|
wide={!showTabs()}
|
||||||
|
/>
|
||||||
|
<Show when={activeMessage()}>
|
||||||
|
<SessionTurn
|
||||||
|
sessionID={params.id!}
|
||||||
|
messageID={activeMessage()!.id}
|
||||||
|
stepsExpanded={store.stepsExpanded}
|
||||||
|
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
||||||
|
onUserInteracted={() => setStore("userInteracted", true)}
|
||||||
|
classes={{
|
||||||
|
root: "pb-20 flex-1 min-w-0",
|
||||||
|
content: "pb-20",
|
||||||
|
container:
|
||||||
|
"w-full " +
|
||||||
|
(!showTabs() ? "max-w-200 mx-auto px-6" : visibleUserMessages().length > 1 ? "pr-6 pl-18" : "px-6"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<NewSessionView />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||||
<div class="min-h-0 grow w-full flex">
|
<div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
|
||||||
{/* Session pane - always visible */}
|
<Switch>
|
||||||
|
<Match when={!params.id}>
|
||||||
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
|
<NewSessionView />
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={diffs().length > 0}>
|
||||||
|
<Tabs class="flex-1 min-h-0 flex flex-col pb-28">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||||
|
Session
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
|
||||||
|
{diffs().length} Files Changed
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
<Tabs.Content value="session" class="flex-1 !overflow-hidden">
|
||||||
|
<MobileTurns />
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content forceMount value="review" class="flex-1 !overflow-hidden hidden data-[selected]:block">
|
||||||
|
<div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
|
||||||
|
<SessionReview
|
||||||
|
diffs={diffs()}
|
||||||
|
classes={{
|
||||||
|
root: "pb-32",
|
||||||
|
header: "px-4",
|
||||||
|
container: "px-4",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
|
<MobileTurns />
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
<div class="absolute inset-x-0 bottom-4 flex flex-col justify-center items-center z-50 px-4">
|
||||||
|
<div class="w-full">
|
||||||
|
<PromptInput
|
||||||
|
ref={(el) => {
|
||||||
|
inputRef = el
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:flex min-h-0 grow w-full">
|
||||||
<div
|
<div
|
||||||
class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
|
class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
|
||||||
style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
|
style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
|
||||||
>
|
>
|
||||||
<div class="flex-1 min-h-0 overflow-hidden">
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
<Switch>
|
<DesktopSessionContent />
|
||||||
<Match when={params.id}>
|
|
||||||
<div class="flex items-start justify-start h-full min-h-0">
|
|
||||||
<SessionMessageRail
|
|
||||||
messages={visibleUserMessages()}
|
|
||||||
current={activeMessage()}
|
|
||||||
onMessageSelect={setActiveMessage}
|
|
||||||
wide={!showTabs()}
|
|
||||||
/>
|
|
||||||
<Show when={activeMessage()}>
|
|
||||||
<SessionTurn
|
|
||||||
sessionID={params.id!}
|
|
||||||
messageID={activeMessage()!.id}
|
|
||||||
stepsExpanded={store.stepsExpanded}
|
|
||||||
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
|
||||||
onUserInteracted={() => setStore("userInteracted", true)}
|
|
||||||
classes={{
|
|
||||||
root: "pb-20 flex-1 min-w-0",
|
|
||||||
content: "pb-20",
|
|
||||||
container:
|
|
||||||
"w-full " +
|
|
||||||
(!showTabs()
|
|
||||||
? "max-w-200 mx-auto px-6"
|
|
||||||
: visibleUserMessages().length > 1
|
|
||||||
? "pr-6 pl-18"
|
|
||||||
: "px-6"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
|
|
||||||
<div class="text-20-medium text-text-weaker">New session</div>
|
|
||||||
<div class="flex justify-center items-center gap-3">
|
|
||||||
<Icon name="folder" size="small" />
|
|
||||||
<div class="text-12-medium text-text-weak">
|
|
||||||
{getDirectory(sync.data.path.directory)}
|
|
||||||
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Show when={sync.project}>
|
|
||||||
{(project) => (
|
|
||||||
<div class="flex justify-center items-center gap-3">
|
|
||||||
<Icon name="pencil-line" size="small" />
|
|
||||||
<div class="text-12-medium text-text-weak">
|
|
||||||
Last modified
|
|
||||||
<span class="text-text-strong">
|
|
||||||
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
|
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
|
||||||
<div
|
<div
|
||||||
|
|
@ -625,7 +725,6 @@ export default function Page() {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs pane - visible when there are diffs or file tabs */}
|
|
||||||
<Show when={showTabs()}>
|
<Show when={showTabs()}>
|
||||||
<div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
|
<div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
|
||||||
<DragDropProvider
|
<DragDropProvider
|
||||||
|
|
@ -683,7 +782,7 @@ export default function Page() {
|
||||||
</div>
|
</div>
|
||||||
<Show when={diffs().length}>
|
<Show when={diffs().length}>
|
||||||
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
|
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
|
||||||
<div class="relative pt-3 flex-1 min-h-0 overflow-hidden">
|
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
|
||||||
<SessionReview
|
<SessionReview
|
||||||
classes={{
|
classes={{
|
||||||
root: "pb-40",
|
root: "pb-40",
|
||||||
|
|
@ -754,9 +853,10 @@ export default function Page() {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={layout.terminal.opened()}>
|
<Show when={layout.terminal.opened()}>
|
||||||
<div
|
<div
|
||||||
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
|
class="hidden md:flex relative w-full flex-col shrink-0 border-t border-border-weak-base"
|
||||||
style={{ height: `${layout.terminal.height()}px` }}
|
style={{ height: `${layout.terminal.height()}px` }}
|
||||||
>
|
>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
@ -10,11 +12,13 @@
|
||||||
"jsxImportSource": "solid-js",
|
"jsxImportSource": "solid-js",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "node_modules/.ts-dist",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"paths": {
|
||||||
"emitDeclarationOnly": false,
|
"@/*": ["./src/*"]
|
||||||
"outDir": "node_modules/.ts-dist"
|
}
|
||||||
},
|
},
|
||||||
"references": [{ "path": "../desktop" }],
|
"exclude": ["dist", "ts-dist"]
|
||||||
"include": ["src"]
|
|
||||||
}
|
}
|
||||||
15
packages/app/vite.config.ts
Normal file
15
packages/app/vite.config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import desktopPlugin from "./vite"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [desktopPlugin] as any,
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
allowedHosts: true,
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: "esnext",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.184",
|
"version": "1.0.191",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ export const config = {
|
||||||
github: {
|
github: {
|
||||||
repoUrl: "https://github.com/sst/opencode",
|
repoUrl: "https://github.com/sst/opencode",
|
||||||
starsFormatted: {
|
starsFormatted: {
|
||||||
compact: "38K",
|
compact: "41K",
|
||||||
full: "38,000",
|
full: "41,000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -22,8 +22,8 @@ export const config = {
|
||||||
|
|
||||||
// Static stats (used on landing page)
|
// Static stats (used on landing page)
|
||||||
stats: {
|
stats: {
|
||||||
contributors: "400",
|
contributors: "450",
|
||||||
commits: "5,000",
|
commits: "6,000",
|
||||||
monthlyUsers: "400,000",
|
monthlyUsers: "400,000",
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const assetNames: Record<string, string> = {
|
||||||
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
||||||
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
||||||
"linux-x64-deb": "opencode-desktop-linux-amd64.deb",
|
"linux-x64-deb": "opencode-desktop-linux-amd64.deb",
|
||||||
|
"linux-x64-appimage": "opencode-desktop-linux-amd64.AppImage",
|
||||||
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
||||||
} satisfies Record<DownloadPlatform, string>
|
} satisfies Record<DownloadPlatform, string>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,22 @@ export default function Download() {
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div data-component="download-row">
|
||||||
|
<div data-component="download-info">
|
||||||
|
<span data-slot="icon">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M4.34591 22.7088C5.61167 22.86 7.03384 23.6799 8.22401 23.8247C9.42058 23.9758 9.79086 23.0098 9.79086 23.0098C9.79086 23.0098 11.1374 22.7088 12.553 22.6741C13.97 22.6344 15.3113 22.9688 15.3113 22.9688C15.3113 22.9688 15.5714 23.5646 16.057 23.8247C16.5426 24.0898 17.588 24.1257 18.258 23.4198C18.9293 22.7088 20.7204 21.8132 21.7261 21.2533C22.7382 20.6922 22.5525 19.8364 21.917 19.5763C21.2816 19.3163 20.7614 18.9063 20.8011 18.1196C20.8357 17.3394 20.24 16.8193 20.24 16.8193C20.24 16.8193 20.7614 15.1025 20.2759 13.6805C19.7903 12.2648 18.1889 9.98819 16.9577 8.27657C15.7266 6.55985 16.7719 4.5779 15.651 2.04503C14.5299 -0.491656 11.623 -0.341713 10.0562 0.739505C8.4893 1.8208 8.96968 4.50225 9.04526 5.77447C9.12084 7.04022 9.07985 7.94598 8.93509 8.27146C8.79033 8.60198 7.77951 9.80243 7.1082 10.8081C6.43818 11.819 5.95254 13.906 5.46187 14.7669C4.98142 15.6228 5.31711 16.403 5.31711 16.403C5.31711 16.403 4.98149 16.5182 4.71628 17.0795C4.45616 17.6342 3.93601 17.8993 2.99948 18.0801C2.06934 18.2709 2.06934 18.8705 2.29357 19.5419C2.51902 20.2119 2.29357 20.5873 2.03346 21.4431C1.77342 22.2988 3.07506 22.5588 4.34591 22.7088ZM17.5034 18.805C18.1683 19.0958 19.124 18.691 19.4149 18.4001C19.7045 18.1106 19.9094 17.6801 19.9094 17.6801C19.9094 17.6801 20.2002 17.8249 20.1707 18.2848C20.14 18.7512 20.3706 19.4161 20.8062 19.6467C21.2418 19.876 21.9067 20.1963 21.5621 20.5166C21.211 20.8369 19.2688 21.6183 18.6885 22.2282C18.1132 22.8341 17.3573 23.33 16.8974 23.1839C16.4324 23.0391 16.0262 22.4037 16.2261 21.4736C16.4324 20.5473 16.6066 19.5313 16.5771 18.951C16.5464 18.3707 16.4324 17.5892 16.5771 17.4738C16.7219 17.3598 16.9525 17.4148 16.9525 17.4148C16.9525 17.4148 16.8371 18.5156 17.5034 18.805ZM13.1885 3.12632C13.829 3.12632 14.3454 3.76175 14.3454 4.54324C14.3454 5.09798 14.0853 5.57844 13.7048 5.80906C13.6087 5.76937 13.5087 5.72449 13.3986 5.67832C13.6292 5.56434 13.7893 5.27352 13.7893 4.93783C13.7893 4.49844 13.519 4.13714 13.1794 4.13714C12.8489 4.13714 12.5734 4.49836 12.5734 4.93783C12.5734 5.09806 12.6132 5.25813 12.6785 5.38369C12.4786 5.30293 12.298 5.23383 12.1532 5.17874C12.0776 4.98781 12.0328 4.77257 12.0328 4.54331C12.0328 3.76183 12.5478 3.12632 13.1885 3.12632ZM11.6024 5.56823C11.9176 5.62331 12.7835 5.9987 13.1039 6.11398C13.4242 6.22415 13.7791 6.4291 13.7445 6.63413C13.7048 6.84548 13.5395 6.84548 13.1039 7.1107C12.6735 7.37082 11.7331 7.95116 11.432 7.99085C11.1322 8.03055 10.9618 7.86141 10.6415 7.65516C10.3211 7.44503 9.72039 6.95436 9.87147 6.69432C9.87147 6.69432 10.3416 6.33432 10.5467 6.14986C10.7517 5.95893 11.2821 5.50925 11.6024 5.56823ZM10.2213 3.35185C10.726 3.35185 11.1373 3.95268 11.1373 4.69318C11.1373 4.82773 11.1219 4.95322 11.0976 5.07878C10.972 5.11847 10.8466 5.18385 10.726 5.28891C10.6671 5.33889 10.612 5.38369 10.5621 5.43367C10.6415 5.28381 10.6722 5.06857 10.6363 4.84305C10.5672 4.44335 10.2968 4.14743 10.0316 4.18712C9.76511 4.232 9.60625 4.5984 9.67033 5.00327C9.74081 5.41325 10.0059 5.7091 10.2763 5.6643C10.2917 5.6592 10.3058 5.65409 10.3211 5.64891C10.1918 5.77447 10.0713 5.88464 9.94576 5.97432C9.58065 5.80388 9.31033 5.29402 9.31033 4.69318C9.31041 3.94758 9.71521 3.35185 10.2213 3.35185ZM7.40915 13.045C7.9293 12.2251 8.26492 10.4328 8.78507 9.83702C9.31041 9.24259 9.71521 7.97554 9.53075 7.41569C9.53075 7.41569 10.6517 8.75702 11.432 8.53668C12.2135 8.31116 13.97 7.00571 14.23 7.22994C14.4901 7.45539 16.727 12.375 16.9525 13.9419C17.178 15.5074 16.8026 16.7041 16.8026 16.7041C16.8026 16.7041 15.9468 16.4785 15.8366 16.9987C15.7264 17.524 15.7264 19.4265 15.7264 19.4265C15.7264 19.4265 14.5695 21.0279 12.7784 21.2931C10.9874 21.5532 10.0905 21.3636 10.0905 21.3636L9.08481 20.2118C9.08481 20.2118 9.86637 20.0965 9.75612 19.3112C9.64595 18.531 7.36801 17.4496 6.95803 16.4785C6.5482 15.5073 6.8826 13.8662 7.40915 13.045ZM2.9802 18.9204C3.06988 18.5361 4.23056 18.5361 4.67643 18.2657C5.12229 17.9954 5.21189 17.219 5.57197 17.0141C5.92679 16.804 6.58279 17.5496 6.85311 17.9697C7.11833 18.3797 8.13433 20.1721 8.54942 20.6179C8.96961 21.0676 9.35528 21.6633 9.23483 22.1988C9.12084 22.7343 8.48923 23.1251 8.48923 23.1251C7.92427 23.2993 6.34843 22.619 5.63231 22.3192C4.9162 22.0182 3.09433 21.9284 2.8599 21.6633C2.61906 21.393 2.97517 20.7972 3.06995 20.2322C3.15445 19.6609 2.8893 19.306 2.9802 18.9204Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>Linux (.AppImage)</span>
|
||||||
|
</div>
|
||||||
|
<a href={getDownloadHref("linux-x64-appimage")} data-component="action-button">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
export type DownloadPlatform = `darwin-${"x64" | "aarch64"}-dmg` | "windows-x64-nsis" | `linux-x64-${"deb" | "rpm"}`
|
export type DownloadPlatform =
|
||||||
|
| `darwin-${"x64" | "aarch64"}-dmg`
|
||||||
|
| "windows-x64-nsis"
|
||||||
|
| `linux-x64-${"deb" | "rpm" | "appimage"}`
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,23 @@ export function createDataDumper(sessionId: string, requestId: string, projectId
|
||||||
if (!data.modelName) return
|
if (!data.modelName) return
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "")
|
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "")
|
||||||
|
const year = timestamp.substring(0, 4)
|
||||||
|
const month = timestamp.substring(4, 6)
|
||||||
|
const day = timestamp.substring(6, 8)
|
||||||
|
const hour = timestamp.substring(8, 10)
|
||||||
|
const minute = timestamp.substring(10, 12)
|
||||||
|
const second = timestamp.substring(12, 14)
|
||||||
|
|
||||||
waitUntil(
|
waitUntil(
|
||||||
Resource.ZenData.put(
|
Resource.ZenDataNew.put(
|
||||||
`data/${data.modelName}/${sessionId}/${requestId}.json`,
|
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
|
||||||
JSON.stringify({ timestamp, ...data }),
|
JSON.stringify({ timestamp, ...data }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
waitUntil(
|
waitUntil(
|
||||||
Resource.ZenData.put(
|
Resource.ZenDataNew.put(
|
||||||
`meta/${data.modelName}/${timestamp}/${requestId}.json`,
|
`meta/${data.modelName}/${sessionId}/${requestId}.json`,
|
||||||
JSON.stringify({ timestamp, ...metadata }),
|
JSON.stringify({ timestamp, ...metadata }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.184",
|
"version": "1.0.191",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
1
packages/console/core/sst-env.d.ts
vendored
1
packages/console/core/sst-env.d.ts
vendored
|
|
@ -132,6 +132,7 @@ declare module "sst" {
|
||||||
"GatewayKv": cloudflare.KVNamespace
|
"GatewayKv": cloudflare.KVNamespace
|
||||||
"LogProcessor": cloudflare.Service
|
"LogProcessor": cloudflare.Service
|
||||||
"ZenData": cloudflare.R2Bucket
|
"ZenData": cloudflare.R2Bucket
|
||||||
|
"ZenDataNew": cloudflare.R2Bucket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.184",
|
"version": "1.0.191",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
1
packages/console/function/sst-env.d.ts
vendored
1
packages/console/function/sst-env.d.ts
vendored
|
|
@ -132,6 +132,7 @@ declare module "sst" {
|
||||||
"GatewayKv": cloudflare.KVNamespace
|
"GatewayKv": cloudflare.KVNamespace
|
||||||
"LogProcessor": cloudflare.Service
|
"LogProcessor": cloudflare.Service
|
||||||
"ZenData": cloudflare.R2Bucket
|
"ZenData": cloudflare.R2Bucket
|
||||||
|
"ZenDataNew": cloudflare.R2Bucket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.184",
|
"version": "1.0.191",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
|
||||||
1
packages/console/resource/sst-env.d.ts
vendored
1
packages/console/resource/sst-env.d.ts
vendored
|
|
@ -132,6 +132,7 @@ declare module "sst" {
|
||||||
"GatewayKv": cloudflare.KVNamespace
|
"GatewayKv": cloudflare.KVNamespace
|
||||||
"LogProcessor": cloudflare.Service
|
"LogProcessor": cloudflare.Service
|
||||||
"ZenData": cloudflare.R2Bucket
|
"ZenData": cloudflare.R2Bucket
|
||||||
|
"ZenDataNew": cloudflare.R2Bucket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
packages/desktop/.gitignore
vendored
25
packages/desktop/.gitignore
vendored
|
|
@ -1 +1,24 @@
|
||||||
src/assets/theme.css
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,7 @@
|
||||||
## Usage
|
# Tauri + Vanilla TS
|
||||||
|
|
||||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`.
|
This template should help get you started developing with Tauri in vanilla HTML, CSS and Typescript.
|
||||||
|
|
||||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template.
|
## Recommended IDE Setup
|
||||||
|
|
||||||
```bash
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
$ npm install # or pnpm install or yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
|
||||||
|
|
||||||
## Available Scripts
|
|
||||||
|
|
||||||
In the project directory, you can run:
|
|
||||||
|
|
||||||
### `npm run dev` or `npm start`
|
|
||||||
|
|
||||||
Runs the app in the development mode.<br>
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
|
||||||
|
|
||||||
The page will reload if you make edits.<br>
|
|
||||||
|
|
||||||
### `npm run build`
|
|
||||||
|
|
||||||
Builds the app for production to the `dist` folder.<br>
|
|
||||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
|
||||||
|
|
||||||
The build is minified and the filenames include the hashes.<br>
|
|
||||||
Your app is ready to be deployed!
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<meta property="og:image" content="/social-share.png" />
|
<meta property="og:image" content="/social-share.png" />
|
||||||
<meta property="twitter:image" content="/social-share.png" />
|
<meta property="twitter:image" content="/social-share.png" />
|
||||||
</head>
|
</head>
|
||||||
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||||
<script>
|
<script>
|
||||||
;(function () {
|
;(function () {
|
||||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||||
|
|
@ -23,6 +23,6 @@
|
||||||
</script>
|
</script>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<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-screen"></div>
|
||||||
<script src="/src/entry.tsx" type="module"></script>
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,37 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.184",
|
"private": true,
|
||||||
"description": "",
|
"version": "1.0.191",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts",
|
|
||||||
"./vite": "./vite.js"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo -b",
|
"typecheck": "tsgo -b",
|
||||||
"start": "vite",
|
"predev": "bun ./scripts/predev.ts",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "bun run typecheck && vite build",
|
||||||
"serve": "vite preview"
|
"preview": "vite preview",
|
||||||
},
|
"tauri": "tauri"
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"@happy-dom/global-registrator": "20.0.11",
|
|
||||||
"@tailwindcss/vite": "catalog:",
|
|
||||||
"@tsconfig/bun": "1.0.9",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@types/luxon": "catalog:",
|
|
||||||
"@types/node": "catalog:",
|
|
||||||
"@typescript/native-preview": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
"vite": "catalog:",
|
|
||||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
|
||||||
"vite-plugin-solid": "catalog:"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
|
||||||
"@opencode-ai/ui": "workspace:*",
|
|
||||||
"@opencode-ai/util": "workspace:*",
|
|
||||||
"@shikijs/transformers": "3.9.2",
|
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
|
||||||
"@solid-primitives/audio": "1.4.2",
|
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
|
||||||
"@solid-primitives/media": "2.3.3",
|
|
||||||
"@solid-primitives/resize-observer": "2.1.3",
|
|
||||||
"@solid-primitives/scroll": "2.1.3",
|
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@solid-primitives/websocket": "1.3.1",
|
"@tauri-apps/api": "^2",
|
||||||
"@solidjs/meta": "catalog:",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
"@solidjs/router": "catalog:",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@thisbeyond/solid-dnd": "0.7.5",
|
"@tauri-apps/plugin-os": "~2",
|
||||||
"diff": "catalog:",
|
"@tauri-apps/plugin-process": "~2",
|
||||||
"fuzzysort": "catalog:",
|
"@tauri-apps/plugin-shell": "~2",
|
||||||
"ghostty-web": "0.3.0",
|
"@tauri-apps/plugin-store": "~2",
|
||||||
"luxon": "catalog:",
|
"@tauri-apps/plugin-updater": "~2",
|
||||||
"marked": "16.2.0",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
"marked-shiki": "1.2.1",
|
"@tauri-apps/plugin-window-state": "~2",
|
||||||
"remeda": "catalog:",
|
"solid-js": "catalog:"
|
||||||
"shiki": "3.9.2",
|
},
|
||||||
"solid-js": "catalog:",
|
"devDependencies": {
|
||||||
"solid-list": "catalog:",
|
"@actions/artifact": "4.0.0",
|
||||||
"tailwindcss": "catalog:",
|
"@tauri-apps/cli": "^2",
|
||||||
"virtua": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"zod": "catalog:"
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
packages/desktop/scripts/prepare.ts
Executable file
15
packages/desktop/scripts/prepare.ts
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env bun
|
||||||
|
import { $ } from "bun"
|
||||||
|
|
||||||
|
import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
|
||||||
|
|
||||||
|
const sidecarConfig = getCurrentSidecar()
|
||||||
|
|
||||||
|
const dir = "src-tauri/target/opencode-binaries"
|
||||||
|
|
||||||
|
await $`mkdir -p ${dir}`
|
||||||
|
await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
|
||||||
|
|
||||||
|
await copyBinaryToSidecarFolder(
|
||||||
|
`${dir}/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`,
|
||||||
|
)
|
||||||
|
|
@ -21,6 +21,11 @@ export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; ass
|
||||||
ocBinary: "opencode-linux-x64",
|
ocBinary: "opencode-linux-x64",
|
||||||
assetExt: "tar.gz",
|
assetExt: "tar.gz",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rustTarget: "aarch64-unknown-linux-gnu",
|
||||||
|
ocBinary: "opencode-linux-arm64",
|
||||||
|
assetExt: "tar.gz",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const RUST_TARGET = Bun.env.RUST_TARGET
|
export const RUST_TARGET = Bun.env.RUST_TARGET
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue