Compare commits

...

863 Commits

Author SHA1 Message Date
Aiden Cline
3b6a21f173 wip 2025-12-28 18:14:31 -06:00
Aiden Cline
f7bdf0b74f wip 2025-12-28 18:14:31 -06:00
Aiden Cline
7539cbeea2 wip 2025-12-28 18:14:31 -06:00
Github Action
b683567c2c Update Nix flake.lock and hashes 2025-12-28 22:30:51 +00:00
Aiden Cline
6965805eeb adjust deps 2025-12-28 16:29:35 -06:00
Aiden Cline
7a511a1a25 wip 2025-12-28 01:20:18 -06:00
Aiden Cline
f3437d9a49 wip 2025-12-28 01:09:26 -06:00
Aiden Cline
ccf1826400 wip 2025-12-26 23:09:58 -06:00
Aiden Cline
f6b24d81d4 wip 2025-12-26 22:37:31 -06:00
Aiden Cline
3127f4f30e chore: kill some unused tools 2025-12-26 14:35:51 -06:00
Aiden Cline
d7a3a13034 test: add more tests to make sure that cwd is locked for read tool 2025-12-26 14:35:50 -06:00
Aiden Cline
a92af6b9ef tweak: bash tool description to avoid unnecessary 'cd &&' usage 2025-12-26 14:35:50 -06:00
Matt Silverlock
21e4fe3718 github: support issues and workflow_dispatch events (#6157) 2025-12-26 14:35:50 -06:00
Aiden Cline
fa64085aba ci: re-enable sync zed 2025-12-26 14:35:50 -06:00
Aiden Cline
06307dc8c3 ci: fix token for gh 2025-12-26 14:35:50 -06:00
Aiden Cline
1bf950179a ci: fix var 2025-12-26 14:35:50 -06:00
Aiden Cline
b112c8132c ci: update zed extension sync 2025-12-26 14:35:50 -06:00
Github Action
b279da3684 Update Nix flake.lock and hashes 2025-12-26 14:35:50 -06:00
GitHub Action
dcfeeb6919 chore: generate 2025-12-26 14:35:50 -06:00
Aiden Cline
0762a794ab Revert "feat(core): optional mdns service (#6192)"
This reverts commit 26e7043718.
2025-12-26 14:35:49 -06:00
Aiden Cline
f28628b744 chore: rm comments 2025-12-26 14:35:49 -06:00
Aiden Cline
336d165fd2 fix: adjust upgrade command to use gh releases page if not npm/bun/pnpm install method 2025-12-26 14:35:49 -06:00
Rohan Godha
af89e7f117 fix: opencode web baseURL error (#6181) 2025-12-26 14:35:49 -06:00
Ariane Emory
57ad8211d5 chore: kill the dead Polaris Alpha code (#6193) 2025-12-26 14:35:49 -06:00
ja
3fc23c3e8b feat: add shfmt formatter for shell scripts (#6204) 2025-12-26 14:35:49 -06:00
Aiden Cline
d6899cf9b0 tweak: make install script handle 404s better 2025-12-26 14:35:49 -06:00
Github Action
58c643f205 Update Nix flake.lock and hashes 2025-12-26 14:35:48 -06:00
GitHub Action
dcc5b7d6db chore: generate 2025-12-26 14:35:48 -06:00
Ayush Walekar
1c35e59e4f chore: createOpencodeServer expose logLevel (#6202) 2025-12-26 14:35:48 -06:00
Roberto Carvajal
c1d21f9b11 fix(dep): Update package.json - fix perplexity provider version (#6199)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-26 14:35:48 -06:00
Daniel Polito
822c20b320 Desktop: MCP UI (#6162)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-26 14:35:47 -06:00
GitHub Action
fbe676d1da chore: generate 2025-12-26 14:35:47 -06:00
Adam
3f3c0f370b feat(core): optional mdns service (#6192)
Co-authored-by: Github Action <action@github.com>
2025-12-26 14:35:47 -06:00
GitHub Action
3e4b8e2bf5 chore: generate 2025-12-26 14:35:47 -06:00
Didier Durand
ccd182f522 doc: fix typos in various files (#6196) 2025-12-26 14:35:47 -06:00
GitHub Action
bbf59fcb12 ignore: update download stats 2025-12-26 2025-12-26 14:35:46 -06:00
Aiden Cline
b40852abe3 tweak: update transform for gemini models so that topP and topK match gemini-cli values 2025-12-26 14:35:46 -06:00
GitHub Action
b6d8779c22 chore: generate 2025-12-26 14:35:46 -06:00
JackNorris
b479893c9a fix: only show diagnostics block when errors exist (#6175) 2025-12-26 14:35:46 -06:00
opencode
fbc9120dc3 release: v1.0.203 2025-12-26 14:35:45 -06:00
Dax Raad
95074ab541 prompt update to prevent searching via bash tool 2025-12-26 14:35:45 -06:00
Donghyun Shin
e98a2a5e47 fix(lsp): make JDTLS use the correct config directory on Windows (#6121) 2025-12-26 14:35:45 -06:00
GitHub Action
e60ef0286d chore: generate 2025-12-26 14:35:45 -06:00
Marco
a64e24a991 feat: haskell lsp support (#6141) 2025-12-26 14:35:45 -06:00
opencode
01ca39791d release: v1.0.202 2025-12-26 14:35:44 -06:00
Adam
13b6a47e77 chore: brain icon 2025-12-26 14:35:44 -06:00
Adam
6fab9d2ac3 fix(desktop): user message display 2025-12-26 14:35:44 -06:00
Adam
2381a83fc7 fix(desktop): padding 2025-12-26 14:35:44 -06:00
Adam
4ed0dc1dcc fix(desktop): move session context to top-right 2025-12-26 14:35:44 -06:00
Adam
01dd70eab1 fix(desktop): missing keybinds in tooltips 2025-12-26 14:35:44 -06:00
Adam
3c5529bf19 fix(desktop): markdown rendering perf 2025-12-26 14:35:44 -06:00
GitHub Action
9a2f955910 chore: generate 2025-12-26 14:35:44 -06:00
Adam
fefb9e5e0c fix(desktop): can't collapse project with active session 2025-12-26 14:35:44 -06:00
Adam
61bad1fd90 chore(ui): radio group primitive 2025-12-26 14:35:43 -06:00
Adam
e42d586460 feat(desktop): better indicator that session is busy 2025-12-26 14:35:43 -06:00
opencode
7c4d1e9443 release: v1.0.201 2025-12-26 14:35:43 -06:00
Adam
b5e5bf549e fix(desktop): so many prompt input fixes, merry christmas 2025-12-26 14:35:43 -06:00
GitHub Action
f91839889f ignore: update download stats 2025-12-25 2025-12-26 14:35:43 -06:00
GitHub Action
512b3088f7 chore: generate 2025-12-26 14:35:42 -06:00
Dax Raad
0b5df9c4c4 remove list tool 2025-12-26 14:35:42 -06:00
opencode
371a1fd115 release: v1.0.200 2025-12-26 14:35:42 -06:00
Adam
87d904422b fix(desktop): scroll jank in session turn and review 2025-12-26 14:35:42 -06:00
GitHub Action
8d85f975d8 chore: generate 2025-12-26 14:35:41 -06:00
Adam
37f98de6e6 fix(desktop): override agent model 2025-12-26 14:35:41 -06:00
Adam
1285f67b39 fix(desktop): reconcile session diff updates 2025-12-26 14:35:41 -06:00
opencode
4c44a09991 release: v1.0.199 2025-12-26 14:35:41 -06:00
Adam
7466c08277 chore: toast on file load error 2025-12-26 14:35:40 -06:00
Adam
d2da59f844 chore: cleanup dead code 2025-12-26 14:35:40 -06:00
Adam
52d81bdfd2 chore: show version on error page 2025-12-26 14:35:40 -06:00
Adam
b3ab52fe79 fix(desktop): show server connection failure 2025-12-26 14:35:40 -06:00
Ahmed Mansour
b39cf15833 fix: correct Content-Type headers for static assets on app.opencode.ai (#6113) 2025-12-26 14:35:40 -06:00
Connor Adams
cc39dbedf2 docs: update skills to use canonical ~/.config/opencode location (#6132) 2025-12-26 14:35:40 -06:00
Robb Tolliver
6242baea0a docs: Corrected the number of built-in subagents in documentation (#6133) 2025-12-26 14:35:40 -06:00
GitHub Action
b43a91db78 chore: generate 2025-12-26 14:35:40 -06:00
Dax Raad
19c1ed11b8 tui: disable tips display in home route 2025-12-26 14:35:40 -06:00
Dax Raad
b4d25efbb9 CI 2025-12-26 14:35:39 -06:00
opencode
fd56d8da8d release: v1.0.198 2025-12-26 14:35:39 -06:00
Dax Raad
8a1c3702a5 ci 2025-12-26 14:35:39 -06:00
GitHub Action
a0621840d4 chore: generate 2025-12-26 14:35:38 -06:00
Dax Raad
073fba09a4 ci 2025-12-26 14:35:38 -06:00
opencode
49fa4fce1c release: v1.0.197 2025-12-26 14:35:38 -06:00
Dax Raad
b59a021c94 Revert "feat: better styling for small screens (short and/or not wide) (#5968)"
This reverts commit ac371d2987.
2025-12-26 14:35:38 -06:00
GitHub Action
571a6ec6fc chore: generate 2025-12-26 14:35:37 -06:00
Patrick Schiel
7b89bb2a3b docs: add infos about server debugging (#6085) 2025-12-26 14:35:37 -06:00
opencode
f6a622d2fd release: v1.0.196 2025-12-26 14:35:37 -06:00
Adam
b47c9a4f9e fix(desktop): last text part streaming 2025-12-26 14:35:37 -06:00
Adam
807d61cb94 fix(desktop): render perf 2025-12-26 14:35:37 -06:00
Jay V
32cf23ffef docs: edits 2025-12-26 14:35:36 -06:00
Jay V
f2ceb55514 docs: add comprehensive CLI command documentation for agent, mcp, session, stats, and web commands 2025-12-26 14:35:36 -06:00
Jay V
cdc9c007bd docs: make MCP server documentation more scannable and add Sentry example 2025-12-26 14:35:36 -06:00
Adam
5de8dd131b chore: cleanup 2025-12-26 14:35:36 -06:00
Adam
a663ab81e5 fix(desktop): summary flicker 2025-12-26 14:35:36 -06:00
Adam
4154067dfd feat(desktop): show read tool args 2025-12-26 14:35:35 -06:00
GitHub Action
4dcd196135 chore: generate 2025-12-26 14:35:35 -06:00
Adam
7fe4e76666 fix(desktop): better session navigation, hide child sessions 2025-12-26 14:35:35 -06:00
opencode
c8b69d2ad0 release: v1.0.195 2025-12-26 14:35:35 -06:00
Adam
754d332692 chore: cleanup 2025-12-26 14:35:34 -06:00
GitHub Action
a2cd45ada1 chore: generate 2025-12-26 14:35:34 -06:00
Adam
a806023bdc refactor(ui): rewrite createAutoScroll with robust event tracking to fix sticky behavior 2025-12-26 14:35:33 -06:00
opencode
74a93f871f release: v1.0.194 2025-12-26 14:35:33 -06:00
Adam
59f1b41f73 chore: cleanup 2025-12-26 14:35:32 -06:00
Adam
d06aae5544 fix(desktop): session sort when multiple active 2025-12-26 14:35:32 -06:00
Adam
4b236451e9 fix(share): page title should be session title 2025-12-26 14:35:32 -06:00
GitHub Action
0ff29879de ignore: update download stats 2025-12-24 2025-12-26 14:35:32 -06:00
Adam
2ade0d8f3e fix(desktop): exclude deprecated models 2025-12-26 14:35:31 -06:00
Adam
78046d06e1 fix(desktop): auto-scroll 2025-12-26 14:35:31 -06:00
Adam
270505b0be fix: don't disable text selection 2025-12-26 14:35:31 -06:00
Github Action
c1cea004f4 Update Nix flake.lock and hashes 2025-12-26 14:35:31 -06:00
Adam
6cd9e5d3ac deps: update marked and marked-shiki 2025-12-26 14:35:30 -06:00
Adam
df04dfc957 fix(desktop): hang on backtracing-prone regex 2025-12-26 14:35:30 -06:00
Adam
fa53ab0cce fix(desktop): conditionally show review pane toggle 2025-12-26 14:35:30 -06:00
Ryan Vogel
f22dd0646e fix: remove SVG favicon to improve SEO (#5755) 2025-12-26 14:35:30 -06:00
Aiden Cline
73e5315077 docs: tweak lsp.mdx 2025-12-26 14:35:30 -06:00
opencode-agent[bot]
7ea4a63257 docs: experimental LSP tool (#5943)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-26 14:35:30 -06:00
opencode-agent[bot]
e94499d178 docs: skill tool/perm + parent keybind (#6001)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:35:29 -06:00
xiantang
05e8dfb8a2 docs: add Neovim to the list of editors (#6081) 2025-12-26 14:35:29 -06:00
Aiden Cline
e8ca1d1f7d tweak: retry err 2025-12-26 14:35:29 -06:00
GitHub Action
b8c2f9db59 chore: generate 2025-12-26 14:35:29 -06:00
Frank
245a63e3cf zen: sync 2025-12-26 14:35:29 -06:00
Abdelkader Boudih
0877ba302d feat(mcp): handle tools/list_changed notifications (#5913)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-26 14:35:29 -06:00
Aiden Cline
3f2aa25a94 test: add test for retry 2025-12-26 14:35:28 -06:00
Aiden Cline
fe91899428 chore: regen sdk 2025-12-26 14:35:28 -06:00
GitHub Action
e8bce0aaf2 chore: generate 2025-12-26 14:35:28 -06:00
Aiden Cline
8767cca3ea make 'The socket connection was closed unexpectedly' errors retryable 2025-12-26 14:35:28 -06:00
Rohan Mukherjee
6ef69dd905 chore: update AGENTS.md to ~150 lines (#5955) 2025-12-26 14:35:28 -06:00
David Hill
24f9ce30fc style: update current todo style (#6077) 2025-12-26 14:35:27 -06:00
rari404
523c47d285 feat(tui): console copy-to-clipboard via opentui (#5658)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:35:27 -06:00
OpeOginni
466ed745ab fix: resize textarea when pasting prompt less than 150 chars (#6070) 2025-12-26 14:35:27 -06:00
Matt Silverlock
1f2a4537bc providers: add Cloudflare AI Gateway (#5174) 2025-12-26 14:35:27 -06:00
Frank
0e50b4adea ci: adam is not a full stack engineer 2025-12-26 14:35:27 -06:00
Adam
e7cf8b0813 fix: remove desktop dup 2025-12-26 14:35:27 -06:00
Viktor Nagy
459db5c655 Update gitlab.mdx to use the 2.x component version (#6062) 2025-12-26 14:35:27 -06:00
Github Action
0e6be83872 Update Nix flake.lock and hashes 2025-12-26 14:35:26 -06:00
Aiden Cline
3612e09e95 Revert "Add animated braille spinner to terminal title when agent is running (#5984)"
This reverts commit 59b87f60f7.
2025-12-26 14:35:26 -06:00
Aiden Cline
67e74aef10 tweak: update import & pr commands to use new share link ur 2025-12-26 14:35:26 -06:00
GitHub Action
3f9ee3330f chore: generate 2025-12-26 14:35:26 -06:00
Jon Redeker
180e1f35ef Add opencode-shell-strategy plugin to ecosystem (#5995) 2025-12-26 14:35:26 -06:00
ja
e2e36adab7 feat(install): add standard CLI flags (--help, --version, --no-modify-path) (#5885) 2025-12-26 14:35:25 -06:00
David Hill
3b4519911e Add animated braille spinner to terminal title when agent is running (#5984)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:35:25 -06:00
GitHub Action
74cd5456b9 chore: generate 2025-12-26 14:35:25 -06:00
David Hill
6417a72f0b "Did you know?" start screen tips (#5982)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
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-26 14:35:25 -06:00
Daniel Gray
dfdde091f5 fix: favorites and recents stay visible when filtering models (#6053) 2025-12-26 14:35:25 -06:00
Matt Silverlock
72cb02f4d5 docs: add MCP OAuth debugging section (#6047) 2025-12-26 14:35:24 -06:00
Aiden Cline
7692af47c5 ci: update zed sync script 2025-12-26 14:35:24 -06:00
ja
441f5ba911 fix(tui): prevent keybinds from executing when dialog is open (#6017) 2025-12-26 14:35:24 -06:00
Frank
ad93850696 ci: fix 2025-12-26 14:35:24 -06:00
GitHub Action
c19a0c5d09 chore: generate 2025-12-26 14:35:24 -06:00
Rhys Sullivan
5d0fd6e7b9 [feat]: prompt stashing (#6021) 2025-12-26 14:35:24 -06:00
GitHub Action
7dc18c35c2 chore: generate 2025-12-26 14:35:24 -06:00
Daniel Polito
9c2d6c7f6a Fix Github Pull Request Event (#6037) 2025-12-26 14:35:23 -06:00
opencode
aafa717d4c release: v1.0.193 2025-12-26 14:35:23 -06:00
GitHub Action
327dd225a8 chore: generate 2025-12-26 14:35:23 -06:00
Sebastian Herrlinger
eb5c544e45 indent wrapped todo items properly 2025-12-26 14:35:23 -06:00
opencode
6d1a9227b9 release: v1.0.192 2025-12-26 14:35:22 -06:00
Frank
a73a36e533 zen: glm 4.7 2025-12-26 14:35:22 -06:00
Frank
2d6c3a886c ci: fix 2025-12-26 14:35:22 -06:00
Frank
b926c4f75a ci: fix 2025-12-26 14:35:22 -06:00
GitHub Action
e2b65f6ebd ignore: update download stats 2025-12-23 2025-12-26 14:35:22 -06:00
Sebastian Herrlinger
3341f3ae42 no intermediate autocomplete result to avoid flickering 2025-12-26 14:35:22 -06:00
GitHub Action
45a147858d chore: generate 2025-12-26 14:35:22 -06:00
Brendan Allan
38a67c2e5e desktop: kill_sidecar before update install on windows 2025-12-26 14:35:21 -06:00
Github Action
d5fd078a42 Update Nix flake.lock and hashes 2025-12-26 14:35:21 -06:00
GitHub Action
5ce08ba95a chore: generate 2025-12-26 14:35:21 -06:00
Adam
80333e13e1 deps: diffs, shiki updates 2025-12-26 14:35:20 -06:00
opencode
4a6facee03 release: v1.0.191 2025-12-26 14:35:20 -06:00
GitHub Action
0cf94b991a chore: generate 2025-12-26 14:35:20 -06:00
Brendan Allan
4c4bbc89e1 console: add AppImage download link 2025-12-26 14:35:19 -06:00
GitHub Action
dd4b348154 chore: generate 2025-12-26 14:35:19 -06:00
Matt Silverlock
9ffa3ab24e improve mcp CLI + ability to debug MCP oauth (#5980) 2025-12-26 14:35:19 -06:00
Aiden Cline
d1581fbcf2 ci: docs sync 2025-12-26 14:35:19 -06:00
opencode-agent[bot]
0c4b36a433 docs: new /global/health API (#6006)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-26 14:35:19 -06:00
lif
139eb94bba fix: handle Windows CRLF line endings in grep tool (#5948)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-26 14:35:18 -06:00
Joel Hooks
ce8ea03bd4 feat(plugin): allow compaction hook to replace prompt entirely (#5907) 2025-12-26 14:35:18 -06:00
Brendan Allan
0ccf6a8e2e ci: rename tauri -> desktop 2025-12-26 14:35:17 -06:00
Github Action
cc35060491 Update Nix flake.lock and hashes 2025-12-26 14:35:17 -06:00
Adam
11878623bb chore: rename packages/tauri -> packages/desktop 2025-12-26 14:35:17 -06:00
Github Action
ed59558070 Update Nix flake.lock and hashes 2025-12-26 14:35:16 -06:00
Adam
992327a05d chore: rename packages/desktop -> packages/app 2025-12-26 14:35:15 -06:00
GitHub Action
4ccd407259 chore: generate 2025-12-26 14:35:15 -06:00
Adam
3fef2d3b43 fix(desktop): better error messages on connection failure 2025-12-26 14:35:15 -06:00
Mohammad Alhashemi
30e6af1b2f feat(skill): add per-agent filtering to skill tool description (#6000) 2025-12-26 14:35:15 -06:00
Frank
1d71f33b71 zen: glm 4.7 2025-12-26 14:35:14 -06:00
Aiden Cline
e96fdcf042 feat: better styling for small screens (short and/or not wide) (#5968) 2025-12-26 14:35:14 -06:00
GitHub Action
2d22216826 chore: generate 2025-12-26 14:35:14 -06:00
Dax Raad
c81633aa3c tui: change task tool container to block layout for better subagent session display 2025-12-26 14:35:13 -06:00
opencode
f8563e901b release: v1.0.190 2025-12-26 14:35:13 -06:00
GitHub Action
6f4ddd15c9 chore: generate 2025-12-26 14:35:13 -06:00
Mohammad Alhashemi
d3eb85e330 feat: add native skill tool with permission system (#5930)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-12-26 14:35:12 -06:00
opencode
86148821eb release: v1.0.189 2025-12-26 14:35:12 -06:00
GitHub Action
15c6052711 chore: generate 2025-12-26 14:35:11 -06:00
Jay V
b0c88929ed docs: edit gitlab 2025-12-26 14:35:11 -06:00
opencode
58a9cb901e release: v1.0.188 2025-12-26 14:35:11 -06:00
Github Action
997f84a5bd Update Nix flake.lock and hashes 2025-12-26 14:35:10 -06:00
Frank
4a347586d0 remove sharp 2025-12-26 14:35:10 -06:00
Josh Thomas
10745d5bef fix(tui): resize textarea if text inserted via appendPrompt TUI API (#5983) 2025-12-26 14:35:09 -06:00
Frank
35e6fc42b0 zen: add glm 4.7 2025-12-26 14:35:09 -06:00
Aiden Cline
523ed96df8 ignore: agents.md 2025-12-26 14:35:09 -06:00
Adam
834f144c52 feat(desktop): review pane toggle 2025-12-26 14:35:09 -06:00
Viktor Nagy
d5bf844e2c 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-26 14:35:09 -06:00
Frank
64a8852637 support glm 4.7 2025-12-26 14:35:09 -06:00
GitHub Action
73578d69cb chore: generate 2025-12-26 14:35:08 -06:00
Jay V
f4bd11a43d ignore: update GitHub stars to 41K and project stats to reflect current growth 2025-12-26 14:35:08 -06:00
opencode
82826c0dde release: v1.0.187 2025-12-26 14:35:07 -06:00
Aiden Cline
cc4a1e8849 ci: add failure case for changelog 2025-12-26 14:35:07 -06:00
Aiden Cline
de20383402 test: rm claude skills test 2025-12-26 14:35:07 -06:00
Aiden Cline
baee918ed0 fix: disable claude skill loading for now 2025-12-26 14:35:07 -06:00
Shpetim
67db1ef771 fix: stop auto execute on sendText vscode extension (#5994)
Co-authored-by: Shpetim <shpetim.alimi@ndbit.net>
2025-12-26 14:35:07 -06:00
Jon Redeker
1ac00cebea docs: add opencode-morph-fast-apply plugin to ecosystem (#5992) 2025-12-26 14:35:07 -06:00
Blake North
2fd23715f4 fix(providers.opencode): check config for api key in addition to auth (#5906) 2025-12-26 14:35:07 -06:00
Github Action
a9ae1ef2c1 Update Nix flake.lock and hashes 2025-12-26 14:35:06 -06:00
Aiden Cline
141d24c28c fix: bundle more providers to fix breaking ai sdk issue 2025-12-26 14:35:06 -06:00
Rohan Godha
f30992001f feat(tui): go to parent keybind for subagents (#5762) 2025-12-26 14:35:06 -06:00
GitHub Action
e79b9ad111 chore: generate 2025-12-26 14:35:06 -06:00
wienans
72a9e23926 Add OpenChamber to ecosystem documentation (#5978) 2025-12-26 14:35:05 -06:00
ja
21d81dcf4e feat(lsp): add Tinymist LSP support for Typst (#5933) 2025-12-26 14:35:05 -06:00
Github Action
811c8aac5e Update Nix flake.lock and hashes 2025-12-26 14:35:05 -06:00
Sebastian Herrlinger
2cf69ec114 upgrade opentui to v0.1.63, enabling kitty alternate keys by default 2025-12-26 14:35:05 -06:00
Tim Kleinschmidt
439af1740a support clojure projects with built-in lsp (#5975) 2025-12-26 14:35:04 -06:00
Shpetim
cf3d91e15b [FEATURE]: Show context usage in OpenCode Desktop Context usage (#5979) 2025-12-26 14:35:04 -06:00
Adam
344cdbb36b fix(prompt): better summary prompt 2025-12-26 14:35:04 -06:00
Daniel Polito
8ca4fdafd9 docs: Github Auto Pull Request Docs (#5974) 2025-12-26 14:35:04 -06:00
Adam
2fb212071b chore(desktop): auto scroll utility 2025-12-26 14:35:04 -06:00
GitHub Action
61be2c7f83 chore: generate 2025-12-26 14:35:04 -06:00
Will Marella
e81c496bb6 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-26 14:35:03 -06:00
Aiden Cline
c9a42f760f ci: tweak docs prompt 2025-12-26 14:35:03 -06:00
Daniel Polito
b6089e4371 GitHub pull request event (#5335) 2025-12-26 14:35:03 -06:00
Adam
f8b0ee9637 fix(share): expanded state and responsiveness 2025-12-26 14:35:03 -06:00
Aiden Cline
0b7b6d105e ci: limit to opencode repo 2025-12-26 14:35:03 -06:00
Lekë Dobruna
59cc15ecde fix: duplicate words in dialog options (#5944) 2025-12-26 14:35:03 -06:00
Adam
9236478182 fix(desktop): diff readability (colors) 2025-12-26 14:35:03 -06:00
Dax Raad
fa9edbe9d2 fix url for web 2025-12-26 14:35:02 -06:00
GitHub Action
99e04ad2ed chore: generate 2025-12-26 14:35:02 -06:00
Buck Evan
e70ae665d0 fix(read): narrow .env file blocking to not block .envrc (#5654)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-26 14:35:02 -06:00
opencode
ac03ab1e10 release: v1.0.186 2025-12-26 14:35:02 -06:00
Adam
cc572343be Revert "server: ensure frontend has correct port for PTY websocket connections (#5898)"
This reverts commit a05915ddc8.
2025-12-26 14:35:01 -06:00
Adam
b0c860a17b Revert "fix: server"
This reverts commit dbaac79039.
2025-12-26 14:35:01 -06:00
GitHub Action
bfcbdaceb7 ignore: update download stats 2025-12-22 2025-12-26 14:35:01 -06:00
Adam
73e8caa5e9 fix: server 2025-12-26 14:35:01 -06:00
Ashutosh Kumar
3c6c7c5278 server: ensure frontend has correct port for PTY websocket connections (#5898) 2025-12-26 14:35:00 -06:00
Adam
86d34aee8e Revert "fix: use current page port instead of hardcoded 4096 (#5949)"
This reverts commit d04a72a4ad.
2025-12-26 14:35:00 -06:00
Adam
dcee57f572 fix(desktop): cleanup auto scroll 2025-12-26 14:35:00 -06:00
Adam
249431d84a feat(desktop): mobile responsiveness 2025-12-26 14:34:59 -06:00
Adam
806c21c535 fix(desktop): filter child sessions from header 2025-12-26 14:34:59 -06:00
Adam
309e594e41 feat(desktop): better task tool rendering 2025-12-26 14:34:58 -06:00
lif
355323f180 fix: use current page port instead of hardcoded 4096 (#5949)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-26 14:34:58 -06:00
Aaron Iker
9f4c5d6736 feat: polish dialog & list styles for the desktop app, add fixed logos from models.dev (#5925) 2025-12-26 14:34:58 -06:00
Brendan Allan
24ed94bc1f ci: fix tauri build args 2025-12-26 14:34:58 -06:00
Brendan Allan
599da3ee8f ci: verbose build and re-enable appimage 2025-12-26 14:34:57 -06:00
GitHub Action
b1cd7d617b chore: generate 2025-12-26 14:34:57 -06:00
Brendan Allan
ddab63be61 ci: run prepare step for tauri build 2025-12-26 14:34:57 -06:00
Brendan Allan
18ff962f6e ci: try downloading artifact in desktop prepare 2025-12-26 14:34:57 -06:00
NN708
5a84872777 feat(desktop): arm64 build for linux (#5935) 2025-12-26 14:34:57 -06:00
Brendan Allan
1282f3457d ci: replace with just upload-artifact whole dir 2025-12-26 14:34:57 -06:00
Brendan Allan
2ea105735d ci: import bun shell 2025-12-26 14:34:56 -06:00
Brendan Allan
2e7086e321 try uploading artifacts in workflow 2025-12-26 14:34:56 -06:00
Brendan Allan
64a50bb030 remove actions artifact uploading 2025-12-26 14:34:56 -06:00
Github Action
320a434ae7 Update Nix flake.lock and hashes 2025-12-26 14:34:56 -06:00
GitHub Action
09875f5984 chore: generate 2025-12-26 14:34:56 -06:00
Brendan Allan
89ce5d9ea2 ci: try to upload cli artifacts 2025-12-26 14:34:55 -06:00
Aiden Cline
330e4a7c73 feat: add experimental lsp tool (#5886) 2025-12-26 14:34:55 -06:00
Luo Chen
79134dd16a feat: add nixd as lsp for nix language (#5929) 2025-12-26 14:34:55 -06:00
opencode-agent[bot]
10e454481c 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-26 14:34:55 -06:00
Valerio Di Maggio
d9b1b876e5 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-26 14:34:55 -06:00
Ben Vargas
e21c6d93e0 docs: add ai-sdk-provider-opencode-sdk to ecosystem (#5772) 2025-12-26 14:34:55 -06:00
Aiden Cline
7eba950c6d ci: update docs prompt 2025-12-26 14:34:55 -06:00
Aiden Cline
53149554c8 ci: add automatic doc update workflow 2025-12-26 14:34:55 -06:00
Neil Daquioag
2dc9528867 fix: support clipboard image paste (Ctrl+V) on Windows (#5919) 2025-12-26 14:34:54 -06:00
Aiden Cline
b7a9cc5fd3 tweak: adjust minimax m2 topK and add minimax m2.1 topP 2025-12-26 14:34:54 -06:00
Noam Bressler
a8f5e69ede fix: Perform snapshot in cases finish-step is not reached (#5912)
Co-authored-by: noamzbr <noamzbr@users.noreply.github.com>
2025-12-26 14:34:54 -06:00
Matt Silverlock
6fe028dd21 prompts: improve built-in /review prompt (#5918) 2025-12-26 14:34:54 -06:00
YeonGyu-Kim
6d6bc0140b feat(server): expose auto param in session.summarize for plugins (#5924) 2025-12-26 14:34:54 -06:00
GitHub Action
a22a67b8bc chore: generate 2025-12-26 14:34:54 -06:00
Dax
f2ddf5a7d4 feat: add Agent Skills support (#5921) 2025-12-26 14:34:54 -06:00
opencode
d4cb80a5e1 release: v1.0.185 2025-12-26 14:34:53 -06:00
Dax Raad
221093a52b 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-26 14:34:53 -06:00
Aiden Cline
80271f939c tweak: update kimi-k2 and kimi-k2-thinking to use recommended temperature values 2025-12-26 14:34:53 -06:00
Dax Raad
0cc373de7b 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-26 14:34:53 -06:00
Github Action
d3f42d5302 Update Nix flake.lock and hashes 2025-12-26 14:34:52 -06:00
GitHub Action
d0fb6e72fe chore: generate 2025-12-26 14:34:52 -06:00
Sherlock Holmes
9e9eaabe68 fix(deps): add missing @opencode-ai/plugin to dependencies (#5797) 2025-12-26 14:34:52 -06:00
Nalin Singh
c04c92778b feat: add syntax highlighting for .ets files (#5889) 2025-12-26 14:34:52 -06:00
Adam Hosker
76b0ac2204 fix: prevent stats workflow from running on forks (#5897) 2025-12-26 14:34:52 -06:00
Abdelkader Boudih
a81cd3c8c4 fix: use official MCP SDK for better tool schema handling (#5463) 2025-12-26 14:34:51 -06:00
GitHub Action
3bd665c5b6 chore: generate 2025-12-26 14:34:51 -06:00
Matt Silverlock
2fa7b01072 github: support schedule events (#5810) 2025-12-26 14:34:51 -06:00
GitHub Action
84a2180d0b ignore: update download stats 2025-12-21 2025-12-26 14:34:51 -06:00
opencode
6b21504dbc release: v1.0.184 2025-12-26 14:34:50 -06:00
Adam
244afdf4ef fix(desktop): layout regression 2025-12-26 14:34:50 -06:00
Adam
8bc49ccf2c fix(desktop): better keybind tooltips 2025-12-26 14:34:50 -06:00
Adam
0f5ce6fb04 fix(desktop): todo tool title 2025-12-26 14:34:50 -06:00
Adam
0d1f8aa40e fix(desktop): allow text selection 2025-12-26 14:34:50 -06:00
GitHub Action
eb6ae39917 chore: generate 2025-12-26 14:34:50 -06:00
Adam
c57e4dc411 fix(desktop): incorrect state dir on macos 2025-12-26 14:34:50 -06:00
opencode
61be42e08c release: v1.0.183 2025-12-26 14:34:49 -06:00
Adam
9f1ed1c186 fix(desktop): better error reporting 2025-12-26 14:34:49 -06:00
Adam
1cd4cafc35 fix(desktop): non-latin file paths failed 2025-12-26 14:34:49 -06:00
Adam
0ec1e14c63 fix(desktop): file loading errors 2025-12-26 14:34:49 -06:00
Github Action
bfeef85d59 Update Nix flake.lock and hashes 2025-12-26 14:34:49 -06:00
Christopher Tso
31de4fc223 Make CLI build script Windows-friendly (#5835) 2025-12-26 14:34:48 -06:00
Aiden Cline
267447018e core: add verification that at least 1 primary agent is enabled, add regression tests (#5881) 2025-12-26 14:34:48 -06:00
GitHub Action
53d9c745cd chore: generate 2025-12-26 14:34:48 -06:00
opencode
bfe2ddf81a release: v1.0.182 2025-12-26 14:34:47 -06:00
Aiden Cline
72bf8b4b6d fix: regression where config would error despite valid agents 2025-12-26 14:34:47 -06:00
opencode
a0c279d75e release: v1.0.181 2025-12-26 14:34:47 -06:00
Adam
0e8303c016 fix(desktop): layout 2025-12-26 14:34:46 -06:00
Adam
7d88443ca8 feat(desktop): new layout 2025-12-26 14:34:46 -06:00
YuY801103
85ef34ca06 docs: add Traditional Chinese (Taiwan) README translation (#5861)
Co-authored-by: Yu <YuY801103@users.noreply.github.com>
2025-12-26 14:34:46 -06:00
Ryan Vogel
3fdeee126a docs: clarify model ID format for OpenCode provider (#5854) 2025-12-26 14:34:46 -06:00
Aiden Cline
7654839159 tweak: better error message if no primary agents are enabled 2025-12-26 14:34:46 -06:00
Ryan Vogel
af66701f44 feat(docs): adding .md to docs pages shows raw markdown (#5823) 2025-12-26 14:34:46 -06:00
shamil2
25e25a2110 feat: add Catppuccin Frappé theme (#5821)
Co-authored-by: shamil2 <shamil2@users.noreply.github.com>
2025-12-26 14:34:45 -06:00
ja
f57538ebba docs: add name property to model configuration example (#5853) 2025-12-26 14:34:45 -06:00
Shpetim
cbe06c47b3 fix: system theme flicker (#5842)
Co-authored-by: Shpetim <shpetim.alimi@ndbit.net>
2025-12-26 14:34:45 -06:00
Frank
cdfdd63bee zen: sync 2025-12-26 14:34:45 -06:00
Matt Silverlock
753b7362a3 feat: support configuring a default_agent across all API/user surfaces (#5843)
Co-authored-by: observerw <observerw@users.noreply.github.com>
2025-12-26 14:34:44 -06:00
Aiden Cline
bd24e1a715 ci: adjust review agent prompt to discourage bad diffs 2025-12-26 14:34:44 -06:00
lif
b1e7975bf8 fix: add transform case for gemini if mcp tool has missing array items (#5846) 2025-12-26 14:34:44 -06:00
ja
2d47d5cf83 fix: prioritize session list loading when resuming with -c (#5816)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:34:44 -06:00
Ryan Cassidy
1e867702a3 feat: add cursor theme (#5850) 2025-12-26 14:34:44 -06:00
GitHub Action
2a3b9bde24 chore: generate 2025-12-26 14:34:44 -06:00
opencode
2fec3c1859 release: v1.0.180 2025-12-26 14:34:43 -06:00
Dax Raad
c60ef697f2 ci 2025-12-26 14:34:43 -06:00
opencode
eb84c5820f release: v1.0.179 2025-12-26 14:34:43 -06:00
Dax Raad
b7c3d0dd9d ci 2025-12-26 14:34:43 -06:00
GitHub Action
e5954a97b4 chore: generate 2025-12-26 14:34:42 -06:00
Dax Raad
13e1091f0f ci 2025-12-26 14:34:42 -06:00
opencode
0d94abc69a release: v1.0.178 2025-12-26 14:34:42 -06:00
Dax Raad
5d5c2523b8 ci 2025-12-26 14:34:42 -06:00
Tommy D. Rossi
5bac846e98 feat: add endpoints to delete and update message parts (#5433) 2025-12-26 14:34:41 -06:00
GitHub Action
997cf6e207 chore: generate 2025-12-26 14:34:41 -06:00
opencode
8dfdd09dcb release: v1.0.177 2025-12-26 14:34:41 -06:00
Dax Raad
1024724cc6 ci 2025-12-26 14:34:40 -06:00
opencode
2774e4f6fd release: v1.0.176 2025-12-26 14:34:40 -06:00
Adam
29b22b46db fix(desktop): sidebar UX issues 2025-12-26 14:34:40 -06:00
Adam
9d75fbd1b2 fix(desktop): task rendering perf 2025-12-26 14:34:40 -06:00
GitHub Action
6557fa2e0b chore: generate 2025-12-26 14:34:40 -06:00
Adam
6e5d7fcae3 fix(desktop): show last text part when summarized 2025-12-26 14:34:39 -06:00
Adam
2912ba9526 fix(desktop): performance with lots of session changes 2025-12-26 14:34:39 -06:00
Adam
8ec421a3be fix(desktop): event reconnect gaps 2025-12-26 14:34:39 -06:00
GitHub Action
b279b5022c ignore: update download stats 2025-12-20 2025-12-26 14:34:39 -06:00
opencode
78364f0137 release: v1.0.175 2025-12-26 14:34:38 -06:00
Adam
67a7483505 fix(desktop): perf tweaks 2025-12-26 14:34:38 -06:00
Adam
a023273d9e fix(desktop): add retries to init promises 2025-12-26 14:34:38 -06:00
Adam
64dd604ea0 fix: types 2025-12-26 14:34:38 -06:00
Adam
34d9c657e8 fix: shouldEncode 2025-12-26 14:34:38 -06:00
Adam
1fcacfd889 fix(desktop): perf stuff 2025-12-26 14:34:38 -06:00
Adam
979a50ecfe fix(desktop): removed projects 2025-12-26 14:34:38 -06:00
Frank
901801d3d8 zen: add minimax m2.1 2025-12-26 14:34:37 -06:00
GitHub Action
f5468a0399 chore: generate 2025-12-26 14:34:37 -06:00
Frank
b9dd35bfcc zen: add minimax m2.1 2025-12-26 14:34:37 -06:00
opencode
192975e07e release: v1.0.174 2025-12-26 14:34:37 -06:00
GitHub Action
669a7e0897 chore: generate 2025-12-26 14:34:36 -06:00
Aiden Cline
1eaa71e236 fix: file permissions 2025-12-26 14:34:36 -06:00
opencode
583f0aa5ef release: v1.0.173 2025-12-26 14:34:36 -06:00
Ariane Emory
6c159d4f78 feat(tui): reinsert forked message text in prompt text input box when forking session (resolves #5495) (#5545)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 14:34:35 -06:00
GitHub Action
160b26f360 chore: generate 2025-12-26 14:34:35 -06:00
Aiden Cline
f85a57d4af ci: fix archive 2025-12-26 14:34:35 -06:00
opencode
38eb36f4f6 release: v1.0.172 2025-12-26 14:34:34 -06:00
Aiden Cline
01ddf7deb6 ci: fix undefined 2025-12-26 14:34:34 -06:00
Aiden Cline
be47071ce5 ci: fix release notes 2025-12-26 14:34:34 -06:00
Aiden Cline
7bc6ac85a3 Revert "tweak: better release notes (grouped changelog) (#5768)"
This reverts commit b99afdad91.
2025-12-26 14:34:34 -06:00
Aiden Cline
65ca3c463a Revert "tweak: DevEx to run changelog independently (#5774)"
This reverts commit 7f8e799392.
2025-12-26 14:34:33 -06:00
Aiden Cline
d30536efa6 Revert "ci: gemini 3 flash doesnt exist in pinned cicd version (#5776)"
This reverts commit 382905602c.
2025-12-26 14:34:33 -06:00
Aiden Cline
1682fdf0d1 test: fixture cleanup 2025-12-26 14:34:33 -06:00
Aiden Cline
d2919b1d74 test: fix test case 2025-12-26 14:34:33 -06:00
Kaspar
72c4cadb22 tweak: Make LSP message more accurate when LSPs disabled (#5814) 2025-12-26 14:34:33 -06:00
Aiden Cline
35787ffaed chore: rm dead code 2025-12-26 14:34:33 -06:00
Aiden Cline
c0fe626748 ci: fix publish auth failure 2025-12-26 14:34:32 -06:00
1XD
3306d8f666 docs: replace deprecated mise ubi backend with github backend (#5811) 2025-12-26 14:34:32 -06:00
Github Action
5e8b1d8f4a Update Nix flake.lock and hashes 2025-12-26 14:34:31 -06:00
Frank
9ce16ac197 zen: sync 2025-12-26 14:34:31 -06:00
Frank
8597343df0 zen: sync 2025-12-26 14:34:31 -06:00
Frank
47444b398b zen: sync 2025-12-26 14:34:31 -06:00
Cameron
b95be947c6 Desktop file encoding issue (#5490) 2025-12-26 14:34:30 -06:00
Adam
f17e4fbaf0 chore: cleanup 2025-12-26 14:34:30 -06:00
Adam
fd9e54978a fix(desktop): auto-scroll and session perf 2025-12-26 14:34:30 -06:00
Adam
59e6454823 fix(desktop): don't use tauri http for sse events 2025-12-26 14:34:30 -06:00
Adam
8199a3b5e5 fix(desktop): error height 2025-12-26 14:34:30 -06:00
Github Action
881465a7d0 Update Nix flake.lock and hashes 2025-12-26 14:34:29 -06:00
Aiden Cline
feb9f9dbd6 tweak: use fetch instead of octokit for now 2025-12-26 14:34:29 -06:00
Steven T. Cramer
5c47adb782 docs: add Windows Terminal Shift+Enter configuration guide (#5788) 2025-12-26 14:34:29 -06:00
Brendan Allan
bf653d44d1 ci: separate standalone publishing from dependent publishing (#5634)
Co-authored-by: GitHub Action <action@github.com>
2025-12-26 14:34:28 -06:00
Dax Raad
71af1859fa core: prevent file system scanning when in root directory to avoid unnecessary operations 2025-12-26 14:34:28 -06:00
Aiden Cline
f084a0cfee ci: change token for gh release to allow discord job to actually trigger see: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow 2025-12-26 14:34:28 -06:00
GitHub Action
28d2ae5208 chore: generate 2025-12-26 14:34:27 -06:00
José Valim
cdb7949280 Do not include hidden agents in ACP (#5791) 2025-12-26 14:34:27 -06:00
Github Action
f02d1ef4a1 Update Nix flake.lock and hashes 2025-12-26 14:34:27 -06:00
Sebastian Herrlinger
59968ebd25 upgrade opentui to v0.1.62, enabling textarea mouse scroll and cursor set 2025-12-26 14:34:26 -06:00
Github Action
a346d22c25 Update Nix flake.lock and hashes 2025-12-26 14:34:26 -06:00
opencode
f15db51fe0 release: v1.0.170 2025-12-26 14:34:26 -06:00
Adam
92dfbdc08b fix(desktop): error handling 2025-12-26 14:34:25 -06:00
Adam
923bad1f70 chore: logging 2025-12-26 14:34:25 -06:00
Adam
551c5283bd fix(desktop): separate prompt history for shell 2025-12-26 14:34:25 -06:00
Adam
a05851b0ea fix(desktop): don't navigate prompt history if dirty 2025-12-26 14:34:25 -06:00
Sebastian Herrlinger
d888a318c5 user messages as markdown with toggle 2025-12-26 14:34:25 -06:00
GitHub Action
cfa95461b2 ignore: update download stats 2025-12-19 2025-12-26 14:34:25 -06:00
Github Action
182cf2d878 Update Nix flake.lock and hashes 2025-12-26 14:34:24 -06:00
Brendan Allan
6473b3f91a Update SolidStart and bring back HttpHeader usage (#5355)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-26 14:34:24 -06:00
Sherlock Holmes
6c8a7f05be feat(tui): implement smooth scrolling for autocomplete dropdown navigation (#5559)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-26 14:34:24 -06:00
Eric Shirley
d8a450ce17 lsp: add oxlint server (#5570) 2025-12-26 14:34:24 -06:00
Aiden Cline
d96fa874b0 docs: contributing 2025-12-26 14:34:24 -06:00
Aiden Cline
29541578b1 ci: only run generate for dev 2025-12-26 14:34:24 -06:00
Luke Parker
4c805fb327 ci: gemini 3 flash doesnt exist in pinned cicd version (#5776) 2025-12-26 14:34:24 -06:00
GitHub Action
906bb8821c chore: generate 2025-12-26 14:34:23 -06:00
Matt Silverlock
df93dbe347 fix: use correct octokit API for PR review comment reactions (#5778) 2025-12-26 14:34:23 -06:00
GitHub Action
dc7562189b chore: generate 2025-12-26 14:34:23 -06:00
Brendan Allan
e88ef5b294 tauri: remove pinch-to-zoom on window 2025-12-26 14:34:23 -06:00
Basit Mustafa
16b7e85256 docs(ecosystem): add opencode-zellij-namer plugin (#5771) 2025-12-26 14:34:23 -06:00
Luke Parker
5e403d3a9e tweak: DevEx to run changelog independently (#5774) 2025-12-26 14:34:22 -06:00
opencode
d058c0aa2d release: v1.0.169 2025-12-26 14:34:22 -06:00
Adam
ba081d64ad fix(desktop): shell mode 2025-12-26 14:34:22 -06:00
Adam
24005e3fc8 fix(desktop): extra reqs 2025-12-26 14:34:21 -06:00
Aiden Cline
9f3942d7da ci: better err msg for generate workflow 2025-12-26 14:34:21 -06:00
Luke Parker
536b4e9055 tweak: better release notes (grouped changelog) (#5768) 2025-12-26 14:34:21 -06:00
Aiden Cline
4d1d499c93 fix: better api call error msgs in some cases 2025-12-26 14:34:21 -06:00
GitHub Action
f5c5eff3ac chore: generate 2025-12-26 14:34:21 -06:00
Rohan Godha
2e0c6d5274 feat(tui): click on subagents to open them (#5761)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:34:21 -06:00
opencode
39e4b85273 release: v1.0.168 2025-12-26 14:34:20 -06:00
Github Action
0b4e84b9fa Update Nix flake.lock and hashes 2025-12-26 14:34:20 -06:00
GitHub Action
72105666cc chore: generate 2025-12-26 14:34:20 -06:00
Adam
2e3c122462 fix: id 2025-12-26 14:34:19 -06:00
Adam
80935a2dd1 fix(desktop): expanded states 2025-12-26 14:34:19 -06:00
Adam
a858dae20b chore: cleanup 2025-12-26 14:34:19 -06:00
Adam
2e431a0269 fix(desktop): smaller max-width when review open 2025-12-26 14:34:19 -06:00
Aiden Cline
66aee1bffd ci: fix generate 2025-12-26 14:34:19 -06:00
Aiden Cline
0c2bf6870b ci: fix file perm 2025-12-26 14:34:19 -06:00
Aiden Cline
e73b8cf171 ci: handle case where generate.yml fails better 2025-12-26 14:34:19 -06:00
Aiden Cline
0c4d68e9d3 tweak: more retry cases 2025-12-26 14:34:19 -06:00
Aiden Cline
92270f582a chore: rm dead code 2025-12-26 14:34:18 -06:00
GitHub Action
9aabebd644 chore: format code 2025-12-26 14:34:18 -06:00
OpeOginni
859ee9904d feat: add experimental support for Ty language server (#5575) 2025-12-26 14:34:18 -06:00
Adam
d02b54085e fix(desktop): markdown styles 2025-12-26 14:34:18 -06:00
Adam
65be48a14d fix(desktop): don't show image button in shell mode 2025-12-26 14:34:18 -06:00
Frank
0b44adf551 zen: cleanup headers 2025-12-26 14:34:18 -06:00
Aiden Cline
eed4a950c9 rm interleaved thinking filter for certain kimi k2 thinking model providers that were bugged 2025-12-26 14:34:18 -06:00
Adam
cc932a5f4f fix(desktop): submit prompt 2025-12-26 14:34:18 -06:00
GitHub Action
d4c89df601 chore: format code 2025-12-26 14:34:17 -06:00
Matt Silverlock
f707f6254e github: add OIDC_BASE_URL for custom GitHub App installs (#5756) 2025-12-26 14:34:17 -06:00
Adam
de933a68c9 fix(desktop): checkbox render in safari fml 2025-12-26 14:34:17 -06:00
Adam
24a9f95cc7 fix(desktop): rendering shell mode messages 2025-12-26 14:34:17 -06:00
Adam
6469ab5f95 fix(desktop): error styles 2025-12-26 14:34:16 -06:00
Adam
c31545351a fix(desktop): prompt history nav, optimistic prompt dup 2025-12-26 14:34:16 -06:00
Adam
51e5bc3e2b fix(desktop): session ordered by most recent 2025-12-26 14:34:16 -06:00
Adam
6759a50bee feat(desktop): shell mode 2025-12-26 14:34:16 -06:00
Adam
71e92a7e76 chore: cleanup 2025-12-26 14:34:15 -06:00
GitHub Action
ac25f3c738 chore: format code 2025-12-26 14:34:15 -06:00
Ariane Emory
52d0dbb2be feat(tui): fork slash command for keyboard-friendly session forking (resolves #5599) (#5610)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 14:34:15 -06:00
Jeon Suyeol
83b3cf6135 fix(command): validate model before executing slash command (#5740) 2025-12-26 14:34:15 -06:00
Frank
b67b876b00 Revert "add client header"
This reverts commit 2fb89161c8.
2025-12-26 14:34:15 -06:00
barış
e1b7bdb35f docs: fix typos (#5753) 2025-12-26 14:34:15 -06:00
Aiden Cline
198ccc5ef7 ci: fix discord 2025-12-26 14:34:15 -06:00
Aiden Cline
04665c8c53 bump bun version 2025-12-26 14:34:14 -06:00
Daniel Polito
89ef686546 Improve Github Action Hallucinations (#5751) 2025-12-26 14:34:14 -06:00
Frank
dc1cc77ee1 add client header 2025-12-26 14:34:14 -06:00
GitHub Action
f5a1cdad2f ignore: update download stats 2025-12-18 2025-12-26 14:34:14 -06:00
Brendan Allan
9c1dbcdaae tauri: disable pinch zoom on linux (#5735) 2025-12-26 14:34:14 -06:00
Brendan Allan
26e80c6ce3 tauri: configure display backends more correctly on linux (#5730) 2025-12-26 14:34:13 -06:00
GitHub Action
de9aee8f40 chore: format code 2025-12-26 14:34:13 -06:00
Adam
d34464593c fix(desktop): disable pinch to zoom 2025-12-26 14:34:13 -06:00
Adam
1ece7ebe50 feat(desktop): custom update toast 2025-12-26 14:34:12 -06:00
opencode
97c6bd5ab2 release: v1.0.167 2025-12-26 14:34:12 -06:00
shuv
81d367bd8e fix: handle empty directory query parameter in server middleware (#5732) 2025-12-26 14:34:12 -06:00
Brendan Allan
ea99db7d72 tauri: say OpenCode Server instead of OpenCode CLI 2025-12-26 14:34:11 -06:00
Brendan Allan
8ecd7566c1 tauri: server spawn fail dialog w/ copy logs button (#5729) 2025-12-26 14:34:11 -06:00
Frank
ede5871cf4 zen: error handling for stream requests 2025-12-26 14:34:11 -06:00
GitHub Action
9aebc4d976 chore: format code 2025-12-26 14:34:11 -06:00
Jeon Suyeol
0a1ef150c6 docs: add OPENCODE_DISABLE_TERMINAL_TITLE to environment variables (#5725) 2025-12-26 14:34:11 -06:00
Jake Nelson
e1b2fd5044 feat(tui): add option to disable terminal title (#5713) 2025-12-26 14:34:10 -06:00
Frank
f84e54517f zen: error handling for stream requests 2025-12-26 14:34:10 -06:00
Rohan Mukherjee
71a1aa5b16 MCP improvements (#5699) 2025-12-26 14:34:10 -06:00
Jay V
f0b2dfce6e docs: add legal pages with privacy policy and terms of service links 2025-12-26 14:34:10 -06:00
GitHub Action
9e842e5fbf chore: format code 2025-12-26 14:34:10 -06:00
Ryan Vogel
7becca83ae docs: add opencode.cafe to ecosystem page (#5714) 2025-12-26 14:34:10 -06:00
opencode
7b333eb352 release: v1.0.166 2025-12-26 14:34:09 -06:00
Adam
7091299de5 chore: cleanup 2025-12-26 14:34:09 -06:00
Adam
09172e9a55 fix: better init error messages 2025-12-26 14:34:09 -06:00
Adam
371a47ca11 fix: auto-scroll 2025-12-26 14:34:09 -06:00
GitHub Action
e5c6960b4c chore: format code 2025-12-26 14:34:09 -06:00
Sercan Sagman
193303a325 fix(tui): exclude reverted assistant reply when copying last message (#5705)
Signed-off-by: assagman <ahmetsercansagman@gmail.com>
2025-12-26 14:34:08 -06:00
opencode
836a7f5d83 release: v1.0.165 2025-12-26 14:34:08 -06:00
Adam
b4b633eee3 feat(desktop): startup errors shown 2025-12-26 14:34:08 -06:00
Adam
2f6e4e7e77 feat(desktop): optimistic prompt submit 2025-12-26 14:34:07 -06:00
Aiden Cline
a43981fa66 fix: better error messages 2025-12-26 14:34:07 -06:00
Aiden Cline
8b344dd8d1 fix: prevent 1 from showing when preparing write 2025-12-26 14:34:07 -06:00
Nalin Singh
ddb1105a94 feat: add viewportOptions to scrollbox for padding adjustments to avoid scrollbar overlap (#5703) 2025-12-26 14:34:07 -06:00
Spoon
6b3e0923fe UI: show plugins in /status (#4515)
Co-authored-by: GitHub Action <action@github.com>
2025-12-26 14:34:07 -06:00
Nalin Singh
8a19de4672 fix: prevent session list selection from jumping to active session when confirming delete (#5666) 2025-12-26 14:34:07 -06:00
Joel Hooks
247d1b34cf feat(plugin): add experimental.session.compacting hook for pre-compaction context injection (#5698)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-26 14:34:07 -06:00
Github Action
3e11f1d3ec Update Nix flake.lock and hashes 2025-12-26 14:34:07 -06:00
Adam
b860ff8901 chore: localStorage -> tauri store 2025-12-26 14:34:06 -06:00
Brendan Allan
9c2ee8320e console: use download proxy to rename mac and windows installers (#5697)
Co-authored-by: GitHub Action <action@github.com>
2025-12-26 14:34:06 -06:00
Ravi Kumar
c83f2f3676 fix(tui): resolve session_status TypeError (#5520) 2025-12-26 14:34:06 -06:00
Aiden Cline
b9eb55b772 fix: keep session dialog open if deleting session 2025-12-26 14:34:06 -06:00
Aiden Cline
c7a7fced41 fix: remove needless tui event publish on session delete 2025-12-26 14:34:06 -06:00
GitHub Action
91d1b65920 chore: format code 2025-12-26 14:34:06 -06:00
Adam
59ddf2a823 fix: sticky visual issues 2025-12-26 14:34:06 -06:00
Aiden Cline
befd8b59ee ci: add windows label to triage bot 2025-12-26 14:34:06 -06:00
GitHub Action
aa51979592 chore: format code 2025-12-26 14:34:05 -06:00
Brendan Allan
5a9b52e92e console: add /download/[platform] endpoint 2025-12-26 14:34:05 -06:00
Qio
4222758324 add OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX to override 32k default (#5679)
Co-authored-by: qio <handsomehust@gmail.com>
2025-12-26 14:34:05 -06:00
Adam
edd7032cb8 feat(desktop): inter and ibm plex mono 2025-12-26 14:34:05 -06:00
Adam
eff31afd33 feat(desktop): inter and ibm plex mono 2025-12-26 14:34:05 -06:00
Paolo Ricciuti
d1fc208e4a fix: send mcpName as state if authUrl doesn't have state (#5681) 2025-12-26 14:34:05 -06:00
Aiden Cline
628cab0f6b test: add regression test for setCacheKey option 2025-12-26 14:34:05 -06:00
Spoon
bf1f0a52c0 batch: enable edit, todoread, clarify error message, minor tool description change (#5659) 2025-12-26 14:34:05 -06:00
Rhys Sullivan
61aecc92ea fix: change subagent navigation order to newest-to-oldest (#5680) 2025-12-26 14:34:05 -06:00
Shantur Rathore
a652fe9076 fix: config option setCacheKey not being respected (#5686) 2025-12-26 14:34:04 -06:00
Aiden Cline
92f4fdc568 docs: update share link 2025-12-26 14:34:04 -06:00
Frank
c64c29821a zen: add gemini 3 flash 2025-12-26 14:34:04 -06:00
Brendan Allan
5b65e14c2b tauri: nsis header and sidebar 2025-12-26 14:34:04 -06:00
Brendan Allan
3f8f6177d4 tauri: update nsis icon 2025-12-26 14:34:04 -06:00
Stoufiler
3a46b78994 docs: Sort LSP Server list (#5688) 2025-12-26 14:34:04 -06:00
Rohan Mukherjee
d14f767381 feat: added lucent-orng theme (#5678)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-26 14:34:04 -06:00
Sachnun
355e027ea6 fix: remove unsupported parameter from bash tool description (#5676) 2025-12-26 14:34:04 -06:00
Brendan Allan
f4e851d9a1 tauri: return after update failures 2025-12-26 14:34:04 -06:00
GitHub Action
5524c9c6dc chore: format code 2025-12-26 14:34:04 -06:00
Brendan Allan
566e0f75a4 tauri: only alert on update failure when triggered manually 2025-12-26 14:34:03 -06:00
Brendan Allan
daf387861d tauri: dev icons + separate prod config (#5691)
Co-authored-by: GitHub Action <action@github.com>
2025-12-26 14:34:03 -06:00
Adam
c0b9f1c147 fix: command shortcuts 2025-12-26 14:34:03 -06:00
Github Action
978c6a30f6 Update Nix flake.lock and hashes 2025-12-26 14:34:03 -06:00
Amadeus Demarzi
96719bdf52 Diffs Performance Improvements (#5653)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-26 14:34:03 -06:00
GitHub Action
561e9f35ae ignore: update download stats 2025-12-17 2025-12-26 14:34:03 -06:00
David Hill
201b3f4014 wip: auto-detect OS and show desktop download button 2025-12-26 14:34:03 -06:00
David Hill
b3e173fb8f fix: website cta button 2025-12-26 14:34:03 -06:00
Adam
9f6032f250 chore: cleanup 2025-12-26 14:34:02 -06:00
Adam
6362cff7cb feat(desktop): share sessions 2025-12-26 14:34:02 -06:00
Adam
0ae1c170c2 feat(desktop): lsp diagnostics displayed 2025-12-26 14:34:02 -06:00
Github Action
6afe7337a9 Update Nix flake.lock and hashes 2025-12-26 14:34:02 -06:00
GitHub Action
831ef65a41 chore: format code 2025-12-26 14:34:02 -06:00
Sebastian Herrlinger
797271e2e2 upgrade opentui to v0.1.61 2025-12-26 14:34:01 -06:00
Jeon Suyeol
f268d81e16 Add availability to disable terminal title using OPENCODE_DISABLE_TERMINAL_TITLE env (#5661) 2025-12-26 14:34:01 -06:00
Dax Raad
1b28bd68f6 ci: update publish workflow configuration 2025-12-26 14:34:01 -06:00
GitHub Action
798dee27d0 chore: format code 2025-12-26 14:34:01 -06:00
Spoon
463bf7ab3a plugin(hook): add task tool execution hooks and command context tracking (#5642) 2025-12-26 14:34:01 -06:00
Aiden Cline
e4d7bb4869 ci: tweak triage 2025-12-26 14:34:01 -06:00
Matt Silverlock
80f1173652 github: add configurable mentions input (#5655) 2025-12-26 14:34:00 -06:00
Adam
be7b9c05a5 tui: increase session width to accommodate longer code blocks and improve readability 2025-12-26 14:34:00 -06:00
David Hill
31897bf974 fix: load more button 2025-12-26 14:34:00 -06:00
Aiden Cline
09a2d3b8b7 ci: fix missing pkg issue 2025-12-26 14:34:00 -06:00
Dax Raad
f80e555a58 ci: fix release draft configuration to prevent automatic draft flag 2025-12-26 14:34:00 -06:00
GitHub Action
59a618ddcb chore: format code 2025-12-26 14:33:59 -06:00
Adam
1cbc9e252c fix(share): content wasn't centered 2025-12-26 14:33:59 -06:00
opencode
9e98bd3283 release: v1.0.164 2025-12-26 14:33:59 -06:00
Adam
15ca31370d fix(desktop): prompt history nav 2025-12-26 14:33:59 -06:00
Aiden Cline
d9a6000a33 fix: user invoked subtasks causing tool_use or missing thinking signa… (#5650) 2025-12-26 14:33:59 -06:00
Adam
daaf88b8e4 fix(desktop): auto-scroll 2025-12-26 14:33:59 -06:00
Adam
c15b581131 fix(desktop): focus prompt input after dialog close 2025-12-26 14:33:59 -06:00
Adam
67973728ef fix(desktop): prompt history navigation 2025-12-26 14:33:58 -06:00
Adam
3d7bdfc52e fix: auto-scroll 2025-12-26 14:33:58 -06:00
Adam
02c8c2cb67 fix: working logic 2025-12-26 14:33:58 -06:00
Adam
53c386e2e5 fix: prompt input multi line input 2025-12-26 14:33:58 -06:00
Adam
196ce5af40 fix: defensive audio init 2025-12-26 14:33:58 -06:00
Eric Guo
299e88b9cf feat(desktop): Loading more session number per project by button (#5616)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:58 -06:00
matvey
0302b8317b feat: add experimental oxfmt formatter (#5620) 2025-12-26 14:33:57 -06:00
Aiden Cline
cc7fa6af83 ci: include desktop & tauri in release notes 2025-12-26 14:33:57 -06:00
Aiden Cline
5dd09b13b1 ci: fix branch name 2025-12-26 14:33:57 -06:00
Aiden Cline
77c7b094a8 fix: git branch filewatcher, add flag to completely disable watcher 2025-12-26 14:33:57 -06:00
Aiden Cline
2fdf6e1029 ci: fix triage 2025-12-26 14:33:57 -06:00
Aiden Cline
c3f1e631a0 ci: auto tag github action once a change is shipped for it 2025-12-26 14:33:57 -06:00
Dax Raad
9c8b670886 core: update plugin dependency and config loading for .opencode directory support 2025-12-26 14:33:57 -06:00
Dax Raad
d0cb59c000 core: fix Tauri desktop app SSE connection timeout
- Add heartbeat events to /global/event and /event SSE endpoints
- Send server.heartbeat event every 30s to prevent WKWebView 60s timeout
- Fixes desktop app disconnecting from global events after 1 minute
2025-12-26 14:33:57 -06:00
Dax Raad
9dbbdcc555 fix 2025-12-26 14:33:57 -06:00
Dax Raad
8b3e0422ee ignore: update opencode plugin dependency 2025-12-26 14:33:57 -06:00
Dax Raad
13e26ff18a core: ensure desktop app loads user shell environment variables
Changes shell spawn flags from -l to -il so that ~/.zshrc and
~/.bashrc are sourced when starting the desktop app on macOS
and Linux. This fixes missing PATH and other environment
variables that users expect to be available.
2025-12-26 14:33:57 -06:00
Adam
183cbc1981 feat(desktop): show retries 2025-12-26 14:33:57 -06:00
Adam
25ca8cec4d fix: layout badness 2025-12-26 14:33:56 -06:00
Adam
01261e7f79 fix: defensive audio init 2025-12-26 14:33:56 -06:00
Adam
e598524b40 feat(desktop): show write tool output 2025-12-26 14:33:56 -06:00
Adam
582d9b4187 chore: cleanup 2025-12-26 14:33:56 -06:00
Github Action
babba4e6db Update Nix flake.lock and hashes 2025-12-26 14:33:56 -06:00
Dax Raad
419db64fc9 chore: update opencode plugin dependencies and fix tauri sidecar path 2025-12-26 14:33:56 -06:00
Dax Raad
f386601cbd tui: fix autocomplete file loading and update dependencies 2025-12-26 14:33:55 -06:00
David Hill
12d5a83ba3 fix: remove the selected state from button when select deselected 2025-12-26 14:33:55 -06:00
David Hill
cc17c3338e wip: add active state to open select 2025-12-26 14:33:55 -06:00
Fran Zekan
3460e29107 fix: enable shell alias expansion in ! command (#5621) 2025-12-26 14:33:55 -06:00
GitHub Action
e11051a29a chore: format code 2025-12-26 14:33:55 -06:00
shekohex
debba2ab37 docs: fix typo in Google Antigravity github link (#5625) 2025-12-26 14:33:54 -06:00
jinzhongjia
91b7c3bfa4 docs: Add new project entry for opencode.nvim frontend (#5626) 2025-12-26 14:33:54 -06:00
Connor Adams
8c15365133 docs: add homebrew command for opencode-desktop (#5631)
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 <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:54 -06:00
Aiden Cline
b4db2b1bdd fix: github install cmd if repo has . in it 2025-12-26 14:33:54 -06:00
David Hill
5570b9f05f fix: breadcrumb dropdown position left aligned 2025-12-26 14:33:53 -06:00
David Hill
370aa7b515 Revert "wip: make the default container wider"
This reverts commit 1f18f389c0.
2025-12-26 14:33:53 -06:00
Tommy D. Rossi
e8c2e494d0 fix: use system prompt field from prompt input (#5633)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-26 14:33:53 -06:00
GitHub Action
434e12666b chore: format code 2025-12-26 14:33:53 -06:00
Aiden Cline
1de89153d8 ci: tweak triage 2025-12-26 14:33:52 -06:00
Brendan Allan
22192a2e92 Keep release a draft until all builds are finished (#5632)
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: GitHub Action <action@github.com>
2025-12-26 14:33:52 -06:00
David Hill
73b6464998 wip: make the default container wider 2025-12-26 14:33:52 -06:00
Simon D'Morias
3f571c6117 fix: preserve argument boundaries in run command (#4979) 2025-12-26 14:33:51 -06:00
David Hill
4126084dfd Revert "fix: strip parentheses from file paths generated by llm"
This reverts commit 6c1a1a77b7.
2025-12-26 14:33:51 -06:00
GitHub Action
5529d1b741 ignore: update download stats 2025-12-16 2025-12-26 14:33:51 -06:00
GitHub Action
3555acbe78 chore: format code 2025-12-26 14:33:51 -06:00
Brendan Allan
331ece741f tauri: explicitly kill sidecar before updater relaunch 2025-12-26 14:33:51 -06:00
David Hill
039f853d80 fix: strip parentheses from file paths generated by llm 2025-12-26 14:33:51 -06:00
David Hill
38a0f49595 fix: font size updates 2025-12-26 14:33:50 -06:00
David Hill
f82ac7a7b8 wip: font-size updates 2025-12-26 14:33:50 -06:00
GitHub Action
a8ff5b5174 chore: format code 2025-12-26 14:33:50 -06:00
Brendan Allan
3b1b621d00 tauri: macos-only app menu 2025-12-26 14:33:50 -06:00
Aiden Cline
200ea62f56 fix: small bug w/ install script 2025-12-26 14:33:50 -06:00
Aiden Cline
158f68070f chore: centralize dep to catalog & fix typos 2025-12-26 14:33:49 -06:00
Aiden Cline
aea35df988 ci: Update issue assignment and labeling guidelines
Clarified assignment responsibilities and labeling for issues related to OpenCode tools.
2025-12-26 14:33:49 -06:00
Dax Raad
73b44d198e tui: fix dialog select items taking up 2 lines when truncated
Prevents text wrapping in dialog select options by removing wrapMode,
ensuring truncated text stays on single line and maintains proper timestamp visibility
2025-12-26 14:33:49 -06:00
Luke Parker
3b75ffdc41 fix: debounce LSP diagnostics to get complete results (#5600) 2025-12-26 14:33:48 -06:00
DS
bf8210d750 fix: rejoin system prompt if experimental plugin hook triggers to preserve caching (#5550)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 14:33:48 -06:00
Lucas Duailibe
033212c201 make install script use tmp dir (#5601) 2025-12-26 14:33:48 -06:00
Aiden Cline
dc5df2cd10 ci: cheaper model 2025-12-26 14:33:48 -06:00
Aiden Cline
ca2f281a30 ci: ignore 2025-12-26 14:33:48 -06:00
GitHub Action
85ba9b7831 chore: format code 2025-12-26 14:33:48 -06:00
Aiden Cline
1f3129fe96 ci: auto triage issues 2025-12-26 14:33:48 -06:00
opencode
b0c241a59f release: v1.0.162 2025-12-26 14:33:47 -06:00
GitHub Action
780e32bc2b chore: format code 2025-12-26 14:33:47 -06:00
Dax Raad
133e0983d6 tui: update dialog context and server to use new single dialog system 2025-12-26 14:33:47 -06:00
Dax Raad
1f24cccd66 tui: refactor dialog system to use single active dialog instead of stack 2025-12-26 14:33:47 -06:00
opencode
3774fafb26 release: v1.0.161 2025-12-26 14:33:46 -06:00
Adam
c0842ec113 fix: undefined events 2025-12-26 14:33:46 -06:00
opencode-agent[bot]
2560cba9ef docs: models --refresh flag in cli.mdx (#5596)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-26 14:33:46 -06:00
Luke Parker
9b470429de fix(win32): Normalise LSP paths on windows (fixes lua) (#5597) 2025-12-26 14:33:46 -06:00
Aiden Cline
e8dba6264f tweak: add .catch for extractZip calls 2025-12-26 14:33:46 -06:00
GitHub Action
bb47a1e0f8 chore: format code 2025-12-26 14:33:45 -06:00
Luke Parker
7e9f77117f fix(win32): Missing LSP can now unzip on windows (#5594) 2025-12-26 14:33:45 -06:00
opencode
ca7ac43cff release: v1.0.160 2025-12-26 14:33:45 -06:00
Dax Raad
471ec0350c tui: fix dialog replacement to prevent nested dialogs from showing simultaneously 2025-12-26 14:33:44 -06:00
Dax Raad
90077b6dd0 tui: fix model selection dialog to properly replace current dialog instead of creating nested dialogs 2025-12-26 14:33:44 -06:00
Luke Parker
5fc0a55f54 fix(win32): correct ElixirLS extension typo (#5590) 2025-12-26 14:33:44 -06:00
GitHub Action
3ad2a40e89 chore: format code 2025-12-26 14:33:43 -06:00
Luke Parker
59da2e16b2 fix(win32): use path.delimiter for PATH separator in LSP server lookups (#5589) 2025-12-26 14:33:43 -06:00
opencode
a9a5cf0a76 release: v1.0.159 2025-12-26 14:33:43 -06:00
Dax Raad
e9f899cb9a fix dialog root complexity 2025-12-26 14:33:42 -06:00
Luke Parker
8b976833a3 fix(windows): opencode github install (#5587) 2025-12-26 14:33:42 -06:00
Ariane Emory
fc5493e8bd fix: restore ability to bind keys for model_cycle_favorite model_cycle_favorite_reverse (resolves #5198) (#5202) 2025-12-26 14:33:42 -06:00
opencode-agent[bot]
ecef4d2a92 docs: enabled_providers docs section (#5586)
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 <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:42 -06:00
Adam
2771367dc4 fix: multiline user input 2025-12-26 14:33:42 -06:00
Adam
e0ca160360 fix: keybinds for agent and model selection 2025-12-26 14:33:42 -06:00
Adam
94d6c1bc92 fix: landing page CLS hero jump down 2025-12-26 14:33:41 -06:00
Adam
f58c96fea5 fix: session nav on homepage 2025-12-26 14:33:41 -06:00
Adam
f4336b504a fix: default steps expanded unless done 2025-12-26 14:33:41 -06:00
Adam
acd2d77e47 fix: landing page CLS hero jump down 2025-12-26 14:33:41 -06:00
Aiden Cline
c0aaba9f65 tweak: add model flag support for agent create command 2025-12-26 14:33:41 -06:00
Jay V
58a37e72e7 docs: make project names clickable links in ecosystem documentation 2025-12-26 14:33:41 -06:00
Adam
589cdc40e3 fix: missing event type (global) 2025-12-26 14:33:41 -06:00
Ariane Emory
b77826f3c5 fix: record shell mode in history (resolves #5454) (#5551) 2025-12-26 14:33:40 -06:00
GitHub Action
46b04f1163 chore: format code 2025-12-26 14:33:40 -06:00
Aiden Cline
31ddd0798a add ability to set topK 2025-12-26 14:33:40 -06:00
opencode
813c330c47 release: v1.0.158 2025-12-26 14:33:40 -06:00
Adam
6205f0bde0 fix: modal search 2025-12-26 14:33:40 -06:00
justfortheloveof
c28709827f tweak: prioritize fuzzysort results that start with user input (#5571) 2025-12-26 14:33:40 -06:00
Adam
ce7f4c0e6e fix: allow for non-vcs projects in desktop 2025-12-26 14:33:40 -06:00
GitHub Action
362502cf6f chore: format code 2025-12-26 14:33:39 -06:00
Dax Raad
5d29af3b45 core: fix server response handling to prevent connection timeouts 2025-12-26 14:33:39 -06:00
opencode
a13af69de5 release: v1.0.157 2025-12-26 14:33:39 -06:00
Dax Raad
653c181312 ci: fix tauri updater version mismatch by checking out the release tag 2025-12-26 14:33:39 -06:00
Dax Raad
8dca9051f1 ci: add app bundle target to fix macOS updater by generating .app.tar.gz files 2025-12-26 14:33:39 -06:00
GitHub Action
6f56b69264 chore: format code 2025-12-26 14:33:39 -06:00
Dax Raad
64b8f0a1a9 core: fix message caching for Anthropic models to improve response consistency 2025-12-26 14:33:38 -06:00
opencode
6fd6eecbf5 release: v1.0.156 2025-12-26 14:33:38 -06:00
Adam
74d4a6bcb1 fix: connect provider on homepage 2025-12-26 14:33:38 -06:00
Adam
47eaef468f fix: terminal in desktop 2025-12-26 14:33:38 -06:00
Adam
1d68c34be6 fix: image attachments in desktop 2025-12-26 14:33:37 -06:00
Github Action
9fb67310bb Update Nix flake.lock and hashes 2025-12-26 14:33:37 -06:00
Dax Raad
f23fc83d3c docs: update header navigation to include desktop download 2025-12-26 14:33:37 -06:00
Adam
8138c36343 fix: share page 2025-12-26 14:33:36 -06:00
Dax Raad
9ffaaf9bbf docs: restore desktop beta banner to homepage 2025-12-26 14:33:36 -06:00
GitHub Action
52bced424f chore: format code 2025-12-26 14:33:36 -06:00
Nalin Singh
ba1cd2777a feat: add F# language server support (#5549) 2025-12-26 14:33:35 -06:00
opencode
f0da095552 release: v1.0.155 2025-12-26 14:33:35 -06:00
Dax Raad
ccaf562c9e 20min 2025-12-26 14:33:35 -06:00
Adam
973162c444 chore: update stats 2025-12-26 14:33:34 -06:00
GitHub Action
be261c6cdb chore: format code 2025-12-26 14:33:34 -06:00
Adam
27da9c1852 chore: cleanup 2025-12-26 14:33:34 -06:00
Adam
779311d064 wip(desktop): progress 2025-12-26 14:33:34 -06:00
Adam
25d6d17296 wip(desktop): progress 2025-12-26 14:33:33 -06:00
Adam
ab48ff0471 wip(desktop): progress 2025-12-26 14:33:33 -06:00
Adam
1078ae03a0 wip(desktop): progress 2025-12-26 14:33:33 -06:00
Adam
a4aa758043 chore: cleanup 2025-12-26 14:33:33 -06:00
Adam
9536746fb7 chore: cleanup 2025-12-26 14:33:33 -06:00
Adam
b088180464 feat(desktop): custom commands 2025-12-26 14:33:33 -06:00
Adam
211524cb0b wip(desktop): progress 2025-12-26 14:33:33 -06:00
Adam
c7b8b7c49e wip(desktop): progress 2025-12-26 14:33:33 -06:00
Adam
105cfa8d4e wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
eab1b44f66 wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
668c932631 wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
5fb20a52a7 wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
73b74581f8 wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
9ac9396aec wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
3df9f708b3 Revert "wip(desktop): session turn state consolidation"
This reverts commit 453f862616dc4d3ac90680581cde279e118b0da1.
2025-12-26 14:33:32 -06:00
Adam
dc65a46fb8 wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
71cb4c3ddc wip(desktop): progress 2025-12-26 14:33:32 -06:00
Adam
0a556bb646 wip(desktop): progress 2025-12-26 14:33:31 -06:00
Adam
c8e1950522 wip(desktop): session turn state consolidation 2025-12-26 14:33:31 -06:00
Adam
32bbc07e36 wip(desktop): progress 2025-12-26 14:33:31 -06:00
Adam
c76a00fc3f wip(desktop): progress 2025-12-26 14:33:31 -06:00
Adam
ce4d0d76ea wip(desktop): progress 2025-12-26 14:33:31 -06:00
Adam
df844dc408 wip(desktop): progress 2025-12-26 14:33:31 -06:00
Adam
10276e8b3f wip(desktop): progress 2025-12-26 14:33:30 -06:00
opencode
ea1c063d98 release: v1.0.154 2025-12-26 14:33:30 -06:00
Dax Raad
83249e69f3 ci: update publish workflow concurrency to include version inputs and upgrade ubuntu runner to 24.04 2025-12-26 14:33:30 -06:00
GitHub Action
b612b939c9 chore: format code 2025-12-26 14:33:30 -06:00
Dax Raad
0ffc67792c core: reorganize agent configuration to separate primary agents (build, plan) from subagents 2025-12-26 14:33:30 -06:00
Dax Raad
37d0ef3362 ci: fix AppImage build hanging by using portable appimage format
- Add appimage target back to tauri.conf.json
- Force reinstall tauri-cli from feat/truly-portable-appimage branch
- Add 10 minute timeout to prevent indefinite hangs
- Add logging to verify correct tauri-cli version is installed

This fixes the issue where AppImage builds would hang at 'Running input plugin: gtk' by using the new portable appimage format that bypasses linuxdeploy entirely.
2025-12-26 14:33:30 -06:00
René
62e5e42707 docs: Fix Wakatime repository link in ecosystem.mdx (#5552)
Co-authored-by: Github Action <action@github.com>
2025-12-26 14:33:29 -06:00
David Hill
f8af042811 fix: font size updates 2025-12-26 14:33:29 -06:00
David Hill
45d85a71c6 fix: add tooltip to close review tab 2025-12-26 14:33:29 -06:00
David Hill
cd76512659 fix: prompt input using border shadow 2025-12-26 14:33:29 -06:00
David Hill
9a4e7319df fix: replace agents dropdown with shadow border 2025-12-26 14:33:29 -06:00
David Hill
c6ed81fa8d fix: fix width and padding on agent select
- conditionally hide the role=presentation element because it was adding extra gap before the first agent
2025-12-26 14:33:28 -06:00
David Hill
ba4cd97d16 fix: hide prompt input send tooltip when the send button is disabled 2025-12-26 14:33:28 -06:00
David Hill
bc2d2297bb fix: avatar and icon size in sidebar 2025-12-26 14:33:28 -06:00
David Hill
731233c96d fix: remove blue border from prompt input 2025-12-26 14:33:28 -06:00
GitHub Action
bcb5840537 ignore: update download stats 2025-12-15 2025-12-26 14:33:28 -06:00
René
0bf1c40eff Provider fix, anthropic Errorhandling if empty image file is read (#5521) 2025-12-26 14:33:28 -06:00
Nalin Singh
48eeb4e80d fix: input lip visibility for transparent themes (#5544) 2025-12-26 14:33:28 -06:00
Spoon
64552fe7e1 fix(edit): add per-file lock to prevent read-before-write race (#4388) 2025-12-26 14:33:28 -06:00
Aiden Cline
c7b5831a0f ignore: fix debug var in last commit 2025-12-26 14:33:27 -06:00
opencode-agent[bot]
70a8efc843 Add dismiss button to Getting Started box (#5543)
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-26 14:33:27 -06:00
DS
b98bd90d50 feat: restore experimental.chat.messages.transform and add experimental.chat.system.transform hooks (#5542) 2025-12-26 14:33:27 -06:00
Aiden Cline
c985ef172d ci: switch model 2025-12-26 14:33:27 -06:00
Brendan Allan
83a320a2e7 tauri: use correct sidecar name 2025-12-26 14:33:27 -06:00
Brendan Allan
cc82a2711b tauri: rename sidecar to opencode-cli 2025-12-26 14:33:26 -06:00
Aiden Cline
7b83294590 ci: smart oc 2025-12-26 14:33:26 -06:00
Ravi Kumar
4e4a24b769 fix(session): fix unshare command not clearing share state (#5523) 2025-12-26 14:33:26 -06:00
GitHub Action
9fbf6d4bd6 chore: format code 2025-12-26 14:33:25 -06:00
Adam
f27b737038 fix: test 2025-12-26 14:33:25 -06:00
Adam
5246ecfc88 fix: update sdk 2025-12-26 14:33:25 -06:00
Adam
d1c3bb2616 wip(desktop): progress 2025-12-26 14:33:25 -06:00
Adam
e9f8b7d91f wip(desktop): progress 2025-12-26 14:33:24 -06:00
Adam
bdd5e63529 wip(desktop): progress 2025-12-26 14:33:24 -06:00
Adam
7584bd0d71 wip(desktop): progress 2025-12-26 14:33:24 -06:00
Adam
60fadb253a wip(desktop): progress 2025-12-26 14:33:23 -06:00
Adam
de635e3c51 wip(desktop): progress 2025-12-26 14:33:23 -06:00
Adam
6c0638c73b fix(desktop): auto scroll 2025-12-26 14:33:22 -06:00
Adam
238ca62ba7 wip(desktop): progress 2025-12-26 14:33:22 -06:00
Adam
d8aba3a35f fix(desktop): layout fixes 2025-12-26 14:33:22 -06:00
Mark Jaquith
8a518c8b2a feat(cli): auto-submit prompt when using --prompt flag (#4510) 2025-12-26 14:33:21 -06:00
Dax Raad
bc7f81c1a3 fix share link 2025-12-26 14:33:21 -06:00
Dax Raad
c8d735de6c ci: fix test failures in CI by pre-populating models cache
Tests were failing in CI because the models.json cache file doesn't exist
and the data() macro fallback only works at build time, not runtime.
The preload now pre-fetches models.json and disables the background
refresh to prevent race conditions during test execution.
2025-12-26 14:33:21 -06:00
Dax Raad
f4e1fa28b8 fix desktop updater 2025-12-26 14:33:21 -06:00
Dax
262d836dd7 LLM cleanup (#5462)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:21 -06:00
Ravi Kumar
01c2ba2e4a fix(tui): --continue selects wrong session (#5513) 2025-12-26 14:33:21 -06:00
GitHub Action
3b59308282 chore: format code 2025-12-26 14:33:21 -06:00
Martijn Baay
b6714c645e feat: add experimental.continue_loop_on_deny config option (#4729)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-26 14:33:20 -06:00
Dax Raad
0b60e82266 ignore: simplify download page to use GitHub latest redirect URLs 2025-12-26 14:33:20 -06:00
GitHub Action
321354529b chore: format code 2025-12-26 14:33:20 -06:00
Dax Raad
3efb17e91e disable app image 2025-12-26 14:33:20 -06:00
opencode
df1bc0e9bb release: v1.0.153 2025-12-26 14:33:19 -06:00
Aiden Cline
ac843d39ea tweak: fallback to provider default for temperature 2025-12-26 14:33:19 -06:00
GitHub Action
dbfdb54315 chore: format code 2025-12-26 14:33:19 -06:00
Nalin Singh
2816e84f0a fix: ensure input borders are drawn in transparent themes (#5524) 2025-12-26 14:33:19 -06:00
GitHub Action
5b7581ebbe chore: format code 2025-12-26 14:33:19 -06:00
Sellers Crisp
62bcdf3144 feat: add server_error, rate_limit, and no_kv_space retry logic to accommodate Foundry API issues (#5527)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:19 -06:00
shekohex
9746d89cc1 docs: add opencode-pty and opencode-google-antigravity-auth plugins to the echosystem (#5530) 2025-12-26 14:33:18 -06:00
Lawrence Sarpong
74fb054040 Add Gleam LSP and formatter (#5514) 2025-12-26 14:33:18 -06:00
Aymvn
527836a90b docs: Fix Wakatime link in ecosystem documentation (#5528) 2025-12-26 14:33:18 -06:00
GitHub Action
481147c946 ignore: update download stats 2025-12-14 2025-12-26 14:33:18 -06:00
Aiden Cline
3babb33f8d rm unnecessary code 2025-12-26 14:33:18 -06:00
Aiden Cline
1192e16278 add topK function to transform, add temp defaults for glm and minimax 2025-12-26 14:33:18 -06:00
YeonGyu-Kim
e59d479947 fix(ui): guard Node reference for SSR compatibility in isTriggerTitle (#5509) 2025-12-26 14:33:18 -06:00
Brendan Allan
2980eff805 tauri: change mainBinaryName to just OpenCode 2025-12-26 14:33:17 -06:00
Brendan Allan
72c0af5b6c tauri: bring back appimage 2025-12-26 14:33:17 -06:00
Sachnun
deaf35707d fix(tui): open parent session instead of subagent on continue flag (#5503) 2025-12-26 14:33:17 -06:00
Zhou Rui
73b9705410 docs: Add opencode-websearch-cited to plugin list (#5501) 2025-12-26 14:33:17 -06:00
Adam
782ed12244 fix: sort models 2025-12-26 14:33:17 -06:00
Adam
696d14167c fix: use opencode icon 2025-12-26 14:33:17 -06:00
Adam
e9eb799464 chore: cleanup 2025-12-26 14:33:17 -06:00
Adam
05846ed29d chore: cleanup 2025-12-26 14:33:17 -06:00
Tommy D. Rossi
d80634642c fix: limit LSP diagnostics to prevent context window waste (#5480)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:17 -06:00
Aiden Cline
d7f32a57ff docs: networking 2025-12-26 14:33:17 -06:00
Aiden Cline
51267f256e chore: reduce duplication of field in transform 2025-12-26 14:33:17 -06:00
Adam
860b62a6e8 chore: cleanup 2025-12-26 14:33:17 -06:00
Adam
c952505526 fix(desktop): archive button 2025-12-26 14:33:17 -06:00
Adam
1f8ac107e9 fix(desktop): auto scroll 2025-12-26 14:33:17 -06:00
Adam
eceac1e727 feat(desktop): show richer status when thinking 2025-12-26 14:33:16 -06:00
Adam
4202298422 fix: css scroll jitter 2025-12-26 14:33:16 -06:00
Adam
43e50b713e fix: don't rotate placeholders in session 2025-12-26 14:33:16 -06:00
Adam
f7544b729c fix: don't open shell by default 2025-12-26 14:33:16 -06:00
Github Action
b84530821e Update Nix flake.lock and hashes 2025-12-26 14:33:16 -06:00
Adam
c808815ec8 chore: cleanup 2025-12-26 14:33:16 -06:00
Adam
280d309a52 fix(desktop): terminal light mode 2025-12-26 14:33:15 -06:00
Adam
a8d7b8e946 feat(desktop): message history 2025-12-26 14:33:15 -06:00
Adam
7a494abea9 fix: session turn scroll 2025-12-26 14:33:15 -06:00
Felipe Oduardo Sierra
48ff68512a add ARM64 Docker image support (#5483) 2025-12-26 14:33:15 -06:00
Github Action
deb6e37cc1 Update Nix flake.lock and hashes 2025-12-26 14:33:15 -06:00
Aiden Cline
5257b22135 bump bun version & set flags this time 2025-12-26 14:33:15 -06:00
GitHub Action
b376d5b622 chore: format code 2025-12-26 14:33:14 -06:00
Jan-Niklas W.
484416b503 docs: fix title for JetBrains ACP config file (#5479) 2025-12-26 14:33:14 -06:00
YeonGyu-Kim
8ed42c8bbb docs: add oh-my-opencode to plugins list (#5481) 2025-12-26 14:33:14 -06:00
GitHub Action
91a6f77ff0 chore: format code 2025-12-26 14:33:14 -06:00
rari404
84975a5d82 feat: add dockerfile language server (#5252)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:14 -06:00
GitHub Action
74319df2e3 ignore: update download stats 2025-12-13 2025-12-26 14:33:14 -06:00
rari404
a9c179db7a feat: add texlab language server and latexindent formatter (#5251) 2025-12-26 14:33:14 -06:00
GitHub Action
e85d53f2a9 chore: format code 2025-12-26 14:33:14 -06:00
Jan-Niklas W.
06e93668ee docs: JetBrains IDEs to ACP config docs page (#5465) 2025-12-26 14:33:14 -06:00
Matt Silverlock
a837cef3cb github: support GITHUB_TOKEN + skip OIDC (#5459) 2025-12-26 14:33:14 -06:00
Charles Cooper
5ee52d2f84 fix: add --session flag to attach command (#5460)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-26 14:33:14 -06:00
David Hill
f1ceee1f2e fix: mute the project path in the sidebar that proceeds the final directory 2025-12-26 14:33:14 -06:00
Dax Raad
8e7269d985 fix 2025-12-26 14:33:14 -06:00
GitHub Action
829d1bc084 chore: format code 2025-12-26 14:33:14 -06:00
Aiden Cline
10e0cd619d docs: add env vars sections 2025-12-26 14:33:13 -06:00
opencode
c8e64f208a release: v1.0.152 2025-12-26 14:33:13 -06:00
Adam
5ddc0fd901 Revert "fix: archive button"
This reverts commit bc3286de46.
2025-12-26 14:33:13 -06:00
Aiden Cline
9bd8c6d3d3 shell tweaks, better handling for windows (#5455)
Co-authored-by: GitHub Action <action@github.com>
2025-12-26 14:33:13 -06:00
Adam
11100c4ba2 fix: max height on bash tool 2025-12-26 14:33:13 -06:00
Adam
7bc072c9ad fix: archive button 2025-12-26 14:33:12 -06:00
Dax Raad
e2530833e3 desktop: fix build on Linux and Windows by making macOS title bar styling conditional 2025-12-26 14:33:12 -06:00
Sebastian Herrlinger
cd3f2412e8 only exit app when prompt is empty, otherwise fallthrough, fix #5457 2025-12-26 14:33:12 -06:00
GitHub Action
e8a20cfe9e chore: format code 2025-12-26 14:33:12 -06:00
Dan Brown
071a87177a Shell: No -l in fallback, for max compatibility (#5452) 2025-12-26 14:33:12 -06:00
opencode
c1fda41fad release: v1.0.151 2025-12-26 14:33:11 -06:00
Github Action
fa275b0bf9 Update Nix flake.lock and hashes 2025-12-26 14:33:11 -06:00
Adam
1e3cd35cb8 fix: desktop layout 2025-12-26 14:33:11 -06:00
Adam
4425f13430 fix: desktop layout 2025-12-26 14:33:11 -06:00
Adam
5c2f616c73 fix: desktop layout 2025-12-26 14:33:11 -06:00
Adam
04d71a1820 fix: desktop layout 2025-12-26 14:33:10 -06:00
Adam
11062f83ae fix: desktop layout 2025-12-26 14:33:10 -06:00
Adam
0925ea6c7e fix: desktop layout 2025-12-26 14:33:10 -06:00
Adam
fe1e0b52c7 fix: desktop layout 2025-12-26 14:33:09 -06:00
Adam
ac8f4ab678 fix: desktop layout 2025-12-26 14:33:09 -06:00
Adam
72d1aadea1 fix: desktop layout 2025-12-26 14:33:09 -06:00
Adam
4ca8dcf79d fix: desktop layout 2025-12-26 14:33:09 -06:00
Adam
db8ee3471b chore: cleanup 2025-12-26 14:33:08 -06:00
Adam
c06b78859a chore: cleanup 2025-12-26 14:33:08 -06:00
Adam
b319187151 wip: desktop timeline changes 2025-12-26 14:33:08 -06:00
Dax Raad
4746338e8e only gen summary if diffs 2025-12-26 14:33:08 -06:00
Aiden Cline
5221251b5c tweak: 5.1 -> 5. in transform 2025-12-26 14:33:08 -06:00
GitHub Action
e6520dd6a9 chore: format code 2025-12-26 14:33:08 -06:00
David Hill
845a21da35 fix: mute the whole prompt area when leader key is active 2025-12-26 14:33:08 -06:00
Dax Raad
b41333d30f reuse existing server query 2025-12-26 14:33:07 -06:00
Dax Raad
f9c24ba266 sync 2025-12-26 14:33:07 -06:00
Dax Raad
0d6eba5392 sync 2025-12-26 14:33:07 -06:00
Aiden Cline
9abbd74a51 tweak: 5.1 -> 5. reasoning effort match 2025-12-26 14:33:07 -06:00
Frank
376639f6b8 Zen: add gpt5.2 2025-12-26 14:33:06 -06:00
Luke Parker
f31151deaf fix: osascript for clipboard typo (#5430) 2025-12-26 14:33:06 -06:00
Adam
22487904ab fix: desktop layout 2025-12-26 14:33:06 -06:00
Jeremy Osih
8b1b411989 Change tooltip text from 'Open file' to 'New Terminal' (#5435) 2025-12-26 14:33:06 -06:00
Adam
70b6c4dfc9 fix: tweak missing colors 2025-12-26 14:33:06 -06:00
Adam
03be72bb2b chore: cleanup 2025-12-26 14:33:06 -06:00
xu0o0
c4c36ea54b acp: fix internal error on /compact (#5424) 2025-12-26 14:33:06 -06:00
Brendan Allan
3d751c0da1 tauri: add basic custom titlebar (#5438) 2025-12-26 14:33:05 -06:00
Adam
385ac04c1d feat(desktop): archive sessions 2025-12-26 14:33:05 -06:00
Adam
7cdd33199e fix(desktop): audio stuff 2025-12-26 14:33:05 -06:00
Adam
1a83ffdf01 fix(desktop): homedir aware path on home 2025-12-26 14:33:05 -06:00
GitHub Action
44084dac5f ignore: update download stats 2025-12-12 2025-12-26 14:33:05 -06:00
Github Action
28066ecfbd Update Nix flake.lock and hashes 2025-12-26 14:33:04 -06:00
Adam
b2d028693d feat(desktop): basic alerting 2025-12-26 14:33:04 -06:00
GitHub Action
dfe6c58b04 chore: format code 2025-12-26 14:33:04 -06:00
David Hill
644e360a63 fix: increase font-size-small to 13px 2025-12-26 14:33:04 -06:00
David Hill
8cee140196 fix: make syntax colors have more contrast 2025-12-26 14:33:04 -06:00
Brendan Allan
c301703f89 tauri: create window with full screen size 2025-12-26 14:33:04 -06:00
Github Action
57b7f112d0 Update Nix flake.lock and hashes 2025-12-26 14:33:04 -06:00
GitHub Action
e45d014b7b chore: format code 2025-12-26 14:33:03 -06:00
Brendan Allan
9dfacb8dd1 tauri: initialise store and window-state plugins 2025-12-26 14:33:03 -06:00
Viktor Forsman
ddf44233e7 fix: debug lsp diagnostics cmd for certain lsps (#5420) 2025-12-26 14:33:03 -06:00
Frank
010d1298c0 Zen: sync 2025-12-26 14:33:03 -06:00
Rhys Sullivan
729eca8095 [feat]: show indicator for in progress chats in the sessions list (#5417) 2025-12-26 14:33:03 -06:00
Sachnun
aed32dd840 fix(tui): restore input on timeline revert and show newest first (#5366) 2025-12-26 14:33:02 -06:00
GitHub Action
24c9d2b891 chore: format code 2025-12-26 14:33:02 -06:00
Sachnun
2062dd1086 fix(server): make time field optional in session update validator (#5372) 2025-12-26 14:33:02 -06:00
xu0o0
3f580ef53e acp: replay conversation history in session/load (#5385) 2025-12-26 14:33:02 -06:00
opencode-agent[bot]
fb9ea51018 Removed cache mention from webfetch prompt. (#5412)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-26 14:33:02 -06:00
Zeno Jiricek
807c421248 docs: Add opencode-md-table-formatter and plugin template (#5405) 2025-12-26 14:33:02 -06:00
Koichi Nakayamada
8992072b95 fix(tui): ensure fatal error UI is readable in light mode (#5387) 2025-12-26 14:33:02 -06:00
David Hill
41319a2885 wip: desktop sidebar icon updates 2025-12-26 14:33:02 -06:00
David Hill
c316ef35d9 fix: make the logo on the home screen non-selectable 2025-12-26 14:33:01 -06:00
GitHub Action
86f9c3acce chore: format code 2025-12-11 23:08:04 +00:00
Aiden Cline
bacf705ee5 wip 2025-12-11 17:07:22 -06:00
629 changed files with 25494 additions and 9292 deletions

View File

@@ -1,63 +0,0 @@
name: Auto-label TUI Issues
on:
issues:
types: [opened]
jobs:
auto-label:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
issues: write
steps:
- name: Auto-label and assign issues
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issue = context.payload.issue;
const title = issue.title;
const description = issue.body || '';
// Check for "opencode web" keyword
const webPattern = /(opencode web)/i;
const isWebRelated = webPattern.test(title) || webPattern.test(description);
// Check for version patterns like v1.0.x or 1.0.x
const versionPattern = /[v]?1\.0\./i;
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
// Check for "nix" keyword
const nixPattern = /\bnix\b/i;
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
const labels = [];
if (isWebRelated) {
labels.push('web');
// Assign to adamdotdevin
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: ['adamdotdevin']
});
} else if (isVersionRelated) {
// Only add opentui if NOT web-related
labels.push('opentui');
}
if (isNixRelated) {
labels.push('nix');
}
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labels
});
}

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

@@ -16,6 +16,8 @@ jobs:
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash

View File

@@ -2,11 +2,8 @@ name: generate
on:
push:
branches-ignore:
- production
pull_request:
branches-ignore:
- production
branches:
- dev
workflow_dispatch:
jobs:
@@ -14,6 +11,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -25,14 +23,29 @@ jobs:
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Generate SDK
run: |
bun ./packages/sdk/js/script/build.ts
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
bun x prettier --write packages/sdk/openapi.json
- name: Generate
run: ./script/generate.ts
- name: Format
run: ./script/format.ts
env:
CI: true
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
- name: Commit and push
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "No changes to commit"
exit 0
fi
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add -A
git commit -m "chore: generate"
git push origin HEAD:${{ github.ref_name }} --no-verify
# if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then
# echo ""
# echo "============================================"
# echo "Failed to push generated code."
# echo "Please run locally and push:"
# echo ""
# echo " ./script/generate.ts"
# echo " git add -A && git commit -m \"chore: generate\" && git push"
# echo ""
# echo "============================================"
# exit 1
# fi

View File

@@ -2,7 +2,7 @@ name: discord
on:
release:
types: [published] # fires only when a release is published
types: [released] # fires when a draft release is published
jobs:
notify:

View File

@@ -31,4 +31,4 @@ jobs:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_PERMISSION: '{"bash": "deny"}'
with:
model: opencode/claude-haiku-4-5
model: opencode/claude-opus-4-5

View File

@@ -21,7 +21,7 @@ on:
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
permissions:
id-token: write
@@ -31,7 +31,7 @@ permissions:
jobs:
publish:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
if: github.repository == 'sst/opencode'
steps:
- uses: actions/checkout@v3
with:
@@ -41,21 +41,9 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Setup SSH for AUR
if: inputs.bump || inputs.version
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai@1.0.143
run: bun i -g opencode-ai@1.0.169
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -64,14 +52,26 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup Git Identity
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
- name: Publish
id: publish
run: ./script/publish.ts
run: ./script/publish-start.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
@@ -79,9 +79,16 @@ jobs:
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
- uses: actions/upload-artifact@v4
with:
name: opencode-cli
path: packages/opencode/dist
outputs:
releaseId: ${{ steps.publish.outputs.releaseId }}
tagName: ${{ steps.publish.outputs.tagName }}
release: ${{ steps.publish.outputs.release }}
tag: ${{ steps.publish.outputs.tag }}
version: ${{ steps.publish.outputs.version }}
publish-tauri:
needs: publish
@@ -98,11 +105,14 @@ jobs:
target: x86_64-pc-windows-msvc
- host: blacksmith-4vcpu-ubuntu-2404
target: x86_64-unknown-linux-gnu
- host: blacksmith-4vcpu-ubuntu-2404-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tag }}
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
@@ -141,29 +151,33 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/tauri/src-tauri
workspaces: packages/desktop/src-tauri
shared-key: ${{ matrix.settings.target }}
- name: Prepare
run: |
cd packages/tauri
cd packages/desktop
bun ./scripts/prepare.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_CHANNEL: latest
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
- name: Install tauri-cli from portable appimage branch
if: contains(matrix.settings.host, 'ubuntu')
run: |
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
echo "Installed tauri-cli version:"
cargo tauri --version
- name: Build and upload artifacts
timeout-minutes: 20
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -177,11 +191,43 @@ jobs:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
with:
projectPath: packages/tauri
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }}
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.publish.outputs.releaseId }}
tagName: ${{ needs.publish.outputs.tagName }}
releaseId: ${{ needs.publish.outputs.release }}
tagName: ${{ needs.publish.outputs.tag }}
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
releaseDraft: true
publish-release:
needs:
- publish
- publish-tauri
if: needs.publish.outputs.tag
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tag }}
- uses: ./.github/actions/setup-bun
- name: Setup SSH for AUR
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- run: ./script/publish-complete.ts
env:
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}

View File

@@ -0,0 +1,29 @@
name: release-github-action
on:
push:
branches:
- dev
paths:
- "github/**"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
jobs:
release:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git fetch --force --tags
- name: Release
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./github/script/release

View File

@@ -29,6 +29,8 @@ jobs:
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
@@ -65,6 +67,8 @@ jobs:
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors.
Generally, write a comment instead of writing suggested change if you can help it.
Command MUST be like this.
\`\`\`

View File

@@ -5,8 +5,11 @@ on:
- cron: "0 12 * * *" # Run daily at 12:00 UTC
workflow_dispatch: # Allow manual trigger
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
stats:
if: github.repository == 'sst/opencode'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write

View File

@@ -2,8 +2,8 @@ name: "sync-zed-extension"
on:
workflow_dispatch:
# release:
# types: [published]
release:
types: [published]
jobs:
zed:
@@ -31,4 +31,4 @@ jobs:
run: |
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}

37
.github/workflows/triage.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Issue Triage
on:
issues:
types: [opened]
jobs:
triage:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Triage issue
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
opencode run --agent triage "The following issue was just opened, triage it:
Title: $ISSUE_TITLE
$ISSUE_BODY"

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@ Session.vim
opencode.json
a.out
target
.scripts

View File

@@ -6,6 +6,8 @@ You are an expert technical documentation writer
You are not verbose
Use a relaxed and friendly tone
The title of the page should be a word or a 2-3 word phrase
The description should be one short line, should not start with "The", should

77
.opencode/agent/triage.md Normal file
View File

@@ -0,0 +1,77 @@
---
mode: primary
hidden: true
model: opencode/claude-haiku-4-5
tools:
"*": false
"github-triage": true
---
You are a triage agent responsible for triaging github issues.
Use your github-triage tool to triage issues.
## Labels
### windows
Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows.
- Use if they mention WSL too
#### perf
Performance-related issues:
- Slow performance
- High RAM usage
- High CPU usage
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
#### desktop
Desktop app issues:
- `opencode web` command
- The desktop app itself
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
#### nix
**Only** add if the issue explicitly mentions nix.
#### zen
**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers.
If the issue doesn't have "zen" in it then don't add zen label
#### docs
Add if the issue requests better documentation or docs updates.
#### opentui
TUI issues potentially caused by our underlying TUI library:
- Keybindings not working
- Scroll speed issues (too fast/slow/laggy)
- Screen flickering
- Crashes with opentui in the log
**Do not** add for general TUI bugs.
When assigning to people here are the following rules:
adamdotdev:
ONLY assign adam if the issue will have the "desktop" label.
fwang:
ONLY assign fwang if the issue will have the "zen" label.
jayair:
ONLY assign jayair if the issue will have the "docs" label.
In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node.

View File

@@ -1,6 +1,7 @@
---
description: git commit and push
model: opencode/glm-4.6
subtask: true
---
commit and push

4
.opencode/env.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.txt" {
const content: string
export default content
}

View File

@@ -10,4 +10,8 @@
"options": {},
},
},
"mcp": {},
"tools": {
"github-triage": false,
},
}

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

@@ -0,0 +1,90 @@
/// <reference path="../env.d.ts" />
// import { Octokit } from "@octokit/rest"
import { tool } from "@opencode-ai/plugin"
import DESCRIPTION from "./github-triage.txt"
function getIssueNumber(): number {
const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10)
if (!issue) throw new Error("ISSUE_NUMBER env var not set")
return issue
}
async function githubFetch(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`https://api.github.com${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
Accept: "application/vnd.github+json",
"Content-Type": "application/json",
...options.headers,
},
})
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
}
return response.json()
}
export default tool({
description: DESCRIPTION,
args: {
assignee: tool.schema
.enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"])
.describe("The username of the assignee")
.default("rekram1-node"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"]))
.describe("The labels(s) to add to the issue")
.default([]),
},
async execute(args) {
const issue = getIssueNumber()
// const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
const owner = "sst"
const repo = "opencode"
const results: string[] = []
if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) {
throw new Error("Only desktop issues should be assigned to adamdotdevin")
}
if (args.assignee === "fwang" && !args.labels.includes("zen")) {
throw new Error("Only zen issues should be assigned to fwang")
}
if (args.assignee === "kommander" && !args.labels.includes("opentui")) {
throw new Error("Only opentui issues should be assigned to kommander")
}
// await octokit.rest.issues.addAssignees({
// owner,
// repo,
// issue_number: issue,
// assignees: [args.assignee],
// })
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
method: "POST",
body: JSON.stringify({ assignees: [args.assignee] }),
})
results.push(`Assigned @${args.assignee} to issue #${issue}`)
const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label))
if (labels.length > 0) {
// await octokit.rest.issues.addLabels({
// owner,
// repo,
// issue_number: issue,
// labels,
// })
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
method: "POST",
body: JSON.stringify({ labels }),
})
results.push(`Added labels: ${args.labels.join(", ")}`)
}
return results.join("\n")
},
})

View File

@@ -0,0 +1,88 @@
Use this tool to assign and/or label a Github issue.
You can assign the following users:
- thdxr
- adamdotdevin
- fwang
- jayair
- kommander
- rekram1-node
You can use the following labels:
- nix
- opentui
- perf
- web
- zen
- docs
Always try to assign an issue, if in doubt, assign rekram1-node to it.
## Breakdown of responsibilities:
### thdxr
Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him.
This relates to OpenCode server primarily but has overlap with just about anything
### adamdotdevin
Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him.
### fwang
Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue.
### jayair
Jay is responsible for documentation. If there is an issue relating to documentation assign him.
### kommander
Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about:
- random characters on screen
- keybinds not working on different terminals
- general terminal stuff
Then assign the issue to Him.
### rekram1-node
ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label.
Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things.
If no one else makes sense to assign, assign rekram1-node to it.
Always assign to aiden if the issue mentions "acp", "zed", or model performance issues
## Breakdown of Labels:
### nix
Any issue that mentions nix, or nixos should have a nix label
### opentui
Anything relating to the TUI itself should have an opentui label
### perf
Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label
### desktop
Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related
### zen
Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label
### docs
Anything related to the documentation should have a docs label
### windows
Use for any issue that involves the windows OS

View File

@@ -4,31 +4,4 @@
## Tool Calling
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
json
{
"recipient_name": "multi_tool_use.parallel",
"parameters": {
"tool_uses": [
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.tsx"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.ts"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.md"
}
}
]
}
}
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.

View File

@@ -40,7 +40,7 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
- `packages/plugin`: Source for `@opencode-ai/plugin`
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
Please try to follow the [style guide](./STYLE_GUIDE.md)
@@ -53,12 +53,12 @@ your debugger via that URL. Other methods can result in breakpoints being mapped
Caveats:
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
is triggered.
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
- If `spawn` does not work for you, you can debug the server separately:
- Debug server: `bun run --inspect=ws://localhost:6499/ ./src/index.ts serve --port 4096`,
then attach TUI with `opencode attach http://localhost:4096`
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --conditions=browser ./src/index.ts`
Other tips and tricks:

View File

@@ -30,13 +30,29 @@ scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
mise use -g ubi:sst/opencode # Any OS
mise use -g github:sst/opencode # Any OS
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
```
> [!TIP]
> Remove versions older than 0.1.x before installing.
### Desktop App (BETA)
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
| Platform | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, or AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
```
#### Installation Directory
The install script respects the following priority order for the installation path:
@@ -63,7 +79,7 @@ you can switch between these using the `Tab` key.
- Asks permission before running bash commands
- Ideal for exploring unfamiliar codebases or planning changes
Also, included is a **general** subagent for complex searches and multi-step tasks.
Also, included is a **general** subagent for complex searches and multistep tasks.
This is used internally and can be invoked using `@general` in messages.
Learn more about [agents](https://opencode.ai/docs/agents).
@@ -78,11 +94,11 @@ If you're interested in contributing to OpenCode, please read our [contributing
### Building on OpenCode
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway.
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way.
### FAQ
#### How is this different than Claude Code?
#### How is this different from Claude Code?
It's very similar to Claude Code in terms of capability. Here are the key differences:

115
README.zh-TW.md Normal file
View File

@@ -0,0 +1,115 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">開源的 AI Coding Agent。</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### 安裝
```bash
# 直接安裝 (YOLO)
curl -fsSL https://opencode.ai/install | bash
# 套件管理員
npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS 與 Linux
paru -S opencode-bin # Arch Linux
mise use -g github:sst/opencode # 任何作業系統
nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支
```
> [!TIP]
> 安裝前請先移除 0.1.x 以前的舊版本。
### 桌面應用程式 (BETA)
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
| 平台 | 下載連結 |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, 或 AppImage |
```bash
# macOS (Homebrew Cask)
brew install --cask opencode-desktop
```
#### 安裝目錄
安裝腳本會依據以下優先順序決定安裝路徑:
1. `$OPENCODE_INSTALL_DIR` - 自定義安裝目錄
2. `$XDG_BIN_DIR` - 符合 XDG 基礎目錄規範的路徑
3. `$HOME/bin` - 標準使用者執行檔目錄 (若存在或可建立)
4. `$HOME/.opencode/bin` - 預設備用路徑
```bash
# 範例
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode 內建了兩種 Agent您可以使用 `Tab` 鍵快速切換。
- **build** - 預設模式,具備完整權限的 Agent適用於開發工作。
- **plan** - 唯讀模式,適用於程式碼分析與探索。
- 預設禁止修改檔案。
- 執行 bash 指令前會詢問權限。
- 非常適合用來探索陌生的程式碼庫或規劃變更。
此外OpenCode 還包含一個 **general** 子 Agent用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。
了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。
### 線上文件
關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。
### 參與貢獻
如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。
### 基於 OpenCode 進行開發
如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。
### 常見問題 (FAQ)
#### 這跟 Claude Code 有什麼不同?
在功能面上與 Claude Code 非常相似。以下是關鍵差異:
- 100% 開源。
- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
- 內建 LSP (語言伺服器協定) 支援。
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
#### 另一個同名的 Repo 是什麼?
另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。
---
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)

View File

@@ -167,3 +167,18 @@
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |

784
bun.lock

File diff suppressed because it is too large Load Diff

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1765270179,
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
"lastModified": 1766870016,
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
"type": "github"
},
"original": {

View File

@@ -6,7 +6,7 @@ Mention `/opencode` in your comment, and opencode will execute tasks within your
## Features
#### Explain an issues
#### Explain an issue
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
@@ -14,7 +14,7 @@ Leave the following comment on a GitHub issue. `opencode` will read the entire t
/opencode explain this issue
```
#### Fix an issues
#### Fix an issue
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.

View File

@@ -9,6 +9,10 @@ inputs:
description: "Model to use"
required: true
agent:
description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found."
required: false
share:
description: "Share the opencode session (defaults to true for public repos)"
required: false
@@ -17,6 +21,19 @@ inputs:
description: "Custom prompt to override the default prompt"
required: false
use_github_token:
description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
required: false
default: "false"
mentions:
description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'"
required: false
oidc_base_url:
description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai"
required: false
runs:
using: "composite"
steps:
@@ -49,5 +66,9 @@ runs:
run: opencode github run
env:
MODEL: ${{ inputs.model }}
AGENT: ${{ inputs.agent }}
SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
MENTIONS: ${{ inputs.mentions }}
OIDC_BASE_URL: ${{ inputs.oidc_base_url }}

View File

@@ -318,6 +318,10 @@ function useEnvRunUrl() {
return `/${repo.owner}/${repo.repo}/actions/runs/${runId}`
}
function useEnvAgent() {
return process.env["AGENT"] || undefined
}
function useEnvShare() {
const value = process.env["SHARE"]
if (!value) return undefined
@@ -570,24 +574,49 @@ async function subscribeSessionEvents() {
}
async function summarize(response: string) {
const payload = useContext().payload as IssueCommentEvent
try {
return await chat(`Summarize the following in less than 40 characters:\n\n${response}`)
} catch (e) {
if (isScheduleEvent()) {
return "Scheduled task changes"
}
const payload = useContext().payload as IssueCommentEvent
return `Fix issue: ${payload.issue.title}`
}
}
async function resolveAgent(): Promise<string | undefined> {
const envAgent = useEnvAgent()
if (!envAgent) return undefined
// Validate the agent exists and is a primary agent
const agents = await client.agent.list<true>()
const agent = agents.data?.find((a) => a.name === envAgent)
if (!agent) {
console.warn(`agent "${envAgent}" not found. Falling back to default agent`)
return undefined
}
if (agent.mode === "subagent") {
console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`)
return undefined
}
return envAgent
}
async function chat(text: string, files: PromptFiles = []) {
console.log("Sending message to opencode...")
const { providerID, modelID } = useEnvModel()
const agent = await resolveAgent()
const chat = await client.session.chat<true>({
path: session,
body: {
providerID,
modelID,
agent: "build",
agent,
parts: [
{
type: "text",

View File

@@ -13,7 +13,7 @@
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@octokit/rest": "catalog:",
"@opencode-ai/sdk": "workspace:*"
}
}

View File

@@ -44,3 +44,12 @@ new sst.cloudflare.x.Astro("Web", {
VITE_API_URL: api.url.apply((url) => url!),
},
})
new sst.cloudflare.StaticSite("WebApp", {
domain: "app." + domain,
path: "packages/app",
build: {
command: "bun turbo build",
output: "./dist",
},
})

View File

@@ -102,6 +102,8 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS2"),
new sst.Secret("ZEN_MODELS3"),
new sst.Secret("ZEN_MODELS4"),
new sst.Secret("ZEN_MODELS5"),
new sst.Secret("ZEN_MODELS6"),
]
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
@@ -117,6 +119,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
////////////////
const bucket = new sst.cloudflare.Bucket("ZenData")
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
@@ -135,6 +138,7 @@ new sst.cloudflare.x.SolidStart("Console", {
path: "packages/console/app",
link: [
bucket,
bucketNew,
database,
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,

View File

@@ -1,10 +0,0 @@
import { domain } from "./stage"
new sst.cloudflare.StaticSite("Desktop", {
domain: "desktop." + domain,
path: "packages/desktop",
build: {
command: "bun turbo build",
output: "./dist",
},
})

141
install
View File

@@ -7,7 +7,51 @@ RED='\033[0;31m'
ORANGE='\033[38;5;214m'
NC='\033[0m' # No Color
usage() {
cat <<EOF
OpenCode Installer
Usage: install.sh [options]
Options:
-h, --help Display this help message
-v, --version <version> Install a specific version (e.g., 1.0.180)
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
Examples:
curl -fsSL https://opencode.ai/install | bash
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180
EOF
}
requested_version=${VERSION:-}
no_modify_path=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
-v|--version)
if [[ -n "${2:-}" ]]; then
requested_version="$2"
shift 2
else
echo -e "${RED}Error: --version requires a version argument${NC}"
exit 1
fi
;;
--no-modify-path)
no_modify_path=true
shift
;;
*)
echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
shift
;;
esac
done
raw_os=$(uname -s)
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
@@ -111,8 +155,18 @@ if [ -z "$requested_version" ]; then
exit 1
fi
else
# Strip leading 'v' if present
requested_version="${requested_version#v}"
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
specific_version=$requested_version
# Verify the release exists before downloading
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/sst/opencode/releases/tag/v${requested_version}")
if [ "$http_status" = "404" ]; then
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
echo -e "${MUTED}Available releases: https://github.com/sst/opencode/releases${NC}"
exit 1
fi
fi
print_message() {
@@ -240,22 +294,23 @@ download_with_progress() {
download_and_install() {
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
mkdir -p opencodetmp && cd opencodetmp
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
mkdir -p "$tmp_dir"
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
# Fallback to standard curl on Windows or if custom progress fails
curl -# -L -o "$filename" "$url"
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
curl -# -L -o "$tmp_dir/$filename" "$url"
fi
if [ "$os" = "linux" ]; then
tar -xzf "$filename"
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
else
unzip -q "$filename"
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
fi
mv opencode "$INSTALL_DIR"
mv "$tmp_dir/opencode" "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/opencode"
cd .. && rm -rf opencodetmp
rm -rf "$tmp_dir"
}
check_version
@@ -303,42 +358,42 @@ case $current_shell in
;;
esac
config_file=""
for file in $config_files; do
if [[ -f $file ]]; then
config_file=$file
break
if [[ "$no_modify_path" != "true" ]]; then
config_file=""
for file in $config_files; do
if [[ -f $file ]]; then
config_file=$file
break
fi
done
if [[ -z $config_file ]]; then
print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
case $current_shell in
fish)
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
;;
zsh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
bash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
ash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
sh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
*)
export PATH=$INSTALL_DIR:$PATH
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
;;
esac
fi
done
if [[ -z $config_file ]]; then
print_message error "No config file found for $current_shell. Checked files: ${config_files[@]}"
exit 1
fi
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
case $current_shell in
fish)
add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
;;
zsh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
bash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
ash)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
sh)
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
*)
export PATH=$INSTALL_DIR:$PATH
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
;;
esac
fi
if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then

View File

@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-3GaqUwomnIUW8MqUi1jDVPHQ/C5Z+D9wMR//tAGxvSQ="
"nodeModules": "sha256-d10pu1tpwBbZ4YZqG0WcaK6W1+K2+Q0wdPjuI1rVIpY="
}

View File

@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.3",
"packageManager": "bun@1.3.5",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
@@ -20,7 +20,8 @@
"packages/slack"
],
"catalog": {
"@types/bun": "1.3.3",
"@types/bun": "1.3.4",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
@@ -30,7 +31,8 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.6.1",
"@pierre/diffs": "1.0.2",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.97",
@@ -38,10 +40,13 @@
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"marked": "17.0.1",
"marked-shiki": "1.2.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"shiki": "3.20.0",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"virtua": "0.42.3",
@@ -54,6 +59,7 @@
}
},
"devDependencies": {
"@actions/artifact": "5.0.1",
"@tsconfig/bun": "catalog:",
"husky": "9.1.7",
"prettier": "3.6.2",
@@ -62,6 +68,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"typescript": "catalog:"
@@ -78,7 +85,6 @@
"trustedDependencies": [
"esbuild",
"protobufjs",
"sharp",
"tree-sitter",
"tree-sitter-bash",
"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="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none select-none text-12-regular">
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<script>
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
@@ -22,7 +22,7 @@
})()
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
<div id="root" class="flex flex-col h-screen"></div>
<script src="/src/entry.tsx" type="module"></script>
</body>
</html>

62
packages/app/package.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "@opencode-ai/app",
"version": "1.0.203",
"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": "catalog:",
"marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:",
"zod": "catalog:"
}
}

View File

@@ -0,0 +1,17 @@
/assets/*.js
Content-Type: application/javascript
/assets/*.mjs
Content-Type: application/javascript
/assets/*.css
Content-Type: text/css
/*.js
Content-Type: application/javascript
/*.mjs
Content-Type: application/javascript
/*.css
Content-Type: text/css

92
packages/app/src/app.tsx Normal file
View File

@@ -0,0 +1,92 @@
import "@/index.css"
import { ErrorBoundary, Show } from "solid-js"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
import { GlobalSyncProvider } from "@/context/global-sync"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
import { TerminalProvider } from "@/context/terminal"
import { PromptProvider } from "@/context/prompt"
import { NotificationProvider } from "@/context/notification"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { CommandProvider } from "@/context/command"
import Layout from "@/pages/layout"
import Home from "@/pages/home"
import DirectoryLayout from "@/pages/directory-layout"
import Session from "@/pages/session"
import { ErrorPage } from "./pages/error"
import { iife } from "@opencode-ai/util/iife"
declare global {
interface Window {
__OPENCODE__?: { updaterEnabled?: boolean; port?: number }
}
}
const url = iife(() => {
const param = new URLSearchParams(document.location.search).get("url")
if (param) return param
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (window.__OPENCODE__) return `http://127.0.0.1:${window.__OPENCODE__.port}`
if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return window.location.origin
})
export function App() {
return (
<MetaProvider>
<Font />
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<NotificationProvider>
<Router
root={(props) => (
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
)}
>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
</MetaProvider>
)
}

View File

@@ -0,0 +1,383 @@
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import type { IconName } from "@opencode-ai/ui/icons/provider"
import { List, type ListRef } from "@opencode-ai/ui/list"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Spinner } from "@opencode-ai/ui/spinner"
import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast"
import { iife } from "@opencode-ai/util/iife"
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { usePlatform } from "@/context/platform"
import { DialogSelectModel } from "./dialog-select-model"
import { DialogSelectProvider } from "./dialog-select-provider"
export function DialogConnectProvider(props: { provider: string }) {
const dialog = useDialog()
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const platform = usePlatform()
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
const methods = createMemo(
() =>
globalSync.data.provider_auth[props.provider] ?? [
{
type: "api",
label: "API key",
},
],
)
const [store, setStore] = createStore({
methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization,
state: "pending" as undefined | "pending" | "complete" | "error",
error: undefined as string | undefined,
})
const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
async function selectMethod(index: number) {
const method = methods()[index]
setStore(
produce((draft) => {
draft.methodIndex = index
draft.authorization = undefined
draft.state = undefined
draft.error = undefined
}),
)
if (method.type === "oauth") {
setStore("state", "pending")
const start = Date.now()
await globalSDK.client.provider.oauth
.authorize(
{
providerID: props.provider,
method: index,
},
{ throwOnError: true },
)
.then((x) => {
const elapsed = Date.now() - start
const delay = 1000 - elapsed
if (delay > 0) {
setTimeout(() => {
setStore("state", "complete")
setStore("authorization", x.data!)
}, delay)
return
}
setStore("state", "complete")
setStore("authorization", x.data!)
})
.catch((e) => {
setStore("state", "error")
setStore("error", String(e))
})
}
}
let listRef: ListRef | undefined
function handleKey(e: KeyboardEvent) {
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
return
}
if (e.key === "Escape") return
listRef?.onKeyDown(e)
}
onMount(() => {
if (methods().length === 1) {
selectMethod(0)
}
document.addEventListener("keydown", handleKey)
onCleanup(() => {
document.removeEventListener("keydown", handleKey)
})
})
async function complete() {
await globalSDK.client.global.dispose()
dialog.close()
showToast({
variant: "success",
icon: "circle-check",
title: `${provider().name} connected`,
description: `${provider().name} models are now available to use.`,
})
}
function goBack() {
if (methods().length === 1) {
dialog.show(() => <DialogSelectProvider />)
return
}
if (store.authorization) {
setStore("authorization", undefined)
setStore("methodIndex", undefined)
return
}
if (store.methodIndex) {
setStore("methodIndex", undefined)
return
}
dialog.show(() => <DialogSelectProvider />)
}
return (
<Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
<div class="flex flex-col gap-6 px-2.5 pb-3">
<div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
<div class="text-16-medium text-text-strong">
<Switch>
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
Login with Claude Pro/Max
</Match>
<Match when={true}>Connect {provider().name}</Match>
</Switch>
</div>
</div>
<div class="px-2.5 pb-10 flex flex-col gap-6">
<Switch>
<Match when={store.methodIndex === undefined}>
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
<div class="">
<List
ref={(ref) => {
listRef = ref
}}
items={methods}
key={(m) => m?.label}
onSelect={async (method, index) => {
if (!method) return
selectMethod(index)
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-2">
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
</div>
<span>{i.label}</span>
</div>
)}
</List>
</div>
</Match>
<Match when={store.state === "pending"}>
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-2">
<Spinner />
<span>Authorization in progress...</span>
</div>
</div>
</Match>
<Match when={store.state === "error"}>
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-2">
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
<span>Authorization failed: {store.error}</span>
</div>
</div>
</Match>
<Match when={method()?.type === "api"}>
{iife(() => {
const [formStore, setFormStore] = createStore({
value: "",
error: undefined as string | undefined,
})
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const form = e.currentTarget as HTMLFormElement
const formData = new FormData(form)
const apiKey = formData.get("apiKey") as string
if (!apiKey?.trim()) {
setFormStore("error", "API key is required")
return
}
setFormStore("error", undefined)
await globalSDK.client.auth.set({
providerID: props.provider,
auth: {
type: "api",
key: apiKey,
},
})
await complete()
}
return (
<div class="flex flex-col gap-6">
<Switch>
<Match when={provider().id === "opencode"}>
<div class="flex flex-col gap-4">
<div class="text-14-regular text-text-base">
OpenCode Zen gives you access to a curated set of reliable optimized models for coding
agents.
</div>
<div class="text-14-regular text-text-base">
With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
</div>
<div class="text-14-regular text-text-base">
Visit{" "}
<Link href="https://opencode.ai/zen" tabIndex={-1}>
opencode.ai/zen
</Link>{" "}
to collect your API key.
</div>
</div>
</Match>
<Match when={true}>
<div class="text-14-regular text-text-base">
Enter your {provider().name} API key to connect your account and use {provider().name} models
in OpenCode.
</div>
</Match>
</Switch>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label={`${provider().name} API key`}
placeholder="API key"
name="apiKey"
value={formStore.value}
onChange={setFormStore.bind(null, "value")}
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
Submit
</Button>
</form>
</div>
)
})}
</Match>
<Match when={method()?.type === "oauth"}>
<Switch>
<Match when={store.authorization?.method === "code"}>
{iife(() => {
const [formStore, setFormStore] = createStore({
value: "",
error: undefined as string | undefined,
})
onMount(() => {
if (store.authorization?.method === "code" && store.authorization?.url) {
platform.openLink(store.authorization.url)
}
})
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const form = e.currentTarget as HTMLFormElement
const formData = new FormData(form)
const code = formData.get("code") as string
if (!code?.trim()) {
setFormStore("error", "Authorization code is required")
return
}
setFormStore("error", undefined)
const { error } = await globalSDK.client.provider.oauth.callback({
providerID: props.provider,
method: store.methodIndex,
code,
})
if (!error) {
await complete()
return
}
setFormStore("error", "Invalid authorization code")
}
return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization
code to connect your account and use {provider().name} models in OpenCode.
</div>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label={`${method()?.label} authorization code`}
placeholder="Authorization code"
name="code"
value={formStore.value}
onChange={setFormStore.bind(null, "value")}
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
Submit
</Button>
</form>
</div>
)
})}
</Match>
<Match when={store.authorization?.method === "auto"}>
{iife(() => {
const code = createMemo(() => {
const instructions = store.authorization?.instructions
if (instructions?.includes(":")) {
return instructions?.split(":")[1]?.trim()
}
return instructions
})
onMount(async () => {
const result = await globalSDK.client.provider.oauth.callback({
providerID: props.provider,
method: store.methodIndex,
})
if (result.error) {
// TODO: show error
dialog.close()
return
}
await complete()
})
return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to
connect your account and use {provider().name} models in OpenCode.
</div>
<TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable />
<div class="text-14-regular text-text-base flex items-center gap-4">
<Spinner />
<span>Waiting for authorization...</span>
</div>
</div>
)
})}
</Match>
</Switch>
</Match>
</Switch>
</div>
</div>
</Dialog>
)
}

View File

@@ -0,0 +1,57 @@
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import type { Component } from "solid-js"
import { useLocal } from "@/context/local"
import { popularProviders } from "@/hooks/use-providers"
export const DialogManageModels: Component = () => {
const local = useLocal()
return (
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
<List
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.name}
sortGroupsBy={(a, b) => {
const aProvider = a.items[0].provider.id
const bProvider = b.items[0].provider.id
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
}}
onSelect={(x) => {
if (!x) return
const visible = local.model.visible({
modelID: x.id,
providerID: x.provider.id,
})
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
}}
>
{(i) => (
<div class="w-full flex items-center justify-between gap-x-3">
<span>{i.name}</span>
<div onClick={(e) => e.stopPropagation()}>
<Switch
checked={
!!local.model.visible({
modelID: i.id,
providerID: i.provider.id,
})
}
onChange={(checked) => {
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
}}
/>
</div>
</div>
)}
</List>
</Dialog>
)
}

View File

@@ -0,0 +1,48 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { List } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { useParams } from "@solidjs/router"
import { createMemo } from "solid-js"
import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local"
export function DialogSelectFile() {
const layout = useLayout()
const local = useLocal()
const dialog = useDialog()
const params = useParams()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
return (
<Dialog title="Select file">
<List
search={{ placeholder: "Search files", autofocus: true }}
emptyMessage="No files found"
items={local.file.searchFiles}
key={(x) => x}
onSelect={(path) => {
if (path) {
tabs().open("file://" + path)
}
dialog.close()
}}
>
{(i) => (
<div class="w-full flex items-center justify-between rounded-md">
<div class="flex items-center gap-x-3 grow min-w-0">
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
</div>
</div>
</div>
)}
</List>
</Dialog>
)
}

View File

@@ -0,0 +1,91 @@
import { Component, createMemo, createSignal, Show } from "solid-js"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
export const DialogSelectMcp: Component = () => {
const sync = useSync()
const sdk = useSDK()
const [loading, setLoading] = createSignal<string | null>(null)
const items = createMemo(() =>
Object.entries(sync.data.mcp ?? {})
.map(([name, status]) => ({ name, status: status.status }))
.sort((a, b) => a.name.localeCompare(b.name)),
)
const toggle = async (name: string) => {
if (loading()) return
setLoading(name)
const status = sync.data.mcp[name]
if (status?.status === "connected") {
await sdk.client.mcp.disconnect({ name })
} else {
await sdk.client.mcp.connect({ name })
}
const result = await sdk.client.mcp.status()
if (result.data) sync.set("mcp", result.data)
setLoading(null)
}
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
const totalCount = createMemo(() => items().length)
return (
<Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
<List
search={{ placeholder: "Search", autofocus: true }}
emptyMessage="No MCPs configured"
key={(x) => x?.name ?? ""}
items={items}
filterKeys={["name", "status"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
onSelect={(x) => {
if (x) toggle(x.name)
}}
>
{(i) => {
const mcpStatus = () => sync.data.mcp[i.name]
const status = () => mcpStatus()?.status
const error = () => {
const s = mcpStatus()
return s?.status === "failed" ? s.error : undefined
}
const enabled = () => status() === "connected"
return (
<div class="w-full flex items-center justify-between gap-x-3">
<div class="flex flex-col gap-0.5 min-w-0">
<div class="flex items-center gap-2">
<span class="truncate">{i.name}</span>
<Show when={status() === "connected"}>
<span class="text-11-regular text-text-weaker">connected</span>
</Show>
<Show when={status() === "failed"}>
<span class="text-11-regular text-text-weaker">failed</span>
</Show>
<Show when={status() === "needs_auth"}>
<span class="text-11-regular text-text-weaker">needs auth</span>
</Show>
<Show when={status() === "disabled"}>
<span class="text-11-regular text-text-weaker">disabled</span>
</Show>
<Show when={loading() === i.name}>
<span class="text-11-regular text-text-weak">...</span>
</Show>
</div>
<Show when={error()}>
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
</Show>
</div>
<div onClick={(e) => e.stopPropagation()}>
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
</div>
</div>
)
}}
</List>
</Dialog>
)
}

View File

@@ -0,0 +1,110 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
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 { Tag } from "@opencode-ai/ui/tag"
import { type Component, onCleanup, onMount, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider"
export const DialogSelectModelUnpaid: Component = () => {
const local = useLocal()
const dialog = useDialog()
const providers = useProviders()
let listRef: ListRef | undefined
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") return
listRef?.onKeyDown(e)
}
onMount(() => {
document.addEventListener("keydown", handleKey)
onCleanup(() => {
document.removeEventListener("keydown", handleKey)
})
})
return (
<Dialog title="Select model">
<div class="flex flex-col gap-3 px-2.5">
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
<List
ref={(ref) => (listRef = ref)}
items={local.model.list}
current={local.model.current()}
key={(x) => `${x.provider.id}:${x.id}`}
onSelect={(x) => {
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
dialog.close()
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Tag>Free</Tag>
<Show when={i.latest}>
<Tag>Latest</Tag>
</Show>
</div>
)}
</List>
<div />
<div />
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
<div class="w-full">
<List
class="w-full px-0"
key={(x) => x?.id}
items={providers.popular}
activeIcon="plus-small"
sortBy={(a, b) => {
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
onSelect={(x) => {
if (!x) return
dialog.show(() => <DialogConnectProvider provider={x.id} />)
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
</Show>
</div>
)}
</List>
<Button
variant="ghost"
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
icon="dot-grid"
onClick={() => {
dialog.show(() => <DialogSelectProvider />)
}}
>
View all providers
</Button>
</div>
</div>
</div>
</div>
</Dialog>
)
}

View File

@@ -0,0 +1,83 @@
import { Component, createMemo, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
import { Button } from "@opencode-ai/ui/button"
import { Tag } from "@opencode-ai/ui/tag"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogManageModels } from "./dialog-manage-models"
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
const local = useLocal()
const dialog = useDialog()
const models = createMemo(() =>
local.model
.list()
.filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
)
return (
<Dialog
title="Select model"
action={
<Button
class="h-7 -my-1 text-14-medium"
icon="plus-small"
tabIndex={-1}
onClick={() => dialog.show(() => <DialogSelectProvider />)}
>
Connect provider
</Button>
}
>
<List
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
key={(x) => `${x.provider.id}:${x.id}`}
items={models}
current={local.model.current()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.name}
sortGroupsBy={(a, b) => {
if (a.category === "Recent" && b.category !== "Recent") return -1
if (b.category === "Recent" && a.category !== "Recent") return 1
const aProvider = a.items[0].provider.id
const bProvider = b.items[0].provider.id
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
}}
onSelect={(x) => {
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
dialog.close()
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-3">
<span>{i.name}</span>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Tag>Free</Tag>
</Show>
<Show when={i.latest}>
<Tag>Latest</Tag>
</Show>
</div>
)}
</List>
<Button
variant="ghost"
class="ml-3 mt-5 mb-6 text-text-base self-start"
onClick={() => dialog.show(() => <DialogManageModels />)}
>
Manage models
</Button>
</Dialog>
)
}

View File

@@ -0,0 +1,54 @@
import { Component, Show } from "solid-js"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Tag } from "@opencode-ai/ui/tag"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
import { DialogConnectProvider } from "./dialog-connect-provider"
export const DialogSelectProvider: Component = () => {
const dialog = useDialog()
const providers = useProviders()
return (
<Dialog title="Connect provider">
<List
search={{ placeholder: "Search providers", autofocus: true }}
activeIcon="plus-small"
key={(x) => x?.id}
items={providers.all}
filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
sortBy={(a, b) => {
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
sortGroupsBy={(a, b) => {
if (a.category === "Popular" && b.category !== "Popular") return -1
if (b.category === "Popular" && a.category !== "Popular") return 1
return 0
}}
onSelect={(x) => {
if (!x) return
dialog.show(() => <DialogConnectProvider provider={x.id} />)
}}
>
{(i) => (
<div class="px-1.25 w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
</Show>
</div>
)}
</List>
</Dialog>
)
}

View File

@@ -2,7 +2,7 @@ import { useLocal, type LocalFile } from "@/context/local"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
import { Dynamic } from "solid-js/web"
export default function FileTree(props: {
@@ -57,14 +57,14 @@ export default function FileTree(props: {
"text-text-muted/40": p.node.ignored,
"text-text-muted/80": !p.node.ignored,
// "!text-text": local.file.active()?.path === p.node.path,
"!text-primary": local.file.changed(p.node.path),
// "!text-primary": local.file.changed(p.node.path),
}}
>
{p.node.name}
</span>
<Show when={local.file.changed(p.node.path)}>
<span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" />
</Show>
{/* <Show when={local.file.changed(p.node.path)}> */}
{/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
{/* </Show> */}
</Dynamic>
)

View File

@@ -0,0 +1,211 @@
import { useGlobalSync } from "@/context/global-sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { useLayout } from "@/context/layout"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
import { Popover } from "@opencode-ai/ui/popover"
import { Select } from "@opencode-ai/ui/select"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
import { useCommand } from "@/context/command"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
import { createMemo, createResource, Show } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { iife } from "@opencode-ai/util/iife"
export function Header(props: {
navigateToProject: (directory: string) => void
navigateToSession: (session: Session | undefined) => void
onMobileMenuToggle?: () => void
}) {
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const command = useCommand()
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<button
type="button"
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
onClick={props.onMobileMenuToggle}
>
<Icon name="menu" size="small" />
</button>
<A
href="/"
classList={{
"hidden xl:flex": true,
"w-12 shrink-0 px-4 py-3.5": true,
"items-center justify-start self-stretch": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
data-tauri-drag-region
>
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<Show when={layout.projects.list().length > 0 && params.dir}>
{(directory) => {
const currentDirectory = createMemo(() => base64Decode(directory()))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => (store().session ?? []).filter((s) => !s.parentID))
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<>
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={props.navigateToSession}
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Tooltip
class="hidden xl:block"
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Tooltip>
</Show>
</div>
<div class="flex items-center gap-4">
<Show when={currentSession()?.summary?.files}>
<Tooltip
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
name={layout.review.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</Show>
<Tooltip
class="hidden md:block shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: currentDirectory() })
.then((r) => r.data?.share?.url)
}
return shareURL
},
)
return (
<Show when={url()}>
{(url) => <TextField value={url()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
</>
)
}}
</Show>
</div>
</header>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
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
value={
<div class="flex flex-col gap-1">
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Tokens</span>
<span class="text-left flex-1">{ctx().tokens}</span>
</div>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Usage</span>
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
</div>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Cost</span>
<span class="text-left flex-1">{cost()}</span>
</div>
</div>
}
placement="top"
>
<div class="flex items-center gap-1.5">
<ProgressCircle size={16} strokeWidth={2} percentage={ctx().percentage ?? 0} />
{/* <span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span> */}
</div>
</Tooltip>
)}
</Show>
)
}

View File

@@ -0,0 +1,40 @@
import { createMemo, Show } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import { useSync } from "@/context/sync"
import { Tooltip } from "@opencode-ai/ui/tooltip"
export function SessionLspIndicator() {
const sync = useSync()
const lspStats = createMemo(() => {
const lsp = sync.data.lsp ?? []
const connected = lsp.filter((s) => s.status === "connected").length
const hasError = lsp.some((s) => s.status === "error")
const total = lsp.length
return { connected, hasError, total }
})
const tooltipContent = createMemo(() => {
const lsp = sync.data.lsp ?? []
if (lsp.length === 0) return "No LSP servers"
return lsp.map((s) => s.name).join(", ")
})
return (
<Show when={lspStats().total > 0}>
<Tooltip placement="top" value={tooltipContent()}>
<div class="flex items-center gap-1 px-2 cursor-default select-none">
<Icon
name="code"
size="small"
classList={{
"text-icon-critical-base": lspStats().hasError,
"text-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
}}
/>
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
</div>
</Tooltip>
</Show>
)
}

View File

@@ -0,0 +1,36 @@
import { createMemo, Show } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useSync } from "@/context/sync"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
export function SessionMcpIndicator() {
const sync = useSync()
const dialog = useDialog()
const mcpStats = createMemo(() => {
const mcp = sync.data.mcp ?? {}
const entries = Object.entries(mcp)
const enabled = entries.filter(([, status]) => status.status === "connected").length
const failed = entries.some(([, status]) => status.status === "failed")
const total = entries.length
return { enabled, failed, total }
})
return (
<Show when={mcpStats().total > 0}>
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
<Icon
name="mcp"
size="small"
classList={{
"text-icon-critical-base": mcpStats().failed,
"text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
}}
/>
<span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
</Button>
</Show>
)
}

View File

@@ -0,0 +1,14 @@
import { Show, type ParentProps } from "solid-js"
import { usePlatform } from "@/context/platform"
export function StatusBar(props: ParentProps) {
const platform = usePlatform()
return (
<div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
<Show when={platform.version}>
<span class="text-12-regular text-text-weak">v{platform.version}</span>
</Show>
<div class="flex items-center">{props.children}</div>
</div>
)
}

View File

@@ -2,7 +2,8 @@ import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session"
import { LocalPTY } from "@/context/terminal"
import { usePrefersDark } from "@solid-primitives/media"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
@@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
const prefersDark = usePrefersDark()
onMount(async () => {
ghostty = await Ghostty.load()
@@ -29,12 +31,19 @@ export const Terminal = (props: TerminalProps) => {
term = new Term({
cursorBlink: true,
fontSize: 14,
fontFamily: "TX-02, monospace",
fontFamily: "IBM Plex Mono, monospace",
allowTransparency: true,
theme: {
background: "#191515",
foreground: "#d4d4d4",
},
theme: prefersDark()
? {
background: "#191515",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
}
: {
background: "#fcfcfc",
foreground: "#211e1e",
cursor: "#211e1e",
},
scrollback: 10_000,
ghostty,
})
@@ -139,6 +148,7 @@ export const Terminal = (props: TerminalProps) => {
<div
ref={container}
data-component="terminal"
data-prevent-autofocus
classList={{
...(local.classList ?? {}),
"size-full px-6 py-3 font-mono": true,

View File

@@ -0,0 +1,243 @@
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
export type KeybindConfig = string
export interface Keybind {
key: string
ctrl: boolean
meta: boolean
shift: boolean
alt: boolean
}
export interface CommandOption {
id: string
title: string
description?: string
category?: string
keybind?: KeybindConfig
slash?: string
suggested?: boolean
disabled?: boolean
onSelect?: (source?: "palette" | "keybind" | "slash") => void
}
export function parseKeybind(config: string): Keybind[] {
if (!config || config === "none") return []
return config.split(",").map((combo) => {
const parts = combo.trim().toLowerCase().split("+")
const keybind: Keybind = {
key: "",
ctrl: false,
meta: false,
shift: false,
alt: false,
}
for (const part of parts) {
switch (part) {
case "ctrl":
case "control":
keybind.ctrl = true
break
case "meta":
case "cmd":
case "command":
keybind.meta = true
break
case "mod":
if (IS_MAC) keybind.meta = true
else keybind.ctrl = true
break
case "alt":
case "option":
keybind.alt = true
break
case "shift":
keybind.shift = true
break
default:
keybind.key = part
break
}
}
return keybind
})
}
export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean {
const eventKey = event.key.toLowerCase()
for (const kb of keybinds) {
const keyMatch = kb.key === eventKey
const ctrlMatch = kb.ctrl === (event.ctrlKey || false)
const metaMatch = kb.meta === (event.metaKey || false)
const shiftMatch = kb.shift === (event.shiftKey || false)
const altMatch = kb.alt === (event.altKey || false)
if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) {
return true
}
}
return false
}
export function formatKeybind(config: string): string {
if (!config || config === "none") return ""
const keybinds = parseKeybind(config)
if (keybinds.length === 0) return ""
const kb = keybinds[0]
const parts: string[] = []
if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl")
if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt")
if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift")
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
if (kb.key) {
const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)
parts.push(displayKey)
}
return IS_MAC ? parts.join("") : parts.join("+")
}
function DialogCommand(props: { options: CommandOption[] }) {
const dialog = useDialog()
return (
<Dialog title="Commands">
<List
search={{ placeholder: "Search commands", autofocus: true }}
emptyMessage="No commands found"
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
key={(x) => x?.id}
filterKeys={["title", "description", "category"]}
groupBy={(x) => x.category ?? ""}
onSelect={(option) => {
if (option) {
dialog.close()
option.onSelect?.("palette")
}
}}
>
{(option) => (
<div class="w-full flex items-center justify-between gap-4">
<div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
<Show when={option.description}>
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
</Show>
</div>
<Show when={option.keybind}>
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
</Show>
</div>
)}
</List>
</Dialog>
)
}
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
name: "Command",
init: () => {
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const [suspendCount, setSuspendCount] = createSignal(0)
const dialog = useDialog()
const options = createMemo(() => {
const all = registrations().flatMap((x) => x())
const suggested = all.filter((x) => x.suggested && !x.disabled)
return [
...suggested.map((x) => ({
...x,
id: "suggested." + x.id,
category: "Suggested",
})),
...all,
]
})
const suspended = () => suspendCount() > 0
const showPalette = () => {
if (!dialog.active) {
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
}
}
const handleKeyDown = (event: KeyboardEvent) => {
if (suspended()) return
const paletteKeybinds = parseKeybind("mod+shift+p")
if (matchKeybind(paletteKeybinds, event)) {
event.preventDefault()
showPalette()
return
}
for (const option of options()) {
if (option.disabled) continue
if (!option.keybind) continue
const keybinds = parseKeybind(option.keybind)
if (matchKeybind(keybinds, event)) {
event.preventDefault()
option.onSelect?.("keybind")
return
}
}
}
onMount(() => {
document.addEventListener("keydown", handleKeyDown)
})
onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
})
return {
register(cb: () => CommandOption[]) {
const results = createMemo(cb)
setRegistrations((arr) => [results, ...arr])
onCleanup(() => {
setRegistrations((arr) => arr.filter((x) => x !== results))
})
},
trigger(id: string, source?: "palette" | "keybind" | "slash") {
for (const option of options()) {
if (option.id === id || option.id === "suggested." + id) {
option.onSelect?.(source)
return
}
}
},
keybind(id: string) {
const option = options().find((x) => x.id === id || x.id === "suggested." + id)
if (!option?.keybind) return ""
return formatKeybind(option.keybind)
},
show: showPalette,
keybinds(enabled: boolean) {
setSuspendCount((count) => count + (enabled ? -1 : 1))
},
suspended,
get options() {
return options()
},
}
},
})

View File

@@ -1,30 +1,32 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
import { usePlatform } from "./platform"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
init: (props: { url: string }) => {
const abort = new AbortController()
const sdk = createOpencodeClient({
const eventSdk = createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
// signal: AbortSignal.timeout(1000 * 60 * 10),
})
const emitter = createGlobalEmitter<{
[key: string]: Event
}>()
sdk.global.event().then(async (events) => {
eventSdk.global.event().then(async (events) => {
for await (const event of events.stream) {
// console.log("event", event)
emitter.emit(event.directory ?? "global", event.payload)
}
})
onCleanup(() => {
abort.abort()
const platform = usePlatform()
const sdk = createOpencodeClient({
baseUrl: props.url,
signal: AbortSignal.timeout(1000 * 60 * 10),
fetch: platform.fetch,
throwOnError: true,
})
return { url: props.url, client: sdk, event: emitter }

View File

@@ -0,0 +1,402 @@
import {
type Message,
type Agent,
type Session,
type Part,
type Config,
type Path,
type Project,
type FileDiff,
type Todo,
type SessionStatus,
type ProviderListResponse,
type ProviderAuthResponse,
type Command,
type McpStatus,
type LspStatus,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
type State = {
ready: boolean
agent: Agent[]
command: Command[]
project: string
provider: ProviderListResponse
config: Config
path: Path
session: Session[]
session_status: {
[sessionID: string]: SessionStatus
}
session_diff: {
[sessionID: string]: FileDiff[]
}
todo: {
[sessionID: string]: Todo[]
}
mcp: {
[name: string]: McpStatus
}
lsp: LspStatus[]
limit: number
message: {
[sessionID: string]: Message[]
}
part: {
[messageID: string]: Part[]
}
}
function createGlobalSync() {
const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
error?: InitError
path: Path
project: Project[]
provider: ProviderListResponse
provider_auth: ProviderAuthResponse
children: Record<string, State>
}>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
provider: { all: [], connected: [], default: {} },
provider_auth: {},
children: {},
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
setGlobalStore("children", directory, {
project: "",
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
agent: [],
command: [],
session: [],
session_status: {},
session_diff: {},
todo: {},
mcp: {},
lsp: [],
limit: 5,
message: {},
part: {},
})
children[directory] = createStore(globalStore.children[directory])
bootstrapInstance(directory)
}
return children[directory]
}
async function loadSessions(directory: string) {
const [store, setStore] = child(directory)
globalSDK.client.session
.list({ directory })
.then((x) => {
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
const nonArchived = (x.data ?? [])
.slice()
.filter((s) => !s.time.archived)
.sort((a, b) => a.id.localeCompare(b.id))
// Include up to the limit, plus any updated in the last 4 hours
const sessions = nonArchived.filter((s, i) => {
if (i < store.limit) return true
const updated = new Date(s.time.updated).getTime()
return updated > fourHoursAgo
})
setStore("session", sessions)
})
.catch((err) => {
console.error("Failed to load sessions", err)
const project = getFilename(directory)
showToast({ title: `Failed to load sessions for ${project}`, description: err.message })
})
}
async function bootstrapInstance(directory: string) {
if (!directory) return
const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
throwOnError: true,
})
const load = {
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
provider: () =>
sdk.provider.list().then((x) => {
const data = x.data!
setStore("provider", {
...data,
all: data.all.map((provider) => ({
...provider,
models: Object.fromEntries(
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
),
})),
})
}),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})),
lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])),
}
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
.then(() => setStore("ready", true))
.catch((e) => setGlobalStore("error", e))
}
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
if (directory === "global") {
switch (event?.type) {
case "global.disposed": {
bootstrap()
break
}
case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
if (result.found) {
setGlobalStore("project", result.index, reconcile(event.properties))
return
}
setGlobalStore(
"project",
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
)
break
}
}
return
}
const [store, setStore] = child(directory)
switch (event.type) {
case "server.instance.disposed": {
bootstrapInstance(directory)
break
}
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (event.properties.info.time.archived) {
if (result.found) {
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "session.diff":
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
break
case "todo.updated":
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos))
break
case "session.status": {
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
break
}
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info])
break
}
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) {
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
break
}
setStore(
"message",
event.properties.info.sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "message.removed": {
const messages = store.message[event.properties.sessionID]
if (!messages) break
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
setStore(
"message",
event.properties.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
case "message.part.updated": {
const part = event.properties.part
const parts = store.part[part.messageID]
if (!parts) {
setStore("part", part.messageID, [part])
break
}
const result = Binary.search(parts, part.id, (p) => p.id)
if (result.found) {
setStore("part", part.messageID, result.index, reconcile(part))
break
}
setStore(
"part",
part.messageID,
produce((draft) => {
draft.splice(result.index, 0, part)
}),
)
break
}
case "message.part.removed": {
const parts = store.part[event.properties.messageID]
if (!parts) break
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found) {
setStore(
"part",
event.properties.messageID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
}
})
async function bootstrap() {
const health = await globalSDK.client.global
.health()
.then((x) => x.data)
.catch(() => undefined)
if (!health?.healthy) {
setGlobalStore(
"error",
new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`),
)
return
}
return Promise.all([
retry(() =>
globalSDK.client.path.get().then((x) => {
setGlobalStore("path", x.data!)
}),
),
retry(() =>
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
)
}),
),
retry(() =>
globalSDK.client.provider.list().then((x) => {
const data = x.data!
setGlobalStore("provider", {
...data,
all: data.all.map((provider) => ({
...provider,
models: Object.fromEntries(
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
),
})),
})
}),
),
retry(() =>
globalSDK.client.provider.auth().then((x) => {
setGlobalStore("provider_auth", x.data ?? {})
}),
),
])
.then(() => setGlobalStore("ready", true))
.catch((e) => setGlobalStore("error", e))
}
onMount(() => {
bootstrap()
})
return {
data: globalStore,
get ready() {
return globalStore.ready
},
get error() {
return globalStore.error
},
child,
bootstrap,
project: {
loadSessions,
},
}
}
const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
export function GlobalSyncProvider(props: ParentProps) {
const value = createGlobalSync()
return (
<Switch>
<Match when={value.error}>
<ErrorPage error={value.error} />
</Match>
<Match when={value.ready}>
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
</Match>
</Switch>
)
}
export function useGlobalSync() {
const context = useContext(GlobalSyncContext)
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
return context
}

View File

@@ -0,0 +1,260 @@
import { createStore, produce } from "solid-js/store"
import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
import { Project } from "@opencode-ai/sdk/v2"
import { persisted } from "@/utils/persist"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
export function getAvatarColors(key?: string) {
if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) {
return {
background: `var(--avatar-background-${key})`,
foreground: `var(--avatar-text-${key})`,
}
}
return {
background: "var(--surface-info-base)",
foreground: "var(--text-base)",
}
}
type SessionTabs = {
active?: string
all: string[]
}
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore, _, ready] = persisted(
"layout.v3",
createStore({
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
opened: false,
width: 280,
},
terminal: {
opened: false,
height: 280,
},
review: {
opened: true,
},
session: {
width: 600,
},
sessionTabs: {} as Record<string, SessionTabs>,
}),
)
const usedColors = new Set<AvatarColorKey>()
function pickAvailableColor(): AvatarColorKey {
const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c))
if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)]
return available[Math.floor(Math.random() * available.length)]
}
function enrich(project: { worktree: string; expanded: boolean }) {
const metadata = globalSync.data.project.find((x) => x.worktree === project.worktree)
return [
{
...project,
...(metadata ?? {}),
},
]
}
function colorize(project: LocalProject) {
if (project.icon?.color) return project
const color = pickAvailableColor()
usedColors.add(color)
project.icon = { ...project.icon, color }
if (project.id) {
globalSdk.client.project.update({ projectID: project.id, icon: { color } })
}
return project
}
const enriched = createMemo(() => store.projects.flatMap(enrich))
const list = createMemo(() => enriched().flatMap(colorize))
onMount(() => {
Promise.all(
store.projects.map((project) => {
return globalSync.project.loadSessions(project.worktree)
}),
)
})
return {
ready,
projects: {
list,
open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) {
return
}
globalSync.project.loadSessions(directory)
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
},
close(directory: string) {
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
},
expand(directory: string) {
const index = store.projects.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", index, "expanded", true)
},
collapse(directory: string) {
const index = store.projects.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", index, "expanded", false)
},
move(directory: string, toIndex: number) {
setStore("projects", (projects) => {
const fromIndex = projects.findIndex((x) => x.worktree === directory)
if (fromIndex === -1 || fromIndex === toIndex) return projects
const result = [...projects]
const [item] = result.splice(fromIndex, 1)
result.splice(toIndex, 0, item)
return result
})
},
},
sidebar: {
opened: createMemo(() => store.sidebar.opened),
open() {
setStore("sidebar", "opened", true)
},
close() {
setStore("sidebar", "opened", false)
},
toggle() {
setStore("sidebar", "opened", (x) => !x)
},
width: createMemo(() => store.sidebar.width),
resize(width: number) {
setStore("sidebar", "width", width)
},
},
terminal: {
opened: createMemo(() => store.terminal.opened),
open() {
setStore("terminal", "opened", true)
},
close() {
setStore("terminal", "opened", false)
},
toggle() {
setStore("terminal", "opened", (x) => !x)
},
height: createMemo(() => store.terminal.height),
resize(height: number) {
setStore("terminal", "height", height)
},
},
review: {
opened: createMemo(() => store.review?.opened ?? true),
open() {
setStore("review", "opened", true)
},
close() {
setStore("review", "opened", false)
},
toggle() {
setStore("review", "opened", (x) => !x)
},
},
session: {
width: createMemo(() => store.session?.width ?? 600),
resize(width: number) {
if (!store.session) {
setStore("session", { width })
} else {
setStore("session", "width", width)
}
},
},
tabs(sessionKey: string) {
const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
return {
tabs,
active: createMemo(() => tabs().active),
all: createMemo(() => tabs().all),
setActive(tab: string | undefined) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [], active: tab })
} else {
setStore("sessionTabs", sessionKey, "active", tab)
}
},
setAll(all: string[]) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all, active: undefined })
} else {
setStore("sessionTabs", sessionKey, "all", all)
}
},
async open(tab: string) {
const current = store.sessionTabs[sessionKey] ?? { all: [] }
if (tab !== "review") {
if (!current.all.includes(tab)) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
} else {
setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
setStore("sessionTabs", sessionKey, "active", tab)
}
return
}
}
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [], active: tab })
} else {
setStore("sessionTabs", sessionKey, "active", tab)
}
},
close(tab: string) {
const current = store.sessionTabs[sessionKey]
if (!current) return
batch(() => {
setStore(
"sessionTabs",
sessionKey,
"all",
current.all.filter((x) => x !== tab),
)
if (current.active === tab) {
const index = current.all.findIndex((f) => f === tab)
const previous = current.all[Math.max(0, index - 1)]
setStore("sessionTabs", sessionKey, "active", previous)
}
})
},
move(tab: string, to: number) {
const current = store.sessionTabs[sessionKey]
if (!current) return
const index = current.all.findIndex((f) => f === tab)
if (index === -1) return
setStore(
"sessionTabs",
sessionKey,
"all",
produce((opened) => {
opened.splice(to, 0, opened.splice(index, 1)[0])
}),
)
},
}
},
}
},
})

View File

@@ -1,12 +1,15 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo } from "solid-js"
import { uniqueBy } from "remeda"
import { batch, createMemo } from "solid-js"
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { base64Encode } from "@opencode-ai/util/encode"
import { useProviders } from "@/hooks/use-providers"
import { DateTime } from "luxon"
import { persisted } from "@/utils/persist"
import { showToast } from "@opencode-ai/ui/toast"
export type LocalFile = FileNode &
Partial<{
@@ -59,26 +62,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
// Automatically update model when agent changes
createEffect(() => {
const value = agent.current()
if (value.model) {
if (isModelValid(value.model))
model.set({
providerID: value.model.providerID,
modelID: value.model.modelID,
})
// else
// toast.show({
// type: "warning",
// message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
// duration: 3000,
// })
}
})
const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [store, setStore] = createStore<{
current: string
}>({
@@ -108,30 +93,62 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})()
const model = (() => {
const [store, setStore] = createStore<{
const [store, setStore, _, modelReady] = persisted(
"model.v1",
createStore<{
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
recent: ModelKey[]
}>({
user: [],
recent: [],
}),
)
const [ephemeral, setEphemeral] = createStore<{
model: Record<string, ModelKey>
recent: ModelKey[]
}>({
model: {},
recent: [],
})
const value = localStorage.getItem("model")
setStore("recent", JSON.parse(value ?? "[]"))
createEffect(() => {
localStorage.setItem("model", JSON.stringify(store.recent))
})
const list = createMemo(() =>
const available = createMemo(() =>
providers.connected().flatMap((p) =>
Object.values(p.models).map((m) => ({
...m,
name: m.name.replace("(latest)", "").trim(),
provider: p,
latest: m.name.includes("(latest)"),
})),
),
)
const latest = createMemo(() =>
pipe(
available(),
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
groupBy((x) => x.provider.id),
mapValues((models) =>
pipe(
models,
groupBy((x) => x.family),
values(),
(groups) =>
groups.flatMap((g) => {
const first = firstBy(g, [(x) => x.release_date, "desc"])
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
}),
),
),
values(),
flat(),
),
)
const list = createMemo(() =>
available().map((m) => ({
...m,
name: m.name.replace("(latest)", "").trim(),
latest: m.name.includes("(latest)"),
})),
)
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
const fallbackModel = createMemo(() => {
@@ -163,10 +180,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
throw new Error("No default model found")
})
const currentModel = createMemo(() => {
const current = createMemo(() => {
const a = agent.current()
const key = getFirstValidModel(
() => store.model[a.name],
() => ephemeral.model[a.name],
() => a.model,
fallbackModel,
)!
@@ -177,10 +194,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const cycle = (direction: 1 | -1) => {
const recentList = recent()
const current = currentModel()
if (!current) return
const currentModel = current()
if (!currentModel) return
const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
const index = recentList.findIndex(
(x) => x?.provider.id === currentModel.provider.id && x?.id === currentModel.id,
)
if (index === -1) return
let next = index + direction
@@ -196,14 +215,25 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
}
function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
if (index >= 0) {
setStore("user", index, { visibility })
} else {
setStore("user", store.user.length, { ...model, visibility })
}
}
return {
current: currentModel,
ready: modelReady,
current,
recent,
list,
cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
setStore("model", agent.current().name, model ?? fallbackModel())
setEphemeral("model", agent.current().name, model ?? fallbackModel())
if (model) updateVisibility(model, "show")
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop()
@@ -211,6 +241,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})
},
visible(model: ModelKey) {
const user = store.user.find((x) => x.modelID === model.modelID && x.providerID === model.providerID)
return (
user?.visibility !== "hide" &&
(latest().find((x) => x.modelID === model.modelID && x.providerID === model.providerID) ||
user?.visibility === "show")
)
},
setVisibility(model: ModelKey, visible: boolean) {
updateVisibility(model, visible ? "show" : "hide")
},
}
})()
@@ -218,11 +259,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const [store, setStore] = createStore<{
node: Record<string, LocalFile>
}>({
node: Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
})
const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
// createEffect((prev: FileStatus[]) => {
// const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
@@ -250,16 +291,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
// return sync.data.changes
// }, sync.data.changes)
const changed = (path: string) => {
const node = store.node[path]
if (node?.status) return true
const set = changeset()
if (set.has(path)) return true
for (const p of set) {
if (p.startsWith(path ? path + "/" : "")) return true
}
return false
}
// const changed = (path: string) => {
// const node = store.node[path]
// if (node?.status) return true
// const set = changeset()
// if (set.has(path)) return true
// for (const p of set) {
// if (p.startsWith(path ? path + "/" : "")) return true
// }
// return false
// }
// const resetNode = (path: string) => {
// setStore("node", path, {
@@ -278,16 +319,26 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const load = async (path: string) => {
const relativePath = relative(path)
await sdk.client.file.read({ path: relativePath }).then((x) => {
setStore(
"node",
relativePath,
produce((draft) => {
draft.loaded = true
draft.content = x.data
}),
)
})
await sdk.client.file
.read({ path: relativePath })
.then((x) => {
if (!store.node[relativePath]) return
setStore(
"node",
relativePath,
produce((draft) => {
draft.loaded = true
draft.content = x.data
}),
)
})
.catch((e) => {
showToast({
variant: "error",
title: "Failed to load file",
description: e.message,
})
})
}
const fetch = async (path: string) => {
@@ -301,7 +352,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const init = async (path: string) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
if (store.node[relativePath].loaded) return
if (store.node[relativePath]?.loaded) return
return load(relativePath)
}
@@ -321,7 +372,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
context.addActive()
if (options?.pinned) setStore("node", path, "pinned", true)
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
if (store.node[relativePath].loaded) return
if (store.node[relativePath]?.loaded) return
return load(relativePath)
}
@@ -349,7 +400,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
case "file.watcher.updated":
const relativePath = relative(event.properties.file)
if (relativePath.startsWith(".git/")) return
load(relativePath)
if (store.node[relativePath]) load(relativePath)
break
}
})
@@ -367,7 +418,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
init,
expand(path: string) {
setStore("node", path, "expanded", true)
if (store.node[path].loaded) return
if (store.node[path]?.loaded) return
setStore("node", path, "loaded", true)
list(path)
},
@@ -407,8 +458,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setChangeIndex(path: string, index: number | undefined) {
setStore("node", path, "selectedChange", index)
},
changes,
changed,
// changes,
// changed,
children(path: string) {
return Object.values(store.node).filter(
(x) =>

View File

@@ -0,0 +1,127 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync"
import { Binary } from "@opencode-ai/util/binary"
import { EventSessionError } from "@opencode-ai/sdk/v2"
import { makeAudioPlayer } from "@solid-primitives/audio"
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
import { persisted } from "@/utils/persist"
type NotificationBase = {
directory?: string
session?: string
metadata?: any
time: number
viewed: boolean
}
type TurnCompleteNotification = NotificationBase & {
type: "turn-complete"
}
type ErrorNotification = NotificationBase & {
type: "error"
error: EventSessionError["properties"]["error"]
}
export type Notification = TurnCompleteNotification | ErrorNotification
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
name: "Notification",
init: () => {
let idlePlayer: ReturnType<typeof makeAudioPlayer> | undefined
let errorPlayer: ReturnType<typeof makeAudioPlayer> | undefined
try {
idlePlayer = makeAudioPlayer(idleSound)
errorPlayer = makeAudioPlayer(errorSound)
} catch (err) {
console.log("Failed to load audio", err)
}
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore, _, ready] = persisted(
"notification.v1",
createStore({
list: [] as Notification[],
}),
)
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
const base = {
directory,
time: Date.now(),
viewed: false,
}
switch (event.type) {
case "session.idle": {
const sessionID = event.properties.sessionID
const [syncStore] = globalSync.child(directory)
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const isChild = match.found && syncStore.session[match.index].parentID
if (isChild) break
try {
idlePlayer?.play()
} catch {}
setStore("list", store.list.length, {
...base,
type: "turn-complete",
session: sessionID,
})
break
}
case "session.error": {
const sessionID = event.properties.sessionID
if (sessionID) {
const [syncStore] = globalSync.child(directory)
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const isChild = match.found && syncStore.session[match.index].parentID
if (isChild) break
}
try {
errorPlayer?.play()
} catch {}
setStore("list", store.list.length, {
...base,
type: "error",
session: sessionID ?? "global",
error: "error" in event.properties ? event.properties.error : undefined,
})
break
}
}
})
return {
ready,
session: {
all(session: string) {
return store.list.filter((n) => n.session === session)
},
unseen(session: string) {
return store.list.filter((n) => n.session === session && !n.viewed)
},
markViewed(session: string) {
setStore("list", (n) => n.session === session, "viewed", true)
},
},
project: {
all(directory: string) {
return store.list.filter((n) => n.directory === directory)
},
unseen(directory: string) {
return store.list.filter((n) => n.directory === directory && !n.viewed)
},
markViewed(directory: string) {
setStore("list", (n) => n.directory === directory, "viewed", true)
},
},
}
},
})

View File

@@ -1,9 +1,19 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
export type Platform = {
/** Platform discriminator */
platform: "web" | "tauri"
/** App version */
version?: string
/** Open a URL in the default browser */
openLink(url: string): void
/** Restart the app */
restart(): Promise<void>
/** Open native directory picker dialog (Tauri only) */
openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
@@ -13,8 +23,17 @@ export type Platform = {
/** Save file picker dialog (Tauri only) */
saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise<string | null>
/** Open a URL in the default browser */
openLink(url: string): void
/** Storage mechanism, defaults to localStorage */
storage?: (name?: string) => SyncStorage | AsyncStorage
/** Check for updates (Tauri only) */
checkUpdate?(): Promise<{ updateAvailable: boolean; version?: string }>
/** Install updates (Tauri only) */
update?(): Promise<void>
/** Fetch override */
fetch?: typeof fetch
}
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({

View File

@@ -0,0 +1,111 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { useParams } from "@solidjs/router"
import { TextSelection } from "./local"
import { persisted } from "@/utils/persist"
interface PartBase {
content: string
start: number
end: number
}
export interface TextPart extends PartBase {
type: "text"
}
export interface FileAttachmentPart extends PartBase {
type: "file"
path: string
selection?: TextSelection
}
export interface ImageAttachmentPart {
type: "image"
id: string
filename: string
mime: string
dataUrl: string
}
export type ContentPart = TextPart | FileAttachmentPart | ImageAttachmentPart
export type Prompt = ContentPart[]
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
if (promptA.length !== promptB.length) return false
for (let i = 0; i < promptA.length; i++) {
const partA = promptA[i]
const partB = promptB[i]
if (partA.type !== partB.type) return false
if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
return false
}
if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
return false
}
if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) {
return false
}
}
return true
}
function cloneSelection(selection?: TextSelection) {
if (!selection) return undefined
return { ...selection }
}
function clonePart(part: ContentPart): ContentPart {
if (part.type === "text") return { ...part }
if (part.type === "image") return { ...part }
return {
...part,
selection: cloneSelection(part.selection),
}
}
function clonePrompt(prompt: Prompt): Prompt {
return prompt.map(clonePart)
}
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
name: "Prompt",
init: () => {
const params = useParams()
const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`)
const [store, setStore, _, ready] = persisted(
name(),
createStore<{
prompt: Prompt
cursor?: number
}>({
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
}),
)
return {
ready,
current: createMemo(() => store.prompt),
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
set(prompt: Prompt, cursorPosition?: number) {
const next = clonePrompt(prompt)
batch(() => {
setStore("prompt", next)
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
})
},
reset() {
batch(() => {
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
setStore("cursor", 0)
})
},
}
},
})

View File

@@ -1,18 +1,20 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
import { useGlobalSDK } from "./global-sdk"
import { usePlatform } from "./platform"
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK",
init: (props: { directory: string }) => {
const platform = usePlatform()
const globalSDK = useGlobalSDK()
const abort = new AbortController()
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
signal: abort.signal,
signal: AbortSignal.timeout(1000 * 60 * 10),
fetch: platform.fetch,
directory: props.directory,
throwOnError: true,
})
const emitter = createGlobalEmitter<{
@@ -23,10 +25,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
emitter.emit(event.type, event)
})
onCleanup(() => {
abort.abort()
})
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
},
})

View File

@@ -1,9 +1,11 @@
import { produce } from "solid-js/store"
import { createMemo } from "solid-js"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
@@ -30,12 +32,40 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (match.found) return store.session[match.index]
return undefined
},
addOptimisticMessage(input: {
sessionID: string
messageID: string
parts: Part[]
agent: string
model: { providerID: string; modelID: string }
}) {
const message: Message = {
id: input.messageID,
sessionID: input.sessionID,
role: "user",
time: { created: Date.now() },
agent: input.agent,
model: input.model,
}
setStore(
produce((draft) => {
const messages = draft.message[input.sessionID]
if (!messages) {
draft.message[input.sessionID] = [message]
} else {
const result = Binary.search(messages, input.messageID, (m) => m.id)
messages.splice(result.index, 0, message)
}
draft.part[input.messageID] = input.parts.slice()
}),
)
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ sessionID }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }),
sdk.client.session.todo({ sessionID }),
sdk.client.session.diff({ sessionID }),
retry(() => sdk.client.session.get({ sessionID })),
retry(() => sdk.client.session.messages({ sessionID, limit: 100 })),
retry(() => sdk.client.session.todo({ sessionID })),
retry(() => sdk.client.session.diff({ sessionID })),
])
setStore(
produce((draft) => {
@@ -65,6 +95,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
},
more: createMemo(() => store.session.length >= store.limit),
archive: async (sessionID: string) => {
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)
},
},
absolute,
get directory() {

View File

@@ -0,0 +1,105 @@
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
import { persisted } from "@/utils/persist"
export type LocalPTY = {
id: string
title: string
rows?: number
cols?: number
buffer?: string
scrollY?: number
}
export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
name: "Terminal",
init: () => {
const sdk = useSDK()
const params = useParams()
const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
const [store, setStore, _, ready] = persisted(
name(),
createStore<{
active?: string
all: LocalPTY[]
}>({
all: [],
}),
)
return {
ready,
all: createMemo(() => Object.values(store.all)),
active: createMemo(() => store.active),
new() {
sdk.client.pty.create({ title: `Terminal ${store.all.length + 1}` }).then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("all", [
...store.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("active", id)
})
},
update(pty: Partial<LocalPTY> & { id: string }) {
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
sdk.client.pty.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
})
},
async clone(id: string) {
const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index]
if (!pty) return
const clone = await sdk.client.pty.create({
title: pty.title,
})
if (!clone.data) return
setStore("all", index, {
...pty,
...clone.data,
})
if (store.active === pty.id) {
setStore("active", clone.data.id)
}
},
open(id: string) {
setStore("active", id)
},
async close(id: string) {
batch(() => {
setStore(
"all",
store.all.filter((x) => x.id !== id),
)
if (store.active === id) {
const index = store.all.findIndex((f) => f.id === id)
const previous = store.all[Math.max(0, index - 1)]
setStore("active", previous?.id)
}
})
await sdk.client.pty.remove({ ptyID: id })
},
move(id: string, to: number) {
const index = store.all.findIndex((f) => f.id === id)
if (index === -1) return
setStore(
"all",
produce((all) => {
all.splice(to, 0, all.splice(index, 1)[0])
}),
)
},
}
},
})

View File

@@ -2,6 +2,7 @@
import { render } from "solid-js/web"
import { App } from "@/app"
import { Platform, PlatformProvider } from "@/context/platform"
import pkg from "../package.json"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -12,9 +13,13 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
const platform: Platform = {
platform: "web",
version: pkg.version,
openLink(url: string) {
window.open(url, "_blank")
},
restart: async () => {
window.location.reload()
},
}
render(

View File

@@ -6,8 +6,8 @@ import { createMemo } from "solid-js"
export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
export function useProviders() {
const params = useParams()
const globalSync = useGlobalSync()
const params = useParams()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const providers = createMemo(() => {
if (currentDirectory()) {

View File

@@ -0,0 +1,160 @@
import { TextField } from "@opencode-ai/ui/text-field"
import { Logo } from "@opencode-ai/ui/logo"
import { Button } from "@opencode-ai/ui/button"
import { Component, Show } from "solid-js"
import { usePlatform } from "@/context/platform"
import { Icon } from "@opencode-ai/ui/icon"
export type InitError = {
name: string
data: Record<string, unknown>
}
function isInitError(error: unknown): error is InitError {
return (
typeof error === "object" &&
error !== null &&
"name" in error &&
"data" in error &&
typeof (error as InitError).data === "object"
)
}
function formatInitError(error: InitError): string {
const data = error.data
switch (error.name) {
case "MCPFailed":
return `MCP server "${data.name}" failed. Note, opencode does not support MCP authentication yet.`
case "ProviderModelNotFoundError": {
const { providerID, modelID, suggestions } = data as {
providerID: string
modelID: string
suggestions?: string[]
}
return [
`Model not found: ${providerID}/${modelID}`,
...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
`Check your config (opencode.json) provider/model names`,
].join("\n")
}
case "ProviderInitError":
return `Failed to initialize provider "${data.providerID}". Check credentials and configuration.`
case "ConfigJsonError":
return `Config file at ${data.path} is not valid JSON(C)` + (data.message ? `: ${data.message}` : "")
case "ConfigDirectoryTypoError":
return `Directory "${data.dir}" in ${data.path} is not valid. Rename the directory to "${data.suggestion}" or remove it. This is a common typo.`
case "ConfigFrontmatterError":
return `Failed to parse frontmatter in ${data.path}:\n${data.message}`
case "ConfigInvalidError": {
const issues = Array.isArray(data.issues)
? data.issues.map(
(issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."),
)
: []
return [`Config file at ${data.path} is invalid` + (data.message ? `: ${data.message}` : ""), ...issues].join(
"\n",
)
}
case "UnknownError":
return String(data.message)
default:
return data.message ? String(data.message) : JSON.stringify(data, null, 2)
}
}
function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): string {
if (!error) return "Unknown error"
if (isInitError(error)) {
const message = formatInitError(error)
if (depth > 0 && parentMessage === message) return ""
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
return indent + message
}
if (error instanceof Error) {
const isDuplicate = depth > 0 && parentMessage === error.message
const parts: string[] = []
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
if (!isDuplicate) {
// Stack already includes error name and message, so prefer it
parts.push(indent + (error.stack ?? `${error.name}: ${error.message}`))
} else if (error.stack) {
// Duplicate message - only show the stack trace lines (skip message)
const trace = error.stack.split("\n").slice(1).join("\n").trim()
if (trace) {
parts.push(trace)
}
}
if (error.cause) {
const causeResult = formatErrorChain(error.cause, depth + 1, error.message)
if (causeResult) {
parts.push(causeResult)
}
}
return parts.join("\n\n")
}
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)
}
function formatError(error: unknown): string {
return formatErrorChain(error, 0)
}
interface ErrorPageProps {
error: unknown
}
export const ErrorPage: Component<ErrorPageProps> = (props) => {
const platform = usePlatform()
return (
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
<Logo class="w-58.5 opacity-12 shrink-0" />
<div class="flex flex-col items-center gap-2 text-center">
<h1 class="text-lg font-medium text-text-strong">Something went wrong</h1>
<p class="text-sm text-text-weak">An error occurred while loading the application.</p>
</div>
<TextField
value={formatError(props.error)}
readOnly
copyable
multiline
class="max-h-96 w-full font-mono text-xs no-scrollbar whitespace-pre"
label="Error Details"
hideLabel
/>
<Button size="large" onClick={platform.restart}>
Restart
</Button>
<div class="flex flex-col items-center gap-2">
<div class="flex items-center justify-center gap-1">
Please report this error to the OpenCode team
<button
type="button"
class="flex items-center text-text-interactive-base gap-1"
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
>
<div>on Discord</div>
<Icon name="discord" class="text-text-interactive-base" />
</button>
</div>
<Show when={platform.version}>
<p class="text-xs text-text-weak">Version: {platform.version}</p>
</Show>
</div>
</div>
</div>
)
}

View File

@@ -1,5 +1,5 @@
import { useGlobalSync } from "@/context/global-sync"
import { For, Match, Show, Switch } from "solid-js"
import { createMemo, For, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo"
import { useLayout } from "@/context/layout"
@@ -14,6 +14,7 @@ export default function Home() {
const layout = useLayout()
const platform = usePlatform()
const navigate = useNavigate()
const homedir = createMemo(() => sync.data.path.home)
function openProject(directory: string) {
layout.projects.open(directory)
@@ -61,7 +62,7 @@ export default function Home() {
class="text-14-mono text-left justify-between px-3"
onClick={() => openProject(project.worktree)}
>
{project.worktree}
{project.worktree.replace(homedir(), "~")}
<div class="text-14-regular text-text-weak">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</div>

View File

@@ -0,0 +1,885 @@
import {
createEffect,
createMemo,
createSignal,
For,
Match,
onCleanup,
onMount,
ParentProps,
Show,
Switch,
untrack,
type JSX,
} from "solid-js"
import { DateTime } from "luxon"
import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout, getAvatarColors, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Spinner } from "@opencode-ai/ui/spinner"
import { getFilename } from "@opencode-ai/util/path"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore, produce } from "solid-js/store"
import {
DragDropProvider,
DragDropSensors,
DragOverlay,
SortableProvider,
closestCenter,
createSortable,
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import { useProviders } from "@/hooks/use-providers"
import { showToast, Toast } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { useCommand } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
lastSession: {} as { [directory: string]: string },
activeDraggable: undefined as string | undefined,
mobileSidebarOpen: false,
mobileProjectsExpanded: {} as Record<string, boolean>,
})
const mobileSidebar = {
open: () => store.mobileSidebarOpen,
show: () => setStore("mobileSidebarOpen", true),
hide: () => setStore("mobileSidebarOpen", false),
toggle: () => setStore("mobileSidebarOpen", (x) => !x),
}
const mobileProjects = {
expanded: (directory: string) => store.mobileProjectsExpanded[directory] ?? true,
expand: (directory: string) => setStore("mobileProjectsExpanded", directory, true),
collapse: (directory: string) => setStore("mobileProjectsExpanded", directory, false),
}
let scrollContainerRef: HTMLDivElement | undefined
const xlQuery = window.matchMedia("(min-width: 1280px)")
const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches)
xlQuery.addEventListener("change", handleViewportChange)
onCleanup(() => xlQuery.removeEventListener("change", handleViewportChange))
const params = useParams()
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const notification = useNotification()
const navigate = useNavigate()
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()
onMount(async () => {
if (platform.checkUpdate && platform.update && platform.restart) {
const { updateAvailable, version } = await platform.checkUpdate()
if (updateAvailable) {
showToast({
persistent: true,
icon: "download",
title: "Update available",
description: `A new version of OpenCode (${version}) is now available to install.`,
actions: [
{
label: "Install and restart",
onClick: async () => {
await platform.update!()
await platform.restart!()
},
},
{
label: "Not yet",
onClick: "dismiss",
},
],
})
}
}
})
function sortSessions(a: Session, b: Session) {
const now = Date.now()
const oneMinuteAgo = now - 60 * 1000
const aUpdated = a.time.updated ?? a.time.created
const bUpdated = b.time.updated ?? b.time.created
const aRecent = aUpdated > oneMinuteAgo
const bRecent = bUpdated > oneMinuteAgo
if (aRecent && bRecent) return a.id.localeCompare(b.id)
if (aRecent && !bRecent) return -1
if (!aRecent && bRecent) return 1
return bUpdated - aUpdated
}
function scrollToSession(sessionId: string) {
if (!scrollContainerRef) return
const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`)
if (element) {
element.scrollIntoView({ block: "nearest", behavior: "smooth" })
}
}
function projectSessions(directory: string) {
if (!directory) return []
const sessions = globalSync.child(directory)[0].session.toSorted(sortSessions)
return (sessions ?? []).filter((s) => !s.parentID)
}
const currentSessions = createMemo(() => {
if (!params.dir) return []
const directory = base64Decode(params.dir)
return projectSessions(directory)
})
function navigateSessionByOffset(offset: number) {
const projects = layout.projects.list()
if (projects.length === 0) return
const currentDirectory = params.dir ? base64Decode(params.dir) : undefined
const projectIndex = currentDirectory ? projects.findIndex((p) => p.worktree === currentDirectory) : -1
if (projectIndex === -1) {
const targetProject = offset > 0 ? projects[0] : projects[projects.length - 1]
if (targetProject) navigateToProject(targetProject.worktree)
return
}
const sessions = currentSessions()
const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1
let targetIndex: number
if (sessionIndex === -1) {
targetIndex = offset > 0 ? 0 : sessions.length - 1
} else {
targetIndex = sessionIndex + offset
}
if (targetIndex >= 0 && targetIndex < sessions.length) {
const session = sessions[targetIndex]
navigateToSession(session)
queueMicrotask(() => scrollToSession(session.id))
return
}
const nextProjectIndex = projectIndex + (offset > 0 ? 1 : -1)
const nextProject = projects[nextProjectIndex]
if (!nextProject) return
const nextProjectSessions = projectSessions(nextProject.worktree)
if (nextProjectSessions.length === 0) {
navigateToProject(nextProject.worktree)
return
}
const targetSession = offset > 0 ? nextProjectSessions[0] : nextProjectSessions[nextProjectSessions.length - 1]
navigate(`/${base64Encode(nextProject.worktree)}/session/${targetSession.id}`)
queueMicrotask(() => scrollToSession(targetSession.id))
}
async function archiveSession(session: Session) {
const [store, setStore] = globalSync.child(session.directory)
const sessions = store.session ?? []
const index = sessions.findIndex((s) => s.id === session.id)
const nextSession = sessions[index + 1] ?? sessions[index - 1]
await globalSDK.client.session.update({
directory: session.directory,
sessionID: session.id,
time: { archived: Date.now() },
})
setStore(
produce((draft) => {
const match = Binary.search(draft.session, session.id, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)
if (session.id === params.id) {
if (nextSession) {
navigate(`/${params.dir}/session/${nextSession.id}`)
} else {
navigate(`/${params.dir}/session`)
}
}
}
command.register(() => [
{
id: "sidebar.toggle",
title: "Toggle sidebar",
category: "View",
keybind: "mod+b",
onSelect: () => layout.sidebar.toggle(),
},
...(platform.openDirectoryPickerDialog
? [
{
id: "project.open",
title: "Open project",
category: "Project",
keybind: "mod+o",
onSelect: () => chooseProject(),
},
]
: []),
{
id: "provider.connect",
title: "Connect provider",
category: "Provider",
onSelect: () => connectProvider(),
},
{
id: "session.previous",
title: "Previous session",
category: "Session",
keybind: "alt+arrowup",
onSelect: () => navigateSessionByOffset(-1),
},
{
id: "session.next",
title: "Next session",
category: "Session",
keybind: "alt+arrowdown",
onSelect: () => navigateSessionByOffset(1),
},
{
id: "session.archive",
title: "Archive session",
category: "Session",
keybind: "mod+shift+backspace",
disabled: !params.dir || !params.id,
onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id)
if (session) archiveSession(session)
},
},
])
function connectProvider() {
dialog.show(() => <DialogSelectProvider />)
}
function navigateToProject(directory: string | undefined) {
if (!directory) return
const lastSession = store.lastSession[directory]
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
mobileSidebar.hide()
}
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
mobileSidebar.hide()
}
function openProject(directory: string, navigate = true) {
layout.projects.open(directory)
if (navigate) navigateToProject(directory)
}
function closeProject(directory: string) {
const index = layout.projects.list().findIndex((x) => x.worktree === directory)
const next = layout.projects.list()[index + 1]
layout.projects.close(directory)
if (next) navigateToProject(next.worktree)
else navigate("/")
}
async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory, false)
}
navigateToProject(result[0])
} else if (result) {
openProject(result)
}
}
createEffect(() => {
if (!params.dir || !params.id) return
const directory = base64Decode(params.dir)
const id = params.id
setStore("lastSession", directory, id)
notification.session.markViewed(id)
untrack(() => layout.projects.expand(directory))
requestAnimationFrame(() => scrollToSession(id))
})
createEffect(() => {
if (isLargeViewport()) {
const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
} else {
document.documentElement.style.setProperty("--dialog-left-margin", "0px")
}
})
function getDraggableId(event: unknown): string | undefined {
if (typeof event !== "object" || event === null) return undefined
if (!("draggable" in event)) return undefined
const draggable = (event as { draggable?: { id?: unknown } }).draggable
if (!draggable) return undefined
return typeof draggable.id === "string" ? draggable.id : undefined
}
function handleDragStart(event: unknown) {
const id = getDraggableId(event)
if (!id) return
setStore("activeDraggable", id)
}
function handleDragOver(event: DragEvent) {
const { draggable, droppable } = event
if (draggable && droppable) {
const projects = layout.projects.list()
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== -1) {
layout.projects.move(draggable.id.toString(), toIndex)
}
}
}
function handleDragEnd() {
setStore("activeDraggable", undefined)
}
const ProjectAvatar = (props: {
project: LocalProject
class?: string
expandable?: boolean
notify?: boolean
}): JSX.Element => {
const notification = useNotification()
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const name = createMemo(() => getFilename(props.project.worktree))
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
return (
<div class="relative size-5 shrink-0 rounded-sm">
<Avatar
fallback={name()}
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
{...getAvatarColors(props.project.icon?.color)}
class={`size-full ${props.class ?? ""}`}
style={
notifications().length > 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined
}
/>
<Show when={props.expandable}>
<Icon
name="chevron-right"
size="normal"
class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
/>
</Show>
<Show when={notifications().length > 0 && props.notify}>
<div
classList={{
"absolute -top-0.5 -right-0.5 size-1.5 rounded-full": true,
"bg-icon-critical-base": hasError(),
"bg-text-interactive-base": !hasError(),
}}
/>
</Show>
</div>
)
}
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
const current = createMemo(() => base64Decode(params.dir ?? ""))
return (
<Switch>
<Match when={layout.sidebar.opened()}>
<Button
as={"div"}
variant="ghost"
data-active
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
>
<div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
<ProjectAvatar project={props.project} />
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</div>
</Button>
</Match>
<Match when={true}>
<Button
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
data-selected={props.project.worktree === current()}
onClick={() => navigateToProject(props.project.worktree)}
>
<ProjectAvatar project={props.project} notify />
</Button>
</Match>
</Switch>
)
}
const SessionItem = (props: {
session: Session
slug: string
project: LocalProject
mobile?: boolean
}): JSX.Element => {
const notification = useNotification()
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
const notifications = createMemo(() => notification.session.unseen(props.session.id))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const isWorking = createMemo(() => {
if (props.session.id === params.id) return false
const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
return status?.type === "busy" || status?.type === "retry"
})
return (
<>
<div
data-session-id={props.session.id}
class="group/session relative w-full pr-2 py-1 rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
style={{ "padding-left": "16px" }}
>
<Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
<A
href={`${props.slug}/session/${props.session.id}`}
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
>
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
<span
classList={{
"text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
"animate-pulse": isWorking(),
}}
>
{props.session.title}
</span>
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
<Switch>
<Match when={isWorking()}>
<Spinner class="size-2.5 mr-0.5" />
</Match>
<Match when={hasError()}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
<Match when={notifications().length > 0}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
</Match>
<Match when={true}>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{Math.abs(updated().diffNow().as("seconds")) < 60
? "Now"
: updated()
.toRelative({
style: "short",
unit: ["days", "hours", "minutes"],
})
?.replace(" ago", "")
?.replace(/ days?/, "d")
?.replace(" min.", "m")
?.replace(" hr.", "h")}
</span>
</Match>
</Switch>
</div>
</div>
<Show when={props.session.summary?.files}>
<div class="flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${props.session.summary?.files || "No"} file${props.session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={props.session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div>
</Show>
</A>
</Tooltip>
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
<Tooltip
placement={props.mobile ? "bottom" : "right"}
value={
<div class="flex items-center gap-2">
<span>Archive session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.archive")}</span>
</div>
}
>
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
</Tooltip>
</div>
</div>
</>
)
}
const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
const sortable = createSortable(props.project.worktree)
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const [store, setProjectStore] = globalSync.child(props.project.worktree)
const sessions = createMemo(() => store.session.toSorted(sortSessions))
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
const loadMoreSessions = async () => {
setProjectStore("limit", (limit) => limit + 5)
await globalSync.project.loadSessions(props.project.worktree)
}
const isExpanded = createMemo(() =>
props.mobile ? mobileProjects.expanded(props.project.worktree) : props.project.expanded,
)
const handleOpenChange = (open: boolean) => {
if (props.mobile) {
if (open) mobileProjects.expand(props.project.worktree)
else mobileProjects.collapse(props.project.worktree)
} else {
if (open) layout.projects.expand(props.project.worktree)
else layout.projects.collapse(props.project.worktree)
}
}
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Switch>
<Match when={showExpanded()}>
<Collapsible variant="ghost" open={isExpanded()} class="gap-2 shrink-0" onOpenChange={handleOpenChange}>
<Button
as={"div"}
variant="ghost"
class="group/session flex items-center justify-between gap-3 w-full px-1.5 self-stretch h-auto border-none rounded-lg"
>
<Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
<ProjectAvatar
project={props.project}
class="group-hover/session:hidden"
expandable
notify={!isExpanded()}
/>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</Collapsible.Trigger>
<div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
<DropdownMenu>
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
<DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Tooltip
placement="top"
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
</Tooltip>
</div>
</Button>
<Collapsible.Content>
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
<For each={rootSessions()}>
{(session) => (
<SessionItem session={session} slug={slug()} project={props.project} mobile={props.mobile} />
)}
</For>
<Show when={rootSessions().length === 0}>
<div
class="group/session relative w-full pl-4 pr-2 py-1 rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
>
<div class="flex items-center self-stretch w-full">
<div class="flex-1 min-w-0">
<Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
<A
href={`${slug()}/session`}
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
>
<div class="flex items-center self-stretch gap-6 justify-between">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
New session
</span>
</div>
</A>
</Tooltip>
</div>
</div>
</div>
</Show>
<Show when={hasMoreSessions()}>
<div class="relative w-full py-1">
<Button
variant="ghost"
class="flex w-full text-left justify-start text-12-medium opacity-50 px-3.5"
size="large"
onClick={loadMoreSessions}
>
Load more
</Button>
</div>
</Show>
</nav>
</Collapsible.Content>
</Collapsible>
</Match>
<Match when={true}>
<Tooltip placement="right" value={props.project.worktree}>
<ProjectVisual project={props.project} />
</Tooltip>
</Match>
</Switch>
</div>
)
}
const ProjectDragOverlay = (): JSX.Element => {
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeDraggable))
return (
<Show when={project()}>
{(p) => (
<div class="bg-background-base rounded-md">
<ProjectVisual project={p()} />
</div>
)}
</Show>
)
}
const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
return (
<>
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Show when={!sidebarProps.mobile}>
<Tooltip
class="shrink-0"
placement="right"
value={
<div class="flex items-center gap-2">
<span>Toggle sidebar</span>
<span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
</div>
}
inactive={expanded()}
>
<Button
variant="ghost"
size="large"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
size="small"
class="group-hover/sidebar-toggle:hidden"
/>
<Icon
name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
size="small"
class="hidden group-hover/sidebar-toggle:inline-block"
/>
<Icon
name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
size="small"
class="hidden group-active/sidebar-toggle:inline-block"
/>
</div>
<Show when={layout.sidebar.opened()}>
<div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
Toggle sidebar
</div>
</Show>
</Button>
</Tooltip>
</Show>
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragXAxis />
<div
ref={(el) => {
if (!sidebarProps.mobile) scrollContainerRef = el
}}
class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
>
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
<For each={layout.projects.list()}>
{(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
</For>
</SortableProvider>
</div>
<DragOverlay>
<ProjectDragOverlay />
</DragOverlay>
</DragDropProvider>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Switch>
<Match when={!providers.paid().length && expanded()}>
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
<div class="p-3 flex flex-col gap-2">
<div class="text-12-medium text-text-strong">Getting started</div>
<div class="text-text-base">OpenCode includes free models so you can start immediately.</div>
<div class="text-text-base">Connect any provider to use models, inc. Claude, GPT, Gemini etc.</div>
</div>
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
<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}
>
Connect provider
</Button>
</Tooltip>
</div>
</Match>
<Match when={true}>
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="plus"
onClick={connectProvider}
>
<Show when={expanded()}>Connect provider</Show>
</Button>
</Tooltip>
</Match>
</Switch>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip
placement="right"
value={
<div class="flex items-center gap-2">
<span>Open project</span>
<Show when={!sidebarProps.mobile}>
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
</Show>
</div>
}
inactive={expanded()}
>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={chooseProject}
>
<Show when={expanded()}>Open project</Show>
</Button>
</Tooltip>
</Show>
<Tooltip placement="right" value="Share feedback" inactive={expanded()}>
<Button
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="bubble-5"
>
<Show when={expanded()}>Share feedback</Show>
</Button>
</Tooltip>
</div>
</>
)
}
return (
<div class="relative flex-1 min-h-0 flex flex-col">
<Header
navigateToProject={navigateToProject}
navigateToSession={navigateToSession}
onMobileMenuToggle={mobileSidebar.toggle}
/>
<div class="flex-1 min-h-0 flex">
<div
classList={{
"hidden xl:flex": true,
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
"flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base contain-strict": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
>
<Show when={layout.sidebar.opened()}>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
min={150}
max={window.innerWidth * 0.3}
collapseThreshold={80}
onResize={layout.sidebar.resize}
onCollapse={layout.sidebar.close}
/>
</Show>
<SidebarContent />
</div>
<div class="xl:hidden">
<div
classList={{
"fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": mobileSidebar.open(),
"opacity-0 pointer-events-none": !mobileSidebar.open(),
}}
onClick={(e) => {
if (e.target === e.currentTarget) mobileSidebar.hide()
}}
/>
<div
classList={{
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pt-12 pb-5 transition-transform duration-200 ease-out": true,
"translate-x-0": mobileSidebar.open(),
"-translate-x-full": !mobileSidebar.open(),
}}
onClick={(e) => e.stopPropagation()}
>
<SidebarContent mobile />
</div>
</div>
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
</div>
<Toast.Region />
</div>
)
}

View File

@@ -0,0 +1,943 @@
import {
For,
onCleanup,
onMount,
Show,
Match,
Switch,
createResource,
createMemo,
createEffect,
on,
createRenderEffect,
batch,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
import { PromptInput } from "@/components/prompt-input"
import { DateTime } from "luxon"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useCodeComponent } from "@opencode-ai/ui/context/code"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
import { SessionReview } from "@opencode-ai/ui/session-review"
import {
DragDropProvider,
DragDropSensors,
DragOverlay,
SortableProvider,
closestCenter,
createSortable,
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { useTerminal, type LocalPTY } from "@/context/terminal"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Terminal } from "@/components/terminal"
import { checksum } from "@opencode-ai/util/encode"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { extractPromptFromParts } from "@/utils/prompt"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { StatusBar } from "@/components/status-bar"
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
export default function Page() {
const layout = useLayout()
const local = useLocal()
const sync = useSync()
const terminal = useTerminal()
const dialog = useDialog()
const codeComponent = useCodeComponent()
const command = useCommand()
const params = useParams()
const navigate = useNavigate()
const sdk = useSDK()
const prompt = usePrompt()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const revertMessageID = createMemo(() => info()?.revert?.messageID)
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const userMessages = createMemo(() =>
messages()
.filter((m) => m.role === "user")
.sort((a, b) => a.id.localeCompare(b.id)),
)
const visibleUserMessages = createMemo(() => {
const revert = revertMessageID()
if (!revert) return userMessages()
return userMessages().filter((m) => m.id < revert)
})
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
const [store, setStore] = createStore({
clickTimer: undefined as number | undefined,
activeDraggable: undefined as string | undefined,
activeTerminalDraggable: undefined as string | undefined,
userInteracted: false,
stepsExpanded: true,
mobileStepsExpanded: {} as Record<string, boolean>,
messageId: undefined as string | undefined,
})
const activeMessage = createMemo(() => {
if (!store.messageId) return lastUserMessage()
// If the stored message is no longer visible (e.g., was reverted), fall back to last visible
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
return found ?? lastUserMessage()
})
const setActiveMessage = (message: UserMessage | undefined) => {
setStore("messageId", message?.id)
}
function navigateMessageByOffset(offset: number) {
const msgs = visibleUserMessages()
if (msgs.length === 0) return
const current = activeMessage()
const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1
let targetIndex: number
if (currentIndex === -1) {
targetIndex = offset > 0 ? 0 : msgs.length - 1
} else {
targetIndex = currentIndex + offset
}
if (targetIndex < 0 || targetIndex >= msgs.length) return
setActiveMessage(msgs[targetIndex])
}
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
let inputRef!: HTMLDivElement
createEffect(() => {
if (!params.id) return
sync.session.sync(params.id)
})
createEffect(() => {
if (layout.terminal.opened()) {
if (terminal.all().length === 0) {
terminal.new()
}
}
})
createEffect(
on(
() => visibleUserMessages().at(-1)?.id,
(lastId, prevLastId) => {
if (lastId && prevLastId && lastId > prevLastId) {
setStore("messageId", undefined)
}
},
{ defer: true },
),
)
createEffect(() => {
params.id
const status = sync.data.session_status[params.id ?? ""] ?? { type: "idle" }
batch(() => {
setStore("userInteracted", false)
setStore("stepsExpanded", status.type !== "idle")
})
})
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
createRenderEffect((prev) => {
const isWorking = working()
if (!prev && isWorking) {
setStore("stepsExpanded", true)
}
if (prev && !isWorking && !store.userInteracted) {
setStore("stepsExpanded", false)
}
return isWorking
}, working())
command.register(() => [
{
id: "session.new",
title: "New session",
description: "Create a new session",
category: "Session",
keybind: "mod+shift+s",
slash: "new",
onSelect: () => navigate(`/${params.dir}/session`),
},
{
id: "file.open",
title: "Open file",
description: "Search and open a file",
category: "File",
keybind: "mod+p",
slash: "open",
onSelect: () => dialog.show(() => <DialogSelectFile />),
},
// {
// id: "theme.toggle",
// title: "Toggle theme",
// description: "Switch between themes",
// category: "View",
// keybind: "ctrl+t",
// slash: "theme",
// onSelect: () => {
// const currentTheme = localStorage.getItem("theme") ?? "oc-1"
// const themes = ["oc-1", "oc-2-paper"]
// const nextTheme = themes[(themes.indexOf(currentTheme) + 1) % themes.length]
// localStorage.setItem("theme", nextTheme)
// document.documentElement.setAttribute("data-theme", nextTheme)
// },
// },
{
id: "terminal.toggle",
title: "Toggle terminal",
description: "Show or hide the terminal",
category: "View",
keybind: "ctrl+`",
slash: "terminal",
onSelect: () => layout.terminal.toggle(),
},
{
id: "review.toggle",
title: "Toggle review",
description: "Show or hide the review panel",
category: "View",
keybind: "mod+shift+r",
onSelect: () => layout.review.toggle(),
},
{
id: "terminal.new",
title: "New terminal",
description: "Create a new terminal tab",
category: "Terminal",
keybind: "ctrl+shift+`",
onSelect: () => terminal.new(),
},
{
id: "steps.toggle",
title: "Toggle steps",
description: "Show or hide the steps",
category: "View",
keybind: "mod+e",
slash: "steps",
disabled: !params.id,
onSelect: () => setStore("stepsExpanded", (x) => !x),
},
{
id: "message.previous",
title: "Previous message",
description: "Go to the previous user message",
category: "Session",
keybind: "mod+arrowup",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(-1),
},
{
id: "message.next",
title: "Next message",
description: "Go to the next user message",
category: "Session",
keybind: "mod+arrowdown",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(1),
},
{
id: "model.choose",
title: "Choose model",
description: "Select a different model",
category: "Model",
keybind: "mod+'",
slash: "model",
onSelect: () => dialog.show(() => <DialogSelectModel />),
},
{
id: "mcp.toggle",
title: "Toggle MCPs",
description: "Toggle MCPs",
category: "MCP",
keybind: "mod+;",
slash: "mcp",
onSelect: () => dialog.show(() => <DialogSelectMcp />),
},
{
id: "agent.cycle",
title: "Cycle agent",
description: "Switch to the next agent",
category: "Agent",
keybind: "mod+.",
slash: "agent",
onSelect: () => local.agent.move(1),
},
{
id: "agent.cycle.reverse",
title: "Cycle agent backwards",
description: "Switch to the previous agent",
category: "Agent",
keybind: "shift+mod+.",
onSelect: () => local.agent.move(-1),
},
{
id: "session.undo",
title: "Undo",
description: "Undo the last message",
category: "Session",
slash: "undo",
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: async () => {
const sessionID = params.id
if (!sessionID) return
if (status()?.type !== "idle") {
await sdk.client.session.abort({ sessionID }).catch(() => {})
}
const revert = info()?.revert?.messageID
// Find the last user message that's not already reverted
const message = userMessages().findLast((x) => !revert || x.id < revert)
if (!message) return
await sdk.client.session.revert({ sessionID, messageID: message.id })
// Restore the prompt from the reverted message
const parts = sync.data.part[message.id]
if (parts) {
const restored = extractPromptFromParts(parts)
prompt.set(restored)
}
// Navigate to the message before the reverted one (which will be the new last visible message)
const priorMessage = userMessages().findLast((x) => x.id < message.id)
setActiveMessage(priorMessage)
},
},
{
id: "session.redo",
title: "Redo",
description: "Redo the last undone message",
category: "Session",
slash: "redo",
disabled: !params.id || !info()?.revert?.messageID,
onSelect: async () => {
const sessionID = params.id
if (!sessionID) return
const revertMessageID = info()?.revert?.messageID
if (!revertMessageID) return
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
if (!nextMessage) {
// Full unrevert - restore all messages and navigate to last
await sdk.client.session.unrevert({ sessionID })
prompt.reset()
// Navigate to the last message (the one that was at the revert point)
const lastMsg = userMessages().findLast((x) => x.id >= revertMessageID)
setActiveMessage(lastMsg)
return
}
// Partial redo - move forward to next message
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
// Navigate to the message before the new revert point
const priorMsg = userMessages().findLast((x) => x.id < nextMessage.id)
setActiveMessage(priorMsg)
},
},
])
const handleKeyDown = (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement | undefined
if (activeElement) {
const isProtected = activeElement.closest("[data-prevent-autofocus]")
const isInput = /^(INPUT|TEXTAREA|SELECT)$/.test(activeElement.tagName) || activeElement.isContentEditable
if (isProtected || isInput) return
}
if (dialog.active) return
if (activeElement === inputRef) {
if (event.key === "Escape") inputRef?.blur()
return
}
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
inputRef?.focus()
}
}
onMount(() => {
document.addEventListener("keydown", handleKeyDown)
})
onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
})
const resetClickTimer = () => {
if (!store.clickTimer) return
clearTimeout(store.clickTimer)
setStore("clickTimer", undefined)
}
const startClickTimer = () => {
const newClickTimer = setTimeout(() => {
setStore("clickTimer", undefined)
}, 300)
setStore("clickTimer", newClickTimer as unknown as number)
}
const handleTabClick = async (tab: string) => {
if (store.clickTimer) {
resetClickTimer()
} else {
if (tab.startsWith("file://")) {
local.file.open(tab.replace("file://", ""))
}
startClickTimer()
}
}
const handleDragStart = (event: unknown) => {
const id = getDraggableId(event)
if (!id) return
setStore("activeDraggable", id)
}
const handleDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const currentTabs = tabs().all()
const fromIndex = currentTabs?.indexOf(draggable.id.toString())
const toIndex = currentTabs?.indexOf(droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== undefined) {
tabs().move(draggable.id.toString(), toIndex)
}
}
}
const handleDragEnd = () => {
setStore("activeDraggable", undefined)
}
const handleTerminalDragStart = (event: unknown) => {
const id = getDraggableId(event)
if (!id) return
setStore("activeTerminalDraggable", id)
}
const handleTerminalDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const terminals = terminal.all()
const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
terminal.move(draggable.id.toString(), toIndex)
}
}
}
const handleTerminalDragEnd = () => {
setStore("activeTerminalDraggable", undefined)
}
const SortableTerminalTab = (props: { terminal: LocalPTY }): JSX.Element => {
const sortable = createSortable(props.terminal.id)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.terminal.id}
closeButton={
terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} />
)
}
>
{props.terminal.title}
</Tabs.Trigger>
</div>
</div>
)
}
const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => {
return (
<div class="flex items-center gap-x-1.5">
<FileIcon
node={props.file}
classList={{
"grayscale-100 group-data-[selected]/tab:grayscale-0": !props.active,
"grayscale-0": props.active,
}}
/>
<span
classList={{
"text-14-medium": true,
"text-primary": !!props.file.status?.status,
italic: !props.file.pinned,
}}
>
{props.file.name}
</span>
<span class="hidden opacity-70">
<Switch>
<Match when={props.file.status?.status === "modified"}>
<span class="text-primary">M</span>
</Match>
<Match when={props.file.status?.status === "added"}>
<span class="text-success">A</span>
</Match>
<Match when={props.file.status?.status === "deleted"}>
<span class="text-error">D</span>
</Match>
</Switch>
</span>
</div>
)
}
const SortableTab = (props: {
tab: string
onTabClick: (tab: string) => void
onTabClose: (tab: string) => void
}): JSX.Element => {
const sortable = createSortable(props.tab)
const [file] = createResource(
() => props.tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.tab}
closeButton={
<Tooltip value="Close tab" placement="bottom">
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
</Tooltip>
}
hideCloseButton
onClick={() => props.onTabClick(props.tab)}
>
<Switch>
<Match when={file()}>{(f) => <FileVisual file={f()} />}</Match>
</Switch>
</Tabs.Trigger>
</div>
</div>
)
}
const showTabs = createMemo(() => layout.review.opened() && (diffs().length > 0 || tabs().all().length > 0))
const mobileWorking = createMemo(() => status().type !== "idle")
const mobileAutoScroll = createAutoScroll({
working: mobileWorking,
onUserInteracted: () => setStore("userInteracted", true),
})
const MobileTurns = () => (
<div
ref={mobileAutoScroll.scrollRef}
onScroll={mobileAutoScroll.handleScroll}
onClick={mobileAutoScroll.handleInteraction}
class="relative mt-2 min-w-0 w-full h-full overflow-y-auto no-scrollbar pb-12"
>
<div ref={mobileAutoScroll.contentRef} class="flex flex-col gap-45 items-start justify-start mt-4">
<For each={visibleUserMessages()}>
{(message) => (
<SessionTurn
sessionID={params.id!}
messageID={message.id}
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
onUserInteracted={() => setStore("userInteracted", true)}
classes={{
root: "min-w-0 w-full relative",
content:
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
container: "px-4",
}}
/>
)}
</For>
</div>
</div>
)
const NewSessionView = () => (
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
)
const DesktopSessionContent = () => (
<Switch>
<Match when={params.id}>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={visibleUserMessages()}
current={activeMessage()}
onMessageSelect={setActiveMessage}
wide={!showTabs()}
/>
<Show when={activeMessage()}>
<SessionTurn
sessionID={params.id!}
messageID={activeMessage()!.id}
stepsExpanded={store.stepsExpanded}
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
onUserInteracted={() => setStore("userInteracted", true)}
classes={{
root: "pb-20 flex-1 min-w-0",
content: "pb-20",
container:
"w-full " +
(!showTabs() ? "max-w-200 mx-auto px-6" : visibleUserMessages().length > 1 ? "pr-6 pl-18" : "px-6"),
}}
/>
</Show>
</div>
</Match>
<Match when={true}>
<NewSessionView />
</Match>
</Switch>
)
return (
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
<div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
<Switch>
<Match when={!params.id}>
<div class="flex-1 min-h-0 overflow-hidden">
<NewSessionView />
</div>
</Match>
<Match when={diffs().length > 0}>
<Tabs class="flex-1 min-h-0 flex flex-col pb-28">
<Tabs.List>
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
Session
</Tabs.Trigger>
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
{diffs().length} Files Changed
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="session" class="flex-1 !overflow-hidden">
<MobileTurns />
</Tabs.Content>
<Tabs.Content forceMount value="review" class="flex-1 !overflow-hidden hidden data-[selected]:block">
<div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
<SessionReview
diffs={diffs()}
classes={{
root: "pb-32",
header: "px-4",
container: "px-4",
}}
/>
</div>
</Tabs.Content>
</Tabs>
</Match>
<Match when={true}>
<div class="flex-1 min-h-0 overflow-hidden">
<MobileTurns />
</div>
</Match>
</Switch>
<div class="absolute inset-x-0 bottom-4 flex flex-col justify-center items-center z-50 px-4">
<div class="w-full">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</div>
</div>
<div class="hidden md:flex min-h-0 grow w-full">
<div
class="@container relative shrink-0 py-3 flex flex-col gap-6 min-h-0 h-full bg-background-stronger"
style={{ width: showTabs() ? `${layout.session.width()}px` : "100%" }}
>
<div class="flex-1 min-h-0 overflow-hidden">
<DesktopSessionContent />
</div>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div
classList={{
"w-full px-6": true,
"max-w-200": !showTabs(),
}}
>
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</div>
<Show when={showTabs()}>
<ResizeHandle
direction="horizontal"
size={layout.session.width()}
min={450}
max={window.innerWidth * 0.45}
onResize={layout.session.resize}
/>
</Show>
</div>
<Show when={showTabs()}>
<div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={tabs().active() ?? "review"} onChange={tabs().open}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Show when={diffs().length}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-3">
<Show when={diffs()}>
<DiffChanges changes={diffs()} variant="bars" />
</Show>
<div class="flex items-center gap-1.5">
<div>Review</div>
<Show when={info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{info()?.summary?.files ?? 0}
</div>
</Show>
</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={tabs().all() ?? []}>
<For each={tabs().all() ?? []}>
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
<Tooltip
value={
<div class="flex items-center gap-2">
<span>Open file</span>
<span class="text-icon-base text-12-medium">{command.keybind("file.open")}</span>
</div>
}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => dialog.show(() => <DialogSelectFile />)}
/>
</Tooltip>
</div>
</Tabs.List>
</div>
<Show when={diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionReview
classes={{
root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={diffs()}
split
/>
</div>
</Tabs.Content>
</Show>
<For each={tabs().all()}>
{(tab) => {
const [file] = createResource(
() => tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<Tabs.Content value={tab} class="select-text mt-3">
<Switch>
<Match when={file()}>
{(f) => (
<Dynamic
component={codeComponent}
file={{
name: f().path,
contents: f().content?.content ?? "",
cacheKey: checksum(f().content?.content ?? ""),
}}
overflow="scroll"
class="pb-40"
/>
)}
</Match>
</Switch>
</Tabs.Content>
)
}}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedFile) => {
const [file] = createResource(
() => draggedFile(),
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</Show>
</div>
<Show when={layout.terminal.opened()}>
<div
class="hidden md:flex relative w-full flex-col shrink-0 border-t border-border-weak-base"
style={{ height: `${layout.terminal.height()}px` }}
>
<ResizeHandle
direction="vertical"
size={layout.terminal.height()}
min={100}
max={window.innerHeight * 0.6}
collapseThreshold={50}
onResize={layout.terminal.resize}
onCollapse={layout.terminal.close}
/>
<DragDropProvider
onDragStart={handleTerminalDragStart}
onDragEnd={handleTerminalDragEnd}
onDragOver={handleTerminalDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
<Tabs.List class="h-10">
<SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
<For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<Tooltip
value={
<div class="flex items-center gap-2">
<span>New terminal</span>
<span class="text-icon-base text-12-medium">{command.keybind("terminal.new")}</span>
</div>
}
class="flex items-center"
>
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
</Tooltip>
</div>
</Tabs.List>
<For each={terminal.all()}>
{(pty) => (
<Tabs.Content value={pty.id}>
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
</Tabs.Content>
)}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeTerminalDraggable}>
{(draggedId) => {
const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
return (
<Show when={pty()}>
{(t) => (
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
{t().title}
</div>
)}
</Show>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</Show>
<StatusBar>
<SessionLspIndicator />
<SessionMcpIndicator />
</StatusBar>
</div>
)
}

View File

@@ -0,0 +1,99 @@
import z from "zod"
const prefixes = {
session: "ses",
message: "msg",
permission: "per",
user: "usr",
part: "prt",
pty: "pty",
} as const
const LENGTH = 26
let lastTimestamp = 0
let counter = 0
type Prefix = keyof typeof prefixes
export namespace Identifier {
export function schema(prefix: Prefix) {
return z.string().startsWith(prefixes[prefix])
}
export function ascending(prefix: Prefix, given?: string) {
return generateID(prefix, false, given)
}
export function descending(prefix: Prefix, given?: string) {
return generateID(prefix, true, given)
}
}
function generateID(prefix: Prefix, descending: boolean, given?: string): string {
if (!given) {
return create(prefix, descending)
}
if (!given.startsWith(prefixes[prefix])) {
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
}
return given
}
function create(prefix: Prefix, descending: boolean, timestamp?: number): string {
const currentTimestamp = timestamp ?? Date.now()
if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp
counter = 0
}
counter += 1
let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
if (descending) {
now = ~now
}
const timeBytes = new Uint8Array(6)
for (let i = 0; i < 6; i += 1) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
return prefixes[prefix] + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12)
}
function bytesToHex(bytes: Uint8Array): string {
let hex = ""
for (let i = 0; i < bytes.length; i += 1) {
hex += bytes[i].toString(16).padStart(2, "0")
}
return hex
}
function randomBase62(length: number): string {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const bytes = getRandomBytes(length)
let result = ""
for (let i = 0; i < length; i += 1) {
result += chars[bytes[i] % 62]
}
return result
}
function getRandomBytes(length: number): Uint8Array {
const bytes = new Uint8Array(length)
const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : undefined
if (cryptoObj && typeof cryptoObj.getRandomValues === "function") {
cryptoObj.getRandomValues(bytes)
return bytes
}
for (let i = 0; i < length; i += 1) {
bytes[i] = Math.floor(Math.random() * 256)
}
return bytes
}

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