Compare commits

..
123 Commits
Author SHA1 Message Date
opencode 66563fb974 release: v1.0.193
Update Nix Hashes / update (push) Has been cancelled
2025-12-23 15:16:20 +00:00
GitHub Action fbece0dc4d chore: generate 2025-12-23 14:45:27 +00:00
Sebastian Herrlinger 1d9e181da0 indent wrapped todo items properly 2025-12-23 15:44:45 +01:00
opencode c81721e9fc release: v1.0.192
Update Nix Hashes / update (push) Has been cancelled
2025-12-23 14:43:07 +00:00
Frank a94899ed36 zen: glm 4.7 2025-12-23 07:56:37 -05:00
Frank b18d22498c ci: fix 2025-12-23 07:53:33 -05:00
Frank c75584a31b ci: fix 2025-12-23 07:38:42 -05:00
GitHub Action b474f65547 ignore: update download stats 2025-12-23 2025-12-23 12:04:42 +00:00
Sebastian Herrlinger c352999b41 no intermediate autocomplete result to avoid flickering 2025-12-23 12:22:34 +01:00
GitHub Action f4cd708ca0 chore: generate 2025-12-23 10:15:49 +00:00
Brendan Allan c20f2731ab desktop: kill_sidecar before update install on windows 2025-12-23 18:14:44 +08:00
Github Action 01ca1a384a Update Nix flake.lock and hashes 2025-12-23 10:12:37 +00:00
GitHub Action f330dadd89 chore: generate 2025-12-23 10:11:53 +00:00
Adam 43e92b4932 deps: diffs, shiki updates 2025-12-23 04:08:42 -06:00
opencode 83397ebde2 release: v1.0.191
Update Nix Hashes / update (push) Has been cancelled
2025-12-23 05:57:23 +00:00
GitHub Action fde74a72bb chore: generate 2025-12-23 05:53:02 +00:00
Brendan Allan 10ee8e5b3d console: add AppImage download link 2025-12-23 13:52:23 +08:00
GitHub Action 96d3f1fe7c chore: generate 2025-12-23 04:28:11 +00:00
Matt SilverlockandGitHub 1a2b656c4d improve mcp CLI + ability to debug MCP oauth (#5980) 2025-12-22 22:27:38 -06:00
Aiden Cline 161e9287a8 ci: docs sync 2025-12-22 22:27:21 -06:00
opencode-agent[bot]GitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
968543af39 docs: new /global/health API (#6006)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-22 22:26:47 -06:00
5af35117db fix: handle Windows CRLF line endings in grep tool (#5948)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 22:26:15 -06:00
Joel HooksandGitHub eab177f5e7 feat(plugin): allow compaction hook to replace prompt entirely (#5907) 2025-12-22 22:19:14 -06:00
Brendan Allan 279dc04b3c ci: rename tauri -> desktop 2025-12-23 11:15:19 +08:00
Github Action cbc5903aa1 Update Nix flake.lock and hashes 2025-12-23 02:03:30 +00:00
Adam 81c3c63895 chore: rename packages/tauri -> packages/desktop 2025-12-22 20:01:25 -06:00
Github Action b76bd4141d Update Nix flake.lock and hashes 2025-12-23 01:40:34 +00:00
Adam 794fe8f381 chore: rename packages/desktop -> packages/app 2025-12-22 19:39:00 -06:00
GitHub Action a4eebf9f08 chore: generate 2025-12-23 01:17:33 +00:00
Adam 680a63e3de fix(desktop): better error messages on connection failure 2025-12-22 19:16:54 -06:00
Mohammad AlhashemiandGitHub 3a54ab68d1 feat(skill): add per-agent filtering to skill tool description (#6000) 2025-12-22 20:14:33 -05:00
Frank 44fd0eee64 zen: glm 4.7 2025-12-22 19:36:07 -05:00
Aiden ClineandGitHub ac371d2987 feat: better styling for small screens (short and/or not wide) (#5968) 2025-12-22 18:00:26 -06:00
GitHub Action a7baa5ce18 chore: generate 2025-12-22 23:40:52 +00:00
Dax Raad b129f809b9 tui: change task tool container to block layout for better subagent session display 2025-12-22 18:40:15 -05:00
opencode 92c0ab51e2 release: v1.0.190
Update Nix Hashes / update (push) Has been cancelled
2025-12-22 23:31:20 +00:00
GitHub Action b25418e68b chore: generate 2025-12-22 23:24:39 +00:00
046e351140 feat: add native skill tool with permission system (#5930)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-12-22 18:24:06 -05:00
opencode b9029afa22 release: v1.0.189
Update Nix Hashes / update (push) Has been cancelled
2025-12-22 23:15:23 +00:00
GitHub Actionandopencode b229aeec0b chore: generate 2025-12-22 22:47:22 +00:00
Jay Vandopencode c9140c6bab docs: edit gitlab 2025-12-22 22:47:22 +00:00
opencode 38551bda38 release: v1.0.188
Update Nix Hashes / update (push) Has been cancelled
2025-12-22 22:47:21 +00:00
Github Action cd16d31510 Update Nix flake.lock and hashes 2025-12-22 22:37:48 +00:00
Frank 54ba1af5d6 remove sharp 2025-12-22 17:36:23 -05:00
Josh ThomasandGitHub fe3144ce5b fix(tui): resize textarea if text inserted via appendPrompt TUI API (#5983) 2025-12-22 16:29:18 -06:00
Frank a1c0bae3af zen: add glm 4.7 2025-12-22 17:23:33 -05:00
Aiden Cline 85f8655dfd ignore: agents.md 2025-12-22 16:21:56 -06:00
Adam 9b6c9f64f7 feat(desktop): review pane toggle 2025-12-22 16:20:17 -06:00
Viktor NagyandJay V 1aae1c795d Add gitlab-opencode to GitLab docs
The current GitLab page describes OpenCode integration through GitLab Duo.

GitLab Duo is a paying functionality and is limited to workflows supported by GitLab.

GitLab-OpenCode is a community project that offers more flexiblity, better customization and easier setup to use OpenCode in GitLab. On the downside, it does not have the level of integration into GitLab as Duo does.
2025-12-22 17:14:59 -05:00
Frank 526c723e62 support glm 4.7 2025-12-22 17:11:02 -05:00
GitHub Action 6011200128 chore: generate 2025-12-22 22:01:27 +00:00
Jay V 740fcd243c ignore: update GitHub stars to 41K and project stats to reflect current growth 2025-12-22 17:00:27 -05:00
opencode e4d8a117c4 release: v1.0.187
Update Nix Hashes / update (push) Has been cancelled
2025-12-22 21:58:41 +00:00
Aiden Cline 8c4a816cf6 ci: add failure case for changelog 2025-12-22 15:53:41 -06:00
Aiden Cline 5605fc3f38 test: rm claude skills test 2025-12-22 15:45:31 -06:00
Aiden Cline 009b096004 fix: disable claude skill loading for now 2025-12-22 15:40:08 -06:00
64f898601b fix: stop auto execute on sendText vscode extension (#5994)
Co-authored-by: Shpetim <shpetim.alimi@ndbit.net>
2025-12-22 15:38:54 -06:00
Jon RedekerandGitHub 224e5466c1 docs: add opencode-morph-fast-apply plugin to ecosystem (#5992) 2025-12-22 15:21:14 -06:00
Blake NorthandGitHub 87b5b34280 fix(providers.opencode): check config for api key in addition to auth (#5906) 2025-12-22 15:20:40 -06:00
Github Action 855fd07d22 Update Nix flake.lock and hashes 2025-12-22 21:13:50 +00:00
Aiden Cline f9be2bab3a fix: bundle more providers to fix breaking ai sdk issue 2025-12-22 15:12:18 -06:00
Rohan GodhaandGitHub 25f1643e8e feat(tui): go to parent keybind for subagents (#5762) 2025-12-22 14:50:45 -06:00
GitHub Action e015bea462 chore: generate 2025-12-22 20:34:21 +00:00
wienansandGitHub 7dc55ac3ca Add OpenChamber to ecosystem documentation (#5978) 2025-12-22 14:33:45 -06:00
jaandGitHub cd8ecf9722 feat(lsp): add Tinymist LSP support for Typst (#5933) 2025-12-22 14:31:47 -06:00
Github Action eb021a5f92 Update Nix flake.lock and hashes 2025-12-22 20:27:29 +00:00
Sebastian Herrlinger 7f5e30834f upgrade opentui to v0.1.63, enabling kitty alternate keys by default 2025-12-22 21:26:03 +01:00
Tim KleinschmidtandGitHub 750a936ae1 support clojure projects with built-in lsp (#5975) 2025-12-22 14:20:15 -06:00
ShpetimandGitHub 8dfef670b3 [FEATURE]: Show context usage in OpenCode Desktop Context usage (#5979) 2025-12-22 13:56:36 -06:00
Adam 1b1b73b5b3 fix(prompt): better summary prompt 2025-12-22 13:09:12 -06:00
Daniel PolitoandGitHub 6baee0791f docs: Github Auto Pull Request Docs (#5974) 2025-12-22 11:53:58 -06:00
Adam 291b65977c chore(desktop): auto scroll utility 2025-12-22 11:27:27 -06:00
GitHub Action 90f232d7f1 chore: generate 2025-12-22 17:06:35 +00:00
Will MarellaGitHubWill@Cambridge <willcambridge@MacBook-Pro-59.local>Will@Cambridge <willcambridge@macbookpro.mynetworksettings.com>
af214d35cb Add keybindable commands to navigate between user messages (#5078)
Co-authored-by: Will@Cambridge <willcambridge@MacBook-Pro-59.local>
Co-authored-by: Will@Cambridge <willcambridge@macbookpro.mynetworksettings.com>
2025-12-22 11:06:00 -06:00
Aiden Cline 3f0afd7cf6 ci: tweak docs prompt 2025-12-22 11:00:32 -06:00
Daniel PolitoandGitHub 0545c5da2d GitHub pull request event (#5335) 2025-12-22 10:59:02 -06:00
Adam 4a32fa6f02 fix(share): expanded state and responsiveness 2025-12-22 10:13:57 -06:00
Aiden Cline 29c99ed4ab ci: limit to opencode repo 2025-12-22 09:56:51 -06:00
Lekë DobrunaandGitHub 753abbe164 fix: duplicate words in dialog options (#5944) 2025-12-22 09:56:32 -06:00
Adam 8e01f6cc13 fix(desktop): diff readability (colors) 2025-12-22 09:51:21 -06:00
Dax Raad 33c0b125cb fix url for web 2025-12-22 10:45:51 -05:00
GitHub Action dab2e54df8 chore: generate 2025-12-22 14:38:20 +00:00
60db171b44 fix(read): narrow .env file blocking to not block .envrc (#5654)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 08:37:43 -06:00
opencode c6e9a5c800 release: v1.0.186
Update Nix Hashes / update (push) Has been cancelled
2025-12-22 12:14:06 +00:00
Adam 2c16b9fa61 Revert "server: ensure frontend has correct port for PTY websocket connections (#5898)"
This reverts commit a05915ddc8.
2025-12-22 06:05:46 -06:00
Adam 240ad31edd Revert "fix: server"
This reverts commit dbaac79039.
2025-12-22 06:05:46 -06:00
GitHub Action a97631f769 ignore: update download stats 2025-12-22 2025-12-22 12:05:15 +00:00
Adam dbaac79039 fix: server 2025-12-22 06:02:16 -06:00
Ashutosh KumarandGitHub a05915ddc8 server: ensure frontend has correct port for PTY websocket connections (#5898) 2025-12-22 05:56:23 -06:00
Adam eebbd73346 Revert "fix: use current page port instead of hardcoded 4096 (#5949)"
This reverts commit d04a72a4ad.
2025-12-22 05:55:15 -06:00
Adam d4c981495a fix(desktop): cleanup auto scroll 2025-12-22 05:46:07 -06:00
Adam 653c206688 feat(desktop): mobile responsiveness 2025-12-22 05:46:07 -06:00
Adam 580f46b589 fix(desktop): filter child sessions from header 2025-12-22 05:46:07 -06:00
Adam 986d12fd20 feat(desktop): better task tool rendering 2025-12-22 05:46:06 -06:00
d04a72a4ad fix: use current page port instead of hardcoded 4096 (#5949)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 05:45:08 -06:00
Aaron IkerandGitHub 5fd873a35a feat: polish dialog & list styles for the desktop app, add fixed logos from models.dev (#5925) 2025-12-22 05:41:38 -06:00
Brendan Allan a9fbd786b3 ci: fix tauri build args 2025-12-22 18:55:01 +08:00
Brendan Allan abde984b3e ci: verbose build and re-enable appimage 2025-12-22 18:48:58 +08:00
GitHub Action a95aa037a3 chore: generate 2025-12-22 10:06:29 +00:00
Brendan Allan 11a92b24c2 ci: run prepare step for tauri build 2025-12-22 18:05:52 +08:00
Brendan Allan f9c10c62d8 ci: try downloading artifact in desktop prepare 2025-12-22 17:45:57 +08:00
NN708andGitHub 6339f39871 feat(desktop): arm64 build for linux (#5935) 2025-12-22 03:45:30 -06:00
Brendan Allan 68b09b30a1 ci: replace with just upload-artifact whole dir 2025-12-22 17:17:17 +08:00
Brendan Allan 92ade2a320 ci: import bun shell 2025-12-22 17:10:43 +08:00
Brendan Allan cb1a1fb26c try uploading artifacts in workflow 2025-12-22 17:08:44 +08:00
Brendan Allan af5ebabd03 remove actions artifact uploading 2025-12-22 17:01:52 +08:00
Github Action fe2626a4ea Update Nix flake.lock and hashes 2025-12-22 08:58:57 +00:00
GitHub Action 45447e3336 chore: generate 2025-12-22 08:58:16 +00:00
Brendan Allan 7a3e82ec5d ci: try to upload cli artifacts 2025-12-22 16:57:28 +08:00
Aiden ClineandGitHub 345f4801e8 feat: add experimental lsp tool (#5886) 2025-12-22 00:34:21 -06:00
Luo ChenandGitHub ac4b8d62e3 feat: add nixd as lsp for nix language (#5929) 2025-12-22 00:30:08 -06:00
opencode-agent[bot]GitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-nodeAiden Cline
236ce7a8c0 docs: Agent Skills (#5931)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-21 23:49:28 -06:00
8bdc0c8f79 fix: ensure installation commands are using .quiet (#5758)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-21 23:44:25 -06:00
Ben VargasandGitHub 04650f01fe docs: add ai-sdk-provider-opencode-sdk to ecosystem (#5772) 2025-12-21 22:59:36 -06:00
Aiden Cline 02d4594abf ci: update docs prompt 2025-12-21 22:32:04 -06:00
Aiden Cline c1894b4e3d ci: add automatic doc update workflow 2025-12-21 21:52:44 -06:00
Neil DaquioagandGitHub 2062247e72 fix: support clipboard image paste (Ctrl+V) on Windows (#5919) 2025-12-21 21:18:47 -06:00
Aiden Cline 8785bec29c tweak: adjust minimax m2 topK and add minimax m2.1 topP 2025-12-21 21:17:58 -06:00
d4b7f75ce3 fix: Perform snapshot in cases finish-step is not reached (#5912)
Co-authored-by: noamzbr <noamzbr@users.noreply.github.com>
2025-12-21 21:13:11 -06:00
Matt SilverlockandGitHub 4f73d58031 prompts: improve built-in /review prompt (#5918) 2025-12-21 21:11:48 -06:00
YeonGyu-KimandGitHub b906f2de88 feat(server): expose auto param in session.summarize for plugins (#5924) 2025-12-21 21:05:30 -06:00
GitHub Action 4035afe5c8 chore: generate 2025-12-22 00:45:30 +00:00
DaxandGitHub 8fe0715928 feat: add Agent Skills support (#5921) 2025-12-21 19:44:56 -05:00
377 changed files with 5117 additions and 2265 deletions
+69
View 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.
+13 -5
View File
@@ -79,6 +79,12 @@ jobs:
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
- uses: actions/upload-artifact@v4
with:
name: opencode-cli
path: packages/opencode/dist
outputs:
release: ${{ steps.publish.outputs.release }}
tag: ${{ steps.publish.outputs.tag }}
@@ -99,6 +105,8 @@ jobs:
target: x86_64-pc-windows-msvc
- host: blacksmith-4vcpu-ubuntu-2404
target: x86_64-unknown-linux-gnu
- host: blacksmith-4vcpu-ubuntu-2404-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v3
@@ -143,13 +151,12 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/tauri/src-tauri
workspaces: packages/desktop/src-tauri
shared-key: ${{ matrix.settings.target }}
- name: Prepare
if: inputs.bump || inputs.version
run: |
cd packages/tauri
cd packages/desktop
bun ./scripts/prepare.ts
env:
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
@@ -159,6 +166,7 @@ jobs:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
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
- name: Install tauri-cli from portable appimage branch
@@ -183,10 +191,10 @@ jobs:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
with:
projectPath: packages/tauri
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.publish.outputs.release }}
tagName: ${{ needs.publish.outputs.tag }}
+6
View File
@@ -0,0 +1,6 @@
---
name: test-skill
description: use this when asked to test skill
---
woah this is a test skill
+1 -28
View File
@@ -4,31 +4,4 @@
## Tool Calling
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
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"
}
}
]
}
}
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
+2
View File
@@ -177,3 +177,5 @@
| 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-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) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
+334 -299
View File
File diff suppressed because it is too large Load Diff
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766125104,
"narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
"lastModified": 1766314097,
"narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
"rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055",
"type": "github"
},
"original": {
+9
View File
@@ -44,3 +44,12 @@ new sst.cloudflare.x.Astro("Web", {
VITE_API_URL: api.url.apply((url) => url!),
},
})
new sst.cloudflare.StaticSite("WebApp", {
domain: "app." + domain,
path: "packages/app",
build: {
command: "bun turbo build",
output: "./dist",
},
})
+2
View File
@@ -118,6 +118,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
////////////////
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_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",
link: [
bucket,
bucketNew,
database,
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,
+1 -5
View File
@@ -2,9 +2,5 @@ import { domain } from "./stage"
new sst.cloudflare.StaticSite("Desktop", {
domain: "desktop." + domain,
path: "packages/desktop",
build: {
command: "bun turbo build",
output: "./dist",
},
path: "packages/app/public",
})
+1 -1
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-XhU8gEwLPUtzFhMfg+QxExn5/WiDo5VVOiZ0AmklRwc="
"nodeModules": "sha256-CDOAY2h2AAcSuVqV1uyxDmfzSa/vV8lnXOKDgAC4mgg="
}
+11 -3
View File
@@ -31,7 +31,7 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.0.0-beta.3",
"@pierre/diffs": "1.0.2",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
@@ -44,6 +44,7 @@
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"shiki": "3.20.0",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"virtua": "0.42.3",
@@ -56,6 +57,7 @@
}
},
"devDependencies": {
"@actions/artifact": "5.0.1",
"@tsconfig/bun": "catalog:",
"husky": "9.1.7",
"prettier": "3.6.2",
@@ -63,10 +65,17 @@
"turbo": "2.5.6"
},
"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",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"typescript": "catalog:"
},
"repository": {
@@ -81,7 +90,6 @@
"trustedDependencies": [
"esbuild",
"protobufjs",
"sharp",
"tree-sitter",
"tree-sitter-bash",
"web-tree-sitter"
+1
View File
@@ -0,0 +1 @@
src/assets/theme.css
+34
View 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="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
<script>
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
@@ -23,6 +23,6 @@
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<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>
</html>
+62
View File
@@ -0,0 +1,62 @@
{
"name": "@opencode-ai/app",
"version": "1.0.193",
"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 Session from "@/pages/session"
import { ErrorPage } from "./pages/error"
import { iife } from "@opencode-ai/util/iife"
declare global {
interface Window {
@@ -28,14 +29,17 @@ declare global {
}
}
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
const port = window.__OPENCODE__?.port ?? import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
const url = iife(() => {
const param = new URLSearchParams(document.location.search).get("url")
if (param) return param
const url =
new URLSearchParams(document.location.search).get("url") ||
(location.hostname.includes("opencode.ai") || location.hostname.includes("localhost")
? `http://${host}:${port}`
: "/")
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
if (import.meta.env.DEV)
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() {
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 { 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 { 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 { DialogSelectProvider } from "./dialog-select-provider"
export function DialogConnectProvider(props: { provider: string }) {
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="">
<List
ref={(ref) => (listRef = ref)}
ref={(ref) => {
listRef = ref
}}
items={methods}
key={(m) => m?.label}
onSelect={async (method, index) => {
@@ -163,7 +165,7 @@ export function DialogConnectProvider(props: { provider: string }) {
}}
>
{(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-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
</div>
@@ -175,7 +177,7 @@ export function DialogConnectProvider(props: { provider: string }) {
</Match>
<Match when={store.state === "pending"}>
<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 />
<span>Authorization in progress...</span>
</div>
@@ -183,7 +185,7 @@ export function DialogConnectProvider(props: { provider: string }) {
</Match>
<Match when={store.state === "error"}>
<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" />
<span>Authorization failed: {store.error}</span>
</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 { List } from "@opencode-ai/ui/list"
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 = () => {
const local = useLocal()
return (
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
<List
class="px-2.5"
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
key={(x) => `${x?.provider?.id}:${x?.id}`}
@@ -27,16 +26,24 @@ export const DialogManageModels: Component = () => {
}}
onSelect={(x) => {
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)
}}
>
{(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>
<div onClick={(e) => e.stopPropagation()}>
<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) => {
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 { 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 { createMemo } from "solid-js"
import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local"
export function DialogSelectFile() {
const layout = useLayout()
@@ -18,7 +18,6 @@ export function DialogSelectFile() {
return (
<Dialog title="Select file">
<List
class="px-2.5"
search={{ placeholder: "Search files", autofocus: true }}
emptyMessage="No files found"
items={local.file.searchFiles}
@@ -32,7 +31,7 @@ export function DialogSelectFile() {
>
{(i) => (
<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" />
<div class="flex items-center text-14-regular">
<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 { Tag } from "@opencode-ai/ui/tag"
import { useDialog } from "@opencode-ai/ui/context/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 { IconName } from "@opencode-ai/ui/icons/provider"
import { DialogSelectProvider } from "./dialog-select-provider"
import { Tag } from "@opencode-ai/ui/tag"
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 { DialogSelectProvider } from "./dialog-select-provider"
export const DialogSelectModelUnpaid: Component = () => {
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="w-full">
<List
class="w-full"
class="w-full px-0"
key={(x) => x?.id}
items={providers.popular}
activeIcon="plus-small"
@@ -79,17 +79,8 @@ export const DialogSelectModelUnpaid: Component = () => {
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-4">
<ProviderIcon
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",
}}
/>
<div class="w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
@@ -35,7 +35,6 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
}
>
<List
class="px-2.5"
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
key={(x) => `${x.provider.id}:${x.id}`}
@@ -61,7 +60,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
}}
>
{(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>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Tag>Free</Tag>
@@ -15,7 +15,6 @@ export const DialogSelectProvider: Component = () => {
return (
<Dialog title="Connect provider">
<List
class="px-2.5"
search={{ placeholder: "Search providers", autofocus: true }}
activeIcon="plus-small"
key={(x) => x?.id}
@@ -38,17 +37,8 @@ export const DialogSelectProvider: Component = () => {
}}
>
{(i) => (
<div class="px-1.25 w-full flex items-center gap-x-4">
<ProviderIcon
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",
}}
/>
<div class="px-1.25 w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
@@ -20,6 +20,7 @@ import { iife } from "@opencode-ai/util/iife"
export function Header(props: {
navigateToProject: (directory: string) => void
navigateToSession: (session: Session | undefined) => void
onMobileMenuToggle?: () => void
}) {
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
@@ -29,11 +30,19 @@ export function Header(props: {
return (
<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
href="/"
classList={{
"hidden xl:flex": 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,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
@@ -46,30 +55,32 @@ export function Header(props: {
{(directory) => {
const currentDirectory = createMemo(() => base64Decode(directory()))
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 shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
</div>
<Select
options={sessions()}
current={currentSession()}
@@ -77,12 +88,13 @@ export function Header(props: {
label={(x) => x.title}
value={(x) => x.id}
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"
/>
</div>
<Show when={currentSession()}>
<Tooltip
class="hidden xl:block"
value={
<div class="flex items-center gap-2">
<span>New session</span>
@@ -98,7 +110,36 @@ export function Header(props: {
</div>
<div class="flex items-center gap-4">
<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={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
@@ -22,6 +22,7 @@ import { useProviders } from "@/hooks/use-providers"
import { useCommand } from "@/context/command"
import { persisted } from "@/utils/persist"
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_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}>
<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"
? "Enter shell command..."
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
@@ -1026,12 +1027,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
>
{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" />
</Button>
</Tooltip>
</Match>
</Switch>
<SessionContextUsage />
</div>
<div class="flex items-center gap-1 absolute right-2 bottom-2">
<input
@@ -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 (
<Dialog title="Commands">
<List
class="px-2.5"
search={{ placeholder: "Search commands", autofocus: true }}
emptyMessage="No commands found"
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
@@ -295,6 +295,15 @@ function createGlobalSync() {
})
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([
retry(() =>
globalSDK.client.path.get().then((x) => {
@@ -46,6 +46,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
opened: false,
height: 280,
},
review: {
opened: true,
},
session: {
width: 600,
},
@@ -108,10 +111,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
},
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) {
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) {
setStore("projects", (projects) => {
@@ -156,6 +161,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
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: {
width: createMemo(() => store.session?.width ?? 600),
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"
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
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) {
const parts = [indent + `${error.name}: ${error.message}`]
if (error.stack) {
parts.push(error.stack)
const isDuplicate = depth > 0 && parentMessage === error.message
const parts: string[] = []
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) {
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")
}
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)
}
@@ -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 { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
@@ -42,9 +54,29 @@ export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
lastSession: {} as { [directory: string]: string },
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
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 globalSDK = useGlobalSDK()
@@ -259,11 +291,13 @@ export default function Layout(props: ParentProps) {
if (!directory) return
const lastSession = store.lastSession[directory]
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
mobileSidebar.hide()
}
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
mobileSidebar.hide()
}
function openProject(directory: string, navigate = true) {
@@ -302,8 +336,12 @@ export default function Layout(props: ParentProps) {
})
createEffect(() => {
const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
if (isLargeViewport()) {
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 {
@@ -419,6 +457,7 @@ export default function Layout(props: ParentProps) {
project: LocalProject
depth?: number
childrenMap: Map<string, Session[]>
mobile?: boolean
}): JSX.Element => {
const notification = useNotification()
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"
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
href={`${props.slug}/session/${props.session.id}`}
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>
</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">
<Tooltip placement="right" value="Archive session">
<Tooltip placement={props.mobile ? "bottom" : "right"} value="Archive session">
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
</Tooltip>
</div>
@@ -499,6 +538,7 @@ export default function Layout(props: ParentProps) {
project={props.project}
depth={depth + 1}
childrenMap={props.childrenMap}
mobile={props.mobile}
/>
)}
</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 showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(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)
await globalSync.project.loadSessions(props.project.worktree)
}
const isExpanded = createMemo(() =>
props.mobile ? mobileProjects.expanded(props.project.worktree) : props.project.expanded,
)
const handleOpenChange = (open: boolean) => {
if (open) layout.projects.expand(props.project.worktree)
else layout.projects.collapse(props.project.worktree)
if (props.mobile) {
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 (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Switch>
<Match when={layout.sidebar.opened()}>
<Collapsible
variant="ghost"
open={props.project.expanded}
class="gap-2 shrink-0"
onOpenChange={handleOpenChange}
>
<Match when={showExpanded()}>
<Collapsible variant="ghost" open={isExpanded()} class="gap-2 shrink-0" onOpenChange={handleOpenChange}>
<Button
as={"div"}
variant="ghost"
@@ -556,7 +600,7 @@ export default function Layout(props: ParentProps) {
project={props.project}
class="group-hover/session:hidden"
expandable
notify={!props.project.expanded}
notify={!isExpanded()}
/>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</Collapsible.Trigger>
@@ -585,6 +629,7 @@ export default function Layout(props: ParentProps) {
slug={slug()}
project={props.project}
childrenMap={childSessionsByParent()}
mobile={props.mobile}
/>
)}
</For>
@@ -595,7 +640,7 @@ export default function Layout(props: ParentProps) {
>
<div class="flex items-center self-stretch w-full">
<div class="flex-1 min-w-0">
<Tooltip placement="right" value="New session">
<Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
<A
href={`${slug()}/session`}
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 (
<div class="relative flex-1 min-h-0 flex flex-col">
<Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
<div class="flex-1 min-h-0 flex">
<div
classList={{
"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">
const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
return (
<>
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Show when={!sidebarProps.mobile}>
<Tooltip
class="shrink-0"
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>
</div>
}
inactive={layout.sidebar.opened()}
inactive={expanded()}
>
<Button
variant="ghost"
@@ -715,110 +742,160 @@ export default function Layout(props: ParentProps) {
</Show>
</Button>
</Tooltip>
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
</Show>
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
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 />
<ConstrainDragXAxis />
<div
ref={scrollContainerRef}
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
>
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
</SortableProvider>
</div>
<DragOverlay>
<ProjectDragOverlay />
</DragOverlay>
</DragDropProvider>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Switch>
<Match when={!providers.paid().length && layout.sidebar.opened()}>
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
<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>
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
<For each={layout.projects.list()}>
{(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
</For>
</SortableProvider>
</div>
<DragOverlay>
<ProjectDragOverlay />
</DragOverlay>
</DragDropProvider>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Switch>
<Match when={!providers.paid().length && expanded()}>
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
<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>
</Match>
<Match when={true}>
<Tooltip placement="right" value="Connect provider" inactive={layout.sidebar.opened()}>
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
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>
Connect provider
</Button>
</Tooltip>
</Match>
</Switch>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip
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()}
>
</div>
</Match>
<Match when={true}>
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={chooseProject}
icon="plus"
onClick={connectProvider}
>
<Show when={layout.sidebar.opened()}>Open project</Show>
<Show when={expanded()}>Connect provider</Show>
</Button>
</Tooltip>
</Show>
{/* <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}> */}
{/* <Button */}
{/* disabled */}
{/* class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2" */}
{/* variant="ghost" */}
{/* size="large" */}
{/* icon="settings-gear" */}
{/* > */}
{/* <Show when={layout.sidebar.opened()}>Settings</Show> */}
{/* </Button> */}
{/* </Tooltip> */}
<Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
</Match>
</Switch>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip
placement="right"
value={
<div class="flex items-center gap-2">
<span>Open project</span>
<Show when={!sidebarProps.mobile}>
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
</Show>
</div>
}
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"
icon="folder-add-left"
onClick={chooseProject}
>
<Show when={layout.sidebar.opened()}>Share feedback</Show>
<Show when={expanded()}>Open project</Show>
</Button>
</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>
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
</div>
<Toast.Region />
@@ -12,6 +12,7 @@ import {
createRenderEffect,
batch,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import { useLocal, type LocalFile } from "@/context/local"
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 { useCodeComponent } from "@opencode-ai/ui/context/code"
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 { SessionReview } from "@opencode-ai/ui/session-review"
import {
@@ -70,7 +72,6 @@ export default function Page() {
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const revertMessageID = createMemo(() => info()?.revert?.messageID)
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
@@ -79,7 +80,6 @@ export default function Page() {
.filter((m) => m.role === "user")
.sort((a, b) => a.id.localeCompare(b.id)),
)
// Visible user messages excludes reverted messages (those >= revertMessageID)
const visibleUserMessages = createMemo(() => {
const revert = revertMessageID()
if (!revert) return userMessages()
@@ -87,15 +87,24 @@ export default function Page() {
})
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(() => {
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
const found = visibleUserMessages()?.find((m) => m.id === messageStore.messageId)
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
return found ?? lastUserMessage()
})
const setActiveMessage = (message: UserMessage | undefined) => {
setMessageStore("messageId", message?.id)
setStore("messageId", message?.id)
}
function navigateMessageByOffset(offset: number) {
@@ -119,13 +128,6 @@ export default function Page() {
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
createEffect(() => {
@@ -146,7 +148,7 @@ export default function Page() {
() => visibleUserMessages().at(-1)?.id,
(lastId, prevLastId) => {
if (lastId && prevLastId && lastId > prevLastId) {
setMessageStore("messageId", undefined)
setStore("messageId", undefined)
}
},
{ defer: true },
@@ -219,6 +221,15 @@ export default function Page() {
slash: "terminal",
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",
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&nbsp;
<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 (
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
<div class="min-h-0 grow w-full flex">
{/* Session pane - always visible */}
<div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
<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
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%" }}
>
<div class="flex-1 min-h-0 overflow-hidden">
<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}>
<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&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
</Match>
</Switch>
<DesktopSessionContent />
</div>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div
@@ -625,7 +725,6 @@ export default function Page() {
</Show>
</div>
{/* Tabs pane - visible when there are diffs or file tabs */}
<Show when={showTabs()}>
<div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
<DragDropProvider
@@ -683,7 +782,7 @@ export default function Page() {
</div>
<Show when={diffs().length}>
<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
classes={{
root: "pb-40",
@@ -754,9 +853,10 @@ export default function Page() {
</div>
</Show>
</div>
<Show when={layout.terminal.opened()}>
<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` }}
>
<ResizeHandle
@@ -1,5 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"skipLibCheck": true,
@@ -10,11 +12,13 @@
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": false,
"emitDeclarationOnly": true,
"outDir": "node_modules/.ts-dist",
"isolatedModules": true,
"noEmit": true,
"emitDeclarationOnly": false,
"outDir": "node_modules/.ts-dist"
"paths": {
"@/*": ["./src/*"]
}
},
"references": [{ "path": "../desktop" }],
"include": ["src"]
"exclude": ["dist", "ts-dist"]
}
+15
View 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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.185",
"version": "1.0.193",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+4 -4
View File
@@ -9,8 +9,8 @@ export const config = {
github: {
repoUrl: "https://github.com/sst/opencode",
starsFormatted: {
compact: "38K",
full: "38,000",
compact: "41K",
full: "41,000",
},
},
@@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "400",
commits: "5,000",
contributors: "450",
commits: "6,000",
monthlyUsers: "400,000",
},
} as const
@@ -6,6 +6,7 @@ const assetNames: Record<string, string> = {
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
"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",
} satisfies Record<DownloadPlatform, string>
@@ -244,6 +244,22 @@ export default function Download() {
Download
</a>
</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>
</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
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(
Resource.ZenData.put(
`data/${data.modelName}/${sessionId}/${requestId}.json`,
Resource.ZenDataNew.put(
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
JSON.stringify({ timestamp, ...data }),
),
)
waitUntil(
Resource.ZenData.put(
`meta/${data.modelName}/${timestamp}/${requestId}.json`,
Resource.ZenDataNew.put(
`meta/${data.modelName}/${sessionId}/${requestId}.json`,
JSON.stringify({ timestamp, ...metadata }),
),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.185",
"version": "1.0.193",
"private": true,
"type": "module",
"dependencies": {
+1
View File
@@ -132,6 +132,7 @@ declare module "sst" {
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.185",
"version": "1.0.193",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1
View File
@@ -132,6 +132,7 @@ declare module "sst" {
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.185",
"version": "1.0.193",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1
View File
@@ -132,6 +132,7 @@ declare module "sst" {
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
"ZenDataNew": cloudflare.R2Bucket
}
}
+24 -1
View File
@@ -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?
+4 -31
View File
@@ -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
$ 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.)
- [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)
+2 -2
View File
@@ -14,7 +14,7 @@
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<script>
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
@@ -23,6 +23,6 @@
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<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>
</html>
+26 -51
View File
@@ -1,62 +1,37 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.185",
"description": "",
"private": true,
"version": "1.0.193",
"type": "module",
"exports": {
".": "./src/index.ts",
"./vite": "./vite.js"
},
"scripts": {
"typecheck": "tsgo -b",
"start": "vite",
"predev": "bun ./scripts/predev.ts",
"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:"
"build": "bun run typecheck && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"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",
"@opencode-ai/app": "workspace:*",
"@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": "3.9.2",
"solid-js": "catalog:",
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:",
"zod": "catalog:"
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-store": "~2",
"@tauri-apps/plugin-updater": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-window-state": "~2",
"solid-js": "catalog:"
},
"devDependencies": {
"@actions/artifact": "4.0.0",
"@tauri-apps/cli": "^2",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "~5.6.2",
"vite": "catalog:"
}
}
+15
View 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",
assetExt: "tar.gz",
},
{
rustTarget: "aarch64-unknown-linux-gnu",
ocBinary: "opencode-linux-arm64",
assetExt: "tar.gz",
},
]
export const RUST_TARGET = Bun.env.RUST_TARGET
@@ -1,8 +1,8 @@
[package]
name = "opencode-desktop"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
description = "The open source AI coding agent"
authors = ["Anomaly Innovations"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Some files were not shown because too many files have changed in this diff Show More