Compare commits

...

121 commits

Author SHA1 Message Date
Adam
43e92b4932
deps: diffs, shiki updates
Some checks failed
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
generate / generate (push) Has been cancelled
publish / publish (push) Has been cancelled
Update Nix Hashes / update (push) Has been cancelled
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Has been cancelled
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404-arm target:aarch64-unknown-linux-gnu]) (push) Has been cancelled
publish / publish-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Has been cancelled
publish / publish-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
publish / publish-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
publish / publish-release (push) Has been cancelled
2025-12-23 04:08:42 -06:00
opencode
83397ebde2 release: v1.0.191 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 Silverlock
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]
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
lif
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 Hooks
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
Some checks are pending
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
publish / publish-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / publish (push) Waiting to run
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404-arm target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / publish-release (push) Blocked by required conditions
test / test (push) Waiting to run
Update Nix Hashes / update (push) Waiting to run
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 Alhashemi
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 Cline
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 2025-12-22 23:31:20 +00:00
GitHub Action
b25418e68b chore: generate 2025-12-22 23:24:39 +00:00
Mohammad Alhashemi
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 2025-12-22 23:15:23 +00:00
GitHub Action
b229aeec0b chore: generate 2025-12-22 22:47:22 +00:00
Jay V
c9140c6bab docs: edit gitlab 2025-12-22 22:47:22 +00:00
opencode
38551bda38 release: v1.0.188 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
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 Nagy
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 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
Shpetim
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 Redeker
224e5466c1
docs: add opencode-morph-fast-apply plugin to ecosystem (#5992) 2025-12-22 15:21:14 -06:00
Blake North
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 Godha
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
wienans
7dc55ac3ca
Add OpenChamber to ecosystem documentation (#5978) 2025-12-22 14:33:45 -06:00
ja
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 Kleinschmidt
750a936ae1
support clojure projects with built-in lsp (#5975) 2025-12-22 14:20:15 -06:00
Shpetim
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 Polito
6baee0791f
docs: Github Auto Pull Request Docs (#5974) 2025-12-22 11:53:58 -06:00
Adam
291b65977c
chore(desktop): auto scroll utility
Some checks are pending
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
publish / publish (push) Waiting to run
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404-arm target:aarch64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / publish-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / publish-release (push) Blocked by required conditions
test / test (push) Waiting to run
Update Nix Hashes / update (push) Waiting to run
2025-12-22 11:27:27 -06:00
GitHub Action
90f232d7f1 chore: generate 2025-12-22 17:06:35 +00:00
Will Marella
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 Polito
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ë Dobruna
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
Buck Evan
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 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 Kumar
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
lif
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 Iker
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
NN708
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
Some checks are pending
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
publish / publish (push) Waiting to run
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / publish-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / publish-release (push) Blocked by required conditions
test / test (push) Waiting to run
Update Nix Hashes / update (push) Waiting to run
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 Cline
345f4801e8
feat: add experimental lsp tool (#5886) 2025-12-22 00:34:21 -06:00
Luo Chen
ac4b8d62e3
feat: add nixd as lsp for nix language (#5929) 2025-12-22 00:30:08 -06:00
opencode-agent[bot]
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
Valerio Di Maggio
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 Vargas
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 Daquioag
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
Noam Bressler
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 Silverlock
4f73d58031
prompts: improve built-in /review prompt (#5918) 2025-12-21 21:11:48 -06:00
YeonGyu-Kim
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
Some checks are pending
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
publish / publish (push) Waiting to run
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / publish-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / publish-release (push) Blocked by required conditions
test / test (push) Waiting to run
Update Nix Hashes / update (push) Waiting to run
2025-12-22 00:45:30 +00:00
Dax
8fe0715928
feat: add Agent Skills support (#5921) 2025-12-21 19:44:56 -05:00
opencode
cb8af962cd release: v1.0.185 2025-12-21 23:38:40 +00:00
Dax Raad
c333ffa38b core: fix LSP server binary installation and shell command execution
- Ensure proper file permissions are set for installed LSP binaries on non-Windows platforms
- Add error handling for shell command execution in prompt system to prevent crashes
2025-12-21 18:33:37 -05:00
Aiden Cline
3456f4ed80 tweak: update kimi-k2 and kimi-k2-thinking to use recommended temperature values 2025-12-21 15:54:57 -06:00
Dax Raad
2536e9f45b tui: fix SDK context usage and server port fallback
- Update SDK context to return client instead of event for proper usage
- Add server port fallback to 4096 when port 0 is specified but unavailable
- Fix SDK event listener usage in TUI app
2025-12-21 14:57:55 -05:00
Github Action
9188bc542c Update Nix flake.lock and hashes 2025-12-21 17:46:30 +00:00
GitHub Action
cbaba10994 chore: generate 2025-12-21 17:45:51 +00:00
Sherlock Holmes
85d3604309
fix(deps): add missing @opencode-ai/plugin to dependencies (#5797) 2025-12-21 11:45:20 -06:00
Nalin Singh
507ba644cf
feat: add syntax highlighting for .ets files (#5889) 2025-12-21 11:42:47 -06:00
Adam Hosker
3d6f62746a
fix: prevent stats workflow from running on forks (#5897) 2025-12-21 11:32:00 -06:00
Abdelkader Boudih
2f48c8c05f
fix: use official MCP SDK for better tool schema handling (#5463) 2025-12-21 11:31:07 -06:00
GitHub Action
4828fd1eac chore: generate
Some checks failed
deploy / deploy (push) Waiting to run
generate / generate (push) Waiting to run
publish / publish (push) Waiting to run
publish / publish-tauri (map[host:blacksmith-4vcpu-ubuntu-2404 target:x86_64-unknown-linux-gnu]) (push) Blocked by required conditions
publish / publish-tauri (map[host:blacksmith-4vcpu-windows-2025 target:x86_64-pc-windows-msvc]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:aarch64-apple-darwin]) (push) Blocked by required conditions
publish / publish-tauri (map[host:macos-latest target:x86_64-apple-darwin]) (push) Blocked by required conditions
publish / publish-release (push) Blocked by required conditions
test / test (push) Waiting to run
Update Nix Hashes / update (push) Waiting to run
release-github-action / release (push) Has been cancelled
2025-12-21 14:47:27 +00:00
378 changed files with 5192 additions and 2329 deletions

69
.github/workflows/docs-update.yml vendored Normal file
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.

View file

@ -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 }}

View file

@ -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

View file

@ -0,0 +1,6 @@
---
name: test-skill
description: use this when asked to test skill
---
woah this is a test skill

View file

@ -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"
}
}
]
}
}

View file

@ -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) |

634
bun.lock

File diff suppressed because it is too large Load diff

6
flake.lock generated
View file

@ -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": {

View file

@ -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",
},
})

View file

@ -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,

View file

@ -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",

View file

@ -1,3 +1,3 @@
{ {
"nodeModules": "sha256-cpXmqJQJeFj3eED/aOb4YLUdkZFV//7u4f0STBxzUhk=" "nodeModules": "sha256-QlQblkUq49DOdvNNMNAzHHAfHxR6cZNmJtyzc4rD168="
} }

View file

@ -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
View file

@ -0,0 +1 @@
src/assets/theme.css

34
packages/app/README.md Normal file
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.)

View file

@ -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
View 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:"
}
}

View file

@ -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 (

View file

@ -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>

View file

@ -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)
}} }}

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View 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>
)
}

View file

@ -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)}

View file

@ -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) => {

View file

@ -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) {

View file

@ -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)
} }

View file

@ -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 />

View file

@ -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&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 ( 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&nbsp;
<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

View file

@ -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"]
} }

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,
},
})

View file

@ -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",

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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"}`

View file

@ -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 }),
), ),
) )

View file

@ -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": {

View file

@ -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
} }
} }

View file

@ -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",

View file

@ -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
} }
} }

View file

@ -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",

View file

@ -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
} }
} }

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?

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 - [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.)

View file

@ -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>

View file

@ -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:"
} }
} }

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" : ""}`,
)

View file

@ -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