Compare commits

...

775 Commits

Author SHA1 Message Date
opencode 77efa92bfe release: v1.2.4 2026-02-15 01:55:33 +00:00
Dax Raad d53ded7e9f core: add db command for database inspection and querying 2026-02-14 20:37:17 -05:00
Dax Raad 8047a789ab test: add tests for path-derived IDs in json migration
Tests verify that file paths are used for IDs even when JSON contains
different values - ensuring robustness against stale JSON content.
2026-02-14 20:37:17 -05:00
Dax Raad 69f47bbead fix: derive all IDs from file paths during json migration
Earlier migrations moved data to new directories without updating JSON
fields. Now consistently derives all IDs from file paths:

- Projects: id from filename
- Sessions: id from filename, projectID from parent directory
- Messages: id from filename, sessionID from parent directory
- Parts: id from filename, messageID from parent directory

This ensures migrated data matches the actual file layout regardless of
stale values in JSON content.
2026-02-14 20:37:17 -05:00
Adam 47f48aecef feat(app): clear notifications action (#13668)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com>
2026-02-14 19:33:22 -06:00
Adam 8671381071 fix(app): stack overflow in filetree (#13667)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com>
2026-02-14 19:24:48 -06:00
opencode 69d967dc74 release: v1.2.3 2026-02-15 00:34:56 +00:00
opencode-agent[bot] af5452b624 chore: update nix node_modules hashes 2026-02-14 20:38:57 +00:00
Aiden Cline b4bd78ce79 fix: ensure anthropic models on OR also have variant support (#13498) 2026-02-14 14:30:07 -06:00
Dax a16afa7f25 fix: add WAL checkpoint on database open (#13633) 2026-02-14 19:33:08 +00:00
Alberto Valverde 8a4643fee4 chore: bump nixpkgs to get bun 1.3.9 (#13302) 2026-02-14 13:21:31 -06:00
Aiden Cline eb3e9c74c6 fix: ensure vercel variants pass amazon models under bedrock key (#13631) 2026-02-14 13:18:52 -06:00
opencode d28116ff35 release: v1.2.2 2026-02-14 19:08:58 +00:00
Dax Raad 595aab70c5 core: add comprehensive test coverage for Session.list() filters
Adds test cases for filtering sessions by directory, root sessions only,

start time, search terms, and result limits to ensure the listing

functionality works correctly for all filter combinations.
2026-02-14 13:43:41 -05:00
Dax Raad 0b5c218fd3 core: filter sessions at database level to improve session list loading performance 2026-02-14 13:41:15 -05:00
opencode-agent[bot] f96f5bfaff chore: update nix node_modules hashes 2026-02-14 18:38:18 +00:00
Aiden Cline 442fc7c1c7 fix vercel gateway variants (#13541)
Co-authored-by: Benjamin Woodruff <github@benjam.info>"
2026-02-14 12:32:29 -06:00
Aiden Cline f32d70e2b3 bump vertex ai packages (#13625) 2026-02-14 12:29:01 -06:00
Brendan Allan d9cf22304e ci: move signpath policy 2026-02-14 14:47:50 +08:00
Brendan Allan cb8fbbb52c ci: test-signing signpath policy 2026-02-14 06:39:53 +00:00
opencode 8b7b63e86b release: v1.2.1 2026-02-14 06:39:47 +00:00
Dax Raad 8aa91f5646 tui: show all project sessions from any working directory
Previously sessions were only listed if they were created in the current
working directory or its subdirectories. Users can now view and switch
to any session in the project regardless of which directory they're in.
2026-02-14 01:21:41 -05:00
Aiden Cline 580474d9c9 fix: tweak websearch tool description date info to avoid cache busts (#13559) 2026-02-13 23:59:10 -06:00
Frank a9aef2452b zen: minimax m2.5 & glm5 2026-02-14 00:47:26 -05:00
opencode 2f9fddc3c1 release: v1.2.0 2026-02-14 05:20:11 +00:00
Brendan Allan 1c4a2f364c desktop: fix rust 2026-02-14 12:48:16 +08:00
opencode-agent[bot] 320089517f chore: update nix node_modules hashes 2026-02-14 04:28:27 +00:00
Brendan Allan 5e6542a048 desktop: remote OPENCODE_SQLITE env (#13545) 2026-02-13 23:20:09 -05:00
opencode-agent[bot] 90f1ec7aa1 chore: generate 2026-02-14 04:19:48 +00:00
Dax 3010323ee0 sqlite again (#10597)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-14 04:19:02 +00:00
Kevin 3585a8b57a fix: prevent opencode run crash on malformed tool inputs (#13051)
Co-authored-by: 0xK3vin <kevin@git-pu.sh>
2026-02-13 20:54:20 -06:00
Kit Langton d1383711cf fix(ui): support cmd-click links in inline code (#12552) 2026-02-13 13:57:38 -06:00
Niu Shuai 784ed20f89 fix: standardize zh-CN docs character set and terminology (#13500) 2026-02-13 12:58:12 -06:00
严浩 42b479ca38 fix(test): move timeout config to CLI flag (#13494)
Co-authored-by: 严浩 <h_mini2024@oo1.dev>
2026-02-13 10:20:02 -05:00
G36maid f01a65b9f2 docs: add pacman installation option for Arch Linux alongside AUR (#13293) 2026-02-13 09:07:48 -06:00
Jun ba69269b31 docs(ko): polish Korean phrasing in acp, agents, config, and custom-tools docs (#13446) 2026-02-13 09:05:29 -06:00
Adam 54d541a20e chore: cleanup 2026-02-13 05:52:43 -06:00
Adam 71578b0944 fix(app): terminal resize 2026-02-13 05:52:42 -06:00
eytans 6f912fab89 fix(web): use prompt_async endpoint to avoid timeout over VPN/tunnel (#12749) 2026-02-13 05:25:47 -06:00
opencode-agent[bot] 17c0f8d126 chore: generate 2026-02-13 11:19:37 +00:00
Annopick 5e74a01d6c docs: Fix zh-cn translation mistake in tools.mdx (#13407) 2026-02-13 05:18:47 -06:00
Chris Yang fdd054e79a fix(app): remount SDK/sync tree when server URL changes (#13437) 2026-02-13 05:16:14 -06:00
Jun 8e3ef4ef40 fix(web): sync docs locale cookie on alias redirects (#13109) 2026-02-13 05:12:28 -06:00
Filip fc2f7b2e2e fix(desktop): performance optimization for showing large diff & files (#13460) 2026-02-13 05:08:13 -06:00
opencode-agent[bot] 066b5fd157 chore: update nix node_modules hashes 2026-02-13 07:06:28 +00:00
Rahul Mishra f8e94fd002 feat(cli): add --dir option to run command (#12443) 2026-02-13 00:59:37 -06:00
Aiden Cline b4fe75a169 feat: update some ai sdk packages and uuse adaptive reasoning for opus 4.6 on vertex/bedrock/anthropic (#13439) 2026-02-13 00:56:11 -06:00
opencode 87f670136c release: v1.1.65 2026-02-13 05:51:04 +00:00
Aiden Cline 231fcfe6fd Revert "fix: token substitution in OPENCODE_CONFIG_CONTENT" (#13429) 2026-02-12 23:24:31 -06:00
Aiden Cline b672abb2f4 fix: ensure @-ing a dir uses the read tool instead of dead list tool (#13428) 2026-02-12 23:20:33 -06:00
Spoon 6bb8334384 feat(hook): add tool.definition hook for plugins to modify tool description and parameters (#4956) 2026-02-12 22:52:17 -06:00
Brendan Allan 9d626c92bb ci: remove signpath policy 2026-02-13 10:46:45 +08:00
Adam 51d41ac541 fix(app): reconnect event stream on disconnect 2026-02-12 20:20:24 -06:00
Adam 859142d98e feat(app): toggle all provider models 2026-02-12 20:19:26 -06:00
Brendan Allan 0d1314716b cleanup desktop loading page 2026-02-13 10:15:36 +08:00
Adam 894252dae2 fix(app): emoji as avatar 2026-02-12 20:05:58 -06:00
Adam 24b0ba5af7 fix(app): notification should navigate to session 2026-02-12 20:04:36 -06:00
Adam 4ac7e2c1c4 fix(app): prompt input quirks 2026-02-12 19:58:57 -06:00
Adam adee23caee chore: cleanup 2026-02-12 19:52:20 -06:00
Adam 4588a0f6dc fix(app): worktree delete 2026-02-12 19:38:13 -06:00
Adam 0fe62dda07 fix(app): failed to create store 2026-02-12 19:38:06 -06:00
Brendan Allan aed38b4179 desktop: only show loading window if sqlite migration is necessary 2026-02-13 08:49:52 +08:00
projectArtur a9e44df330 fix: resolve ACP hanging indefinitely in thinking state on Windows (#13222)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-02-12 18:20:00 -06:00
opencode 71564d475e release: v1.1.64 2026-02-12 23:18:40 +00:00
Ariane Emory b833c4de7d fix: token substitution in OPENCODE_CONFIG_CONTENT (#13384) 2026-02-12 16:59:44 -06:00
Aiden Cline 56f63313f3 chore: cleanup flag code (#13389) 2026-02-12 22:38:51 +00:00
opencode-agent[bot] 51f3a5e7a3 chore: generate 2026-02-12 22:14:45 +00:00
Smit Chaudhary 939c63627e fix: look for recent model in fallback in cli (#12582) 2026-02-12 16:13:48 -06:00
opencode-agent[bot] 8f424fefbe chore: update nix node_modules hashes 2026-02-12 22:04:31 +00:00
Luke Parker 4ac99b73d8 fix: baseline CPU detection (#13371) 2026-02-13 07:50:43 +10:00
Luke Parker 2dd654eea6 feat: windows selection behavior, manual ctrl+c (#13315) 2026-02-13 07:38:27 +10:00
Sebastian 89a6043fa5 do not open console on error (#13374) 2026-02-12 21:29:58 +00:00
opencode-agent[bot] 95ef7ef7e6 chore: generate 2026-02-12 21:23:32 +00:00
Aman Kalra 9ab6f85b04 docs: update STACKIT provider documentation with typo fix (#13357)
Co-authored-by: amankalra172 <aman.kalra@st.ovgu.de>
2026-02-12 15:22:35 -06:00
Adam e01225bf8c fix(app): terminal pty isolation 2026-02-12 15:15:34 -06:00
Adam 2109a38435 feat(app): option to turn off sound effects 2026-02-12 15:03:05 -06:00
Adam 4141e50ff4 fix(app): normalize oauth error messages 2026-02-12 14:58:25 -06:00
Adam 31ffb33678 chore: cleanup 2026-02-12 14:48:09 -06:00
Adam 1f08c79612 chore: cleanup 2026-02-12 14:43:20 -06:00
Adam fb5082f677 chore: cleanup 2026-02-12 14:39:02 -06:00
Adam f678b09a5a chore: cleanup 2026-02-12 14:32:31 -06:00
Adam 411cc62153 chore: cleanup 2026-02-12 14:24:19 -06:00
Adam 72802e39be fix(app): suggestion active state broken 2026-02-12 14:17:05 -06:00
Adam 0985166f1d fix(app): remote http server connections 2026-02-12 13:41:22 -06:00
Aiden Cline b4aac36f17 test: add more test cases for project.test.ts (#13355) 2026-02-12 18:46:44 +00:00
opencode 8d8f9219bc release: v1.1.63 2026-02-12 18:46:38 +00:00
Dax Raad a027d1e8be improve codex model list 2026-02-12 18:15:30 +00:00
opencode 982b7260ed release: v1.1.62 2026-02-12 18:15:24 +00:00
Adam e9a9a790d6 fix(app): project icons unloading 2026-02-12 11:50:17 -06:00
Rasheed da767b4ca7 fix(app): preserve undo history for plain-text paste (#13351) 2026-02-12 17:27:53 +00:00
Adam 79b45e54bf chore(app): refactor for better solidjs hygiene (#13344) 2026-02-12 11:26:19 -06:00
Dax Raad 1684eec40e ignore: document test fixtures for agents 2026-02-12 12:10:21 -05:00
Ryan Vogel 29646a5a82 feat(tool): return image attachments from webfetch (#13331) 2026-02-12 12:09:29 -05:00
opencode-agent[bot] e1ca44157e chore: update nix node_modules hashes 2026-02-12 17:09:09 +00:00
Aiden Cline e5a327ce53 fix: downgrade bun to 1.3.5 (#13347) 2026-02-12 16:59:08 +00:00
Dylan Fiedler b9a32ced51 fix(docs): correct format attribute in StructuredOutputs (#13340) 2026-02-12 10:55:43 -06:00
Frank 2576bb482a wip: zen 2026-02-12 11:11:25 -05:00
Frank b13e7cbda1 zen: minimax m2.5 2026-02-12 11:04:09 -05:00
Adam 2ee4a81321 fix(app): more defensive code component 2026-02-12 10:00:58 -06:00
Adam 02a93393ec fix(app): more defensive session context metrics 2026-02-12 10:00:58 -06:00
Adam 217cacf56f chore: refactor packages/app files (#13236)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
2026-02-12 09:49:14 -06:00
Dax Raad 74ab58cbdb core: expose tool arguments in shell hook for plugin visibility 2026-02-12 09:54:47 -05:00
Frank 6a5f0e79a5 wip: zen 2026-02-12 14:45:52 +00:00
opencode-agent[bot] e0dfa352e3 chore: generate 2026-02-12 14:45:52 +00:00
opencode 7cb614a9fa release: v1.1.61 2026-02-12 14:45:45 +00:00
Dax Raad 3c66ef216f core: allow model configurations without npm/api provider details
Makes npm and api fields optional in the provider schema so model definitions

can be more flexible when provider package details aren't needed.
2026-02-12 09:26:28 -05:00
Frank b2a47de786 wip: zen 2026-02-12 09:17:49 -05:00
Ryan Vogel 5660c02033 feat: update to not post comment on workflows when no duplicates found (#13238) 2026-02-12 09:13:38 -05:00
opencode-agent[bot] c46ecab99e chore: update nix node_modules hashes 2026-02-12 13:36:53 +00:00
Adam 62728c0073 wip(ui): diff virtualization (#12693) 2026-02-12 07:25:58 -06:00
Adam cfdf2904bf chore: style loading screen 2026-02-12 07:16:30 -06:00
Brendan Allan ac42b0c008 ci: move test-sigining policy 2026-02-12 18:50:00 +08:00
Brendan Allan 9c5e8d8fab Testing SignPath Integration (#13308) 2026-02-12 18:46:56 +08:00
Brendan Allan b91ac7f746 desktop: sqlite migration progress bar (#13294) 2026-02-12 09:44:06 +00:00
Aiden Cline a50f91fa54 tweak: tool outputs to be more llm friendly (#13269) 2026-02-12 00:33:18 -06:00
Frank 59863cb1a9 zen: return cost 2026-02-12 01:08:02 -05:00
opencode 18100a1d48 release: v1.1.60 2026-02-12 05:58:24 +00:00
Luke Parker 210bb1dc65 fix(win32): use ffi to get around bun raw input/ctrl+c issues (#13052) 2026-02-12 15:39:31 +10:00
opencode-agent[bot] f60b87f01b chore: generate 2026-02-12 04:55:00 +00:00
Kyle Mistele a65133f183 feat: support claude agent SDK-style structured outputs in the OpenCode SDK (#8161)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Dax Raad <d@ironbay.co>
2026-02-12 04:54:05 +00:00
opencode-agent[bot] 967916191c chore: generate 2026-02-12 04:11:57 +00:00
Dax Raad 10f9176a93 support custom api url per model 2026-02-11 23:10:47 -05:00
github-actions[bot] 3217faa2d1 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/13076#issuecomment-3888412535
2026-02-12 03:05:00 +00:00
Kit Langton c5b32c1a0a feat(tui): add toggle to hide session header (#13244) 2026-02-12 02:21:42 +00:00
dpuyosa 442d41191c feat(opencode): Venice Add automatic variant generation for Venice models (#12106) 2026-02-11 20:21:29 -06:00
Adam 35a31457d4 fix(app): guard randomUUID in insecure browser contexts (#13237)
Co-authored-by: Selim <31136147+selimerunkut@users.noreply.github.com>
2026-02-12 01:05:15 +00:00
Adam c3f977a99f fix(docs): locale translations for nav elements and headings 2026-02-11 18:51:50 -06:00
Adam bf3d7776b1 docs(ar): second-pass localization cleanup 2026-02-11 18:51:50 -06:00
Aiden Cline 3dfea6eed9 tweak: use promise all for mcp listTools calls (#13229) 2026-02-12 00:19:53 +00:00
opencode-agent[bot] 08c1620982 chore: update nix node_modules hashes 2026-02-11 23:39:24 +00:00
Luke Parker c9346781a0 chore: upgrade bun to 1.3.9 (#13223) 2026-02-12 09:27:58 +10:00
opencode-agent[bot] 585b0da2e5 chore: update nix node_modules hashes 2026-02-11 23:22:01 +00:00
Sebastian b53b3520a1 upgrade opentui to 0.1.79 (#13036) 2026-02-12 00:13:11 +01:00
Aiden Cline b933e3c6f2 tweak: compaction check (#13214) 2026-02-11 21:43:04 +00:00
github-actions[bot] 973d72174e Update VOUCHED list
https://github.com/anomalyco/opencode/issues/13204#issuecomment-3887062726
2026-02-11 20:40:39 +00:00
Aiden Cline 0b621489f2 tweak: make read tool offset 1 indexed instead of 0 to avoid confusion that could be caused by line #s being 1 based (#13198) 2026-02-11 14:02:30 -06:00
Aiden Cline 69138c2d69 feat: adjust read tool so that it can handle dirs too (#13090) 2026-02-11 13:23:00 -06:00
Dax Raad 365cdb87a9 add square logo variants to brand page 2026-02-11 13:47:54 -05:00
Adam e42600565b chore: update docs sync workflow 2026-02-11 11:47:32 -06:00
Adam c79b28ef2f fix(app): translations 2026-02-11 11:03:35 -06:00
opencode-agent[bot] 2bbb91b0c2 chore: generate 2026-02-11 17:00:20 +00:00
Filip 5a90d54fbc fix(app):workspace reset (#13170)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-02-11 10:59:09 -06:00
opencode-agent[bot] 834630daad chore: generate 2026-02-11 15:37:06 +00:00
opencode db75c00cb5 release: v1.1.59 2026-02-11 15:37:00 +00:00
Adam bbe94de069 fix(app): translations 2026-02-11 09:19:16 -06:00
Filip 1b8aecf693 Fix/reverception (#13166)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-02-11 09:18:44 -06:00
Adam ad3dd88c4a test(app): more e2e tests (#13162) 2026-02-11 15:12:28 +00:00
Adam fd857bf3e1 fix(app): sidebar remount 2026-02-11 15:12:28 +00:00
opencode 12cc616afa release: v1.1.58 2026-02-11 15:12:01 +00:00
Adam d9f668cf8a Revert "feat(desktop): add WSL backend mode (#12914)"
This reverts commit ca23655d93.
2026-02-11 08:51:41 -06:00
Adam c456a4b888 fix(app): notifications on child sessions 2026-02-11 08:47:27 -06:00
Adam bd73ef60ea fix(app): terminal PTY buffer carryover 2026-02-11 08:47:26 -06:00
Adam e9d85096dd fix(console): translations 2026-02-11 08:47:26 -06:00
Adam 6fd21102ea fix(app): terminal resize 2026-02-11 08:47:26 -06:00
Kit Langton b32dd17092 fix(tui): dismiss dialogs with ctrl+c (#12884) 2026-02-11 09:03:59 -05:00
Kit Langton 1f09649711 fix(tui): keep /share available to copy existing link (#12532) 2026-02-11 08:58:24 -05:00
Kit Langton 9cd8e6dc86 feat(prompt): mode-specific input placeholders (#12388) 2026-02-11 08:57:59 -05:00
Kit Langton 304ee5c7b6 fix(tui): prevent home wordmark corruption in height-constrained terminals (#13069) 2026-02-11 08:52:14 -05:00
opencode c5febb9dc1 release: v1.1.57 2026-02-11 13:45:34 +00:00
Adam 0e19470df1 fix(app): terminal copy/paste 2026-02-11 07:19:33 -06:00
Adam c22619147f fix(app): copy path button styles 2026-02-11 07:19:33 -06:00
Shintaro Jokagi 806d4323c5 fix(web): prevent language select label truncation (#13100) 2026-02-11 06:43:48 -06:00
Brendan Allan 0a5198d770 desktop: use tracing for logging (#13135) 2026-02-11 11:36:27 +00:00
Jack b78892c416 fix: encode non-ASCII directory paths in v1 SDK HTTP headers (#13131) 2026-02-11 05:33:25 -06:00
Filip 6cbeb3ec4a feat(desktop): enhance Windows app resolution and UI loading states (#13084) 2026-02-11 04:40:52 -06:00
opencode-agent[bot] 8500fac931 chore: generate 2026-02-11 10:38:52 +00:00
Jun 40aefc1d13 docs(ko): improve translations for intro, cli, and commands (#13094) 2026-02-11 04:37:52 -06:00
webwww123 bc7f1fc515 fix(docs): avoid footer language selector truncation (#13124) 2026-02-11 04:33:50 -06:00
Adam 07cc07fa95 fix(desktop): server spawn resilience (#13028)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-11 09:44:26 +00:00
Brendan Allan 6de1e786b6 fix(desktop): read wayland preference from store (#13081) 2026-02-11 08:10:24 +00:00
Brendan Allan ca23655d93 feat(desktop): add WSL backend mode (#12914) 2026-02-11 07:49:48 +00:00
Aiden Cline 525cf17315 fix: add additional context overflow cases, remove overcorrecting ones (#13077) 2026-02-10 23:30:29 -06:00
Frank 8db235941d wip: zen 2026-02-11 00:05:04 -05:00
Ariane Emory 8bc8888c23 docs: remove 'Migrating to 1.0' documentation section (#13076) 2026-02-10 22:52:02 -06:00
opencode-agent[bot] 43709d3ba4 chore: generate 2026-02-11 01:56:08 +00:00
Aiden Cline 61c5e8ab80 fix(core): ensure compaction is more reliable, add reserve token buffer to ensure that input window has enough room to compact (#12924)
Co-authored-by: James Lal <james@littlebearlabs.io>
2026-02-10 19:55:22 -06:00
Aiden Cline 075cbc0905 tweak: /review prompt to look for behavior changes more explicitly (#13049) 2026-02-10 19:44:42 -06:00
Dax c872cfa5f0 fix(tui): default session sidebar to auto (#13046) 2026-02-11 01:26:01 +00:00
OpeOginni 3cfcc21e5a fix(tui): improve amazon-bedrock check to include container credentials (#13037) 2026-02-10 18:51:49 -06:00
github-actions[bot] 2cd0fbd78d Update VOUCHED list
https://github.com/anomalyco/opencode/issues/12841#issuecomment-3881500535
2026-02-11 00:37:36 +00:00
Frank 0c0d056735 wip: zen 2026-02-10 17:56:10 -05:00
Frank 60869b6e8f wip: zen 2026-02-10 17:44:00 -05:00
Frank 9e0a36f35a wip: zen 2026-02-10 17:36:32 -05:00
Frank 1efb6ccf85 wip: zen 2026-02-10 17:24:03 -05:00
Frank 2315ee17a0 wip: zen 2026-02-10 17:15:01 -05:00
Adam 1538110ff5 fix(docs): locale translations 2026-02-10 22:12:32 +00:00
Frank a2993acfb2 wip: zen 2026-02-10 22:12:32 +00:00
Adam ec17a95462 chore: update website stats 2026-02-10 22:12:32 +00:00
opencode 6f3ac1cc74 release: v1.1.56 2026-02-10 22:12:25 +00:00
Adam 01c10b61fa fix(app): task tool rendering 2026-02-10 15:28:46 -06:00
Adam 82d57ac0c2 fix(docs): invalid markdown 2026-02-10 15:18:57 -06:00
Filip fd72c883bf fix(desktop): open apps with executables on Windows (#13022) 2026-02-10 15:10:58 -06:00
Frank 635706f13f zen: log error 2026-02-10 15:55:33 -05:00
Adam 144fb2d383 fix(app): don't close sidebar on session change (#13013) 2026-02-10 14:45:52 -06:00
opencode-agent[bot] ca232ff757 chore: generate 2026-02-10 20:22:31 +00:00
Adam 9b1077d019 fix(app): don't scroll code search input 2026-02-10 20:22:31 +00:00
Adam e1a628e680 fix(docs): locale translations 2026-02-10 20:22:30 +00:00
opencode e551fd2f69 release: v1.1.55 2026-02-10 20:21:34 +00:00
Dax Raad 46b6f338c7 core: increase test timeout to 30s to prevent failures during package installation 2026-02-10 14:25:25 -05:00
Adam a2f72f48fe fix(app): memory leak with platform fetch for events 2026-02-10 13:18:33 -06:00
Adam 91fd29c30e fix(app): disable terminal transparency 2026-02-10 13:11:31 -06:00
Dax bdb5499ffe fix: show helpful message when free usage limit is exceeded (#13005) 2026-02-10 19:04:10 +00:00
opencode 3d312761f7 release: v1.1.54 2026-02-10 19:04:02 +00:00
Adam 10d1c08eb3 fix(app): back to platform fetch for now 2026-02-10 12:40:26 -06:00
Adam 80589c0565 fix(app): include basic auth 2026-02-10 12:37:28 -06:00
Frank 4b6084b807 zen: retry on 429 2026-02-10 13:35:16 -05:00
Adam cea78889c2 fix(app): global event default fetch 2026-02-10 12:29:01 -06:00
Frank 691c691474 wip: zen 2026-02-10 13:07:08 -05:00
Adam a532bf6dd2 fix(app): no changes in review pane 2026-02-10 11:53:33 -06:00
Adam 4308427661 fix(app): memory leak with event fetch 2026-02-10 11:30:58 -06:00
Adam cfb2166da8 fix(app): don't dispose instance after reset workspace 2026-02-10 10:57:50 -06:00
Adam 04383b4b96 chore: translator agent 2026-02-10 10:36:28 -06:00
Adam a7312ba2eb fix(app): regressions 2026-02-10 10:15:37 -06:00
Adam 6f06932523 fix(app): terminal replay (#12991) 2026-02-10 10:15:19 -06:00
Adam 74625cf979 fix(app): use agent configured variant (#12993) 2026-02-10 10:15:09 -06:00
Dax f567602ebc refactor: clean up dialog-model.tsx per code review (#12983) 2026-02-10 15:13:37 +00:00
Adam 8225593c88 fix(app): persist defensiveness (#12973) 2026-02-10 07:47:05 -06:00
Filip a6007f1e3f test(e2e): redo & undo test (#12974) 2026-02-10 07:46:48 -06:00
opencode-agent[bot] 3cdced95ca chore: generate 2026-02-10 13:39:21 +00:00
Adam 5fed71d8b9 chore: fix e2e tests 2026-02-10 07:38:13 -06:00
Adam cee3709909 fix(docs): locale translations 2026-02-10 07:11:19 -06:00
Adam 8a2432840c chore: fix test 2026-02-10 07:06:20 -06:00
Peter Dave Hello 451a4a9ca1 chore: fix typos and GitHub capitalization (#12852)
publish-github-action / publish (push) Waiting to run
2026-02-10 06:53:38 -06:00
Paul 174c097402 fix: use absolute paths for sidebar session navigation (#12898) 2026-02-10 06:48:55 -06:00
Ole-Martin Bratteng 48c1ed2a8e chore: fix some norwegian i18n issues (#12935) 2026-02-10 06:46:32 -06:00
Adam 24ea84fa67 fix(app): new session in workspace choosing wrong workspace 2026-02-10 06:02:17 -06:00
Adam cc8da0d88a fix(app): workspace reset issues 2026-02-10 05:52:34 -06:00
Dax Raad 1f2781e6f3 Revert "feat: add version to session header and /status dialog (#8802)"
This reverts commit e8497e8513.
2026-02-10 00:01:28 -05:00
Dax Raad 8b464c53f9 Revert "feat(tui): highlight esc label on hover in dialog (#12383)"
This reverts commit af5a9dfcac.
2026-02-09 23:57:37 -05:00
Dax Raad eeeedd7962 Revert "feat: show connected providers in /connect dialog (#8351)"
This reverts commit d436276330.
2026-02-09 23:46:57 -05:00
Harsh Sharma 7a943ed795 fix: resolve homebrew upgrade requiring multiple runs (#5375) (#10118) 2026-02-09 22:18:57 -06:00
github-actions[bot] 056ee3261e Update VOUCHED list
https://github.com/anomalyco/opencode/issues/12881#issuecomment-3875123178
2026-02-10 03:32:35 +00:00
Ryan Vogel 9547f1a3ce ci: filter daily recaps to community-only and fix vouch workflow authentication (#12910) 2026-02-09 22:29:18 -05:00
Kit Langton fd2d00aad2 feat(app): add Cmd+[/] keybinds for session history navigation (#12880) 2026-02-09 19:25:42 -06:00
opencode-agent[bot] 3e8eed1296 chore: generate 2026-02-10 00:13:01 +00:00
Adam 5414556d62 fix(docs): translations 2026-02-09 18:11:59 -06:00
Ryan Vogel b2dc228898 feat: integrate vouch & stricter issue trust management system (#12640) 2026-02-09 18:15:06 -05:00
Dax Raad c79bf18fdf ci: sort beta PRs by number for consistent display order 2026-02-09 18:00:44 -05:00
Marcio 43f53bc901 fix(tui): use sender color for queued messages (#12832) 2026-02-09 16:54:12 -06:00
Surma e11f6db029 fix: publish session.error event for invalid model selection (#8451) 2026-02-09 16:27:48 -06:00
Luke Parker 852fc12a66 fix(desktop): correct module name for linux_display in main.rs (#12862) 2026-02-09 21:13:47 +00:00
Adam 9d89c4874a chore: more defensive 2026-02-09 14:12:23 -06:00
Adam 7dac84558d chore: cleanup 2026-02-09 14:02:14 -06:00
Adam d25eae59e5 fix(app): max widths 2026-02-09 13:59:26 -06:00
opencode-agent[bot] d7f2b1fe89 chore: generate 2026-02-09 19:57:32 +00:00
Adam b0b02dbf01 fix(docs): locale markdown issues 2026-02-09 13:55:55 -06:00
opencode-agent[bot] 6a2c5d3eaa chore: generate 2026-02-09 19:27:52 +00:00
Adam 18be23211a fix: locale routing 2026-02-09 13:26:50 -06:00
Bryce Ryan 8efdd9802b fix(opencode): ACP File write should create the file if it doesn't exist (#12854) 2026-02-09 12:56:34 -06:00
Adam a47e21fc39 fix: locale routing 2026-02-09 12:52:06 -06:00
Adam e4f8424f7e fix(docs): dev docs links 2026-02-09 12:35:46 -06:00
Adam f87081f6b2 chore: cleanup 2026-02-09 12:20:09 -06:00
Adam 3838c87f86 fix(app): incorrect workspace on new session 2026-02-09 12:19:04 -06:00
opencode-agent[bot] 7504aa4048 chore: update nix node_modules hashes 2026-02-09 17:44:51 +00:00
opencode-agent[bot] 64751bda7b chore: update nix node_modules hashes 2026-02-09 17:37:37 +00:00
opencode-agent[bot] 5a3cfe02e5 chore: generate 2026-02-09 17:36:43 +00:00
opencode-agent[bot] 8be8e366c9 chore: generate 2026-02-09 17:35:30 +00:00
Adam f97a3650ef wip(docs): i18n (#12681) 2026-02-09 11:34:35 -06:00
Aiden Cline 910b56442a test: fix failing prompt test (#12847) 2026-02-09 11:25:22 -06:00
Jérôme Benoit 262349d096 fix(nix): restore install script in fileset for desktop build (#12842) 2026-02-09 11:23:34 -06:00
Aiden Cline 8477394f3a tweak: adjust agent variant logic to not require exact match on model, and instead check if the variant is available for model (#12838) 2026-02-09 11:00:06 -06:00
Jérôme Benoit b33b478819 fix(nix): watch scripts in nix-hashes workflow (#12818) 2026-02-09 10:13:25 -06:00
Aiden Cline af4d616766 Revert "feat(tui): restore footer to session view (#12245)" (#12836) 2026-02-09 10:08:26 -06:00
Adam 2c727bda80 chore: cleanup 2026-02-09 09:58:55 -06:00
Joseph Campuzano 56b32df400 fix(app): make keyboard focus visible in settings (#12612) 2026-02-09 09:12:06 -06:00
Adam b267824e01 fix(app): tooltip children focus loop 2026-02-09 07:43:36 -06:00
Adam 9ea74541fd chore: cleanup 2026-02-09 07:41:17 -06:00
Adam 9da2aeeafb chore: cleanup 2026-02-09 07:38:06 -06:00
Adam 48ccb048e9 chore(app): cleanup 2026-02-09 07:38:06 -06:00
Adam 87f58d4a12 fix(app): update tab file contents on change 2026-02-09 07:38:06 -06:00
Adam 4ad8646813 feat(app): polish Open in icon treatment
Bring in the Open in button-group and transparent icon updates from #12641 while keeping locale strings unchanged. Replace CSS inversion with dedicated light/dark Zed icon assets for cleaner theme handling.

Co-authored-by: Edin <86423329+edoedac0@users.noreply.github.com>
2026-02-09 07:38:05 -06:00
Anton Volkov d8fd979fa1 chore: remove unused themes (#12753) 2026-02-09 13:33:00 +00:00
Adam db97f94fe3 fix: add fallback for when crypto.randomUUID is unavailable
Closes #11452

Co-authored-by: Yo'av Moshe <bjesus@users.noreply.github.com>
2026-02-09 06:35:53 -06:00
Kit Langton 65652efdf8 fix(app): allow creating sessions on touch devices (#12765) 2026-02-09 05:55:54 -06:00
Israel Araújo de Oliveira 965dbbcd22 feat(desktop): add native Wayland toggle on Linux (#11971)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-09 17:00:35 +08:00
Brendan Allan a68e22c8c8 app: include sandboxes in project unseen/error notifs 2026-02-09 16:51:04 +08:00
OpeOginni 71507aac29 feat(desktop): persist currentSidecarUrl in state when isSidecar prop is true (#12792) 2026-02-09 07:56:26 +00:00
opencode-agent[bot] 1e04901943 chore: generate 2026-02-09 07:36:24 +00:00
Brendan Allan a8dbf45da7 desktop: track currentSidecarUrl 2026-02-09 15:34:52 +08:00
Silvio Ney d836e0307e feat: exclude devtools from production builds (#12290)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-09 07:30:15 +00:00
opencode-agent[bot] 5a6f9dc65b chore: generate 2026-02-09 07:18:48 +00:00
OpeOginni d30b41d6c6 feat(desktop): add isSidecar prop to AppInterface and logic to persist sidecar server urls (#12366)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-09 07:18:06 +00:00
opencode-agent[bot] 4f61bb45c4 chore: generate 2026-02-09 05:54:56 +00:00
Aiden Cline 671f90a945 tweak: add new ContextOverflowError type (#12777) 2026-02-08 23:54:01 -06:00
fgonzalezurriola 5f70c465ea fix(provider): remove obsolete copilot model enablement instructions (#12739) 2026-02-08 22:43:52 -06:00
AdJIa e8c3d0c705 fix: enable thinking for all reasoning models on alibaba-cn (DashScope) (#12772)
Co-authored-by: lujia <lujiax@zmn.cn>
2026-02-08 22:42:23 -06:00
Aiden Cline e4f069ea64 fix: parse mid stream openai responses style errors to prevent infinite retries for errors that should STOP execution (#12768) 2026-02-08 20:55:41 -06:00
Jérôme Benoit 6e48bafc5c refactor(nix): use native Bun APIs and propagate errors (#12694) 2026-02-08 19:43:10 -06:00
Alex Yaroshuk bc9afbea06 fix(app): localize "close tab" in command pallete (#12756) 2026-02-08 19:14:48 -06:00
Adam 0ae3eb8bf3 chore: cleanup 2026-02-08 16:23:24 -06:00
Adam 017e536164 fix(console): zen workspace translation cleanup 2026-02-08 16:22:39 -06:00
Adam 002bf07988 chore: cleanup 2026-02-08 16:19:13 -06:00
Kit Langton 40bc891434 fix(layout): improve workspace header truncation and item interaction (#12655) 2026-02-08 18:42:04 +00:00
opencode-agent[bot] 7be991a2eb chore: generate 2026-02-08 17:38:44 +00:00
Kit Langton 4a81e19f88 ui: default TextField copy affordance to clipboard (#12714) 2026-02-08 11:37:59 -06:00
Frank f7c8b6119f wip: zen 2026-02-08 10:31:09 -05:00
Adam 0c8b1ec574 fix(ui): context stale in prompt input (#12695) 2026-02-08 12:36:53 +00:00
opencode-agent[bot] c153ba4b59 chore: update nix node_modules hashes 2026-02-08 11:33:28 +00:00
opencode-agent[bot] 87d3561f94 chore: generate 2026-02-08 11:27:37 +00:00
Kit Langton e132f7ab86 refine(app): tighten slash autocomplete matching (#12647) 2026-02-08 05:26:59 -06:00
Devin Griffin ace89c3be0 fix(app): Toast when session is missing on prompt-submit (#12654) 2026-02-08 05:26:31 -06:00
Abdul Rahman ArM 22cb276f96 fix(desktop): add native clipboard image paste and fix text paste (#12682) 2026-02-08 11:24:09 +00:00
Adam c5df9f7a73 chore: refactoring and tests (#12629) 2026-02-08 05:02:19 -06:00
Caleb Norton 62ceaf966c feat(nix): expose overlay for downstream use (#12643) 2026-02-07 23:56:19 -06:00
Caleb Norton e7138a9d2f feat(nix): disable build time models.dev fetching (#12644) 2026-02-07 23:55:56 -06:00
Frank ebdc8b0bb7 wip: zen 2026-02-07 23:25:55 -05:00
opencode-agent[bot] b13d8c3cfd chore: update nix node_modules hashes 2026-02-08 01:34:30 +00:00
Ryan Miville 38a88d2534 fix(app): respect terminal toggle keybind when terminal is focused (#12635) 2026-02-07 19:26:54 -06:00
Caleb Norton 9b109a3fcb fix: revert "feat(app): add web input focus shortcut (#12493)" (#12639) 2026-02-07 19:26:10 -06:00
opencode-agent[bot] a687df5d1b chore: generate 2026-02-07 19:36:27 +00:00
ChangeHow eba3d3a57e feat(app): add web input focus shortcut (#12493) 2026-02-07 13:35:46 -06:00
Kit Langton 8f9106640d fix(web): keep /share available to copy existing link (#12533) 2026-02-07 13:35:15 -06:00
Devin Griffin c64446c2c7 fix(app): disable 3 safari prompt-input annoyances (#12558) 2026-02-07 13:33:54 -06:00
Devin Griffin e8d55200a5 feat(app): drag-n-drop to @mention file (#12569) 2026-02-07 13:33:00 -06:00
Khang Ha (Kelvin) 6d0afbbe2d fix(app): handle Windows paths in frontend file URL encoding (#12601) 2026-02-07 13:27:40 -06:00
Kit Langton 599ab6d294 fix(app): move workspace New session into header (#12624) 2026-02-07 13:21:50 -06:00
Frank bf58824e2f wip: zen 2026-02-07 13:32:23 -05:00
Caleb Norton 421e3baef9 chore: update flake.lock (#11919)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-02-07 10:27:04 -06:00
opencode-agent[bot] 3b0a0a829e chore: generate 2026-02-07 14:41:20 +00:00
Ariane Emory c8d828e63d docs(cli): add documentation for --fork flag (#12561) 2026-02-07 08:40:36 -06:00
opencode-agent[bot] e3b9c81ead chore: generate 2026-02-07 11:03:20 +00:00
Rahul A Mistry 68e7c7bb44 fix(app): toggle file tree and review panel better ux (#12481) 2026-02-07 05:02:40 -06:00
opencode-agent[bot] 04dd99d38d chore: update nix node_modules hashes 2026-02-06 23:54:41 +00:00
opencode-agent[bot] 53103458df chore: generate 2026-02-06 23:47:25 +00:00
Tommy D. Rossi 602a505183 feat: add models.dev schema ref for model autocomplete in opencode.json (#12528) 2026-02-06 17:46:31 -06:00
opencode-agent[bot] e0306eb517 chore: generate 2026-02-06 22:34:23 +00:00
Shantur Rathore d865ac0b4e feat(core): Set variant in assistant messages too (#12531)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-02-06 16:33:47 -06:00
Dax 2b77032343 feat(skill): add skill discovery from URLs via well-known RFC (#12423)
Co-authored-by: Frank <frank@anoma.ly>
2026-02-06 17:31:40 -05:00
opencode-agent[bot] c8f15113a4 chore: update nix node_modules hashes 2026-02-06 22:27:14 +00:00
Maharshi Patel 37d3db3254 fix(opencode): cleanup orphaned worktree directories (#12399) 2026-02-06 16:18:03 -06:00
Khang Ha (Kelvin) c908bc5c51 fix: properly encode file URLs with special characters (#12424) 2026-02-06 16:16:56 -06:00
Abdi Ibrahim 9db2bd3fc4 fix(desktop): allow agent select to use full width on windows (#12428) 2026-02-06 16:15:04 -06:00
opencode-agent[bot] fa09236044 chore: generate 2026-02-06 22:14:34 +00:00
Aiden Cline df95bb4d37 chore: upgrade bun to 1.3.8 (#11892) 2026-02-06 16:13:48 -06:00
Ganesh 221dad417b fix(app): keep startup script field scrollable in edit project dialog (#12431) 2026-02-06 16:13:37 -06:00
Goni Zahavy 3b5c2de804 fix(opencode): SessionPrompt.shell() now triggers loop if messages are queued (#10987) 2026-02-06 16:13:11 -06:00
Daniel Polito 638bdccc0a feat(desktop): Session Review Images (#12360) 2026-02-06 15:01:40 -06:00
OpeOginni 5d274642a5 fix(docs-windows-wsl): update caution note for server security (#12467) 2026-02-06 13:46:32 -06:00
Adam 52c09a8542 fix(ui): markdown table rendering 2026-02-06 13:45:48 -06:00
Ivan Gonzalez 2d2ca2c251 feat(opencode): use reasoning summary auto for gpt-5 models that are not chat (#12502) 2026-02-06 13:19:14 -06:00
opencode-agent[bot] bf819f3636 chore: generate 2026-02-06 19:16:36 +00:00
Ariane Emory dea36da633 feat(tui): add Claude Code-style --fork flag to duplicate sessions before continuing (resolves #11137) (#11340) 2026-02-06 13:15:47 -06:00
Frank 0a6227a96a zen: handle 1m context 2026-02-06 13:57:26 -05:00
Frank 8982e4a346 wip: zen 2026-02-06 13:57:25 -05:00
Adam d444d3df4c fix: locale routing 2026-02-06 12:39:41 -06:00
Adam c13f881353 fix: auth routing 2026-02-06 12:20:50 -06:00
Adam b264c75743 fix: auth routing 2026-02-06 12:11:02 -06:00
Adam 4c0a719d0b fix(app): more terminal replay issues 2026-02-06 12:10:05 -06:00
Mariam Jabara 1371fae11d feat: add specific system prompt for Trinity model (#12144) 2026-02-06 11:43:10 -06:00
Adam bffe384a70 feat(www): locale specific urls (#12508) 2026-02-06 11:30:40 -06:00
OpeOginni 303d497f63 feat(desktop): added Macos support for displaying only installed editors & added sublime text editor (#12501) 2026-02-06 17:21:47 +00:00
opencode-agent[bot] fcabcac26e chore: generate 2026-02-06 17:19:57 +00:00
Nacho F. Lizaur d9ee4b4432 fix(opencode): correct prefix selection for amazon-bedrock provider in getSmallModel (#12281) 2026-02-06 11:18:57 -06:00
Adam f52cfa74b3 fix(app): don't rely on metadata.summary in task tool render (#12497) 2026-02-06 10:54:54 -06:00
opencode-agent[bot] f534d71114 chore: update nix node_modules hashes 2026-02-06 16:32:48 +00:00
Ariane Emory d4c50e5a2d feat(tui): restore footer to session view (#12245) 2026-02-06 10:21:16 -06:00
Matt Silverlock e7cd1281a2 fix(github): handle step-start/step-finish parts in extractResponseText (#12470) 2026-02-06 10:20:33 -06:00
Vladimir Glafirov f0b2f3bb43 chore(deps): bump @gitlab/gitlab-ai-provider to 3.5.0 (#12496) 2026-02-06 10:16:15 -06:00
cooooooooooode 2c4e92d7ee fix: add directory parameter to plugin client for multi-project support (#11344)
Co-authored-by: cooooooooooode <187372691+cooooooooooode@users.noreply.github.com>
2026-02-06 10:15:35 -06:00
Adam 4aae0aeabd chore: refactoring and tests, splitting up files (#12495) 2026-02-06 10:02:31 -06:00
Adam cafb42e548 chore: refactoring and tests (#12468) 2026-02-06 09:37:49 -06:00
Yuvraj Virk af7328fdea fix: correct /data API usage and data format for importing share URLs (#7381) 2026-02-06 15:29:10 +00:00
opencode-agent[bot] b8f9c2013f chore: update nix node_modules hashes 2026-02-06 15:12:29 +00:00
Adam 9d548c9e25 fix(app): remove extra error page and use default error boundary 2026-02-06 09:03:45 -06:00
Brendan Allan 20202f9ca4 desktop: add loading window and restructure rust (#12176) 2026-02-06 23:03:07 +08:00
opencode-agent[bot] 3a950e9a44 chore: generate 2026-02-06 14:55:37 +00:00
Adam 465df7ceeb feat(web): i18n (#12471) 2026-02-06 08:54:51 -06:00
Adam b0ba9ee6ed fix(app): hide 'open in app' button on narrow viewports 2026-02-06 07:10:23 -06:00
Adam 475871103d fix(www): z icon color 2026-02-06 06:28:11 -06:00
opencode-agent[bot] 03621020cf chore: generate 2026-02-06 12:20:53 +00:00
OpeOginni 78be69512d fix(desktop): update server removal logic to clear default server URL if removed (#12372) 2026-02-06 06:20:03 -06:00
Alex Yaroshuk 39a48c3579 feat(app): session last updated time display in command pallete's search (#12376) 2026-02-06 06:19:25 -06:00
Maharshi Patel 696bbc6250 fix(ui): add Windows File Explorer icon for session header (#12386) 2026-02-06 06:18:45 -06:00
Cameron 4832165567 fix(desktop): support desktop titlebar double-click maximize (#12459) 2026-02-06 20:13:24 +08:00
Adam af94fa2c38 ignore: refactoring and tests (#12460) 2026-02-06 05:51:01 -06:00
Adam 8225078c17 fix(app): retry error unwrapping (#12462) 2026-02-06 05:50:38 -06:00
Adam 05ee14e6ca chore: cleanup 2026-02-06 05:35:46 -06:00
opencode-agent[bot] 782a9167fa chore: generate 2026-02-06 10:20:52 +00:00
Brendan Allan ea1d861e6c desktop: add key accelerators to menu itms 2026-02-06 18:20:04 +08:00
opencode-agent[bot] f836c67de3 chore: generate 2026-02-06 10:11:44 +00:00
Brendan Allan 0e636c6d68 desktop: add more basic menu bar items 2026-02-06 18:10:48 +08:00
Devin Griffin 483b990f1e fix(app): always show project menu button for mobile a11y (#11258)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-02-06 07:46:21 +00:00
Brendan Allan 32b19d6c05 desktop: maximize main window by default (#12433) 2026-02-06 07:30:13 +00:00
opencode-agent[bot] 08619ee6fc chore: generate 2026-02-06 05:37:00 +00:00
Dax Raad ab49fa8c9c tui: parallelize skill downloads for faster loading
Refactored skill discovery to download skills in parallel instead of sequentially,

reducing load times when multiple skills need to be fetched from remote URLs.

Also updated AGENTS.md with guidance on using dev branch for diffs.
2026-02-06 00:36:11 -05:00
Dax Raad 3ae16bccba feat(skill): add skill discovery from URLs via well-known RFC
Implement the Agent Skills Discovery RFC to allow fetching skills from URLs:
- Add 'urls' field to config.skills for specifying skill registry URLs
- Create Discovery namespace in skill/discovery.ts with pull() function
- Download skills from /.well-known/skills/index.json endpoints
- Cache downloaded skills to ~/.cache/opencode/skills/
- Skip re-downloading existing files for efficiency

Users can now configure:
{
  "skills": {
    "urls": ["https://example.com/.well-known/skills/"]
  }
}

Implements: https://github.com/cloudflare/agent-skills-discovery-rfc
2026-02-06 00:36:11 -05:00
Frank 8c7d153ef6 zen: fix usage graph 2026-02-06 00:22:09 -05:00
Luke Parker 0d493241fe chore: align windows test runner to blacksmith (#12364) 2026-02-05 20:15:34 -06:00
Akshar Patel af5a9dfcac feat(tui): highlight esc label on hover in dialog (#12383) 2026-02-05 20:11:08 -06:00
Hank Stoever 965c8bb8aa fix(config): handle $ character with {file:} pattern (#12390)
Co-authored-by: Hank Stoever <1109058+hstove@users.noreply.github.com>
2026-02-05 20:10:35 -06:00
Dax Raad 09733f821e increase skill dialog width 2026-02-05 18:38:31 -05:00
Andrew Pashynnyk 3060f17d85 fix(opencode): improve skills dialog readability (#12356) 2026-02-05 18:35:35 -05:00
opencode 49ac7c2e32 release: v1.1.53 2026-02-05 23:08:56 +00:00
Aiden Cline 9e740edd7d chore: make dax shut up (#12375) 2026-02-05 16:51:39 -06:00
Dax Raad a3c06bc6d6 senior engineer senior engineering 2026-02-05 17:51:12 -05:00
Aiden Cline b572658a40 tweak: load user plugins after builtin (#12373) 2026-02-05 22:49:48 +00:00
Aiden Cline a2f84ba5f7 chore: rm hardcoded conditional that was used for testing (#12371) 2026-02-05 16:31:52 -06:00
Dax 5637144bf2 core: fix unhandled errors when aborting with queued messages (#12369) 2026-02-05 22:27:27 +00:00
Akshar Patel 8c55f05069 fix(tui): allow mouse escape via "esc" labels in dialogs (#11421) 2026-02-05 16:14:49 -06:00
Rafi Khardalian ed6f043bf2 fix(plugin): user plugins override built-in plugins for same provider (#12361) 2026-02-05 16:04:02 -06:00
opencode-agent[bot] e01c1cf00d chore: generate 2026-02-05 21:56:26 +00:00
Ariane Emory 0cd4593a73 docs: websearch tool (#12359) 2026-02-05 15:55:50 -06:00
Edin 04f147a77a docs: add Bosnian README translation (#12341) 2026-02-05 15:25:40 -06:00
Alex Yaroshuk 86ab20d3c3 fix(app): make close comment button visible in prompt input (#12349) 2026-02-05 15:23:29 -06:00
Aiden Cline 3c7bfff5d0 tweak: move codex 5.3 model definition to the plugin so that models.dev doesnt have to show unsupported model for others (#12344) 2026-02-05 20:53:26 +00:00
Adam f5aa1a792c fix(app): hide prompt input when there are perms or questions (#12339) 2026-02-05 14:42:56 -06:00
opencode-agent[bot] 1331cbce2b chore: update nix node_modules hashes 2026-02-05 20:31:07 +00:00
Adam b8c23fce71 fix(app): more terminal stability fixes 2026-02-05 14:21:13 -06:00
Adam 93381f84a0 fix(app): modified file color contrast 2026-02-05 14:21:13 -06:00
Adam cc4385b6c1 fix(ui): toast overflow 2026-02-05 14:21:12 -06:00
opencode-agent[bot] d4a464920b chore: generate 2026-02-05 19:56:54 +00:00
Adam 67e093f47b feat(app): open in <app> button (#12322) 2026-02-05 19:55:49 +00:00
Adam a2cd8bc29c fix(app): allow toggling file tree closed independently (#12293) 2026-02-05 13:51:08 -06:00
Daniel Polito 517d25c2cc feat(desktop): Stop Showing SessionSkeleton on New Workspace (#12209) 2026-02-05 13:46:44 -06:00
Daniel Polito df708df551 feat(desktop): Set Workspace Name Earlier to Improve Creation / Deletion (#12213) 2026-02-05 13:38:31 -06:00
opencode-agent[bot] 338bbb169b chore: update nix node_modules hashes 2026-02-05 19:09:43 +00:00
Steffen Deusch 32b17af0a6 feat(acp): add session usage (#12299)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-02-05 12:58:09 -06:00
Aiden Cline 5ad5a59f8e tweak: update transforms for gpt-5.3 (#12325) 2026-02-05 12:49:40 -06:00
opencode 0cbe428c5e release: v1.1.52 2026-02-05 18:48:54 +00:00
Dax Raad e7662f9889 enable 5.3 codex 2026-02-05 13:23:58 -05:00
Goni Zahavy 26bcf888c4 feat(tui): add running spinner to bash tool in TUI (#12317) 2026-02-05 12:23:02 -06:00
Frank fb86aa435b zen: opus 4.6 2026-02-05 13:01:50 -05:00
Dax Raad 9689a1e058 core: silently ignore proxy command failures to prevent config initialization crashes 2026-02-05 12:47:32 -05:00
Aiden Cline 2c92dcc8ab fix: ensure that github copilot plugin properly sets headers when used in other clients than tui (#12316) 2026-02-05 11:40:29 -06:00
opencode-agent[bot] c1936404f8 chore: update nix node_modules hashes 2026-02-05 16:45:41 +00:00
Dax Raad 61ca69317a core: bundle GitLab auth plugin directly instead of dynamic install
Remove dynamic installation of built-in plugins. GitLab auth is now imported

directly as an internal plugin, eliminating network requests during startup

and simplifying the plugin loading logic.

Removes the need for test mocks since plugins are no longer

dynamically installed at runtime.
2026-02-05 11:29:00 -05:00
Dax Raad e776077bac ci: ensure config test waits for dependencies to complete installation 2026-02-05 10:13:52 -05:00
Dax Raad f4db8e4808 core: fix plugin installation to use direct package.json manipulation instead of bun add
This ensures plugins install more reliably by writing dependencies directly
to package.json rather than relying on bun add commands which can fail
in certain environments. Also adds a small delay to ensure filesystem
operations complete before proceeding.
2026-02-05 10:07:59 -05:00
Adam 3168bc8368 chore: cleanup 2026-02-05 07:21:50 -06:00
Adam f8642dadde fix(app): file changes not always available 2026-02-05 07:04:44 -06:00
Adam 5f099da732 fix(app): file tree out of sync 2026-02-05 07:04:43 -06:00
Adam 70bb105f6d chore: package.json scripts 2026-02-05 07:04:34 -06:00
Adam 806b63c7fe chore: package.json scripts 2026-02-05 07:03:29 -06:00
Adam 9a25b5afdd fix(app): copy buttons smaller and out of the way 2026-02-05 07:03:29 -06:00
Edin ae67729347 feat(i18n): add Bosnian locale (#12283) 2026-02-05 06:58:14 -06:00
Adam 7d3bda54bd fix(app): terminal URL issues 2026-02-05 06:20:19 -06:00
Adam d25f961028 chore: cleanup 2026-02-05 06:04:28 -06:00
Brendan Allan 385483b6ce app: remove extra x padding around prompt input on mobile 2026-02-05 16:42:05 +08:00
opencode-agent[bot] 7786d54014 chore: update nix node_modules hashes 2026-02-05 08:00:44 +00:00
Zhiming Guo 8749e2d2a0 fix(opencode): Fixes image reading with OpenAI-compatible providers like Kimi K2.5. (#11323)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-02-05 01:54:07 -06:00
Aiden Cline b4cbab94e7 fix: downgrade xai ai-sdk package due to errors (#12251) 2026-02-05 01:50:05 -06:00
Aiden Cline 33e4898811 Revert: feat: add models.dev schema ref for model autocomplete in ope… (#12242) 2026-02-05 00:31:08 -06:00
opencode-agent[bot] a16a804ae9 chore: generate 2026-02-05 06:08:54 +00:00
Tommy D. Rossi 5d1d1e0a5d feat: add models.dev schema ref for model autocomplete in opencode.json (#12112) 2026-02-05 00:08:08 -06:00
Aiden Cline a1165cc483 tweak: adjust task tool description/input to alleviate tool call failures that sometimes occured w/ gpt models (#12214) 2026-02-05 00:04:03 -06:00
Dax 9235ad6196 fix: wait for dependencies before loading custom tools and plugins (#12227) 2026-02-05 03:25:43 +00:00
Frank c5039b266f wip: zen 2026-02-04 20:56:38 -05:00
Frank eaf20b6823 zen: set session affinity header 2026-02-04 20:56:37 -05:00
Filip d29899cfa7 fix(app): refresh workspace sessions on project switch (#12189) 2026-02-04 19:33:34 -06:00
opencode-agent[bot] 26ed35248a chore: generate 2026-02-05 00:30:42 +00:00
Ariane Emory 1fb442dd7e feat: Allow the function to hide or show thinking blocks to be bound to a key (resolves #12168) (#12171) 2026-02-04 18:29:46 -06:00
Maharshi Patel 643bae3d2f fix(tui): add hover states to question tool tabs (#12203) 2026-02-04 18:28:22 -06:00
Adam 1d8299d322 fix(app): terminal url 2026-02-04 15:43:13 -06:00
Shantur Rathore 294b72e0ea fix(core): skip dependency install in read-only config dirs (#12128) 2026-02-04 14:45:59 -06:00
Adam 04b55b891f fix(app): terminal EOL issues 2026-02-04 14:36:10 -06:00
Parker Smith e03a2acad8 fix(desktop): Refresh file contents when changing workspaces to not have stale contents (#11728) 2026-02-04 14:27:02 -06:00
Dave Dennis 89c7caf24a fix(app): derive terminal WebSocket URL from browser origin instead o… (#12178) 2026-02-04 14:22:50 -06:00
Adam e71fc36134 fix(app): last turn changes rendered in review pane (#12182) 2026-02-04 19:50:56 +00:00
Adam df62ea05ab fix(app): safety triangle for sidebar hover (#12179) 2026-02-04 19:25:55 +00:00
Tom eead871f38 fix: ensure kimi-for-coding plan has thinking on by default for k2p5 (#12147) 2026-02-04 12:39:22 -06:00
Aiden Cline 5c64166f30 fix: cloudflare workers ai provider (#12157) 2026-02-04 18:07:33 +00:00
Aiden Cline 329b41ce95 fix: ensure that plugin installs use --no-cache when using http proxy to prevent random hangs (see bun issue) (#12161) 2026-02-04 12:01:00 -06:00
Adam ca27bd6a29 fix(app): clear comments on prompt submission (#12148) 2026-02-04 11:19:03 -06:00
Adam e3bdc76f0d test(app): fix e2e test action 2026-02-04 11:03:54 -06:00
Adam 1818e4fe69 fix(app): terminal hyperlink clicks 2026-02-04 10:54:55 -06:00
Adam 69827b0d51 fix(ui): review comments z-index stacking 2026-02-04 10:35:11 -06:00
Adam a5e1b2f362 test(app): fix e2e test action 2026-02-04 10:28:24 -06:00
Adam 16e1b2bca9 fix(core): session errors when attachment file not found 2026-02-04 10:21:03 -06:00
Adam d440c54c35 test(app): fix dated e2e tests 2026-02-04 10:10:28 -06:00
Adam 2708502eef fix(app): don't show scroll-to-bottom unecessarily 2026-02-04 10:01:00 -06:00
Dax Raad 8e01ac9a18 ci: remove source-based AUR package from publish script
Simplifies the release process by publishing only the binary package to AUR,

eliminating the need to maintain separate source and binary build configurations.
2026-02-04 10:31:21 -05:00
Adam 5f7208c5a3 fix(app): file tree not staying in sync across projects/sessions 2026-02-04 07:59:46 -06:00
Adam f37f31517f fix(app): move session options to the session page 2026-02-04 07:12:19 -06:00
Adam 2e41c0c192 wip(app): session options 2026-02-04 07:12:18 -06:00
Adam 909983991b fix(app): opened tabs follow created session 2026-02-04 06:22:55 -06:00
opencode-agent[bot] 66e178844b chore: generate 2026-02-04 11:35:05 +00:00
Goni Zahavy d07053157f fix(desktop): removed compression from rpm bundle to save 15m in CI (#12097) 2026-02-04 05:34:18 -06:00
opencode-agent[bot] 426e432d8e chore: update nix node_modules hashes 2026-02-04 11:12:01 +00:00
Lucas (TaeYoung) Jo 78fd919fa6 fix(terminal): support remote server connections and fix GLIBC compatibility (#11906) 2026-02-04 05:01:10 -06:00
opencode d3db87653c release: v1.1.51 2026-02-04 07:06:48 +00:00
Sebastian 59660f5a72 restore direct osc52 (#12071) 2026-02-04 00:35:57 -06:00
Aiden Cline 5d5f0b95f9 fix: revert change that caused headers to be double merged if provider was authed in multiple places (#12072) 2026-02-04 06:30:32 +00:00
Gary Chu 4cda85d2eb docs: add agent-compatible paths to skills documentation (#12067) 2026-02-04 00:12:24 -06:00
Aiden Cline 28b0462384 docs: document the built in agents (#12066) 2026-02-04 00:00:05 -06:00
Sergiy Dybskiy 25e9b34b1a fix: prevent double-prefixing of Bedrock cross-region inference models (#12056) 2026-02-03 23:06:17 -06:00
Devin Griffin d07930b92d fix(app): tighten up session padding-top for mobile (#11247)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-02-04 12:10:56 +08:00
Brendan Allan b1a71f5bf1 chore: add brendonovich as rust codeowner 2026-02-04 10:40:08 +08:00
Brendan Allan 017c19c486 ci: use numeric release id instead of gql one 2026-02-04 10:39:17 +08:00
Łukasz Gładysz cd4ab06096 fix: prioritize OPENCODE_CONFIG_DIR for AGENTS.md (#11536) 2026-02-04 01:35:22 +00:00
opencode 389f530e93 release: v1.1.50 2026-02-04 01:35:15 +00:00
Max Leiter d2ded09bf6 fix: prevent memory leaks from AbortController closures (#12024) 2026-02-03 17:51:26 -06:00
Filip 97fca89a6d refactor(e2e): faster tests (#12021) 2026-02-03 17:45:49 -06:00
Aiden Cline be3850adf7 Revert "feat: add Trinity model system prompt support" (#12029) 2026-02-03 16:39:03 -06:00
Aiden Cline 8abe272175 chore: adjust way skill dirs are whitelisted (#12026) 2026-02-03 16:33:36 -06:00
Mariam Jabara cba69f683a feat: add Trinity model system prompt support (#12025) 2026-02-03 16:33:27 -06:00
opencode-agent[bot] 21d11c31a8 chore: update nix node_modules hashes 2026-02-03 21:28:16 +00:00
opencode-agent[bot] 3d986fa55e chore: generate 2026-02-03 21:22:33 +00:00
Sayeed Mahmoud ac36adcdc5 docs: fix grammar and formatting in README (#11985) 2026-02-03 15:21:51 -06:00
Tomáš Linhart 9c9133271d docs: fix logging example for plugin (#11989) 2026-02-03 15:21:15 -06:00
Tyler Gannon 1f2805e8b8 feat(plugin): add shell.env hook for manipulating environment in tools and shell (#12012) 2026-02-03 15:18:41 -06:00
Matt Silverlock d7918636ea fix(opencode): use official ai-gateway-provider package for Cloudflare AI Gateway (#12014) 2026-02-03 15:18:28 -06:00
opencode-agent[bot] d4d749cc0d chore: generate 2026-02-03 21:10:08 +00:00
Idris Gadi a14fecd516 feat(tui): allow theme colors in agent customization (#11444) 2026-02-03 15:09:21 -06:00
Idris Gadi a260bc05ee docs: add missing environmental flags to the list (#11146) 2026-02-03 15:07:40 -06:00
Dax 1b4e946537 feat: add support for reading skills from .agents/skills directories (#11842)
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
2026-02-03 15:51:54 -05:00
Dax 5634443181 cli: add --thinking flag to show reasoning blocks in run command (#12013) 2026-02-03 20:49:15 +00:00
MartinWie c348fae448 fix: always fall back to native clipboard after OSC52 (#11994) 2026-02-03 20:53:04 +01:00
opencode-agent[bot] 7db8fd95ca chore: generate 2026-02-03 19:10:44 +00:00
Cloudyan 1bf27c231a fix: provider headers from config not applied to fetch requests (#11788) 2026-02-03 13:09:07 -06:00
Aiden Cline aad6f257fa fix: ensure mcp tools are sanitized (#11984) 2026-02-03 12:22:00 -06:00
Adam d173f85f6f fix(app): command palette placeholder text 2026-02-03 11:58:58 -06:00
Adam d43739afd5 fix(app): model selector truncating too soon 2026-02-03 11:51:30 -06:00
Alex 3cb153d1d6 feat: Add .slnx to C#/F# LSP root detection (#11928) 2026-02-03 11:46:04 -06:00
Adam a077838ce7 chore: gitignore codex 2026-02-03 11:44:03 -06:00
Adam 2a51117662 fix(app): spacing 2026-02-03 10:27:53 -06:00
Adam e5212a3a5e chore: cleanup 2026-02-03 10:27:53 -06:00
Aiden Cline 25014b04e7 test: add test for new skill improvements (#11969) 2026-02-03 16:05:10 +00:00
Aiden Cline 119522129d feat: improve skills, better prompting, fix permission asks after invoking skills, ensure agent knows where scripts/resources are (#11737) 2026-02-03 09:58:31 -06:00
neavo 5ee962dcd5 fix: exclude k2p5 from reasoning variants (#11918) 2026-02-03 09:26:37 -06:00
Muhammad Mugni Hadi 70efbca7e7 fix: handle nested array items for Gemini schema validation (#11952) 2026-02-03 09:24:52 -06:00
Daniel Polito caad4f1913 feat(desktop): Allow empty prompt with review comments (#11953) 2026-02-03 09:06:02 -06:00
Luiz Guilherme D'Abruzzo Pereira 6acf8ac3a3 docs: add --mdns-domain flag documentation (#11933) 2026-02-03 07:26:42 -06:00
Adam 53341204c1 fix(app): terminal serialization bug 2026-02-03 07:03:34 -06:00
Adam f19de6a4e5 fix(app): terminal serialization bug 2026-02-03 07:03:34 -06:00
Adam 7186ae1b1e chore(app): don't forceMount tooltips 2026-02-03 07:01:12 -06:00
Rahul A Mistry 340ddfa8c2 fix(app): session tabs to open the previous opened (#11914) 2026-02-03 06:19:56 -06:00
Rahul A Mistry 29bc50d35b fix(app): edit project dialog icon on hover (#11921) 2026-02-03 06:19:07 -06:00
Adam 09fcbfb715 fix(app): move session search to command palette 2026-02-03 05:19:43 -06:00
Devin Griffin 5bd46bfd26 fix(app): custom providers overflow (#11252)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-02-03 16:17:13 +08:00
Filip 129f6c1481 fix(core): plugins are always reinstalled (#9675) 2026-02-03 00:47:52 -06:00
opencode-agent[bot] 3b84de667a chore: generate 2026-02-03 05:19:12 +00:00
Frank 4c928559e6 wip: zen 2026-02-03 00:17:06 -05:00
Kiyoung Chang 2707d10956 fix(provider): strip properties/required from non-object types in Gemini schema (#11888) 2026-02-02 23:13:24 -06:00
Dax 122a765c48 cli: make run non-interactive (#11814) 2026-02-02 23:35:23 -05:00
Aiden Cline 57dc6aae59 ci: add license to npm (#11883) 2026-02-03 03:44:30 +00:00
opencode bba9f05705 release: v1.1.49 2026-02-03 03:44:18 +00:00
Filip 88e5183e91 fix(app): drag region for native controls (#11854) 2026-02-02 18:20:05 -06:00
Alex Yaroshuk f349cbc3a9 fix(app): session scroll bar color (#11857) 2026-02-02 17:01:47 -06:00
Aiden Cline 3377b19dd7 chore: cleanup external_dir perm logic (#11845) 2026-02-02 16:43:55 -06:00
opencode-agent[bot] c2e2a95dc8 chore: generate 2026-02-02 22:30:23 +00:00
Joseph Campuzano d83d0038a3 fix: fixes issue where the autocomplete tui dialog flickers while typing (#11641) 2026-02-02 16:29:43 -06:00
Aiden Cline 299e146376 chore: rm dead code (#11849) 2026-02-02 16:22:03 -06:00
Filip f26e4d5549 fix(app): prompt input overflow issue (#11840) 2026-02-02 22:02:56 +00:00
Adam 92022692c2 fix(app): sidebar losing projects on collapse 2026-02-02 15:54:04 -06:00
Aiden Cline 4389d43895 Revert pr that was mistakenly merged (#11844) 2026-02-02 15:28:02 -06:00
opencode-agent[bot] f994e131fc chore: generate 2026-02-02 20:53:36 +00:00
Luiz Guilherme D'Abruzzo Pereira 4d66e9db31 feat(server): add --mdns-domain flag to customize mDNS hostname (#11796) 2026-02-02 14:52:32 -06:00
Alex Yaroshuk 903399af34 feat(app): add workspace toggle command to command palette and prompt input (#11810) 2026-02-02 14:36:00 -06:00
opencode-agent[bot] 2a621d527e chore: generate 2026-02-02 20:27:15 +00:00
Adam ac0d8fd45f feat(app): search through sessions 2026-02-02 14:26:09 -06:00
Adam 623403f776 chore: cleanup 2026-02-02 14:24:25 -06:00
Adam 9e2a3f9764 chore: cleanup 2026-02-02 14:24:25 -06:00
Adam 38cfa48a23 feat(app): file search 2026-02-02 14:24:24 -06:00
Adam ef24195b9f Revert "feat(ui): Select, dropdown, popover styles & transitions (#11675)"
This reverts commit c1497f8066.
2026-02-02 14:24:24 -06:00
Adam 6e62457731 Revert "feat(ui): Smooth fading out on scroll, style fixes (#11683)"
This reverts commit 80c240da86.
2026-02-02 14:24:24 -06:00
Adam 829f3a348c fix(app): icon sizes 2026-02-02 14:24:24 -06:00
Adam 983fdcd3aa fix(app): added/deleted file status now correctly calculated 2026-02-02 14:24:23 -06:00
Adam 2f760dbf01 fix(app): navigate to last project on open 2026-02-02 14:24:23 -06:00
Adam 23db103f90 fix(app): button heights 2026-02-02 14:24:23 -06:00
Adam c1b78b065c fix(app): user messages not rendering consistently 2026-02-02 14:24:23 -06:00
Adam b8b096543a feat(app): project context menu on right-click 2026-02-02 14:24:22 -06:00
Filip 318ef49c65 fix(app): open project search (#11783) 2026-02-02 12:15:53 -06:00
Pablo P Varela 8a07d20063 fix(tui): respect terminal transparency in system theme (#8467) 2026-02-02 11:36:47 -06:00
opencode-agent[bot] 6c0c548f1b chore: update nix node_modules hashes 2026-02-02 16:56:31 +00:00
Vladimir Glafirov bda07aeec9 feat(provider): add User-Agent header for GitLab AI Gateway requests (#11818) 2026-02-02 10:41:02 -06:00
Goni Zahavy 1918f065ff ci: add ratelimits handling for close-stale-prs.yml (#11578) 2026-02-02 10:13:48 -06:00
Dax Raad 43ea8fd619 ci: use native ARM runner for faster Linux ARM builds
Switch from cross-compilation on x86_64 to native ARM runner, which improves build speed and reliability for Linux ARM binary distribution.
2026-02-02 10:06:21 -05:00
Dax Raad 58854a2198 ci: reduce aarch64 build runner to 4 vcpu to lower infrastructure costs 2026-02-02 09:44:19 -05:00
Dax Raad 74697081a9 ci 2026-02-02 09:37:39 -05:00
Dax Raad 061357e660 ci 2026-02-02 09:37:37 -05:00
Dax 48bf94acf7 tui: truncate session title in exit banner (#11797) 2026-02-02 14:30:10 +00:00
Rahul A Mistry 897cd50811 feat(app): add tab close keybind (#11780) 2026-02-02 06:50:06 -06:00
opencode-agent[bot] 7b76b29b52 chore: generate 2026-02-02 12:07:56 +00:00
OpeOginni 77d3b39466 feat(app): enhance responsive design with additional breakpoints for larger screen layout adjustments (#10459)
Co-authored-by: opencode <opencode@sst.dev>
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>
2026-02-02 06:06:45 -06:00
OpeOginni 540d2c9dd4 fix(desktop): added inverted svg for steps expanded for nice UX (#10462)
Co-authored-by: opencode <opencode@sst.dev>
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>
2026-02-02 06:03:55 -06:00
opencode-agent[bot] f8d19e1068 chore: generate 2026-02-02 12:02:35 +00:00
Ondřej Súkup 614763c3fa feat(opencode): ormolu code formatter for haskell (#10274) 2026-02-02 06:01:49 -06:00
Adam 93ba7f753a chore: update website stats 2026-02-02 05:42:55 -06:00
Sam Huckaby 8911466295 Fix(app): the Vesper theme's light mode (#9892) 2026-02-02 11:30:44 +00:00
Lucio Delelis b81ec01bf3 fix(ui): adjusts alignment of elements to prevent incomplete scroll (#11649) 2026-02-02 05:20:30 -06:00
Brendan Allan a173c0f985 feat(app): unread session navigation keybinds (#11750) 2026-02-02 05:05:47 -06:00
opencode-agent[bot] f2f565edc4 chore: generate 2026-02-02 10:53:46 +00:00
Brendan Allan 344d24625d desktop: fix rust build + bindings formatting 2026-02-02 18:52:57 +08:00
Brendan Allan 040f21fc5c fix(desktop): remove unnecessary setTimeout 2026-02-02 18:19:49 +08:00
Brendan Allan eb2d967ddb fix(desktop): throttle window state persistence (#11746) 2026-02-02 09:54:57 +00:00
opencode-agent[bot] b37cfca798 chore: generate 2026-02-02 07:58:52 +00:00
Brendan Allan f9e5e1c07c chore(desktop): integrate tauri-specta (#11740) 2026-02-02 07:58:08 +00:00
Brendan Allan 5ed4d0a64d fix(desktop): keep mac titlebar stable under zoom (#11747) 2026-02-02 07:54:25 +00:00
opencode-agent[bot] d5c7ba644c chore: update nix node_modules hashes 2026-02-02 07:21:25 +00:00
Sebastian 3abaec44a0 Use opentui OSC52 clipboard, again (#11744) 2026-02-02 08:12:50 +01:00
mohammad 6b087d3eb3 fix(desktop): kill zombie server process on startup timeout (#11602)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-02-02 13:30:46 +08:00
Frank 8213633069 zen: rate limit (#11735) 2026-02-02 00:29:52 -05:00
Jigar 4e4ce894ea fix: convert system message content to string for Copilot provider (#11600)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 23:28:28 -06:00
Dax fa0f567e08 tui: show exit message banner (#11733) 2026-02-02 00:13:47 -05:00
Dax Raad 04c76c9151 Revert "Use opentui OSC52 clipboard (#11718)"
This reverts commit 81bbd2eff0.
2026-02-02 00:13:02 -05:00
OpeOginni 7b1c46d811 fix(opencode): give OPENCODE_CONFIG_CONTENT proper priority for setting config based on docs (#11670) 2026-02-01 23:11:25 -06:00
Mathias Beugnon 304022ad23 fix: session title generation with OpenAI models. (#11678) 2026-02-01 23:10:53 -06:00
Dax c740357f61 Simplify directory tree output for prompts (#11731) 2026-02-01 23:37:07 -05:00
Dax b6248a1f3d Add spinner animation for Task tool (#11725) 2026-02-01 22:51:55 -05:00
Sebastian 81bbd2eff0 Use opentui OSC52 clipboard (#11718)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-02-02 04:48:29 +01:00
Dax Raad a1c315eb3e ci: enable typecheck on push to dev branch to catch type errors immediately after merge 2026-02-01 22:32:45 -05:00
Dax Raad ff538a185a Revert "fix(plugin): correct exports to point to dist instead of src"
This reverts commit 23a4295f41.
2026-02-01 22:29:45 -05:00
Dax Raad 867074f2a0 docs: Restructure AGENTS.md style guide with organized sections and code examples 2026-02-01 22:28:00 -05:00
Frank 24eaf19047 ci: Fix Pulumi version conflict in deploy workflow
Added a workaround to fix Pulumi version conflict in the deployment workflow.
2026-02-01 21:39:24 -05:00
Dax Raad 29d614e5bb ci: restrict nix-hashes workflow to dev branch pushes only
Remove pull_request trigger and limit push trigger to dev branch to prevent

unnecessary workflow runs on feature branches and PRs. The workflow will now

only execute when dependency files change on the dev branch.
2026-02-01 21:16:13 -05:00
Dax Raad 8d47c0c342 ignore: switch commit model to kimi-k2.5 for improved command execution reliability 2026-02-01 21:02:26 -05:00
Dax Raad 8b7de2f851 ci: skip unicode filename test due to inconsistent behavior causing CI failures 2026-02-01 21:01:57 -05:00
Dax Raad dd9cde6ce9 tui: fix task status to show current tool state from message store 2026-02-01 20:52:17 -05:00
Dax Raad 0890284bb9 core: allow starting new sessions after errors by fixing stuck session status 2026-02-01 20:39:58 -05:00
Dax Raad a31cd00201 ci: skip force push when beta branch is unchanged 2026-02-01 20:35:02 -05:00
Dax Raad 9e99257c58 ci: allow manual dispatch for beta workflow 2026-02-01 20:18:12 -05:00
Dax Raad 2f785d96d5 ci: run beta workflow on hourly schedule only 2026-02-01 20:17:50 -05:00
Dax Raad 34083134a3 ci: centralize team list in @opencode-ai/script package and use beta label filter 2026-02-01 20:17:15 -05:00
Dax Raad 3adeaad8c9 ci: add synchronize event and check for beta label using contains() 2026-02-01 19:59:20 -05:00
Dax Raad af5bd6bed2 ci: change trigger from scheduled cron to PR labeled events with beta label condition 2026-02-01 19:59:20 -05:00
Dax Raad eb698d478d ci: add --label beta filter to only process PRs with beta label 2026-02-01 19:59:20 -05:00
Dax Raad b2eeffec06 ci: post PR comments when beta merge fails instead of Discord notifications 2026-02-01 19:59:20 -05:00
Dax Raad 2dad509689 ci: rewrite beta script to use proper Bun shell patterns 2026-02-01 19:59:20 -05:00
Dax Raad 2699c67756 ci: remove parseArgs CLI option and use environment variable directly 2026-02-01 19:59:20 -05:00
Dax Raad 4b1e8db459 ci: add --discord-webhook / -d CLI option for custom Discord webhook URL 2026-02-01 19:59:20 -05:00
Dax Raad 230b4a9fd1 ci: add DISCORD_ISSUES_WEBHOOK_URL secret to beta workflow 2026-02-01 19:59:20 -05:00
Dax Raad ed9d872ce7 ci: collect all failed PR merges and notify Discord 2026-02-01 19:59:20 -05:00
Dax Raad 9e576bfa00 ci: prevent rate limit errors when fetching team PRs for beta releases 2026-02-01 19:29:26 -05:00
R44VC0RP 23a4295f41 fix(plugin): correct exports to point to dist instead of src
The package.json exports were pointing to ./src/*.ts but the published
package only includes the dist/ folder. This caused 'Cannot find module'
errors when custom tools tried to import @opencode-ai/plugin.

Changed exports from:
  ".": "./src/index.ts"
  "./tool": "./src/tool.ts"

To:
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }
  "./tool": { "types": "./dist/tool.d.ts", "import": "./dist/tool.js" }
2026-02-01 19:26:23 -05:00
Aiden Cline a14f992f1c test: add unit test 2026-02-01 19:26:23 -05:00
Aiden Cline 916949305d fix: adjust resolve parts so that when messages with multiple @ references occur, the tool calls are properly ordered 2026-02-01 19:26:23 -05:00
R44VC0RP 5cfbc1a042 fix(app): hide badge for builtin slash commands
Add source: 'command' to builtin and config-defined commands so they
don't show a 'custom' badge. Only MCP and skill commands show badges.
2026-02-01 19:26:23 -05:00
R44VC0RP 5f3abe2257 feat(app): show skill/mcp badges for slash commands
Display 'skill' or 'mcp' badge instead of 'custom' for slash commands
based on their source type. This provides better clarity to users about
where each command comes from.
2026-02-01 19:26:23 -05:00
opencode-agent[bot] d7604f9f22 chore: generate 2026-02-02 00:18:42 +00:00
Aaron Iker 80c240da86 feat(ui): Smooth fading out on scroll, style fixes (#11683) 2026-02-02 01:18:06 +01:00
opencode-agent[bot] 61fe8e0ea5 chore: generate 2026-02-02 00:17:54 +00:00
Aaron Iker c1497f8066 feat(ui): Select, dropdown, popover styles & transitions (#11675) 2026-02-02 01:17:14 +01:00
Frank be92e85893 zen: add minimax logo (#11682) 2026-02-01 17:50:15 -05:00
Sumit Srivastava 0e3a591c9b docs (web): Update incorrect Kimi model name in zen.mdx (#11677) 2026-02-01 16:13:53 -06:00
Filip 7b5b8c6633 test(app): workspace tests (#11659) 2026-02-01 15:39:50 -06:00
Aiden Cline 7453b646dd fix: when using codex sub, send the custom agent prompts as a separate developer message (previously sent as user message but api allows for instructions AND developer messages) (#11667)
Co-authored-by: Carlos <carloscanas942@gmail.com>
2026-02-01 14:57:47 -06:00
Aiden Cline 7b93ac8110 fix: variant logic for anthropic models through openai compat endpoint (#11665) 2026-02-01 14:50:49 -06:00
opencode-agent[bot] ec05615ba5 chore: update nix node_modules hashes 2026-02-01 20:48:33 +00:00
Rohan Godha f9f29f63ba fix: prompt caching for opus on bedrock (#11664) 2026-02-01 14:42:42 -06:00
Caleb Norton a2542872a5 fix(desktop): nix - add missing dep (#11656)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-02-01 14:24:09 -06:00
opencode-agent[bot] 1ac8846fff chore: generate 2026-02-01 20:13:33 +00:00
Caleb Norton e4792c90b7 chore: reduce nix fetching (#11660) 2026-02-01 14:12:54 -06:00
neavo 7e13e7230a fix(opencode): scope agent variant to model (#11410) 2026-02-01 14:12:30 -06:00
YeonGyu-Kim b3b61142df fix: prevent duplicate AGENTS.md injection when reading instruction files (#11581)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-02-01 13:46:08 -06:00
Desmond Sow 7f94fe8fdc fix: opencode hanging when using client.app.log() during initialization (#11642) 2026-02-01 13:14:34 -06:00
zerone0x 7a5da0bf68 fix(tui): remove outer backtick wrapper in session transcript tool formatting (#11566)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-01 13:11:18 -06:00
Axel Sarmiento Mrak 0eebbfe134 fix(ecosystem): fix link Daytona (#11621) 2026-02-01 12:19:37 -06:00
Filip 2e35fa8a2f docs: prefer wsl over native windows stuff (#11637) 2026-02-01 10:52:43 -06:00
Joscha Götzer 37fb1f009e fix: allow user plugins to override built-in auth plugins (#11058)
Co-authored-by: JosXa <info@josxa.dev>
2026-02-01 10:50:41 -06:00
opencode-agent[bot] 36859c5b4d chore: generate 2026-02-01 15:41:11 +00:00
Alex Yaroshuk d1b891c472 fix(app): binary file handling in file view (#11312) 2026-02-01 09:40:33 -06:00
Alex Yaroshuk 76f91baf67 fix(app): use static language names in Thai localization (#11496) 2026-02-01 09:38:23 -06:00
Alper Kartkaya 154fb11c72 docs: add Turkish README translation (#11524) 2026-02-01 09:37:45 -06:00
陆奕丞 c4b2832e13 docs: improve zh-TW punctuation to match Taiwan usage (#11574) (#11589) 2026-02-01 09:33:21 -06:00
Filip faf238cfa9 fix(app): show retry status only on active turn (#11543) 2026-02-01 06:42:33 -06:00
Aiden Cline 5e2d74b72a fix: ensure switching anthropic models mid convo on copilot works without errors, fix issue with reasoning opaque not being picked up for gemini models (#11569) 2026-02-01 01:36:44 -06:00
adamjhf 6d74a10d52 fix: correct pluralization of match count in grep and glob tools (#11565) 2026-02-01 00:15:28 -06:00
Dax 1279b70135 fix(tui): remove extra padding between search and results in dialog-select (#11564) 2026-02-01 04:04:57 +00:00
Dax d473f4699f feat(tui): add UI for skill tool in session view (#11561) 2026-01-31 22:59:46 -05:00
Dax 31abf4a5ee fix(tui): conditionally render bash tool output (#11558) 2026-01-31 22:40:37 -05:00
Goni Zahavy 0b5377428f ci: fixed stale pr workflow (#11310) 2026-01-31 21:16:34 -06:00
Aiden Cline 11a485c444 fix: issue where you couldn't @ folders/files that started with a "." (#11553) 2026-01-31 21:01:51 -06:00
Dax bf44522ef6 feat: add skill dialog for selecting and inserting skills (#11547) 2026-01-31 20:52:54 -05:00
Filip 4a44629f03 fix(app): rendering question tool when the step are collapsed (#11539) 2026-01-31 18:25:06 -06:00
opencode-agent[bot] b06016cb8a chore: update nix node_modules hashes 2026-02-01 00:07:33 +00:00
Aiden Cline 710ede868a ci: fix nix hash issue (#11530)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-01-31 17:57:07 -06:00
Jérôme Benoit a631d601bd fix(ci): portable hash parsing in nix-hashes workflow (#11533) 2026-01-31 17:56:49 -06:00
opencode-agent[bot] 2b90d3980a chore: generate 2026-01-31 20:02:03 +00:00
Filip 3ba865d90e test(app): general settings, shortcuts, providers and status popover (#11517) 2026-01-31 14:01:21 -06:00
Dax Raad 5f842fccc4 tui: enable password authentication for remote session attachment
Allow users to authenticate when attaching to a remote OpenCode session by supporting basic auth via a password flag or OPENCODE_SERVER_PASSWORD environment variable
2026-01-31 14:42:36 -05:00
Aiden Cline fc4fabe8a2 tweak: show actual retry error message instead of generic error msg (#11520) 2026-01-31 13:15:42 -06:00
Jérôme Benoit 2e89dabee3 fix(provider): use process.env directly for runtime env mutations (#11482) 2026-01-31 12:35:23 -06:00
Jérôme Benoit bc230f92d7 fix(nix): restore native runners for darwin hash computation (#11495) 2026-01-31 12:32:11 -06:00
大猫子 58ba3d8bf5 docs: fix documentation issues (#11435)
Co-authored-by: damaozi <1811866786@qq.com>
2026-01-31 12:30:45 -06:00
Jérôme Benoit 8e0808abd0 feat(opencode): add reasoning variants support for SAP AI Core (#8753)
Co-authored-by: Github Action <action@github.com>
2026-01-31 09:26:23 -06:00
Ryan Vogel b10b3df3fe Revert "feat(app): add skill slash commands" (#11484) 2026-01-31 10:20:23 -05:00
Ryan Vogel 512e4a34dd feat(app): add skill slash commands (#11369) 2026-01-31 08:59:28 -06:00
陆奕丞 5cd584915d fix(pty): Add UTF-8 encoding defaults for Windows PTY (#11459) 2026-01-31 08:55:34 -06:00
Alex Yaroshuk 76667ee77e fix(app): session header 'share' button to hug content (#11371) 2026-01-31 08:53:49 -06:00
opencode 3109ed2114 release: v1.1.48
nix-hashes / nix-hashes (push) Waiting to run
2026-01-31 13:48:22 +00:00
Adam af7ee500f9 Revert "feat: Transitions, spacing, scroll fade, prompt area update (#11168)" (#11461)
Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com>
2026-01-31 07:18:51 -06:00
Filip dbe6197fb8 test(app): session actions (#11386) 2026-01-31 06:42:47 -06:00
Aiden Cline 447160ea01 test: fix flaky test (#11427) 2026-01-31 01:07:22 -06:00
opencode-agent[bot] a3a7139678 chore: generate 2026-01-31 06:59:18 +00:00
Dax Raad 02ff0a1779 sync 2026-01-31 01:58:20 -05:00
Dax Raad e50d530613 tui: allow specifying custom models file path via OPENCODE_MODELS_PATH
Users can now configure their own models configuration file by setting the OPENCODE_MODELS_PATH environment variable, providing more flexibility for testing and configuration.
2026-01-31 01:53:23 -05:00
Dax Raad 3a783820cf ci: copy models fixture for e2e test consistency 2026-01-31 01:44:19 -05:00
Dax Raad 42c80d47f0 core: ensure models configuration is not empty before loading 2026-01-31 00:57:17 -05:00
opencode-agent[bot] 61627b98ca chore: generate 2026-01-31 05:42:31 +00:00
Dax 473f1d020e feat: make skills invokable as slash commands in the TUI (#11390) 2026-01-31 00:41:55 -05:00
Aiden Cline 35a2e7bb94 fix: don't --follow by default for grep and other things using ripgrep (#11415) 2026-01-30 23:38:43 -06:00
Dax Raad f41a3106f2 ci: run tests automatically when code is pushed to dev branch 2026-01-31 00:37:55 -05:00
Dax Raad 6241c68bb6 core: prevent parallel test runs from contaminating environment variables 2026-01-31 00:37:21 -05:00
Aiden Cline a8f82e9a59 fix: ensure the mistral ordering fixes also apply to devstral (#11412) 2026-01-30 23:16:07 -06:00
Dax Raad db71130ec8 ci: remove pull-request write permissions from beta workflow 2026-01-30 23:39:01 -05:00
Dax Raad 7f61b94248 ci: remove workflow restrictions to allow all PR triggers for broader CI coverage 2026-01-30 23:36:54 -05:00
Dax Raad ba3dd887f8 ci: allow manual beta workflow trigger so users can release on demand instead of waiting for hourly schedule 2026-01-30 23:36:05 -05:00
Dax Raad b354062b6e ci: schedule beta workflow hourly to automate sync runs 2026-01-30 23:33:33 -05:00
opencode-agent[bot] 1289be7c11 chore: generate 2026-01-31 02:35:22 +00:00
Steffen Deusch 18023af20b feat(opencode): add copilot specific provider to properly handle copilot reasoning tokens (#8900)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-01-30 19:53:22 -06:00
Dax Raad c6945bd417 Merge remote dev and apply revert 2026-01-30 20:38:10 -05:00
Dax Raad f6d9bbe779 Revert "feat: make skills invokable as slash commands in the TUI"
This reverts commit c8418faea0.
2026-01-30 20:37:41 -05:00
Benjamin Bartels 401fa7f578 feat(build): respect OPENCODE_MODELS_URL env var (#11384) 2026-01-30 19:37:37 -06:00
Dax Raad c8418faea0 feat: make skills invokable as slash commands in the TUI
- Add Skill.content() method to load skill template content from SKILL.md files
- Modify Command.list() to include skills as invokable commands
- Add 'skill' boolean property to Command.Info schema
- Update autocomplete to show skills with (Skill) label in slash commands
- Regenerate SDK to include skill property in Command type
2026-01-30 20:36:48 -05:00
opencode-agent[bot] b1ad1961b7 chore: generate 2026-01-31 01:32:46 +00:00
Cesar Garcia 3488a55a9f fix(provider): use snake_case for thinking param with OpenAI-compatible APIs (#10109) 2026-01-31 01:32:02 +00:00
Aiden Cline 9f151f5860 chore(deps): bump ai-sdk packages (#11383)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-01-30 17:58:02 -06:00
Aiden Cline 07b0e58975 fix: ensure ask question tool isn't included when using acp (#11379) 2026-01-30 17:32:51 -06:00
Idris Gadi b0381d08e7 docs: update agents options section to include color and top_p options (#11355) 2026-01-30 17:05:01 -06:00
Aiden Cline 1fd15a7c4d test: add llm.test.ts (#11375) 2026-01-30 17:04:32 -06:00
Patrick Schiel e6e94b5d59 fix: handle redirected_statement treesitter node in bash permissions (#6737) 2026-01-30 16:11:45 -06:00
Michael Yochpaz 9b9d692b2c refactor(provider): remove google-vertex-anthropic special case in ge… (#10743) 2026-01-30 15:57:36 -06:00
Filip 840383ca9c refactor(app): refactored tests + added project tests (#11349) 2026-01-30 21:06:48 +00:00
Ryan Vogel f3dce2b05e fix(provider): exclude chat models from textVerbosity setting (#11363) 2026-01-30 21:06:48 +00:00
Dax Raad 904c8e560d ci: increase ARM runner to 8 vCPUs for faster Tauri builds 2026-01-30 21:06:48 +00:00
opencode a2bb086eb4 release: v1.1.47
nix-hashes / nix-hashes (push) Waiting to run
2026-01-30 21:06:38 +00:00
Dax Raad 9a928c25dd fix(github): add owner parameter to app token for org-wide repo access 2026-01-30 15:22:02 -05:00
Dax Raad 8e75065663 commit 2026-01-30 15:17:55 -05:00
opencode 16be7f5412 release: v1.1.46
nix-hashes / nix-hashes (push) Waiting to run
2026-01-30 19:19:34 +00:00
opencode-agent[bot] 0fe34b9bde chore: generate 2026-01-30 18:16:49 +00:00
Idris Gadi fae12ff5c1 chore(tui): remove unused experimental keys (#11195) 2026-01-30 12:16:01 -06:00
Filip 7942455df6 test(app): change language test (#11295) 2026-01-30 18:04:02 +00:00
opencode-agent[bot] 7ce1532be8 chore: generate 2026-01-30 17:58:31 +00:00
Aaron Iker d1e8c95362 feat: Transitions, spacing, scroll fade, prompt area update (#11168)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aaroniker <4730431+aaroniker@users.noreply.github.com>
2026-01-30 17:57:49 +00:00
Dax Raad f35e0a05e1 ci 2026-01-30 12:55:58 -05:00
Aiden Cline 039b8fde67 ci: update pr template (#11341) 2026-01-30 12:45:42 -05:00
opencode-agent[bot] 529f151c08 chore: generate 2026-01-30 17:20:20 +00:00
Frank 449dda2caa wip: zen (#11343) 2026-01-30 12:19:36 -05:00
Dax Raad 63780e8550 ci 2026-01-30 10:39:59 -05:00
Dax Raad cf701afbb8 ci 2026-01-30 10:38:10 -05:00
Dax Raad 51dd136ebb ci 2026-01-30 10:36:01 -05:00
Dax Raad 06250230c6 ci 2026-01-30 10:35:39 -05:00
Dax Raad ea77625490 ci 2026-01-30 10:28:54 -05:00
Dax Raad 0ec75b790a ci 2026-01-30 10:28:12 -05:00
Dax Raad 75ae093eed ci 2026-01-30 10:26:55 -05:00
Dax Raad 4f7db789f5 ci 2026-01-30 10:25:10 -05:00
Dax Raad b923f70e8f ci 2026-01-30 10:23:19 -05:00
Dax Raad 7d6c5f2af9 ci 2026-01-30 10:21:47 -05:00
Dax Raad 1936972772 ci 2026-01-30 10:20:49 -05:00
Dax Raad 41ba279bc0 ci 2026-01-30 10:15:45 -05:00
Dax Raad 413a1aa0cb ci 2026-01-30 10:12:29 -05:00
Dax Raad 6611ddc30b ci 2026-01-30 10:11:05 -05:00
Dax Raad 5b6a8fba52 ci 2026-01-30 10:09:27 -05:00
Dax Raad 9c248b2479 ci 2026-01-30 10:08:28 -05:00
Dax Raad 483e6ea280 ci 2026-01-30 10:02:05 -05:00
Dax Raad 4922e43508 sync 2026-01-30 09:58:54 -05:00
Dax Raad d32912b7a0 ci 2026-01-30 09:57:17 -05:00
Dax Raad 78a3e87204 ci 2026-01-30 09:56:21 -05:00
Dax Raad ebfd6fff3d ci 2026-01-30 09:56:06 -05:00
Dax Raad a58af50a26 ci 2026-01-30 09:36:57 -05:00
Aiden Cline 3bbaa5d114 fix: rm ai sdk middleware that was preventing <think> blocks from being sent back as assistant message content (#11270)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-01-30 09:36:57 -05:00
opencode f24d0d422c release: v1.1.45
nix-hashes / nix-hashes (push) Waiting to run
2026-01-30 14:32:22 +00:00
Dax Raad 8137b02fec ci 2026-01-30 01:15:24 -05:00
Dax Raad 70e86a8629 ci 2026-01-30 00:56:45 -05:00
Dax Raad 413925cfb6 ci 2026-01-30 00:48:08 -05:00
Dax Raad f4c01a04f4 ci 2026-01-30 00:46:12 -05:00
Dax Raad cd7941e7f7 ci 2026-01-30 00:44:58 -05:00
Dax Raad 47e9774451 ci 2026-01-30 00:43:36 -05:00
Dax Raad 2ea73be24e ci 2026-01-30 00:39:49 -05:00
Dax Raad bb2ef5611f ci 2026-01-30 00:39:23 -05:00
Dax Raad 0e4f4cef7b ci 2026-01-30 00:27:51 -05:00
Dax Raad b8adf210f0 ci 2026-01-30 00:25:52 -05:00
Dax Raad bb3b376d7d ci 2026-01-30 00:23:52 -05:00
Dax Raad e26ef19d0f ci 2026-01-30 00:21:30 -05:00
Dax Raad 194dc74fc8 ci 2026-01-30 00:18:50 -05:00
Dax Raad eef25b6ed5 ci 2026-01-30 00:15:21 -05:00
Dax Raad c188a8c38b ci 2026-01-30 00:10:44 -05:00
Dax Raad a5cab2f070 ci 2026-01-30 00:07:56 -05:00
Dax Raad 0fdcd57135 ci 2026-01-30 00:05:05 -05:00
Dax Raad 56eb4fabc5 ci 2026-01-30 00:04:51 -05:00
Dax Raad 21a6b755a6 ci 2026-01-29 23:58:39 -05:00
Dax Raad 8c47722973 ci 2026-01-29 23:56:56 -05:00
Dax Raad 1aab6fe274 ci 2026-01-29 23:55:48 -05:00
Dax Raad 77cb223a26 ci 2026-01-29 23:52:08 -05:00
Dax Raad 34b7b756b8 ci 2026-01-29 23:49:53 -05:00
1455 changed files with 266778 additions and 24104 deletions
+1
View File
@@ -1,4 +1,5 @@
# web + desktop packages
packages/app/ @adamdotdevin
packages/tauri/ @adamdotdevin
packages/desktop/src-tauri/ @brendonovich
packages/desktop/ @adamdotdevin
+1 -1
View File
@@ -1,4 +1,4 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: 💬 Discord Community
url: https://discord.gg/opencode
+21
View File
@@ -0,0 +1,21 @@
# Vouched contributors for this project.
#
# See https://github.com/mitchellh/vouch for details.
#
# Syntax:
# - One handle per line (without @), sorted alphabetically.
# - Optional platform prefix: platform:username (e.g., github:user).
# - Denounce with minus prefix: -username or -platform:username.
# - Optional details after a space following the handle.
adamdotdevin
ariane-emory
-florianleibert
fwang
iamdavidhill
jayair
kitlangton
kommander
r44vc0rp
rekram1-node
-spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr
+1 -1
View File
@@ -6,7 +6,7 @@ runs:
- name: Mount Bun Cache
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-bun-cache
key: ${{ github.repository }}-bun-cache-${{ runner.os }}
path: ~/.bun
- name: Setup Bun
@@ -0,0 +1,43 @@
name: "Setup Git Committer"
description: "Create app token and configure git user"
inputs:
opencode-app-id:
description: "OpenCode GitHub App ID"
required: true
opencode-app-secret:
description: "OpenCode GitHub App private key"
required: true
outputs:
token:
description: "GitHub App token"
value: ${{ steps.apptoken.outputs.token }}
app-slug:
description: "GitHub App slug"
value: ${{ steps.apptoken.outputs.app-slug }}
runs:
using: "composite"
steps:
- name: Create app token
id: apptoken
uses: actions/create-github-app-token@v2
with:
app-id: ${{ inputs.opencode-app-id }}
private-key: ${{ inputs.opencode-app-secret }}
owner: ${{ github.repository_owner }}
- name: Configure git user
run: |
slug="${{ steps.apptoken.outputs.app-slug }}"
git config --global user.name "${slug}[bot]"
git config --global user.email "${slug}[bot]@users.noreply.github.com"
shell: bash
- name: Clear checkout auth
run: |
git config --local --unset-all http.https://github.com/.extraheader || true
shell: bash
- name: Configure git remote
run: |
git remote set-url origin https://x-access-token:${{ steps.apptoken.outputs.token }}@github.com/${{ github.repository }}
shell: bash
+4
View File
@@ -1,3 +1,7 @@
### What does this PR do?
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**
### How did you verify your code works?
+12 -13
View File
@@ -1,17 +1,12 @@
name: beta
on:
push:
branches: [dev]
pull_request:
types: [opened, synchronize, labeled, unlabeled]
workflow_dispatch:
schedule:
- cron: "0 * * * *"
jobs:
sync:
if: |
github.event_name == 'push' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'contributor'))
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
@@ -19,16 +14,20 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Setup Git Committer
id: setup-git-committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Sync beta branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
run: bun script/beta.ts
+182 -30
View File
@@ -18,6 +18,7 @@ permissions:
jobs:
close-stale-prs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Close inactive PRs
uses: actions/github-script@v8
@@ -25,59 +26,210 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const DAYS_INACTIVE = 60
const MAX_RETRIES = 3
// Adaptive delay: fast for small batches, slower for large to respect
// GitHub's 80 content-generating requests/minute limit
const SMALL_BATCH_THRESHOLD = 10
const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 PRs)
const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 PRs) = ~30 ops/min, well under 80 limit
const startTime = Date.now()
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
const { owner, repo } = context.repo
const dryRun = context.payload.inputs?.dryRun === "true"
const stalePrs = []
core.info(`Dry run mode: ${dryRun}`)
core.info(`Cutoff date: ${cutoff.toISOString()}`)
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
sort: "updated",
direction: "asc",
})
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
for (const pr of prs) {
const lastUpdated = new Date(pr.updated_at)
if (lastUpdated > cutoff) {
core.info(`PR ${pr.number} is fresh`)
continue
async function withRetry(fn, description = 'API call') {
let lastError
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
const result = await fn()
return result
} catch (error) {
lastError = error
const isRateLimited = error.status === 403 &&
(error.message?.includes('rate limit') || error.message?.includes('secondary'))
if (!isRateLimited) {
throw error
}
// Parse retry-after header, default to 60 seconds
const retryAfter = error.response?.headers?.['retry-after']
? parseInt(error.response.headers['retry-after'])
: 60
// Exponential backoff: retryAfter * 2^attempt
const backoffMs = retryAfter * 1000 * Math.pow(2, attempt)
core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`)
await sleep(backoffMs)
}
}
core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`)
throw lastError
}
const query = `
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 100, states: OPEN, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
nodes {
number
title
author {
login
}
createdAt
commits(last: 1) {
nodes {
commit {
committedDate
}
}
}
comments(last: 1) {
nodes {
createdAt
}
}
reviews(last: 1) {
nodes {
createdAt
}
}
}
}
}
}
`
const allPrs = []
let cursor = null
let hasNextPage = true
let pageCount = 0
while (hasNextPage) {
pageCount++
core.info(`Fetching page ${pageCount} of open PRs...`)
const result = await withRetry(
() => github.graphql(query, { owner, repo, cursor }),
`GraphQL page ${pageCount}`
)
allPrs.push(...result.repository.pullRequests.nodes)
hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage
cursor = result.repository.pullRequests.pageInfo.endCursor
core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`)
// Delay between pagination requests (use small batch delay for reads)
if (hasNextPage) {
await sleep(SMALL_BATCH_DELAY_MS)
}
}
core.info(`Found ${allPrs.length} open pull requests`)
const stalePrs = allPrs.filter((pr) => {
const dates = [
new Date(pr.createdAt),
pr.commits.nodes[0] ? new Date(pr.commits.nodes[0].commit.committedDate) : null,
pr.comments.nodes[0] ? new Date(pr.comments.nodes[0].createdAt) : null,
pr.reviews.nodes[0] ? new Date(pr.reviews.nodes[0].createdAt) : null,
].filter((d) => d !== null)
const lastActivity = dates.sort((a, b) => b.getTime() - a.getTime())[0]
if (!lastActivity || lastActivity > cutoff) {
core.info(`PR #${pr.number} is fresh (last activity: ${lastActivity?.toISOString() || "unknown"})`)
return false
}
stalePrs.push(pr)
}
core.info(`PR #${pr.number} is STALE (last activity: ${lastActivity.toISOString()})`)
return true
})
if (!stalePrs.length) {
core.info("No stale pull requests found.")
return
}
core.info(`Found ${stalePrs.length} stale pull requests`)
// ============================================
// Close stale PRs
// ============================================
const requestDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD
? LARGE_BATCH_DELAY_MS
: SMALL_BATCH_DELAY_MS
core.info(`Using ${requestDelayMs}ms delay between operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`)
let closedCount = 0
let skippedCount = 0
for (const pr of stalePrs) {
const issue_number = pr.number
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
if (dryRun) {
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.user.login}`)
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
continue
}
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: closeComment,
})
try {
// Add comment
await withRetry(
() => github.rest.issues.createComment({
owner,
repo,
issue_number,
body: closeComment,
}),
`Comment on PR #${issue_number}`
)
await github.rest.pulls.update({
owner,
repo,
pull_number: issue_number,
state: "closed",
})
// Close PR
await withRetry(
() => github.rest.pulls.update({
owner,
repo,
pull_number: issue_number,
state: "closed",
}),
`Close PR #${issue_number}`
)
core.info(`Closed PR #${issue_number} from ${pr.user.login}`)
closedCount++
core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`)
// Delay before processing next PR
await sleep(requestDelayMs)
} catch (error) {
skippedCount++
core.error(`Failed to close PR #${issue_number}: ${error.message}`)
}
}
const elapsed = Math.round((Date.now() - startTime) / 1000)
core.info(`\n========== Summary ==========`)
core.info(`Total open PRs found: ${allPrs.length}`)
core.info(`Stale PRs identified: ${stalePrs.length}`)
core.info(`PRs closed: ${closedCount}`)
core.info(`PRs skipped (errors): ${skippedCount}`)
core.info(`Elapsed time: ${elapsed}s`)
core.info(`=============================`)
+86
View File
@@ -0,0 +1,86 @@
name: compliance-close
on:
schedule:
# Run every 30 minutes to check for expired compliance windows
- cron: "*/30 * * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
jobs:
close-non-compliant:
runs-on: ubuntu-latest
steps:
- name: Close non-compliant issues and PRs after 2 hours
uses: actions/github-script@v7
with:
script: |
const { data: items } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'needs:compliance',
state: 'open',
per_page: 100,
});
if (items.length === 0) {
core.info('No open issues/PRs with needs:compliance label');
return;
}
const now = Date.now();
const twoHours = 2 * 60 * 60 * 1000;
for (const item of items) {
const isPR = !!item.pull_request;
const kind = isPR ? 'PR' : 'issue';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: item.number,
});
const complianceComment = comments.find(c => c.body.includes('<!-- issue-compliance -->'));
if (!complianceComment) continue;
const commentAge = now - new Date(complianceComment.created_at).getTime();
if (commentAge < twoHours) {
core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`);
continue;
}
const closeMessage = isPR
? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.'
: 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: item.number,
body: closeMessage,
});
if (isPR) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: item.number,
state: 'closed',
});
} else {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: item.number,
state: 'closed',
state_reason: 'not_planned',
});
}
core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`);
}
+6 -2
View File
@@ -48,8 +48,12 @@ jobs:
TODAY'S DATE: ${TODAY}
STEP 1: Gather today's issues
Search for all issues created today (${TODAY}) using:
gh issue list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
Search for all OPEN issues created today (${TODAY}) using:
gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these:
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
This recap is specifically for COMMUNITY (external) issues only.
STEP 2: Analyze and categorize
For each issue created today, categorize it:
+8 -4
View File
@@ -47,14 +47,18 @@ jobs:
TODAY'S DATE: ${TODAY}
STEP 1: Gather PR data
Run these commands to gather PR information. ONLY include PRs created or updated TODAY (${TODAY}):
Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}):
# PRs created today
gh pr list --repo ${{ github.repository }} --state all --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
# Open PRs created today
gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
# PRs with activity today (updated today)
# Open PRs with activity today (updated today)
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these:
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
This recap is specifically for COMMUNITY (external) contributions only.
STEP 2: For high-activity PRs, check comment counts
+9
View File
@@ -21,6 +21,15 @@ jobs:
with:
node-version: "24"
# Workaround for Pulumi version conflict:
# GitHub runners have Pulumi 3.212.0+ pre-installed, which removed the -root flag
# from pulumi-language-nodejs (see https://github.com/pulumi/pulumi/pull/21065).
# SST 3.17.x uses Pulumi SDK 3.210.0 which still passes -root, causing a conflict.
# Removing the system language plugin forces SST to use its bundled compatible version.
# TODO: Remove when sst supports Pulumi >3.210.0
- name: Fix Pulumi version conflict
run: sudo rm -f /usr/local/bin/pulumi-language-nodejs
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+85
View File
@@ -0,0 +1,85 @@
name: docs-locale-sync
on:
push:
branches:
- dev
paths:
- packages/web/src/content/docs/*.mdx
jobs:
sync-locales:
if: github.actor != 'opencode-agent[bot]'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
id-token: write
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Compute changed English docs
id: changes
run: |
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true)
if [ -z "$FILES" ]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No English docs changed in push range"
exit 0
fi
echo "has_changes=true" >> "$GITHUB_OUTPUT"
{
echo "files<<EOF"
echo "$FILES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Sync locale docs with OpenCode
if: steps.changes.outputs.has_changes == 'true'
uses: sst/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/gpt-5.2
agent: docs
prompt: |
Update localized docs to match the latest English docs changes.
Changed English doc files:
<changed_english_docs>
${{ steps.changes.outputs.files }}
</changed_english_docs>
Requirements:
1. Update all relevant locale docs under packages/web/src/content/docs/<locale>/ so they reflect these English page changes.
2. You MUST use the Task tool for translation work and launch subagents with subagent_type `translator` (defined in .opencode/agent/translator.md).
3. Do not translate directly in the primary agent. Use translator subagent output as the source for locale text updates.
4. Run translator subagent Task calls in parallel whenever file/locale translation work is independent.
5. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update.
6. Keep locale docs structure aligned with their corresponding English pages.
7. Do not modify English source docs in packages/web/src/content/docs/*.mdx.
8. If no locale updates are needed, make no changes.
- name: Commit and push locale docs updates
if: steps.changes.outputs.has_changes == 'true'
run: |
if [ -z "$(git status --porcelain)" ]; then
echo "No locale docs changes to commit"
exit 0
fi
git add -A
git commit -m "docs(i18n): sync locale docs from english changes"
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
git push origin HEAD:"$GITHUB_REF_NAME"
+68 -14
View File
@@ -21,7 +21,7 @@ jobs:
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check for duplicate issues
- name: Check duplicates and compliance
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -34,30 +34,84 @@ jobs:
"webfetch": "deny"
}
run: |
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:
Issue number:
${{ github.event.issue.number }}
Issue number: ${{ github.event.issue.number }}
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Lookup this issue with gh issue view ${{ github.event.issue.number }}.
You have TWO tasks. Perform both, then post a SINGLE comment (if needed).
---
TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK
Check whether the issue follows our contributing guidelines and issue templates.
This project has three issue templates that every issue MUST use one of:
1. Bug Report - requires a Description field with real content
2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]:
3. Question - requires the Question field with real content
Additionally check:
- No AI-generated walls of text (long, AI-generated descriptions are not acceptable)
- The issue has real content, not just template placeholder text left unchanged
- Bug reports should include some context about how to reproduce
- Feature requests should explain the problem or need
- We want to push for having the user provide system description & information
Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content.
---
TASK 2: DUPLICATE CHECK
Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates.
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
If you find any potential duplicates, please comment on the new issue with:
- A brief explanation of why it might be a duplicate
- Links to the potentially duplicate issues
- A suggestion to check those issues first
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997.
---
POSTING YOUR COMMENT:
Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows:
If the issue is NOT compliant, start the comment with:
<!-- issue-compliance -->
Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance
If duplicates were found, include a section about potential duplicates with links.
If the issue mentions keybinds/keyboard shortcuts, include a note about #4997.
If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all.
Use this format for the comment:
'This issue might be a duplicate of existing issues. Please check:
[If not compliant:]
<!-- issue-compliance -->
This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md).
**What needs to be fixed:**
- [specific reasons]
Please edit this issue to address the above within **2 hours**, or it will be automatically closed.
[If duplicates found, add:]
---
This issue might be a duplicate of existing issues. Please check:
- #[issue_number]: [brief description of similarity]
Feel free to ignore if none of these address your specific case.'
[If keybind-related, add:]
For keybind-related issues, please also check our pinned keybinds documentation: #4997
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
[End with if not compliant:]
If you believe this was flagged incorrectly, please let a maintainer know.
If no clear duplicates are found, do not comment."
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
+8 -9
View File
@@ -4,8 +4,6 @@ on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
generate:
@@ -16,14 +14,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Generate
run: ./script/generate.ts
@@ -33,10 +34,8 @@ jobs:
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 commit -m "chore: generate" --allow-empty
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 ""
+97 -87
View File
@@ -6,133 +6,143 @@ permissions:
on:
workflow_dispatch:
push:
branches: [dev]
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- ".github/workflows/nix-hashes.yml"
pull_request:
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- "nix/node_modules.nix"
- "nix/scripts/**"
- "patches/**"
- ".github/workflows/nix-hashes.yml"
jobs:
nix-hashes:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
TITLE: node_modules hashes
# Native runners required: bun install cross-compilation flags (--os/--cpu)
# do not produce byte-identical node_modules as native installs.
compute-hash:
strategy:
fail-fast: false
matrix:
include:
- system: x86_64-linux
runner: blacksmith-4vcpu-ubuntu-2404
- system: aarch64-linux
runner: blacksmith-4vcpu-ubuntu-2404-arm
- system: x86_64-darwin
runner: macos-15-intel
- system: aarch64-darwin
runner: macos-latest
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
- name: Configure git
run: |
git config --global user.email "action@github.com"
git config --global user.name "Github Action"
- name: Pull latest changes
- name: Compute node_modules hash
id: hash
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
git pull --rebase --autostash origin "$BRANCH"
- name: Compute all node_modules hashes
SYSTEM: ${{ matrix.system }}
run: |
set -euo pipefail
HASH_FILE="nix/hashes.json"
SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
BUILD_LOG=$(mktemp)
trap 'rm -f "$BUILD_LOG"' EXIT
if [ ! -f "$HASH_FILE" ]; then
mkdir -p "$(dirname "$HASH_FILE")"
echo '{"nodeModules":{}}' > "$HASH_FILE"
# Build with fakeHash to trigger hash mismatch and reveal correct hash
nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true
# Extract hash from build log with portability
HASH="$(grep -oE 'sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
if [ -z "$HASH" ]; then
echo "::error::Failed to compute hash for ${SYSTEM}"
cat "$BUILD_LOG"
exit 1
fi
for SYSTEM in $SYSTEMS; do
echo "Computing hash for ${SYSTEM}..."
BUILD_LOG=$(mktemp)
trap 'rm -f "$BUILD_LOG"' EXIT
echo "$HASH" > hash.txt
echo "Computed hash for ${SYSTEM}: $HASH"
# The updater derivations use fakeHash, so they will fail and reveal the correct hash
UPDATER_ATTR=".#packages.x86_64-linux.${SYSTEM}_node_modules"
- name: Upload hash
uses: actions/upload-artifact@v4
with:
name: hash-${{ matrix.system }}
path: hash.txt
retention-days: 1
nix build "$UPDATER_ATTR" --no-link 2>&1 | tee "$BUILD_LOG" || true
update-hashes:
needs: compute-hash
if: github.event_name != 'pull_request'
runs-on: blacksmith-4vcpu-ubuntu-2404
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
ref: ${{ github.ref_name }}
if [ -z "$CORRECT_HASH" ]; then
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
fi
- name: Setup git committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
if [ -z "$CORRECT_HASH" ]; then
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
cat "$BUILD_LOG"
exit 1
fi
- name: Pull latest changes
run: |
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
echo " ${SYSTEM}: ${CORRECT_HASH}"
jq --arg sys "$SYSTEM" --arg h "$CORRECT_HASH" \
'.nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp"
mv "${HASH_FILE}.tmp" "$HASH_FILE"
done
- name: Download hash artifacts
uses: actions/download-artifact@v4
with:
path: hashes
pattern: hash-*
echo "All hashes computed:"
cat "$HASH_FILE"
- name: Commit ${{ env.TITLE }} changes
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
- name: Update hashes.json
run: |
set -euo pipefail
HASH_FILE="nix/hashes.json"
echo "Checking for changes..."
summarize() {
local status="$1"
{
echo "### Nix $TITLE"
echo ""
echo "- ref: ${GITHUB_REF_NAME}"
echo "- status: ${status}"
} >> "$GITHUB_STEP_SUMMARY"
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
[ -f "$HASH_FILE" ] || echo '{"nodeModules":{}}' > "$HASH_FILE"
for SYSTEM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do
FILE="hashes/hash-${SYSTEM}/hash.txt"
if [ -f "$FILE" ]; then
HASH="$(tr -d '[:space:]' < "$FILE")"
echo "${SYSTEM}: ${HASH}"
jq --arg sys "$SYSTEM" --arg h "$HASH" '.nodeModules[$sys] = $h' "$HASH_FILE" > tmp.json
mv tmp.json "$HASH_FILE"
else
echo "::warning::Missing hash for ${SYSTEM}"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
}
done
FILES=("$HASH_FILE")
STATUS="$(git status --short -- "${FILES[@]}" || true)"
if [ -z "$STATUS" ]; then
echo "No changes detected."
summarize "no changes"
cat "$HASH_FILE"
- name: Commit changes
run: |
set -euo pipefail
HASH_FILE="nix/hashes.json"
if [ -z "$(git status --short -- "$HASH_FILE")" ]; then
echo "No changes to commit"
echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY"
echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "Changes detected:"
echo "$STATUS"
git add "${FILES[@]}"
git add "$HASH_FILE"
git commit -m "chore: update nix node_modules hashes"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
git pull --rebase --autostash origin "$BRANCH"
git push origin HEAD:"$BRANCH"
echo "Changes pushed successfully"
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
git push origin HEAD:"$GITHUB_REF_NAME"
summarize "committed $(git rev-parse --short HEAD)"
echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY"
echo "Status: committed $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY"
+3 -1
View File
@@ -60,9 +60,11 @@ jobs:
run: |
COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates")
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
if [ "$COMMENT" != "No duplicate PRs found" ]; then
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
$COMMENT"
fi
add-contributor-label:
runs-on: ubuntu-latest
+18 -15
View File
@@ -36,7 +36,15 @@ jobs:
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: ./.github/actions/setup-bun
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai
- id: version
run: |
./script/version.ts
@@ -44,6 +52,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
outputs:
version: ${{ steps.version.outputs.version }}
release: ${{ steps.version.outputs.release }}
@@ -94,7 +103,7 @@ jobs:
target: x86_64-pc-windows-msvc
- host: blacksmith-4vcpu-ubuntu-2404
target: x86_64-unknown-linux-gnu
- host: blacksmith-4vcpu-ubuntu-2404-arm
- host: blacksmith-8vcpu-ubuntu-2404-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.settings.host }}
steps:
@@ -155,7 +164,7 @@ jobs:
bun ./scripts/prepare.ts
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -201,15 +210,9 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: ./.github/actions/setup-bun
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -228,11 +231,12 @@ jobs:
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: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- uses: actions/download-artifact@v4
with:
@@ -263,6 +267,5 @@ jobs:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
NPM_CONFIG_PROVENANCE: false
+54
View File
@@ -0,0 +1,54 @@
name: sign-cli
on:
push:
branches:
- brendan/desktop-signpath
workflow_dispatch:
permissions:
contents: read
actions: read
jobs:
sign-cli:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-tags: true
- uses: ./.github/actions/setup-bun
- name: Build
run: |
./packages/opencode/script/build.ts
- name: Upload unsigned Windows CLI
id: upload_unsigned_windows_cli
uses: actions/upload-artifact@v4
with:
name: unsigned-opencode-windows-cli
path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe
if-no-files-found: error
- name: Submit SignPath signing request
id: submit_signpath_signing_request
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: ${{ secrets.SIGNPATH_API_KEY }}
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }}
wait-for-completion: true
output-artifact-directory: signed-opencode-cli
- name: Upload signed Windows CLI
uses: actions/upload-artifact@v4
with:
name: signed-opencode-windows-cli
path: signed-opencode-cli/*.exe
if-no-files-found: error
+49 -89
View File
@@ -1,11 +1,38 @@
name: test
on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
test:
name: test (${{ matrix.settings.name }})
unit:
name: unit (linux)
runs-on: blacksmith-4vcpu-ubuntu-2404
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Configure git identity
run: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
- name: Run unit tests
run: bun turbo test
e2e:
name: e2e (${{ matrix.settings.name }})
needs: unit
strategy:
fail-fast: false
matrix:
@@ -13,17 +40,12 @@ jobs:
- name: linux
host: blacksmith-4vcpu-ubuntu-2404
playwright: bunx playwright install --with-deps
workdir: .
command: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
bun turbo test
- name: windows
host: windows-latest
host: blacksmith-4vcpu-windows-2025
playwright: bunx playwright install
workdir: packages/app
command: bun test:e2e:local
runs-on: ${{ matrix.settings.host }}
env:
PLAYWRIGHT_BROWSERS_PATH: 0
defaults:
run:
shell: bash
@@ -40,87 +62,10 @@ jobs:
working-directory: packages/app
run: ${{ matrix.settings.playwright }}
- name: Set OS-specific paths
run: |
if [ "${{ runner.os }}" = "Windows" ]; then
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}\\opencode-e2e" >> "$GITHUB_ENV"
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}\\opencode-e2e\\home" >> "$GITHUB_ENV"
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}\\opencode-e2e\\share" >> "$GITHUB_ENV"
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}\\opencode-e2e\\cache" >> "$GITHUB_ENV"
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}\\opencode-e2e\\config" >> "$GITHUB_ENV"
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}\\opencode-e2e\\state" >> "$GITHUB_ENV"
else
printf '%s\n' "OPENCODE_E2E_ROOT=${{ runner.temp }}/opencode-e2e" >> "$GITHUB_ENV"
printf '%s\n' "OPENCODE_TEST_HOME=${{ runner.temp }}/opencode-e2e/home" >> "$GITHUB_ENV"
printf '%s\n' "XDG_DATA_HOME=${{ runner.temp }}/opencode-e2e/share" >> "$GITHUB_ENV"
printf '%s\n' "XDG_CACHE_HOME=${{ runner.temp }}/opencode-e2e/cache" >> "$GITHUB_ENV"
printf '%s\n' "XDG_CONFIG_HOME=${{ runner.temp }}/opencode-e2e/config" >> "$GITHUB_ENV"
printf '%s\n' "XDG_STATE_HOME=${{ runner.temp }}/opencode-e2e/state" >> "$GITHUB_ENV"
fi
- name: Seed opencode data
if: matrix.settings.name != 'windows'
working-directory: packages/opencode
run: bun script/seed-e2e.ts
env:
OPENCODE_DISABLE_SHARE: "true"
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
OPENCODE_E2E_PROJECT_DIR: ${{ github.workspace }}
OPENCODE_E2E_SESSION_TITLE: "E2E Session"
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e"
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano"
- name: Run opencode server
if: matrix.settings.name != 'windows'
working-directory: packages/opencode
run: bun dev -- --print-logs --log-level WARN serve --port 4096 --hostname 127.0.0.1 &
env:
OPENCODE_DISABLE_SHARE: "true"
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
OPENCODE_CLIENT: "app"
- name: Wait for opencode server
if: matrix.settings.name != 'windows'
run: |
for i in {1..120}; do
curl -fsS "http://127.0.0.1:4096/global/health" > /dev/null && exit 0
sleep 1
done
exit 1
- name: run
working-directory: ${{ matrix.settings.workdir }}
run: ${{ matrix.settings.command }}
- name: Run app e2e tests
run: bun --cwd packages/app test:e2e:local
env:
CI: true
OPENCODE_DISABLE_SHARE: "true"
OPENCODE_DISABLE_LSP_DOWNLOAD: "true"
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true"
OPENCODE_TEST_HOME: ${{ env.OPENCODE_TEST_HOME }}
XDG_DATA_HOME: ${{ env.XDG_DATA_HOME }}
XDG_CACHE_HOME: ${{ env.XDG_CACHE_HOME }}
XDG_CONFIG_HOME: ${{ env.XDG_CONFIG_HOME }}
XDG_STATE_HOME: ${{ env.XDG_STATE_HOME }}
PLAYWRIGHT_SERVER_HOST: "127.0.0.1"
PLAYWRIGHT_SERVER_PORT: "4096"
VITE_OPENCODE_SERVER_HOST: "127.0.0.1"
VITE_OPENCODE_SERVER_PORT: "4096"
OPENCODE_CLIENT: "app"
timeout-minutes: 30
- name: Upload Playwright artifacts
@@ -133,3 +78,18 @@ jobs:
path: |
packages/app/e2e/test-results
packages/app/e2e/playwright-report
required:
name: test (linux)
runs-on: blacksmith-4vcpu-ubuntu-2404
needs:
- unit
- e2e
if: always()
steps:
- name: Verify upstream test jobs passed
run: |
echo "unit=${{ needs.unit.result }}"
echo "e2e=${{ needs.e2e.result }}"
test "${{ needs.unit.result }}" = "success"
test "${{ needs.e2e.result }}" = "success"
+2
View File
@@ -1,6 +1,8 @@
name: typecheck
on:
push:
branches: [dev]
pull_request:
branches: [dev]
workflow_dispatch:
+96
View File
@@ -0,0 +1,96 @@
name: vouch-check-issue
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check if issue author is denounced
uses: actions/github-script@v7
with:
script: |
const author = context.payload.issue.user.login;
const issueNumber = context.payload.issue.number;
// Skip bots
if (author.endsWith('[bot]')) {
core.info(`Skipping bot: ${author}`);
return;
}
// Read the VOUCHED.td file via API (no checkout needed)
let content;
try {
const response = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/VOUCHED.td',
});
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
} catch (error) {
if (error.status === 404) {
core.info('No .github/VOUCHED.td file found, skipping check.');
return;
}
throw error;
}
// Parse the .td file for denounced users
const denounced = new Map();
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (!trimmed.startsWith('-')) continue;
const rest = trimmed.slice(1).trim();
if (!rest) continue;
const spaceIdx = rest.indexOf(' ');
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
// Handle platform:username or bare username
// Only match bare usernames or github: prefix (skip other platforms)
const colonIdx = handle.indexOf(':');
if (colonIdx !== -1) {
const platform = handle.slice(0, colonIdx).toLowerCase();
if (platform !== 'github') continue;
}
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
if (!username) continue;
denounced.set(username.toLowerCase(), reason);
}
// Check if the author is denounced
const reason = denounced.get(author.toLowerCase());
if (reason === undefined) {
core.info(`User ${author} is not denounced. Allowing issue.`);
return;
}
// Author is denounced — close the issue
const body = 'This issue has been automatically closed.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'not_planned',
});
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
+93
View File
@@ -0,0 +1,93 @@
name: vouch-check-pr
on:
pull_request_target:
types: [opened]
permissions:
contents: read
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check if PR author is denounced
uses: actions/github-script@v7
with:
script: |
const author = context.payload.pull_request.user.login;
const prNumber = context.payload.pull_request.number;
// Skip bots
if (author.endsWith('[bot]')) {
core.info(`Skipping bot: ${author}`);
return;
}
// Read the VOUCHED.td file via API (no checkout needed)
let content;
try {
const response = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/VOUCHED.td',
});
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
} catch (error) {
if (error.status === 404) {
core.info('No .github/VOUCHED.td file found, skipping check.');
return;
}
throw error;
}
// Parse the .td file for denounced users
const denounced = new Map();
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (!trimmed.startsWith('-')) continue;
const rest = trimmed.slice(1).trim();
if (!rest) continue;
const spaceIdx = rest.indexOf(' ');
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
// Handle platform:username or bare username
// Only match bare usernames or github: prefix (skip other platforms)
const colonIdx = handle.indexOf(':');
if (colonIdx !== -1) {
const platform = handle.slice(0, colonIdx).toLowerCase();
if (platform !== 'github') continue;
}
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
if (!username) continue;
denounced.set(username.toLowerCase(), reason);
}
// Check if the author is denounced
const reason = denounced.get(author.toLowerCase());
if (reason === undefined) {
core.info(`User ${author} is not denounced. Allowing PR.`);
return;
}
// Author is denounced — close the PR
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: 'This pull request has been automatically closed.',
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
@@ -0,0 +1,37 @@
name: vouch-manage-by-issue
on:
issue_comment:
types: [created]
concurrency:
group: vouch-manage
cancel-in-progress: false
permissions:
contents: write
issues: write
pull-requests: read
jobs:
manage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- uses: mitchellh/vouch/action/manage-by-issue@main
with:
issue-id: ${{ github.event.issue.number }}
comment-id: ${{ github.event.comment.id }}
env:
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
+1
View File
@@ -5,6 +5,7 @@ node_modules
.env
.idea
.vscode
.codex
*~
playground
tmp
+883
View File
@@ -0,0 +1,883 @@
---
description: Translate content for a specified locale while preserving technical terms
mode: subagent
model: opencode/gemini-3-pro
---
You are a professional translator and localization specialist.
Translate the user's content into the requested target locale (language + region, e.g. fr-FR, de-DE).
Requirements:
- Preserve meaning, intent, tone, and formatting (including Markdown/MDX structure).
- Preserve all technical terms and artifacts exactly: product/company names, API names, identifiers, code, commands/flags, file paths, URLs, versions, error messages, config keys/values, and anything inside inline code or code blocks.
- Also preserve every term listed in the Do-Not-Translate glossary below.
- Do not modify fenced code blocks.
- Output ONLY the translation (no commentary).
If the target locale is missing, ask the user to provide it.
---
# Do-Not-Translate Terms (OpenCode Docs)
Generated from: `packages/web/src/content/docs/*.mdx` (default English docs)
Generated on: 2026-02-10
Use this as a translation QA checklist / glossary. Preserve listed terms exactly (spelling, casing, punctuation).
General rules (verbatim, even if not listed below):
- Anything inside inline code (single backticks) or fenced code blocks (triple backticks)
- MDX/JS code in docs: `import ... from "..."`, component tags, identifiers
- CLI commands, flags, config keys/values, file paths, URLs/domains, and env vars
## Proper nouns and product names
Additional (not reliably captured via link text):
```text
Astro
Bun
Chocolatey
Cursor
Docker
Git
GitHub Actions
GitLab CI
GNOME Terminal
Homebrew
Mise
Neovim
Node.js
npm
Obsidian
opencode
opencode-ai
Paru
pnpm
ripgrep
Scoop
SST
Starlight
Visual Studio Code
VS Code
VSCodium
Windsurf
Windows Terminal
Yarn
Zellij
Zed
anomalyco
```
Extracted from link labels in the English docs (review and prune as desired):
```text
@openspoon/subtask2
302.AI console
ACP progress report
Agent Client Protocol
Agent Skills
Agentic
AGENTS.md
AI SDK
Alacritty
Anthropic
Anthropic's Data Policies
Atom One
Avante.nvim
Ayu
Azure AI Foundry
Azure portal
Baseten
built-in GITHUB_TOKEN
Bun.$
Catppuccin
Cerebras console
ChatGPT Plus or Pro
Cloudflare dashboard
CodeCompanion.nvim
CodeNomad
Configuring Adapters: Environment Variables
Context7 MCP server
Cortecs console
Deep Infra dashboard
DeepSeek console
Duo Agent Platform
Everforest
Fireworks AI console
Firmware dashboard
Ghostty
GitLab CLI agents docs
GitLab docs
GitLab User Settings > Access Tokens
Granular Rules (Object Syntax)
Grep by Vercel
Groq console
Gruvbox
Helicone
Helicone documentation
Helicone Header Directory
Helicone's Model Directory
Hugging Face Inference Providers
Hugging Face settings
install WSL
IO.NET console
JetBrains IDE
Kanagawa
Kitty
MiniMax API Console
Models.dev
Moonshot AI console
Nebius Token Factory console
Nord
OAuth
Ollama integration docs
OpenAI's Data Policies
OpenChamber
OpenCode
OpenCode config
OpenCode Config
OpenCode TUI with the opencode theme
OpenCode Web - Active Session
OpenCode Web - New Session
OpenCode Web - See Servers
OpenCode Zen
OpenCode-Obsidian
OpenRouter dashboard
OpenWork
OVHcloud panel
Pro+ subscription
SAP BTP Cockpit
Scaleway Console IAM settings
Scaleway Generative APIs
SDK documentation
Sentry MCP server
shell API
Together AI console
Tokyonight
Unified Billing
Venice AI console
Vercel dashboard
WezTerm
Windows Subsystem for Linux (WSL)
WSL
WSL (Windows Subsystem for Linux)
WSL extension
xAI console
Z.AI API console
Zed
ZenMux dashboard
Zod
```
## Acronyms and initialisms
```text
ACP
AGENTS
AI
AI21
ANSI
API
AST
AWS
BTP
CD
CDN
CI
CLI
CMD
CORS
DEBUG
EKS
ERROR
FAQ
GLM
GNOME
GPT
HTML
HTTP
HTTPS
IAM
ID
IDE
INFO
IO
IP
IRSA
JS
JSON
JSONC
K2
LLM
LM
LSP
M2
MCP
MR
NET
NPM
NTLM
OIDC
OS
PAT
PATH
PHP
PR
PTY
README
RFC
RPC
SAP
SDK
SKILL
SSE
SSO
TS
TTY
TUI
UI
URL
US
UX
VCS
VPC
VPN
VS
WARN
WSL
X11
YAML
```
## Code identifiers used in prose (CamelCase, mixedCase)
```text
apiKey
AppleScript
AssistantMessage
baseURL
BurntSushi
ChatGPT
ClangFormat
CodeCompanion
CodeNomad
DeepSeek
DefaultV2
FileContent
FileDiff
FileNode
fineGrained
FormatterStatus
GitHub
GitLab
iTerm2
JavaScript
JetBrains
macOS
mDNS
MiniMax
NeuralNomadsAI
NickvanDyke
NoeFabris
OpenAI
OpenAPI
OpenChamber
OpenCode
OpenRouter
OpenTUI
OpenWork
ownUserPermissions
PowerShell
ProviderAuthAuthorization
ProviderAuthMethod
ProviderInitError
SessionStatus
TabItem
tokenType
ToolIDs
ToolList
TypeScript
typesUrl
UserMessage
VcsInfo
WebView2
WezTerm
xAI
ZenMux
```
## OpenCode CLI commands (as shown in docs)
```text
opencode
opencode [project]
opencode /path/to/project
opencode acp
opencode agent [command]
opencode agent create
opencode agent list
opencode attach [url]
opencode attach http://10.20.30.40:4096
opencode attach http://localhost:4096
opencode auth [command]
opencode auth list
opencode auth login
opencode auth logout
opencode auth ls
opencode export [sessionID]
opencode github [command]
opencode github install
opencode github run
opencode import <file>
opencode import https://opncd.ai/s/abc123
opencode import session.json
opencode mcp [command]
opencode mcp add
opencode mcp auth [name]
opencode mcp auth list
opencode mcp auth ls
opencode mcp auth my-oauth-server
opencode mcp auth sentry
opencode mcp debug <name>
opencode mcp debug my-oauth-server
opencode mcp list
opencode mcp logout [name]
opencode mcp logout my-oauth-server
opencode mcp ls
opencode models --refresh
opencode models [provider]
opencode models anthropic
opencode run [message..]
opencode run Explain the use of context in Go
opencode serve
opencode serve --cors http://localhost:5173 --cors https://app.example.com
opencode serve --hostname 0.0.0.0 --port 4096
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
opencode session [command]
opencode session list
opencode stats
opencode uninstall
opencode upgrade
opencode upgrade [target]
opencode upgrade v0.1.48
opencode web
opencode web --cors https://example.com
opencode web --hostname 0.0.0.0
opencode web --mdns
opencode web --mdns --mdns-domain myproject.local
opencode web --port 4096
opencode web --port 4096 --hostname 0.0.0.0
opencode.server.close()
```
## Slash commands and routes
```text
/agent
/auth/:id
/clear
/command
/config
/config/providers
/connect
/continue
/doc
/editor
/event
/experimental/tool?provider=<p>&model=<m>
/experimental/tool/ids
/export
/file?path=<path>
/file/content?path=<p>
/file/status
/find?pattern=<pat>
/find/file
/find/file?query=<q>
/find/symbol?query=<q>
/formatter
/global/event
/global/health
/help
/init
/instance/dispose
/log
/lsp
/mcp
/mnt/
/mnt/c/
/mnt/d/
/models
/oc
/opencode
/path
/project
/project/current
/provider
/provider/{id}/oauth/authorize
/provider/{id}/oauth/callback
/provider/auth
/q
/quit
/redo
/resume
/session
/session/:id
/session/:id/abort
/session/:id/children
/session/:id/command
/session/:id/diff
/session/:id/fork
/session/:id/init
/session/:id/message
/session/:id/message/:messageID
/session/:id/permissions/:permissionID
/session/:id/prompt_async
/session/:id/revert
/session/:id/share
/session/:id/shell
/session/:id/summarize
/session/:id/todo
/session/:id/unrevert
/session/status
/share
/summarize
/theme
/tui
/tui/append-prompt
/tui/clear-prompt
/tui/control/next
/tui/control/response
/tui/execute-command
/tui/open-help
/tui/open-models
/tui/open-sessions
/tui/open-themes
/tui/show-toast
/tui/submit-prompt
/undo
/Users/username
/Users/username/projects/*
/vcs
```
## CLI flags and short options
```text
--agent
--attach
--command
--continue
--cors
--cwd
--days
--dir
--dry-run
--event
--file
--force
--fork
--format
--help
--hostname
--hostname 0.0.0.0
--keep-config
--keep-data
--log-level
--max-count
--mdns
--mdns-domain
--method
--model
--models
--port
--print-logs
--project
--prompt
--refresh
--session
--share
--title
--token
--tools
--verbose
--version
--wait
-c
-d
-f
-h
-m
-n
-s
-v
```
## Environment variables
```text
AI_API_URL
AI_FLOW_CONTEXT
AI_FLOW_EVENT
AI_FLOW_INPUT
AICORE_DEPLOYMENT_ID
AICORE_RESOURCE_GROUP
AICORE_SERVICE_KEY
ANTHROPIC_API_KEY
AWS_ACCESS_KEY_ID
AWS_BEARER_TOKEN_BEDROCK
AWS_PROFILE
AWS_REGION
AWS_ROLE_ARN
AWS_SECRET_ACCESS_KEY
AWS_WEB_IDENTITY_TOKEN_FILE
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
AZURE_RESOURCE_NAME
CI_PROJECT_DIR
CI_SERVER_FQDN
CI_WORKLOAD_REF
CLOUDFLARE_ACCOUNT_ID
CLOUDFLARE_API_TOKEN
CLOUDFLARE_GATEWAY_ID
CONTEXT7_API_KEY
GITHUB_TOKEN
GITLAB_AI_GATEWAY_URL
GITLAB_HOST
GITLAB_INSTANCE_URL
GITLAB_OAUTH_CLIENT_ID
GITLAB_TOKEN
GITLAB_TOKEN_OPENCODE
GOOGLE_APPLICATION_CREDENTIALS
GOOGLE_CLOUD_PROJECT
HTTP_PROXY
HTTPS_PROXY
K2_
MY_API_KEY
MY_ENV_VAR
MY_MCP_CLIENT_ID
MY_MCP_CLIENT_SECRET
NO_PROXY
NODE_ENV
NODE_EXTRA_CA_CERTS
NPM_AUTH_TOKEN
OC_ALLOW_WAYLAND
OPENCODE_API_KEY
OPENCODE_AUTH_JSON
OPENCODE_AUTO_SHARE
OPENCODE_CLIENT
OPENCODE_CONFIG
OPENCODE_CONFIG_CONTENT
OPENCODE_CONFIG_DIR
OPENCODE_DISABLE_AUTOCOMPACT
OPENCODE_DISABLE_AUTOUPDATE
OPENCODE_DISABLE_CLAUDE_CODE
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
OPENCODE_DISABLE_DEFAULT_PLUGINS
OPENCODE_DISABLE_FILETIME_CHECK
OPENCODE_DISABLE_LSP_DOWNLOAD
OPENCODE_DISABLE_MODELS_FETCH
OPENCODE_DISABLE_PRUNE
OPENCODE_DISABLE_TERMINAL_TITLE
OPENCODE_ENABLE_EXA
OPENCODE_ENABLE_EXPERIMENTAL_MODELS
OPENCODE_EXPERIMENTAL
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER
OPENCODE_EXPERIMENTAL_EXA
OPENCODE_EXPERIMENTAL_FILEWATCHER
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY
OPENCODE_EXPERIMENTAL_LSP_TOOL
OPENCODE_EXPERIMENTAL_LSP_TY
OPENCODE_EXPERIMENTAL_MARKDOWN
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX
OPENCODE_EXPERIMENTAL_OXFMT
OPENCODE_EXPERIMENTAL_PLAN_MODE
OPENCODE_FAKE_VCS
OPENCODE_GIT_BASH_PATH
OPENCODE_MODEL
OPENCODE_MODELS_URL
OPENCODE_PERMISSION
OPENCODE_PORT
OPENCODE_SERVER_PASSWORD
OPENCODE_SERVER_USERNAME
PROJECT_ROOT
RESOURCE_NAME
RUST_LOG
VARIABLE_NAME
VERTEX_LOCATION
XDG_CONFIG_HOME
```
## Package/module identifiers
```text
../../../config.mjs
@astrojs/starlight/components
@opencode-ai/plugin
@opencode-ai/sdk
path
shescape
zod
@
@ai-sdk/anthropic
@ai-sdk/cerebras
@ai-sdk/google
@ai-sdk/openai
@ai-sdk/openai-compatible
@File#L37-42
@modelcontextprotocol/server-everything
@opencode
```
## GitHub owner/repo slugs referenced in docs
```text
24601/opencode-zellij-namer
angristan/opencode-wakatime
anomalyco/opencode
apps/opencode-agent
athal7/opencode-devcontainers
awesome-opencode/awesome-opencode
backnotprop/plannotator
ben-vargas/ai-sdk-provider-opencode-sdk
btriapitsyn/openchamber
BurntSushi/ripgrep
Cluster444/agentic
code-yeongyu/oh-my-opencode
darrenhinde/opencode-agents
different-ai/opencode-scheduler
different-ai/openwork
features/copilot
folke/tokyonight.nvim
franlol/opencode-md-table-formatter
ggml-org/llama.cpp
ghoulr/opencode-websearch-cited.git
H2Shami/opencode-helicone-session
hosenur/portal
jamesmurdza/daytona
jenslys/opencode-gemini-auth
JRedeker/opencode-morph-fast-apply
JRedeker/opencode-shell-strategy
kdcokenny/ocx
kdcokenny/opencode-background-agents
kdcokenny/opencode-notify
kdcokenny/opencode-workspace
kdcokenny/opencode-worktree
login/device
mohak34/opencode-notifier
morhetz/gruvbox
mtymek/opencode-obsidian
NeuralNomadsAI/CodeNomad
nick-vi/opencode-type-inject
NickvanDyke/opencode.nvim
NoeFabris/opencode-antigravity-auth
nordtheme/nord
numman-ali/opencode-openai-codex-auth
olimorris/codecompanion.nvim
panta82/opencode-notificator
rebelot/kanagawa.nvim
remorses/kimaki
sainnhe/everforest
shekohex/opencode-google-antigravity-auth
shekohex/opencode-pty.git
spoons-and-mirrors/subtask2
sudo-tee/opencode.nvim
supermemoryai/opencode-supermemory
Tarquinen/opencode-dynamic-context-pruning
Th3Whit3Wolf/one-nvim
upstash/context7
vtemian/micode
vtemian/octto
yetone/avante.nvim
zenobi-us/opencode-plugin-template
zenobi-us/opencode-skillful
```
## Paths, filenames, globs, and URLs
```text
./.opencode/themes/*.json
./<project-slug>/storage/
./config/#custom-directory
./global/storage/
.agents/skills/*/SKILL.md
.agents/skills/<name>/SKILL.md
.clang-format
.claude
.claude/skills
.claude/skills/*/SKILL.md
.claude/skills/<name>/SKILL.md
.env
.github/workflows/opencode.yml
.gitignore
.gitlab-ci.yml
.ignore
.NET SDK
.npmrc
.ocamlformat
.opencode
.opencode/
.opencode/agents/
.opencode/commands/
.opencode/commands/test.md
.opencode/modes/
.opencode/plans/*.md
.opencode/plugins/
.opencode/skills/<name>/SKILL.md
.opencode/skills/git-release/SKILL.md
.opencode/tools/
.well-known/opencode
{ type: "raw" \| "patch", content: string }
{file:path/to/file}
**/*.js
%USERPROFILE%/intelephense/license.txt
%USERPROFILE%\.cache\opencode
%USERPROFILE%\.config\opencode\opencode.jsonc
%USERPROFILE%\.config\opencode\plugins
%USERPROFILE%\.local\share\opencode
%USERPROFILE%\.local\share\opencode\log
<project-root>/.opencode/themes/*.json
<providerId>/<modelId>
<your-project>/.opencode/plugins/
~
~/...
~/.agents/skills/*/SKILL.md
~/.agents/skills/<name>/SKILL.md
~/.aws/credentials
~/.bashrc
~/.cache/opencode
~/.cache/opencode/node_modules/
~/.claude/CLAUDE.md
~/.claude/skills/
~/.claude/skills/*/SKILL.md
~/.claude/skills/<name>/SKILL.md
~/.config/opencode
~/.config/opencode/AGENTS.md
~/.config/opencode/agents/
~/.config/opencode/commands/
~/.config/opencode/modes/
~/.config/opencode/opencode.json
~/.config/opencode/opencode.jsonc
~/.config/opencode/plugins/
~/.config/opencode/skills/*/SKILL.md
~/.config/opencode/skills/<name>/SKILL.md
~/.config/opencode/themes/*.json
~/.config/opencode/tools/
~/.config/zed/settings.json
~/.local/share
~/.local/share/opencode/
~/.local/share/opencode/auth.json
~/.local/share/opencode/log/
~/.local/share/opencode/mcp-auth.json
~/.local/share/opencode/opencode.jsonc
~/.npmrc
~/.zshrc
~/code/
~/Library/Application Support
~/projects/*
~/projects/personal/
${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts
$HOME/intelephense/license.txt
$HOME/projects/*
$XDG_CONFIG_HOME/opencode/themes/*.json
agent/
agents/
build/
commands/
dist/
http://<wsl-ip>:4096
http://127.0.0.1:8080/callback
http://localhost:<port>
http://localhost:4096
http://localhost:4096/doc
https://app.example.com
https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/
https://opencode.ai/zen/v1/chat/completions
https://opencode.ai/zen/v1/messages
https://opencode.ai/zen/v1/models/gemini-3-flash
https://opencode.ai/zen/v1/models/gemini-3-pro
https://opencode.ai/zen/v1/responses
https://RESOURCE_NAME.openai.azure.com/
laravel/pint
log/
model: "anthropic/claude-sonnet-4-5"
modes/
node_modules/
openai/gpt-4.1
opencode.ai/config.json
opencode/<model-id>
opencode/gpt-5.1-codex
opencode/gpt-5.2-codex
opencode/kimi-k2
openrouter/google/gemini-2.5-flash
opncd.ai/s/<share-id>
packages/*/AGENTS.md
plugins/
project/
provider_id/model_id
provider/model
provider/model-id
rm -rf ~/.cache/opencode
skills/
skills/*/SKILL.md
src/**/*.ts
themes/
tools/
```
## Keybind strings
```text
alt+b
Alt+Ctrl+K
alt+d
alt+f
Cmd+Esc
Cmd+Option+K
Cmd+Shift+Esc
Cmd+Shift+G
Cmd+Shift+P
ctrl+a
ctrl+b
ctrl+d
ctrl+e
Ctrl+Esc
ctrl+f
ctrl+g
ctrl+k
Ctrl+Shift+Esc
Ctrl+Shift+P
ctrl+t
ctrl+u
ctrl+w
ctrl+x
DELETE
Shift+Enter
WIN+R
```
## Model ID strings referenced
```text
{env:OPENCODE_MODEL}
anthropic/claude-3-5-sonnet-20241022
anthropic/claude-haiku-4-20250514
anthropic/claude-haiku-4-5
anthropic/claude-sonnet-4-20250514
anthropic/claude-sonnet-4-5
gitlab/duo-chat-haiku-4-5
lmstudio/google/gemma-3n-e4b
openai/gpt-4.1
openai/gpt-5
opencode/gpt-5.1-codex
opencode/gpt-5.2-codex
opencode/kimi-k2
openrouter/google/gemini-2.5-flash
```
+1 -4
View File
@@ -1,6 +1,6 @@
---
description: git commit and push
model: opencode/glm-4.7
model: opencode/kimi-k2.5
subtask: true
---
@@ -16,15 +16,12 @@ wip:
For anything in the packages/web use the docs: prefix.
For anything in the packages/app use the ignore: prefix.
prefer to explain WHY something was done from an end user perspective instead of
WHAT was done.
do not do generic messages like "improved agent experience" be very specific
about what user facing changes were made
if there are changes do a git pull --rebase
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
## GIT DIFF
+1 -7
View File
@@ -1,6 +1,5 @@
{
"$schema": "https://opencode.ai/config.json",
// "plugin": ["opencode-openai-codex-auth"],
// "enterprise": {
// "url": "https://enterprise.dev.opencode.ai",
// },
@@ -9,12 +8,7 @@
"options": {},
},
},
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
},
},
"mcp": {},
"tools": {
"github-triage": false,
"github-pr-search": false,
+3
View File
@@ -32,6 +32,9 @@ description: Use this when you are working on file operations like reading, writ
- Decode tool stderr with `Bun.readableStreamToText`.
- For large writes, use `Bun.write(Bun.file(path), text)`.
NOTE: Bun.file(...).exists() will return `false` if the value is a directory.
Use Filesystem.exists(...) instead if path can be file or directory
## Quick checklist
- Use Bun APIs first.
+1 -1
View File
@@ -1,4 +1,4 @@
Use this tool to assign and/or label a Github issue.
Use this tool to assign and/or label a GitHub issue.
You can assign the following users:
- thdxr
+2 -1
View File
@@ -1 +1,2 @@
sst-env.d.ts
sst-env.d.ts
packages/desktop/src/bindings.ts
@@ -0,0 +1,5 @@
github-policies:
runners:
allowed_groups:
- "GitHub Actions"
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
+65 -34
View File
@@ -1,82 +1,113 @@
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`.
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
## Style Guide
### General Principles
- Keep things in one function unless composable or reusable
- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context
- Avoid `try`/`catch` where possible
- Avoid using the `any` type
- Prefer single word variable names where possible
- Use Bun APIs when possible, like `Bun.file()`
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
### Avoid let statements
### Naming
We don't like `let` statements, especially combined with if/else statements.
Prefer `const`.
Good:
Prefer single word names for variables and functions. Only use multiple words if necessary.
```ts
const foo = condition ? 1 : 2
// Good
const foo = 1
function journal(dir: string) {}
// Bad
const fooBar = 1
function prepareJournal(dir: string) {}
```
Bad:
Reduce total variable count by inlining when a value is only used once.
```ts
let foo
// Good
const journal = await Bun.file(path.join(dir, "journal.json")).json()
// Bad
const journalPath = path.join(dir, "journal.json")
const journal = await Bun.file(journalPath).json()
```
### Destructuring
Avoid unnecessary destructuring. Use dot notation to preserve context.
```ts
// Good
obj.a
obj.b
// Bad
const { a, b } = obj
```
### Variables
Prefer `const` over `let`. Use ternaries or early returns instead of reassignment.
```ts
// Good
const foo = condition ? 1 : 2
// Bad
let foo
if (condition) foo = 1
else foo = 2
```
### Avoid else statements
### Control Flow
Prefer early returns or using an `iife` to avoid else statements.
Good:
Avoid `else` statements. Prefer early returns.
```ts
// Good
function foo() {
if (condition) return 1
return 2
}
```
Bad:
```ts
// Bad
function foo() {
if (condition) return 1
else return 2
}
```
### Prefer single word naming
### Schema Definitions (Drizzle)
Try your best to find a single word name for your variables, functions, etc.
Only use multiple words if you cannot.
Good:
Use snake_case for field names so column names don't need to be redefined as strings.
```ts
const foo = 1
const bar = 2
const baz = 3
```
// Good
const table = sqliteTable("session", {
id: text().primaryKey(),
project_id: text().notNull(),
created_at: integer().notNull(),
})
Bad:
```ts
const fooBar = 1
const barBaz = 2
const bazFoo = 3
// Bad
const table = sqliteTable("session", {
id: text("id").primaryKey(),
projectID: text("project_id").notNull(),
createdAt: integer("created_at").notNull(),
})
```
## Testing
You MUST avoid using `mocks` as much as possible.
Tests MUST test actual implementation, do not duplicate logic into a test.
- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
+46
View File
@@ -258,3 +258,49 @@ These are not strictly enforced, they are just general guidelines:
## Feature Requests
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
## Trust & Vouch System
This project uses [vouch](https://github.com/mitchellh/vouch) to manage contributor trust. The vouch list is maintained in [`.github/VOUCHED.td`](.github/VOUCHED.td).
### How it works
- **Vouched users** are explicitly trusted contributors.
- **Denounced users** are explicitly blocked. Issues and pull requests from denounced users are automatically closed. If you have been denounced, you can request to be unvouched by reaching out to a maintainer on [Discord](https://opencode.ai/discord)
- **Everyone else** can participate normally — you don't need to be vouched to open issues or PRs.
### For maintainers
Collaborators with write access can manage the vouch list by commenting on any issue:
- `vouch` — vouch for the issue author
- `vouch @username` — vouch for a specific user
- `denounce` — denounce the issue author
- `denounce @username` — denounce a specific user
- `denounce @username <reason>` — denounce with a reason
- `unvouch` / `unvouch @username` — remove someone from the list
Changes are committed automatically to `.github/VOUCHED.td`.
### Denouncement policy
Denouncement is reserved for users who repeatedly submit low-quality AI-generated contributions, spam, or otherwise act in bad faith. It is not used for disagreements or honest mistakes.
## Issue Requirements
All issues **must** use one of our issue templates:
- **Bug report** — for reporting bugs (requires a description)
- **Feature request** — for suggesting enhancements (requires verification checkbox and description)
- **Question** — for asking questions (requires the question)
Blank issues are not allowed. When a new issue is opened, an automated check verifies that it follows a template and meets our contributing guidelines. If an issue doesn't meet the requirements, you'll receive a comment explaining what needs to be fixed and have **2 hours** to edit the issue. After that, it will be automatically closed.
Issues may be flagged for:
- Not using a template
- Required fields left empty or filled with placeholder text
- AI-generated walls of text
- Missing meaningful content
If you believe your issue was incorrectly flagged, let a maintainer know.
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS و Linux (موصى به، دائما محدث)
brew install opencode # macOS و Linux (صيغة brew الرسمية، تحديث اقل)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # اي نظام
nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث فرع dev
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS e Linux (recomendado, sempre atualizado)
brew install opencode # macOS e Linux (fórmula oficial do brew, atualiza menos)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # qualquer sistema
nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch dev mais recente
```
+137
View File
@@ -0,0 +1,137 @@
<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">OpenCode je open source AI agent za programiranje.</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/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.bs.md">Bosanski</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Instalacija
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Package manageri
npm i -g opencode-ai@latest # ili bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS i Linux (preporučeno, uvijek ažurno)
brew install opencode # macOS i Linux (zvanična brew formula, rjeđe se ažurira)
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # Bilo koji OS
nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji dev branch
```
> [!TIP]
> Ukloni verzije starije od 0.1.x prije instalacije.
### Desktop aplikacija (BETA)
OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice izdanja](https://github.com/anomalyco/opencode/releases) ili sa [opencode.ai/download](https://opencode.ai/download).
| Platforma | Preuzimanje |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ili AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Instalacijski direktorij
Instalacijska skripta koristi sljedeći redoslijed prioriteta za putanju instalacije:
1. `$OPENCODE_INSTALL_DIR` - Prilagođeni instalacijski direktorij
2. `$XDG_BIN_DIR` - Putanja usklađena sa XDG Base Directory specifikacijom
3. `$HOME/bin` - Standardni korisnički bin direktorij (ako postoji ili se može kreirati)
4. `$HOME/.opencode/bin` - Podrazumijevana rezervna lokacija
```bash
# Primjeri
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
```
### Agenti
OpenCode uključuje dva ugrađena agenta između kojih možeš prebacivati tasterom `Tab`.
- **build** - Podrazumijevani agent sa punim pristupom za razvoj
- **plan** - Agent samo za čitanje za analizu i istraživanje koda
- Podrazumijevano zabranjuje izmjene datoteka
- Traži dozvolu prije pokretanja bash komandi
- Idealan za istraživanje nepoznatih codebase-ova ili planiranje izmjena
Uključen je i **general** pod-agent za složene pretrage i višekoračne zadatke.
Koristi se interno i može se pozvati pomoću `@general` u porukama.
Saznaj više o [agentima](https://opencode.ai/docs/agents).
### Dokumentacija
Za više informacija o konfiguraciji OpenCode-a, [**pogledaj dokumentaciju**](https://opencode.ai/docs).
### Doprinosi
Ako želiš doprinositi OpenCode-u, pročitaj [upute za doprinošenje](./CONTRIBUTING.md) prije slanja pull requesta.
### Gradnja na OpenCode-u
Ako radiš na projektu koji je povezan s OpenCode-om i koristi "opencode" kao dio naziva, npr. "opencode-dashboard" ili "opencode-mobile", dodaj napomenu u svoj README da projekat nije napravio OpenCode tim i da nije povezan s nama.
### FAQ
#### Po čemu se razlikuje od Claude Code-a?
Po mogućnostima je vrlo sličan Claude Code-u. Ključne razlike su:
- 100% open source
- Nije vezan za jednog provajdera. Iako preporučujemo modele koje nudimo kroz [OpenCode Zen](https://opencode.ai/zen), OpenCode možeš koristiti s Claude, OpenAI, Google ili čak lokalnim modelima. Kako modeli napreduju, razlike među njima će se smanjivati, a cijene padati, zato je nezavisnost od provajdera važna.
- LSP podrška odmah po instalaciji
- Fokus na TUI. OpenCode grade neovim korisnici i kreatori [terminal.shop](https://terminal.shop); pomjeraćemo granice onoga što je moguće u terminalu.
- Klijent/server arhitektura. To, recimo, omogućava da OpenCode radi na tvom računaru dok ga daljinski koristiš iz mobilne aplikacije, što znači da je TUI frontend samo jedan od mogućih klijenata.
---
**Pridruži se našoj zajednici** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS og Linux (anbefalet, altid up to date)
brew install opencode # macOS og Linux (officiel brew formula, opdateres sjældnere)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # alle OS
nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS und Linux (empfohlen, immer aktuell)
brew install opencode # macOS und Linux (offizielle Brew-Formula, seltener aktualisiert)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # jedes Betriebssystem
nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neuesten dev-Branch
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS y Linux (recomendado, siempre al día)
brew install opencode # macOS y Linux (fórmula oficial de brew, se actualiza menos)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # cualquier sistema
nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama dev más reciente
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS et Linux (recommandé, toujours à jour)
brew install opencode # macOS et Linux (formule officielle brew, mise à jour moins fréquente)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # n'importe quel OS
nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branche dev la plus récente
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato)
brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # Qualsiasi OS
nix run nixpkgs#opencode # oppure github:anomalyco/opencode per lultima branch di sviluppo
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS と Linux(推奨。常に最新)
brew install opencode # macOS と Linux(公式 brew formula。更新頻度は低め)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # どのOSでも
nix run nixpkgs#opencode # または github:anomalyco/opencode で最新 dev ブランチ
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS 및 Linux (권장, 항상 최신)
brew install opencode # macOS 및 Linux (공식 brew formula, 업데이트 빈도 낮음)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # 어떤 OS든
nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신 dev 브랜치
```
+13 -10
View File
@@ -27,10 +27,12 @@
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.bs.md">Bosanski</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a>
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -49,7 +51,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date)
brew install opencode # macOS and Linux (official brew formula, updated less)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # Any OS
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
```
@@ -81,7 +84,7 @@ The install script respects the following priority order for the installation pa
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
3. `$HOME/bin` - Standard user binary directory (if it exists or can be created)
4. `$HOME/.opencode/bin` - Default fallback
```bash
@@ -94,20 +97,20 @@ XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
OpenCode includes two built-in agents you can switch between with the `Tab` key.
- **build** - Default, full access agent for development work
- **build** - Default, full-access agent for development work
- **plan** - Read-only agent for analysis and code exploration
- Denies file edits by default
- Asks permission before running bash commands
- Ideal for exploring unfamiliar codebases or planning changes
Also, included is a **general** subagent for complex searches and multistep 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).
### Documentation
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
For more info on how to configure OpenCode, [**head over to our docs**](https://opencode.ai/docs).
### Contributing
@@ -115,7 +118,7 @@ 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 any way.
If you are working on a project that's related to OpenCode and is using "opencode" as 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
@@ -124,10 +127,10 @@ If you are working on a project that's related to OpenCode and is using "opencod
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- Out of the box LSP support
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
- Out-of-the-box LSP support
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
---
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS og Linux (anbefalt, alltid oppdatert)
brew install opencode # macOS og Linux (offisiell brew-formel, oppdateres sjeldnere)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # alle OS
nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste dev-branch
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS i Linux (polecane, zawsze aktualne)
brew install opencode # macOS i Linux (oficjalna formuła brew, rzadziej aktualizowana)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # dowolny system
nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowszej gałęzi dev
```
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS и Linux (рекомендуем, всегда актуально)
brew install opencode # macOS и Linux (официальная формула brew, обновляется реже)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # любая ОС
nix run nixpkgs#opencode # или github:anomalyco/opencode для самой свежей ветки dev
```
+4 -2
View File
@@ -30,7 +30,8 @@
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a>
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -49,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ)
brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # ระบบปฏิบัติการใดก็ได้
nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด
```
+136
View File
@@ -0,0 +1,136 @@
<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">Açık kaynaklı yapay zeka kodlama asistanı.</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/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Kurulum
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Paket yöneticileri
npm i -g opencode-ai@latest # veya bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS ve Linux (önerilir, her zaman güncel)
brew install opencode # macOS ve Linux (resmi brew formülü, daha az güncellenir)
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # Tüm işletim sistemleri
nix run nixpkgs#opencode # veya en güncel geliştirme dalı için github:anomalyco/opencode
```
> [!TIP]
> Kurulumdan önce 0.1.x'ten eski sürümleri kaldırın.
### Masaüstü Uygulaması (BETA)
OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm sayfasından](https://github.com/anomalyco/opencode/releases) veya [opencode.ai/download](https://opencode.ai/download) adresinden indirebilirsiniz.
| Platform | İndirme |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` veya AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Kurulum Dizini (Installation Directory)
Kurulum betiği (install script), kurulum yolu (installation path) için aşağıdaki öncelik sırasını takip eder:
1. `$OPENCODE_INSTALL_DIR` - Özel kurulum dizini
2. `$XDG_BIN_DIR` - XDG Base Directory Specification uyumlu yol
3. `$HOME/bin` - Standart kullanıcı binary dizini (varsa veya oluşturulabiliyorsa)
4. `$HOME/.opencode/bin` - Varsayılan yedek konum
```bash
# Örnekler
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
```
### Ajanlar
OpenCode, `Tab` tuşuyla aralarında geçiş yapabileceğiniz iki yerleşik (built-in) ajan içerir.
- **build** - Varsayılan, geliştirme çalışmaları için tam erişimli ajan
- **plan** - Analiz ve kod keşfi için salt okunur ajan
- Varsayılan olarak dosya düzenlemelerini reddeder
- Bash komutlarını çalıştırmadan önce izin ister
- Tanımadığınız kod tabanlarını keşfetmek veya değişiklikleri planlamak için ideal
Ayrıca, karmaşık aramalar ve çok adımlı görevler için bir **genel** alt ajan bulunmaktadır.
Bu dahili olarak kullanılır ve mesajlarda `@general` ile çağrılabilir.
[Ajanlar](https://opencode.ai/docs/agents) hakkında daha fazla bilgi edinin.
### Dokümantasyon
OpenCode'u nasıl yapılandıracağınız hakkında daha fazla bilgi için [**dokümantasyonumuza göz atın**](https://opencode.ai/docs).
### Katkıda Bulunma
OpenCode'a katkıda bulunmak istiyorsanız, lütfen bir pull request göndermeden önce [katkıda bulunma dokümanlarımızı](./CONTRIBUTING.md) okuyun.
### OpenCode Üzerine Geliştirme
OpenCode ile ilgili bir proje üzerinde çalışıyorsanız ve projenizin adının bir parçası olarak "opencode" kullanıyorsanız (örneğin, "opencode-dashboard" veya "opencode-mobile"), lütfen README dosyanıza projenin OpenCode ekibi tarafından geliştirilmediğini ve bizimle hiçbir şekilde bağlantılı olmadığını belirten bir not ekleyin.
### SSS
#### Bu Claude Code'dan nasıl farklı?
Yetenekler açısından Claude Code'a çok benzer. İşte temel farklar:
- %100 açık kaynak
- Herhangi bir sağlayıcıya bağlı değil. [OpenCode Zen](https://opencode.ai/zen) üzerinden sunduğumuz modelleri önermekle birlikte; OpenCode, Claude, OpenAI, Google veya hatta yerel modellerle kullanılabilir. Modeller geliştikçe aralarındaki farklar kapanacak ve fiyatlar düşecek, bu nedenle sağlayıcıdan bağımsız olmak önemlidir.
- Kurulum gerektirmeyen hazır LSP desteği
- TUI odaklı yaklaşım. OpenCode, neovim kullanıcıları ve [terminal.shop](https://terminal.shop)'un geliştiricileri tarafından geliştirilmektedir; terminalde olabileceklerin sınırlarını zorlayacağız.
- İstemci/sunucu (client/server) mimarisi. Bu, örneğin OpenCode'un bilgisayarınızda çalışması ve siz onu bir mobil uygulamadan uzaktan yönetmenizi sağlar. TUI arayüzü olası istemcilerden sadece biridir.
---
**Topluluğumuza katılın** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
+5 -2
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS 和 Linux(推荐,始终保持最新)
brew install opencode # macOS 和 Linux(官方 brew formula,更新频率较低)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # 任意系统
nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最新 dev 分支
```
+6 -3
View File
@@ -29,7 +29,9 @@
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a> |
<a href="README.tr.md">Türkçe</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -48,7 +50,8 @@ scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS 與 Linux(推薦,始終保持最新)
brew install opencode # macOS 與 Linux(官方 brew formula,更新頻率較低)
paru -S opencode-bin # Arch Linux
sudo pacman -S opencode # Arch Linux (Stable)
paru -S opencode-bin # Arch Linux (Latest from AUR)
mise use -g opencode # 任何作業系統
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支
```
@@ -124,7 +127,7 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
- 100% 開源。
- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
- 內建 LSP (語言伺服器協定) 支援。
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造我們將不斷挑戰終端機介面的極限。
- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造我們將不斷挑戰終端機介面的極限。
- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
---
+609 -317
View File
File diff suppressed because it is too large Load Diff
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1768393167,
"narHash": "sha256-n2063BRjHde6DqAz2zavhOOiLUwA3qXt7jQYHyETjX8=",
"lastModified": 1770812194,
"narHash": "sha256-OH+lkaIKAvPXR3nITO7iYZwew2nW9Y7Xxq0yfM/UcUU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2f594d5af95d4fdac67fba60376ec11e482041cb",
"rev": "8482c7ded03bae7550f3d69884f1e611e3bd19e8",
"type": "github"
},
"original": {
+25 -18
View File
@@ -30,6 +30,26 @@
};
});
overlays = {
default =
final: _prev:
let
node_modules = final.callPackage ./nix/node_modules.nix {
inherit rev;
};
opencode = final.callPackage ./nix/opencode.nix {
inherit node_modules;
};
desktop = final.callPackage ./nix/desktop.nix {
inherit opencode;
};
in
{
inherit opencode;
opencode-desktop = desktop;
};
};
packages = forEachSystem (
pkgs:
let
@@ -42,28 +62,15 @@
desktop = pkgs.callPackage ./nix/desktop.nix {
inherit opencode;
};
# nixpkgs cpu naming to bun cpu naming
cpuMap = { x86_64 = "x64"; aarch64 = "arm64"; };
# matrix of node_modules builds - these will always fail due to fakeHash usage
# but allow computation of the correct hash from any build machine for any cpu/os
# see the update-nix-hashes workflow for usage
moduleUpdaters = pkgs.lib.listToAttrs (
pkgs.lib.concatMap (cpu:
map (os: {
name = "${cpu}-${os}_node_modules";
value = node_modules.override {
bunCpu = cpuMap.${cpu};
bunOs = os;
hash = pkgs.lib.fakeHash;
};
}) [ "linux" "darwin" ]
) [ "x86_64" "aarch64" ]
);
in
{
default = opencode;
inherit opencode desktop;
} // moduleUpdaters
# Updater derivation with fakeHash - build fails and reveals correct hash
node_modules_updater = node_modules.override {
hash = pkgs.lib.fakeHash;
};
}
);
};
}
+1 -1
View File
@@ -275,7 +275,7 @@ async function assertOpencodeConnected() {
body: {
service: "github-workflow",
level: "info",
message: "Prepare to react to Github Workflow event",
message: "Prepare to react to GitHub Workflow event",
},
})
connected = true
+17 -9
View File
@@ -133,6 +133,18 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS6"),
new sst.Secret("ZEN_MODELS7"),
new sst.Secret("ZEN_MODELS8"),
new sst.Secret("ZEN_MODELS9"),
new sst.Secret("ZEN_MODELS10"),
new sst.Secret("ZEN_MODELS11"),
new sst.Secret("ZEN_MODELS12"),
new sst.Secret("ZEN_MODELS13"),
new sst.Secret("ZEN_MODELS14"),
new sst.Secret("ZEN_MODELS15"),
new sst.Secret("ZEN_MODELS16"),
new sst.Secret("ZEN_MODELS17"),
new sst.Secret("ZEN_MODELS18"),
new sst.Secret("ZEN_MODELS19"),
new sst.Secret("ZEN_MODELS20"),
]
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
@@ -154,14 +166,10 @@ 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")
let logProcessor
if ($app.stage === "production" || $app.stage === "frank") {
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
logProcessor = new sst.cloudflare.Worker("LogProcessor", {
handler: "packages/console/function/src/log-processor.ts",
link: [HONEYCOMB_API_KEY],
})
}
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
handler: "packages/console/function/src/log-processor.ts",
link: [new sst.Secret("HONEYCOMB_API_KEY")],
})
new sst.cloudflare.x.SolidStart("Console", {
domain,
@@ -199,7 +207,7 @@ new sst.cloudflare.x.SolidStart("Console", {
transform: {
worker: {
placement: { mode: "smart" },
tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [],
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
},
},
},
+15 -1
View File
@@ -130,7 +130,7 @@ else
needs_baseline=false
if [ "$arch" = "x64" ]; then
if [ "$os" = "linux" ]; then
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then
needs_baseline=true
fi
fi
@@ -141,6 +141,20 @@ else
needs_baseline=true
fi
fi
if [ "$os" = "windows" ]; then
ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)"
out=""
if command -v powershell.exe >/dev/null 2>&1; then
out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
elif command -v pwsh >/dev/null 2>&1; then
out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
fi
out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
if [ "$out" != "true" ] && [ "$out" != "1" ]; then
needs_baseline=true
fi
fi
fi
target="$os-$arch"
@@ -1,15 +0,0 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
"files": [
{
"date": 1759827172859,
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
}
],
"hashType": "sha256"
}
-48
View File
@@ -1,48 +0,0 @@
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"}
{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"}
+3 -3
View File
@@ -45,8 +45,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
rustc
jq
makeWrapper
]
++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
] ++ lib.optionals stdenv.hostPlatform.isLinux [ wrapGAppsHook4 ];
buildInputs = lib.optionals stdenv.isLinux [
dbus
@@ -61,6 +60,7 @@ rustPlatform.buildRustPackage (finalAttrs: {
gst_all_1.gstreamer
gst_all_1.gst-plugins-base
gst_all_1.gst-plugins-good
gst_all_1.gst-plugins-bad
];
strictDeps = true;
@@ -97,4 +97,4 @@ rustPlatform.buildRustPackage (finalAttrs: {
mainProgram = "opencode-desktop";
inherit (opencode.meta) platforms;
};
})
})
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-gUWzUsk81miIrjg0fZQmsIQG4pZYmEHgzN6BaXI+lfc=",
"aarch64-linux": "sha256-gwEG75ha/ojTO2iAObTmLTtEkXIXJ7BThzfI5CqlJh8=",
"aarch64-darwin": "sha256-20RGG2GkUItCzD67gDdoSLfexttM8abS//FKO9bfjoM=",
"x86_64-darwin": "sha256-i2VawFuR1UbjPVYoybU6aJDJfFo0tcvtl1aM31Y2mTQ="
"x86_64-linux": "sha256-5pgd2xuvIIkTbIOGIdK5MIXo6O9qRpvk1RKQZ1e1R+8=",
"aarch64-linux": "sha256-FZiHwihM4b82ipQ9XfW08X+sd5CvZhx/+pU/8X1zsns=",
"aarch64-darwin": "sha256-iZv0w1NthV53pY5uvuf3JlI14GeKmCu7WHwGSRdEQeM=",
"x86_64-darwin": "sha256-c3Zm3P1goFPgg3vNAZPMFOhHX/gyTmsCN/PKbGO/v0E="
}
}
+9 -11
View File
@@ -2,8 +2,6 @@
lib,
stdenvNoCC,
bun,
bunCpu ? if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64",
bunOs ? if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin",
rev ? "dirty",
hash ?
(lib.pipe ./hashes.json [
@@ -16,6 +14,9 @@ let
builtins.readFile
builtins.fromJSON
];
platform = stdenvNoCC.hostPlatform;
bunCpu = if platform.isAarch64 then "arm64" else "x64";
bunOs = if platform.isLinux then "linux" else "darwin";
in
stdenvNoCC.mkDerivation {
pname = "opencode-node_modules";
@@ -29,7 +30,7 @@ stdenvNoCC.mkDerivation {
../bun.lock
../package.json
../patches
../install
../install # required by desktop build (cli.rs include_str!)
]
);
};
@@ -39,23 +40,22 @@ stdenvNoCC.mkDerivation {
"SOCKS_SERVER"
];
nativeBuildInputs = [
bun
];
nativeBuildInputs = [ bun ];
dontConfigure = true;
buildPhase = ''
runHook preBuild
export HOME=$(mktemp -d)
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
bun install \
--cpu="${bunCpu}" \
--os="${bunOs}" \
--filter '!./' \
--filter './packages/opencode' \
--filter './packages/desktop' \
--frozen-lockfile \
--ignore-scripts \
--no-progress \
--linker=isolated
--no-progress
bun --bun ${./scripts/canonicalize-node-modules.ts}
bun --bun ${./scripts/normalize-bun-binaries.ts}
runHook postBuild
@@ -63,10 +63,8 @@ stdenvNoCC.mkDerivation {
installPhase = ''
runHook preInstall
mkdir -p $out
find . -type d -name node_modules -exec cp -R --parents {} $out \;
runHook postInstall
'';
+2 -1
View File
@@ -34,6 +34,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
'';
env.MODELS_DEV_API_JSON = "${models-dev}/dist/_api.json";
env.OPENCODE_DISABLE_MODELS_FETCH = true;
env.OPENCODE_VERSION = finalAttrs.version;
env.OPENCODE_CHANNEL = "local";
@@ -79,7 +80,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
writableTmpDirAsHomeHook
];
doInstallCheck = true;
versionCheckKeepEnvironment = [ "HOME" ];
versionCheckKeepEnvironment = [ "HOME" "OPENCODE_DISABLE_MODELS_FETCH" ];
versionCheckProgramArg = "--version";
passthru = {
+22 -34
View File
@@ -1,27 +1,32 @@
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
import { join, relative } from "path"
type SemverLike = {
valid: (value: string) => string | null
rcompare: (left: string, right: string) => number
}
type Entry = {
dir: string
version: string
label: string
}
async function isDirectory(path: string) {
try {
const info = await lstat(path)
return info.isDirectory()
} catch {
return false
}
}
const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x")
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const linkRoot = join(bunRoot, "node_modules")
const directories = (await readdir(bunRoot)).sort()
const versions = new Map<string, Entry[]>()
for (const entry of directories) {
const full = join(bunRoot, entry)
const info = await lstat(full)
if (!info.isDirectory()) {
if (!(await isDirectory(full))) {
continue
}
const parsed = parseEntry(entry)
@@ -29,37 +34,23 @@ for (const entry of directories) {
continue
}
const list = versions.get(parsed.name) ?? []
list.push({ dir: full, version: parsed.version, label: entry })
list.push({ dir: full, version: parsed.version })
versions.set(parsed.name, list)
}
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
| SemverLike
| {
default: SemverLike
}
const semver = "default" in semverModule ? semverModule.default : semverModule
const selections = new Map<string, Entry>()
for (const [slug, list] of versions) {
list.sort((a, b) => {
const left = semver.valid(a.version)
const right = semver.valid(b.version)
if (left && right) {
const delta = semver.rcompare(left, right)
if (delta !== 0) {
return delta
}
}
if (left && !right) {
return -1
}
if (!left && right) {
return 1
}
const aValid = isValidSemver(a.version)
const bValid = isValidSemver(b.version)
if (aValid && bValid) return -Bun.semver.order(a.version, b.version)
if (aValid) return -1
if (bValid) return 1
return b.version.localeCompare(a.version)
})
selections.set(slug, list[0])
const first = list[0]
if (first) selections.set(slug, first)
}
await rm(linkRoot, { recursive: true, force: true })
@@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
await mkdir(parent, { recursive: true })
const linkPath = join(parent, leaf)
const desired = join(entry.dir, "node_modules", slug)
const exists = await lstat(desired)
.then((info) => info.isDirectory())
.catch(() => false)
if (!exists) {
if (!(await isDirectory(desired))) {
continue
}
const relativeTarget = relative(parent, desired)
+4 -12
View File
@@ -8,7 +8,7 @@ type PackageManifest = {
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const bunEntries = (await safeReadDir(bunRoot)).sort()
const bunEntries = (await readdir(bunRoot)).sort()
let rewritten = 0
for (const entry of bunEntries) {
@@ -45,11 +45,11 @@ for (const entry of bunEntries) {
}
}
console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`)
async function collectPackages(modulesRoot: string) {
const found: string[] = []
const topLevel = (await safeReadDir(modulesRoot)).sort()
const topLevel = (await readdir(modulesRoot)).sort()
for (const name of topLevel) {
if (name === ".bin" || name === ".bun") {
continue
@@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) {
continue
}
if (name.startsWith("@")) {
const scoped = (await safeReadDir(full)).sort()
const scoped = (await readdir(full)).sort()
for (const child of scoped) {
const scopedDir = join(full, child)
if (await isDirectory(scopedDir)) {
@@ -121,14 +121,6 @@ async function isDirectory(path: string) {
}
}
async function safeReadDir(path: string) {
try {
return await readdir(path)
} catch {
return []
}
}
function normalizeBinName(name: string) {
const slash = name.lastIndexOf("/")
if (slash >= 0) {
+10 -5
View File
@@ -4,9 +4,11 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.5",
"packageManager": "bun@1.3.9",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop tauri dev",
"dev:web": "bun --cwd packages/app dev",
"typecheck": "bun turbo typecheck",
"prepare": "husky",
"random": "echo 'Random script'",
@@ -21,7 +23,7 @@
"packages/slack"
],
"catalog": {
"@types/bun": "1.3.5",
"@types/bun": "1.3.9",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
@@ -33,12 +35,14 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.0.2",
"@pierre/diffs": "1.1.0-beta.13",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"dompurify": "3.3.1",
"ai": "5.0.119",
"drizzle-kit": "1.0.0-beta.12-a5629fb",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
"ai": "5.0.124",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
@@ -99,6 +103,7 @@
"@types/node": "catalog:"
},
"patchedDependencies": {
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch"
}
}
+1
View File
@@ -1,2 +1,3 @@
[test]
root = "./src"
preload = ["./happydom.ts"]
+176
View File
@@ -0,0 +1,176 @@
# E2E Testing Guide
## Build/Lint/Test Commands
```bash
# Run all e2e tests
bun test:e2e
# Run specific test file
bun test:e2e -- app/home.spec.ts
# Run single test by title
bun test:e2e -- -g "home renders and shows core entrypoints"
# Run tests with UI mode (for debugging)
bun test:e2e:ui
# Run tests locally with full server setup
bun test:e2e:local
# View test report
bun test:e2e:report
# Typecheck
bun typecheck
```
## Test Structure
All tests live in `packages/app/e2e/`:
```
e2e/
├── fixtures.ts # Test fixtures (test, expect, gotoSession, sdk)
├── actions.ts # Reusable action helpers
├── selectors.ts # DOM selectors
├── utils.ts # Utilities (serverUrl, modKey, path helpers)
└── [feature]/
└── *.spec.ts # Test files
```
## Test Patterns
### Basic Test Structure
```typescript
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { withSession } from "../actions"
test("test description", async ({ page, sdk, gotoSession }) => {
await gotoSession() // or gotoSession(sessionID)
// Your test code
await expect(page.locator(promptSelector)).toBeVisible()
})
```
### Using Fixtures
- `page` - Playwright page
- `sdk` - OpenCode SDK client for API calls
- `gotoSession(sessionID?)` - Navigate to session
### Helper Functions
**Actions** (`actions.ts`):
- `openPalette(page)` - Open command palette
- `openSettings(page)` - Open settings dialog
- `closeDialog(page, dialog)` - Close any dialog
- `openSidebar(page)` / `closeSidebar(page)` - Toggle sidebar
- `withSession(sdk, title, callback)` - Create temp session
- `clickListItem(container, filter)` - Click list item by key/text
**Selectors** (`selectors.ts`):
- `promptSelector` - Prompt input
- `terminalSelector` - Terminal panel
- `sessionItemSelector(id)` - Session in sidebar
- `listItemSelector` - Generic list items
**Utils** (`utils.ts`):
- `modKey` - Meta (Mac) or Control (Linux/Win)
- `serverUrl` - Backend server URL
- `sessionPath(dir, id?)` - Build session URL
## Code Style Guidelines
### Imports
Always import from `../fixtures`, not `@playwright/test`:
```typescript
// ✅ Good
import { test, expect } from "../fixtures"
// ❌ Bad
import { test, expect } from "@playwright/test"
```
### Naming Conventions
- Test files: `feature-name.spec.ts`
- Test names: lowercase, descriptive: `"sidebar can be toggled"`
- Variables: camelCase
- Constants: SCREAMING_SNAKE_CASE
### Error Handling
Tests should clean up after themselves:
```typescript
test("test with cleanup", async ({ page, sdk, gotoSession }) => {
await withSession(sdk, "test session", async (session) => {
await gotoSession(session.id)
// Test code...
}) // Auto-deletes session
})
```
### Timeouts
Default: 60s per test, 10s per assertion. Override when needed:
```typescript
test.setTimeout(120_000) // For long LLM operations
test("slow test", async () => {
await expect.poll(() => check(), { timeout: 90_000 }).toBe(true)
})
```
### Selectors
Use `data-component`, `data-action`, or semantic roles:
```typescript
// ✅ Good
await page.locator('[data-component="prompt-input"]').click()
await page.getByRole("button", { name: "Open settings" }).click()
// ❌ Bad
await page.locator(".css-class-name").click()
await page.locator("#id-name").click()
```
### Keyboard Shortcuts
Use `modKey` for cross-platform compatibility:
```typescript
import { modKey } from "../utils"
await page.keyboard.press(`${modKey}+B`) // Toggle sidebar
await page.keyboard.press(`${modKey}+Comma`) // Open settings
```
## Writing New Tests
1. Choose appropriate folder or create new one
2. Import from `../fixtures`
3. Use helper functions from `../actions` and `../selectors`
4. Clean up any created resources
5. Use specific selectors (avoid CSS classes)
6. Test one feature per test file
## Local Development
For UI debugging, use:
```bash
bun test:e2e:ui
```
This opens Playwright's interactive UI for step-through debugging.
+421
View File
@@ -0,0 +1,421 @@
import { expect, type Locator, type Page } from "@playwright/test"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { execSync } from "node:child_process"
import { modKey, serverUrl } from "./utils"
import {
sessionItemSelector,
dropdownMenuTriggerSelector,
dropdownMenuContentSelector,
projectMenuTriggerSelector,
projectWorkspacesToggleSelector,
titlebarRightSelector,
popoverBodySelector,
listItemSelector,
listItemKeySelector,
listItemKeyStartsWithSelector,
workspaceItemSelector,
workspaceMenuTriggerSelector,
} from "./selectors"
import type { createSdk } from "./utils"
export async function defocus(page: Page) {
await page
.evaluate(() => {
const el = document.activeElement
if (el instanceof HTMLElement) el.blur()
})
.catch(() => undefined)
}
export async function openPalette(page: Page) {
await defocus(page)
await page.keyboard.press(`${modKey}+P`)
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("textbox").first()).toBeVisible()
return dialog
}
export async function closeDialog(page: Page, dialog: Locator) {
await page.keyboard.press("Escape")
const closed = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closed) return
await page.keyboard.press("Escape")
const closedSecond = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closedSecond) return
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(dialog).toHaveCount(0)
}
export async function isSidebarClosed(page: Page) {
const main = page.locator("main")
const classes = (await main.getAttribute("class")) ?? ""
return classes.includes("xl:border-l")
}
export async function toggleSidebar(page: Page) {
await defocus(page)
await page.keyboard.press(`${modKey}+B`)
}
export async function openSidebar(page: Page) {
if (!(await isSidebarClosed(page))) return
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
const visible = await button
.isVisible()
.then((x) => x)
.catch(() => false)
if (visible) await button.click()
if (!visible) await toggleSidebar(page)
const main = page.locator("main")
const opened = await expect(main)
.not.toHaveClass(/xl:border-l/, { timeout: 1500 })
.then(() => true)
.catch(() => false)
if (opened) return
await toggleSidebar(page)
await expect(main).not.toHaveClass(/xl:border-l/)
}
export async function closeSidebar(page: Page) {
if (await isSidebarClosed(page)) return
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
const visible = await button
.isVisible()
.then((x) => x)
.catch(() => false)
if (visible) await button.click()
if (!visible) await toggleSidebar(page)
const main = page.locator("main")
const closed = await expect(main)
.toHaveClass(/xl:border-l/, { timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closed) return
await toggleSidebar(page)
await expect(main).toHaveClass(/xl:border-l/)
}
export async function openSettings(page: Page) {
await defocus(page)
const dialog = page.getByRole("dialog")
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
const opened = await dialog
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false)
if (opened) return dialog
await page.getByRole("button", { name: "Settings" }).first().click()
await expect(dialog).toBeVisible()
return dialog
}
export async function seedProjects(page: Page, input: { directory: string; extra?: string[] }) {
await page.addInitScript(
(args: { directory: string; serverUrl: string; extra: string[] }) => {
const key = "opencode.global.dat:server"
const raw = localStorage.getItem(key)
const parsed = (() => {
if (!raw) return undefined
try {
return JSON.parse(raw) as unknown
} catch {
return undefined
}
})()
const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
const list = Array.isArray(store.list) ? store.list : []
const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
const nextProjects = { ...(projects as Record<string, unknown>) }
const add = (origin: string, directory: string) => {
const current = nextProjects[origin]
const items = Array.isArray(current) ? current : []
const existing = items.filter(
(p): p is { worktree: string; expanded?: boolean } =>
!!p &&
typeof p === "object" &&
"worktree" in p &&
typeof (p as { worktree?: unknown }).worktree === "string",
)
if (existing.some((p) => p.worktree === directory)) return
nextProjects[origin] = [{ worktree: directory, expanded: true }, ...existing]
}
const directories = [args.directory, ...args.extra]
for (const directory of directories) {
add("local", directory)
add(args.serverUrl, directory)
}
localStorage.setItem(
key,
JSON.stringify({
list,
projects: nextProjects,
lastProject,
}),
)
},
{ directory: input.directory, serverUrl, extra: input.extra ?? [] },
)
}
export async function createTestProject() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
execSync("git init", { cwd: root, stdio: "ignore" })
execSync("git add -A", { cwd: root, stdio: "ignore" })
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
cwd: root,
stdio: "ignore",
})
return root
}
export async function cleanupTestProject(directory: string) {
await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined)
}
export function sessionIDFromUrl(url: string) {
const match = /\/session\/([^/?#]+)/.exec(url)
return match?.[1]
}
export async function hoverSessionItem(page: Page, sessionID: string) {
const sessionEl = page.locator(sessionItemSelector(sessionID)).first()
await expect(sessionEl).toBeVisible()
await sessionEl.hover()
return sessionEl
}
export async function openSessionMoreMenu(page: Page, sessionID: string) {
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
const scroller = page.locator(".session-scroller").first()
await expect(scroller).toBeVisible()
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
const menu = page
.locator(dropdownMenuContentSelector)
.filter({ has: page.getByRole("menuitem", { name: /rename/i }) })
.filter({ has: page.getByRole("menuitem", { name: /archive/i }) })
.filter({ has: page.getByRole("menuitem", { name: /delete/i }) })
.first()
const opened = await menu
.isVisible()
.then((x) => x)
.catch(() => false)
if (opened) return menu
const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
await expect(menuTrigger).toBeVisible()
await menuTrigger.click()
await expect(menu).toBeVisible()
return menu
}
export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) {
const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first()
await expect(item).toBeVisible()
await item.click({ force: options?.force })
}
export async function confirmDialog(page: Page, buttonName: string | RegExp) {
const dialog = page.getByRole("dialog").first()
await expect(dialog).toBeVisible()
const button = dialog.getByRole("button").filter({ hasText: buttonName }).first()
await expect(button).toBeVisible()
await button.click()
}
export async function openSharePopover(page: Page) {
const rightSection = page.locator(titlebarRightSelector)
const shareButton = rightSection.getByRole("button", { name: "Share" }).first()
await expect(shareButton).toBeVisible()
const popoverBody = page
.locator(popoverBodySelector)
.filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) })
.first()
const opened = await popoverBody
.isVisible()
.then((x) => x)
.catch(() => false)
if (!opened) {
await shareButton.click()
await expect(popoverBody).toBeVisible()
}
return { rightSection, popoverBody }
}
export async function clickPopoverButton(page: Page, buttonName: string | RegExp) {
const button = page.getByRole("button").filter({ hasText: buttonName }).first()
await expect(button).toBeVisible()
await button.click()
}
export async function clickListItem(
container: Locator | Page,
filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string },
): Promise<Locator> {
let item: Locator
if (typeof filter === "string" || filter instanceof RegExp) {
item = container.locator(listItemSelector).filter({ hasText: filter }).first()
} else if (filter.keyStartsWith) {
item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first()
} else if (filter.key) {
item = container.locator(listItemKeySelector(filter.key)).first()
} else if (filter.text) {
item = container.locator(listItemSelector).filter({ hasText: filter.text }).first()
} else {
throw new Error("Invalid filter provided to clickListItem")
}
await expect(item).toBeVisible()
await item.click()
return item
}
export async function withSession<T>(
sdk: ReturnType<typeof createSdk>,
title: string,
callback: (session: { id: string; title: string }) => Promise<T>,
): Promise<T> {
const session = await sdk.session.create({ title }).then((r) => r.data)
if (!session?.id) throw new Error("Session create did not return an id")
try {
return await callback(session)
} finally {
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
}
}
export async function openStatusPopover(page: Page) {
await defocus(page)
const rightSection = page.locator(titlebarRightSelector)
const trigger = rightSection.getByRole("button", { name: /status/i }).first()
const popoverBody = page.locator(popoverBodySelector).filter({ has: page.locator('[data-component="tabs"]') })
const opened = await popoverBody
.isVisible()
.then((x) => x)
.catch(() => false)
if (!opened) {
await expect(trigger).toBeVisible()
await trigger.click()
await expect(popoverBody).toBeVisible()
}
return { rightSection, popoverBody }
}
export async function openProjectMenu(page: Page, projectSlug: string) {
const trigger = page.locator(projectMenuTriggerSelector(projectSlug)).first()
await expect(trigger).toHaveCount(1)
await trigger.focus()
await page.keyboard.press("Enter")
const menu = page.locator(dropdownMenuContentSelector).first()
const opened = await menu
.waitFor({ state: "visible", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (opened) {
const viewport = page.viewportSize()
const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
const y = viewport ? Math.max(viewport.height - 5, 0) : 800
await page.mouse.move(x, y)
return menu
}
await trigger.click({ force: true })
await expect(menu).toBeVisible()
const viewport = page.viewportSize()
const x = viewport ? Math.max(viewport.width - 5, 0) : 1200
const y = viewport ? Math.max(viewport.height - 5, 0) : 800
await page.mouse.move(x, y)
return menu
}
export async function setWorkspacesEnabled(page: Page, projectSlug: string, enabled: boolean) {
const current = await page
.getByRole("button", { name: "New workspace" })
.first()
.isVisible()
.then((x) => x)
.catch(() => false)
if (current === enabled) return
await openProjectMenu(page, projectSlug)
const toggle = page.locator(projectWorkspacesToggleSelector(projectSlug)).first()
await expect(toggle).toBeVisible()
await toggle.click({ force: true })
const expected = enabled ? "New workspace" : "New session"
await expect(page.getByRole("button", { name: expected }).first()).toBeVisible()
}
export async function openWorkspaceMenu(page: Page, workspaceSlug: string) {
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
await expect(item).toBeVisible()
await item.hover()
const trigger = page.locator(workspaceMenuTriggerSelector(workspaceSlug)).first()
await expect(trigger).toBeVisible()
await trigger.click({ force: true })
const menu = page.locator(dropdownMenuContentSelector).first()
await expect(menu).toBeVisible()
return menu
}
@@ -1,5 +1,5 @@
import { test, expect } from "./fixtures"
import { serverName } from "./utils"
import { test, expect } from "../fixtures"
import { serverName } from "../utils"
test("home renders and shows core entrypoints", async ({ page }) => {
await page.goto("/")
@@ -1,5 +1,6 @@
import { test, expect } from "./fixtures"
import { dirPath, promptSelector } from "./utils"
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { dirPath } from "../utils"
test("project route redirects to /session", async ({ page, directory, slug }) => {
await page.goto(dirPath(directory))
+11
View File
@@ -0,0 +1,11 @@
import { test, expect } from "../fixtures"
import { openPalette } from "../actions"
test("search palette opens and closes", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openPalette(page)
await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
})
@@ -1,5 +1,6 @@
import { test, expect } from "./fixtures"
import { serverName, serverUrl } from "./utils"
import { test, expect } from "../fixtures"
import { serverName, serverUrl } from "../utils"
import { clickListItem, closeDialog, clickMenuItem } from "../actions"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
@@ -33,31 +34,18 @@ test("can set a default server on web", async ({ page, gotoSession }) => {
const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first()
await expect(row).toBeVisible()
const menu = row.locator('[data-component="icon-button"]').last()
await menu.click()
await page.getByRole("menuitem", { name: "Set as default" }).click()
const menuTrigger = row.locator('[data-slot="dropdown-menu-trigger"]').first()
await expect(menuTrigger).toBeVisible()
await menuTrigger.click({ force: true })
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
await expect(menu).toBeVisible()
await clickMenuItem(menu, /set as default/i)
await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl)
await expect(row.getByText("Default", { exact: true })).toBeVisible()
await page.keyboard.press("Escape")
const closed = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closed) {
await page.keyboard.press("Escape")
const closedSecond = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closedSecond) {
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(dialog).toHaveCount(0)
}
}
await closeDialog(page, dialog)
await ensurePopoverOpen()
+16
View File
@@ -0,0 +1,16 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { withSession } from "../actions"
test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => {
const title = `e2e smoke ${Date.now()}`
await withSession(sdk, title, async (session) => {
await gotoSession(session.id)
const prompt = page.locator(promptSelector)
await prompt.click()
await page.keyboard.type("hello from e2e")
await expect(prompt).toContainText("hello from e2e")
})
})
@@ -0,0 +1,124 @@
import { test, expect } from "../fixtures"
import { defocus, openSidebar, withSession } from "../actions"
import { promptSelector } from "../selectors"
import { modKey } from "../utils"
test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const stamp = Date.now()
await withSession(sdk, `e2e titlebar history 1 ${stamp}`, async (one) => {
await withSession(sdk, `e2e titlebar history 2 ${stamp}`, async (two) => {
await gotoSession(one.id)
await openSidebar(page)
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
await expect(link).toBeVisible()
await link.scrollIntoViewIfNeeded()
await link.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
const back = page.getByRole("button", { name: "Back" })
const forward = page.getByRole("button", { name: "Forward" })
await expect(back).toBeVisible()
await expect(back).toBeEnabled()
await back.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
await expect(forward).toBeVisible()
await expect(forward).toBeEnabled()
await forward.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
})
})
})
test("titlebar forward is cleared after branching history from sidebar", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const stamp = Date.now()
await withSession(sdk, `e2e titlebar history a ${stamp}`, async (a) => {
await withSession(sdk, `e2e titlebar history b ${stamp}`, async (b) => {
await withSession(sdk, `e2e titlebar history c ${stamp}`, async (c) => {
await gotoSession(a.id)
await openSidebar(page)
const second = page.locator(`[data-session-id="${b.id}"] a`).first()
await expect(second).toBeVisible()
await second.scrollIntoViewIfNeeded()
await second.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${b.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
const back = page.getByRole("button", { name: "Back" })
const forward = page.getByRole("button", { name: "Forward" })
await expect(back).toBeVisible()
await expect(back).toBeEnabled()
await back.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${a.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
await openSidebar(page)
const third = page.locator(`[data-session-id="${c.id}"] a`).first()
await expect(third).toBeVisible()
await third.scrollIntoViewIfNeeded()
await third.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${c.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
await expect(forward).toBeVisible()
await expect(forward).toBeDisabled()
})
})
})
})
test("keyboard shortcuts navigate titlebar history", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const stamp = Date.now()
await withSession(sdk, `e2e titlebar shortcuts 1 ${stamp}`, async (one) => {
await withSession(sdk, `e2e titlebar shortcuts 2 ${stamp}`, async (two) => {
await gotoSession(one.id)
await openSidebar(page)
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
await expect(link).toBeVisible()
await link.scrollIntoViewIfNeeded()
await link.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
await defocus(page)
await page.keyboard.press(`${modKey}+[`)
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
await defocus(page)
await page.keyboard.press(`${modKey}+]`)
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
})
})
})
-45
View File
@@ -1,45 +0,0 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
const title = `e2e smoke context ${Date.now()}`
const created = await sdk.session.create({ title }).then((r) => r.data)
if (!created?.id) throw new Error("Session create did not return an id")
const sessionID = created.id
try {
await sdk.session.promptAsync({
sessionID,
noReply: true,
parts: [
{
type: "text",
text: "seed context",
},
],
})
await expect
.poll(async () => {
const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
return messages.length
})
.toBeGreaterThan(0)
await gotoSession(sessionID)
const contextButton = page
.locator('[data-component="button"]')
.filter({ has: page.locator('[data-component="progress-circle"]').first() })
.first()
await expect(contextButton).toBeVisible()
await contextButton.click()
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
} finally {
await sdk.session.delete({ sessionID }).catch(() => undefined)
}
})
-23
View File
@@ -1,23 +0,0 @@
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
await gotoSession()
await page.keyboard.press(`${modKey}+P`)
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
await input.fill("package.json")
const fileItem = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
await expect(fileItem).toBeVisible()
await fileItem.click()
await expect(dialog).toHaveCount(0)
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible()
})
-37
View File
@@ -1,37 +0,0 @@
import { test, expect } from "./fixtures"
test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
await gotoSession()
const toggle = page.getByRole("button", { name: "Toggle file tree" })
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
await expect(treeTabs).toBeVisible()
await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
const node = (name: string) => treeTabs.getByRole("button", { name, exact: true })
await expect(node("packages")).toBeVisible()
await node("packages").click()
await expect(node("app")).toBeVisible()
await node("app").click()
await expect(node("src")).toBeVisible()
await node("src").click()
await expect(node("components")).toBeVisible()
await node("components").click()
await expect(node("file-tree.tsx")).toBeVisible()
await node("file-tree.tsx").click()
const tab = page.getByRole("tab", { name: "file-tree.tsx" })
await expect(tab).toBeVisible()
await tab.click()
const code = page.locator('[data-component="code"]').first()
await expect(code.getByText("export default function FileTree")).toBeVisible()
})
-35
View File
@@ -1,35 +0,0 @@
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
await gotoSession()
const sep = process.platform === "win32" ? "\\" : "/"
const file = ["packages", "app", "package.json"].join(sep)
await page.keyboard.press(`${modKey}+P`)
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
await input.fill(file)
const fileItem = dialog
.locator(
'[data-slot="list-item"][data-key^="file:"][data-key*="packages"][data-key*="app"][data-key$="package.json"]',
)
.first()
await expect(fileItem).toBeVisible()
await fileItem.click()
await expect(dialog).toHaveCount(0)
const tab = page.getByRole("tab", { name: "package.json" })
await expect(tab).toBeVisible()
await tab.click()
const code = page.locator('[data-component="code"]').first()
await expect(code).toBeVisible()
await expect(code.getByText("@opencode-ai/app")).toBeVisible()
})
+31
View File
@@ -0,0 +1,31 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("can open a file tab from the search palette", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/open")
const command = page.locator('[data-slash-id="file.open"]').first()
await expect(command).toBeVisible()
await page.keyboard.press("Enter")
const dialog = page
.getByRole("dialog")
.filter({ has: page.getByPlaceholder(/search files/i) })
.first()
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
await input.fill("package.json")
const item = dialog.locator('[data-slot="list-item"][data-key^="file:"]').first()
await expect(item).toBeVisible({ timeout: 30_000 })
await item.click()
await expect(dialog).toHaveCount(0)
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
await expect(tabs.locator('[data-slot="tabs-trigger"]').first()).toBeVisible()
})
+49
View File
@@ -0,0 +1,49 @@
import { test, expect } from "../fixtures"
test("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
await gotoSession()
const toggle = page.getByRole("button", { name: "Toggle file tree" })
const panel = page.locator("#file-tree-panel")
const treeTabs = panel.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
await expect(toggle).toBeVisible()
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
await expect(toggle).toHaveAttribute("aria-expanded", "true")
await expect(panel).toBeVisible()
await expect(treeTabs).toBeVisible()
const allTab = treeTabs.getByRole("tab", { name: /^all files$/i })
await expect(allTab).toBeVisible()
await allTab.click()
await expect(allTab).toHaveAttribute("aria-selected", "true")
const tree = treeTabs.locator('[data-slot="tabs-content"]:not([hidden])')
await expect(tree).toBeVisible()
const expand = async (name: string) => {
const folder = tree.getByRole("button", { name, exact: true }).first()
await expect(folder).toBeVisible()
await expect(folder).toHaveAttribute("aria-expanded", /true|false/)
if ((await folder.getAttribute("aria-expanded")) === "false") await folder.click()
await expect(folder).toHaveAttribute("aria-expanded", "true")
}
await expand("packages")
await expand("app")
await expand("src")
await expand("components")
const file = tree.getByRole("button", { name: "file-tree.tsx", exact: true }).first()
await expect(file).toBeVisible()
await file.click()
const tab = page.getByRole("tab", { name: "file-tree.tsx" })
await expect(tab).toBeVisible()
await tab.click()
await expect(tab).toHaveAttribute("aria-selected", "true")
const code = page.locator('[data-component="code"]').first()
await expect(code).toBeVisible()
await expect(code).toContainText("export default function FileTree")
})
@@ -0,0 +1,49 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("smoke file viewer renders real file content", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/open")
const command = page.locator('[data-slash-id="file.open"]').first()
await expect(command).toBeVisible()
await page.keyboard.press("Enter")
const dialog = page
.getByRole("dialog")
.filter({ has: page.getByPlaceholder(/search files/i) })
.first()
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
await input.fill("package.json")
const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
let index = -1
await expect
.poll(
async () => {
const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
return index >= 0
},
{ timeout: 30_000 },
)
.toBe(true)
const item = items.nth(index)
await expect(item).toBeVisible()
await item.click()
await expect(dialog).toHaveCount(0)
const tab = page.getByRole("tab", { name: "package.json" })
await expect(tab).toBeVisible()
await tab.click()
const code = page.locator('[data-component="code"]').first()
await expect(code).toBeVisible()
await expect(code.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
})
+48 -50
View File
@@ -1,9 +1,21 @@
import { test as base, expect } from "@playwright/test"
import { createSdk, dirSlug, getWorktree, promptSelector, serverUrl, sessionPath } from "./utils"
import { test as base, expect, type Page } from "@playwright/test"
import { cleanupTestProject, createTestProject, seedProjects } from "./actions"
import { promptSelector } from "./selectors"
import { createSdk, dirSlug, getWorktree, sessionPath } from "./utils"
export const settingsKey = "settings.v3"
type TestFixtures = {
sdk: ReturnType<typeof createSdk>
gotoSession: (sessionID?: string) => Promise<void>
withProject: <T>(
callback: (project: {
directory: string
slug: string
gotoSession: (sessionID?: string) => Promise<void>
}) => Promise<T>,
options?: { extra?: string[] },
) => Promise<T>
}
type WorkerFixtures = {
@@ -29,54 +41,7 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
await use(createSdk(directory))
},
gotoSession: async ({ page, directory }, use) => {
await page.addInitScript(
(input: { directory: string; serverUrl: string }) => {
const key = "opencode.global.dat:server"
const raw = localStorage.getItem(key)
const parsed = (() => {
if (!raw) return undefined
try {
return JSON.parse(raw) as unknown
} catch {
return undefined
}
})()
const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
const list = Array.isArray(store.list) ? store.list : []
const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
const nextProjects = { ...(projects as Record<string, unknown>) }
const add = (origin: string) => {
const current = nextProjects[origin]
const items = Array.isArray(current) ? current : []
const existing = items.filter(
(p): p is { worktree: string; expanded?: boolean } =>
!!p &&
typeof p === "object" &&
"worktree" in p &&
typeof (p as { worktree?: unknown }).worktree === "string",
)
if (existing.some((p) => p.worktree === input.directory)) return
nextProjects[origin] = [{ worktree: input.directory, expanded: true }, ...existing]
}
add("local")
add(input.serverUrl)
localStorage.setItem(
key,
JSON.stringify({
list,
projects: nextProjects,
lastProject,
}),
)
},
{ directory, serverUrl },
)
await seedStorage(page, { directory })
const gotoSession = async (sessionID?: string) => {
await page.goto(sessionPath(directory, sessionID))
@@ -84,6 +49,39 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
}
await use(gotoSession)
},
withProject: async ({ page }, use) => {
await use(async (callback, options) => {
const directory = await createTestProject()
const slug = dirSlug(directory)
await seedStorage(page, { directory, extra: options?.extra })
const gotoSession = async (sessionID?: string) => {
await page.goto(sessionPath(directory, sessionID))
await expect(page.locator(promptSelector)).toBeVisible()
}
try {
await gotoSession()
return await callback({ directory, slug, gotoSession })
} finally {
await cleanupTestProject(directory)
}
})
},
})
async function seedStorage(page: Page, input: { directory: string; extra?: string[] }) {
await seedProjects(page, input)
await page.addInitScript(() => {
localStorage.setItem(
"opencode.global.dat:model",
JSON.stringify({
recent: [{ providerID: "opencode", modelID: "big-pickle" }],
user: [],
variant: {},
}),
)
})
}
export { expect }
@@ -1,5 +1,6 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { clickListItem } from "../actions"
test("smoke model selection updates prompt footer", async ({ page, gotoSession }) => {
await gotoSession()
@@ -32,9 +33,7 @@ test("smoke model selection updates prompt footer", async ({ page, gotoSession }
await input.fill(model)
const item = dialog.locator(`[data-slot="list-item"][data-key="${key}"]`)
await expect(item).toBeVisible()
await item.click()
await clickListItem(dialog, { key })
await expect(dialog).toHaveCount(0)
@@ -1,5 +1,6 @@
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { closeDialog, openSettings, clickListItem } from "../actions"
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
await gotoSession()
@@ -27,18 +28,7 @@ test("hiding a model removes it from the model picker", async ({ page, gotoSessi
await page.keyboard.press("Escape")
await expect(picker).toHaveCount(0)
const settings = page.getByRole("dialog")
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
const opened = await settings
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false)
if (!opened) {
await page.getByRole("button", { name: "Settings" }).first().click()
await expect(settings).toBeVisible()
}
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Models" }).click()
const search = settings.getByPlaceholder("Search models")
@@ -52,22 +42,7 @@ test("hiding a model removes it from the model picker", async ({ page, gotoSessi
await toggle.locator('[data-slot="switch-control"]').click()
await expect(input).toHaveAttribute("aria-checked", "false")
await page.keyboard.press("Escape")
const closed = await settings
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closed) {
await page.keyboard.press("Escape")
const closedSecond = await settings
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closedSecond) {
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(settings).toHaveCount(0)
}
}
await closeDialog(page, settings)
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
-15
View File
@@ -1,15 +0,0 @@
import { test, expect } from "./fixtures"
import { modKey } from "./utils"
test("search palette opens and closes", async ({ page, gotoSession }) => {
await gotoSession()
await page.keyboard.press(`${modKey}+P`)
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("textbox").first()).toBeVisible()
await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
})
@@ -0,0 +1,53 @@
import { test, expect } from "../fixtures"
import { openSidebar } from "../actions"
test("dialog edit project updates name and startup script", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async () => {
await openSidebar(page)
const open = async () => {
const header = page.locator(".group\\/project").first()
await header.hover()
const trigger = header.getByRole("button", { name: "More options" }).first()
await expect(trigger).toBeVisible()
await trigger.click({ force: true })
const menu = page.locator('[data-component="dropdown-menu-content"]').first()
await expect(menu).toBeVisible()
const editItem = menu.getByRole("menuitem", { name: "Edit" }).first()
await expect(editItem).toBeVisible()
await editItem.click({ force: true })
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("heading", { level: 2 })).toHaveText("Edit project")
return dialog
}
const name = `e2e project ${Date.now()}`
const startup = `echo e2e_${Date.now()}`
const dialog = await open()
const nameInput = dialog.getByLabel("Name")
await nameInput.fill(name)
const startupInput = dialog.getByLabel("Workspace startup script")
await startupInput.fill(startup)
await dialog.getByRole("button", { name: "Save" }).click()
await expect(dialog).toHaveCount(0)
const header = page.locator(".group\\/project").first()
await expect(header).toContainText(name)
const reopened = await open()
await expect(reopened.getByLabel("Name")).toHaveValue(name)
await expect(reopened.getByLabel("Workspace startup script")).toHaveValue(startup)
await reopened.getByRole("button", { name: "Cancel" }).click()
await expect(reopened).toHaveCount(0)
})
})
@@ -0,0 +1,72 @@
import { test, expect } from "../fixtures"
import { createTestProject, cleanupTestProject, openSidebar, clickMenuItem, openProjectMenu } from "../actions"
import { projectCloseHoverSelector, projectSwitchSelector } from "../selectors"
import { dirSlug } from "../utils"
test("can close a project via hover card close button", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject()
const otherSlug = dirSlug(other)
try {
await withProject(
async () => {
await openSidebar(page)
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
await expect(otherButton).toBeVisible()
await otherButton.hover()
const close = page.locator(projectCloseHoverSelector(otherSlug)).first()
await expect(close).toBeVisible()
await close.click()
await expect(otherButton).toHaveCount(0)
},
{ extra: [other] },
)
} finally {
await cleanupTestProject(other)
}
})
test("closing active project navigates to another open project", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject()
const otherSlug = dirSlug(other)
try {
await withProject(
async ({ slug }) => {
await openSidebar(page)
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
await expect(otherButton).toBeVisible()
await otherButton.click()
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
const menu = await openProjectMenu(page, otherSlug)
await clickMenuItem(menu, /^Close$/i, { force: true })
await expect
.poll(() => {
const pathname = new URL(page.url()).pathname
if (new RegExp(`^/${slug}/session(?:/[^/]+)?/?$`).test(pathname)) return "project"
if (pathname === "/") return "home"
return ""
})
.toMatch(/^(project|home)$/)
await expect(page).not.toHaveURL(new RegExp(`/${otherSlug}/session(?:[/?#]|$)`))
await expect(otherButton).toHaveCount(0)
},
{ extra: [other] },
)
} finally {
await cleanupTestProject(other)
}
})
@@ -0,0 +1,35 @@
import { test, expect } from "../fixtures"
import { defocus, createTestProject, cleanupTestProject } from "../actions"
import { projectSwitchSelector } from "../selectors"
import { dirSlug } from "../utils"
test("can switch between projects from sidebar", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject()
const otherSlug = dirSlug(other)
try {
await withProject(
async ({ directory }) => {
await defocus(page)
const currentSlug = dirSlug(directory)
const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
await expect(otherButton).toBeVisible()
await otherButton.click()
await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
const currentButton = page.locator(projectSwitchSelector(currentSlug)).first()
await expect(currentButton).toBeVisible()
await currentButton.click()
await expect(page).toHaveURL(new RegExp(`/${currentSlug}/session`))
},
{ extra: [other] },
)
} finally {
await cleanupTestProject(other)
}
})
@@ -0,0 +1,144 @@
import { base64Decode } from "@opencode-ai/util/encode"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
import { cleanupTestProject, openSidebar, sessionIDFromUrl, setWorkspacesEnabled } from "../actions"
import { promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { createSdk } from "../utils"
function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
}
async function waitWorkspaceReady(page: Page, slug: string) {
await openSidebar(page)
await expect
.poll(
async () => {
const item = page.locator(workspaceItemSelector(slug)).first()
try {
await item.hover({ timeout: 500 })
return true
} catch {
return false
}
},
{ timeout: 60_000 },
)
.toBe(true)
}
async function createWorkspace(page: Page, root: string, seen: string[]) {
await openSidebar(page)
await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const slug = slugFromUrl(page.url())
if (!slug) return ""
if (slug === root) return ""
if (seen.includes(slug)) return ""
return slug
},
{ timeout: 45_000 },
)
.not.toBe("")
const slug = slugFromUrl(page.url())
const directory = base64Decode(slug)
if (!directory) throw new Error(`Failed to decode workspace slug: ${slug}`)
return { slug, directory }
}
async function openWorkspaceNewSession(page: Page, slug: string) {
await waitWorkspaceReady(page, slug)
const item = page.locator(workspaceItemSelector(slug)).first()
await item.hover()
const button = page.locator(workspaceNewSessionSelector(slug)).first()
await expect(button).toBeVisible()
await button.click({ force: true })
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
await expect(page).toHaveURL(new RegExp(`/${slug}/session(?:[/?#]|$)`))
}
async function createSessionFromWorkspace(page: Page, slug: string, text: string) {
await openWorkspaceNewSession(page, slug)
const prompt = page.locator(promptSelector)
await expect(prompt).toBeVisible()
await expect(prompt).toBeEditable()
await prompt.click()
await expect(prompt).toBeFocused()
await prompt.fill(text)
await expect.poll(async () => ((await prompt.textContent()) ?? "").trim()).toContain(text)
await prompt.press("Enter")
await expect.poll(() => slugFromUrl(page.url())).toBe(slug)
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
const sessionID = sessionIDFromUrl(page.url())
if (!sessionID) throw new Error(`Failed to parse session id from url: ${page.url()}`)
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${sessionID}(?:[/?#]|$)`))
return sessionID
}
async function sessionDirectory(directory: string, sessionID: string) {
const info = await createSdk(directory)
.session.get({ sessionID })
.then((x) => x.data)
.catch(() => undefined)
if (!info) return ""
return info.directory
}
test("new sessions from sidebar workspace actions stay in selected workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async ({ directory, slug: root }) => {
const workspaces = [] as { slug: string; directory: string }[]
const sessions = [] as string[]
try {
await openSidebar(page)
await setWorkspacesEnabled(page, root, true)
const first = await createWorkspace(page, root, [])
workspaces.push(first)
await waitWorkspaceReady(page, first.slug)
const second = await createWorkspace(page, root, [first.slug])
workspaces.push(second)
await waitWorkspaceReady(page, second.slug)
const firstSession = await createSessionFromWorkspace(page, first.slug, `workspace one ${Date.now()}`)
sessions.push(firstSession)
const secondSession = await createSessionFromWorkspace(page, second.slug, `workspace two ${Date.now()}`)
sessions.push(secondSession)
const thirdSession = await createSessionFromWorkspace(page, first.slug, `workspace one again ${Date.now()}`)
sessions.push(thirdSession)
await expect.poll(() => sessionDirectory(first.directory, firstSession)).toBe(first.directory)
await expect.poll(() => sessionDirectory(second.directory, secondSession)).toBe(second.directory)
await expect.poll(() => sessionDirectory(first.directory, thirdSession)).toBe(first.directory)
} finally {
const dirs = [directory, ...workspaces.map((workspace) => workspace.directory)]
await Promise.all(
sessions.map((sessionID) =>
Promise.all(
dirs.map((dir) =>
createSdk(dir)
.session.delete({ sessionID })
.catch(() => undefined),
),
),
),
)
await Promise.all(workspaces.map((workspace) => cleanupTestProject(workspace.directory)))
}
})
})
@@ -0,0 +1,409 @@
import { base64Decode } from "@opencode-ai/util/encode"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
test.describe.configure({ mode: "serial" })
import {
cleanupTestProject,
clickMenuItem,
confirmDialog,
openSidebar,
openWorkspaceMenu,
setWorkspacesEnabled,
} from "../actions"
import { dropdownMenuContentSelector, inlineInputSelector, workspaceItemSelector } from "../selectors"
import { createSdk, dirSlug } from "../utils"
function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
}
async function setupWorkspaceTest(page: Page, project: { slug: string }) {
const rootSlug = project.slug
await openSidebar(page)
await setWorkspacesEnabled(page, rootSlug, true)
await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const slug = slugFromUrl(page.url())
return slug.length > 0 && slug !== rootSlug
},
{ timeout: 45_000 },
)
.toBe(true)
const slug = slugFromUrl(page.url())
const dir = base64Decode(slug)
await openSidebar(page)
await expect
.poll(
async () => {
const item = page.locator(workspaceItemSelector(slug)).first()
try {
await item.hover({ timeout: 500 })
return true
} catch {
return false
}
},
{ timeout: 60_000 },
)
.toBe(true)
return { rootSlug, slug, directory: dir }
}
test("can enable and disable workspaces from project menu", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async ({ slug }) => {
await openSidebar(page)
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
await setWorkspacesEnabled(page, slug, true)
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
await expect(page.locator(workspaceItemSelector(slug)).first()).toBeVisible()
await setWorkspacesEnabled(page, slug, false)
await expect(page.getByRole("button", { name: "New session" }).first()).toBeVisible()
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0)
})
})
test("can create a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async ({ slug }) => {
await openSidebar(page)
await setWorkspacesEnabled(page, slug, true)
await expect(page.getByRole("button", { name: "New workspace" }).first()).toBeVisible()
await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const currentSlug = slugFromUrl(page.url())
return currentSlug.length > 0 && currentSlug !== slug
},
{ timeout: 45_000 },
)
.toBe(true)
const workspaceSlug = slugFromUrl(page.url())
const workspaceDir = base64Decode(workspaceSlug)
await openSidebar(page)
await expect
.poll(
async () => {
const item = page.locator(workspaceItemSelector(workspaceSlug)).first()
try {
await item.hover({ timeout: 500 })
return true
} catch {
return false
}
},
{ timeout: 60_000 },
)
.toBe(true)
await expect(page.locator(workspaceItemSelector(workspaceSlug)).first()).toBeVisible()
await cleanupTestProject(workspaceDir)
})
})
test("non-git projects keep workspace mode disabled", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const nonGit = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-nongit-"))
const nonGitSlug = dirSlug(nonGit)
await fs.writeFile(path.join(nonGit, "README.md"), "# e2e nongit\n")
try {
await withProject(async () => {
await page.goto(`/${nonGitSlug}/session`)
await expect.poll(() => slugFromUrl(page.url()), { timeout: 30_000 }).not.toBe("")
const activeDir = base64Decode(slugFromUrl(page.url()))
expect(path.basename(activeDir)).toContain("opencode-e2e-project-nongit-")
await openSidebar(page)
await expect(page.getByRole("button", { name: "New workspace" })).toHaveCount(0)
const trigger = page.locator('[data-action="project-menu"]').first()
const hasMenu = await trigger
.isVisible()
.then((x) => x)
.catch(() => false)
if (!hasMenu) return
await trigger.click({ force: true })
const menu = page.locator(dropdownMenuContentSelector).first()
await expect(menu).toBeVisible()
const toggle = menu.locator('[data-action="project-workspaces-toggle"]').first()
await expect(toggle).toBeVisible()
await expect(toggle).toBeDisabled()
await expect(menu.getByRole("menuitem", { name: "New workspace" })).toHaveCount(0)
})
} finally {
await cleanupTestProject(nonGit)
}
})
test("can rename a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async (project) => {
const { slug } = await setupWorkspaceTest(page, project)
const rename = `e2e workspace ${Date.now()}`
const menu = await openWorkspaceMenu(page, slug)
await clickMenuItem(menu, /^Rename$/i, { force: true })
await expect(menu).toHaveCount(0)
const item = page.locator(workspaceItemSelector(slug)).first()
await expect(item).toBeVisible()
const input = item.locator(inlineInputSelector).first()
await expect(input).toBeVisible()
await input.fill(rename)
await input.press("Enter")
await expect(item).toContainText(rename)
})
})
test("can reset a workspace", async ({ page, sdk, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async (project) => {
const { slug, directory: createdDir } = await setupWorkspaceTest(page, project)
const readme = path.join(createdDir, "README.md")
const extra = path.join(createdDir, `e2e_reset_${Date.now()}.txt`)
const original = await fs.readFile(readme, "utf8")
const dirty = `${original.trimEnd()}\n\nchange_${Date.now()}\n`
await fs.writeFile(readme, dirty, "utf8")
await fs.writeFile(extra, `created_${Date.now()}\n`, "utf8")
await expect
.poll(async () => {
return await fs
.stat(extra)
.then(() => true)
.catch(() => false)
})
.toBe(true)
await expect
.poll(async () => {
const files = await sdk.file
.status({ directory: createdDir })
.then((r) => r.data ?? [])
.catch(() => [])
return files.length
})
.toBeGreaterThan(0)
const menu = await openWorkspaceMenu(page, slug)
await clickMenuItem(menu, /^Reset$/i, { force: true })
await confirmDialog(page, /^Reset workspace$/i)
await expect
.poll(
async () => {
const files = await sdk.file
.status({ directory: createdDir })
.then((r) => r.data ?? [])
.catch(() => [])
return files.length
},
{ timeout: 60_000 },
)
.toBe(0)
await expect.poll(() => fs.readFile(readme, "utf8"), { timeout: 60_000 }).toBe(original)
await expect
.poll(async () => {
return await fs
.stat(extra)
.then(() => true)
.catch(() => false)
})
.toBe(false)
})
})
test("can delete a workspace", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async (project) => {
const sdk = createSdk(project.directory)
const { rootSlug, slug, directory } = await setupWorkspaceTest(page, project)
await expect
.poll(
async () => {
const worktrees = await sdk.worktree
.list()
.then((r) => r.data ?? [])
.catch(() => [] as string[])
return worktrees.includes(directory)
},
{ timeout: 30_000 },
)
.toBe(true)
const menu = await openWorkspaceMenu(page, slug)
await clickMenuItem(menu, /^Delete$/i, { force: true })
await confirmDialog(page, /^Delete workspace$/i)
await expect(page).toHaveURL(new RegExp(`/${rootSlug}/session`))
await expect
.poll(
async () => {
const worktrees = await sdk.worktree
.list()
.then((r) => r.data ?? [])
.catch(() => [] as string[])
return worktrees.includes(directory)
},
{ timeout: 60_000 },
)
.toBe(false)
await project.gotoSession()
await openSidebar(page)
await expect(page.locator(workspaceItemSelector(slug))).toHaveCount(0, { timeout: 60_000 })
await expect(page.locator(workspaceItemSelector(rootSlug)).first()).toBeVisible()
})
})
test("can reorder workspaces by drag and drop", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
await withProject(async ({ slug: rootSlug }) => {
const workspaces = [] as { directory: string; slug: string }[]
const listSlugs = async () => {
const nodes = page.locator('[data-component="sidebar-nav-desktop"] [data-component="workspace-item"]')
const slugs = await nodes.evaluateAll((els) => {
return els.map((el) => el.getAttribute("data-workspace") ?? "").filter((x) => x.length > 0)
})
return slugs
}
const waitReady = async (slug: string) => {
await expect
.poll(
async () => {
const item = page.locator(workspaceItemSelector(slug)).first()
try {
await item.hover({ timeout: 500 })
return true
} catch {
return false
}
},
{ timeout: 60_000 },
)
.toBe(true)
}
const drag = async (from: string, to: string) => {
const src = page.locator(workspaceItemSelector(from)).first()
const dst = page.locator(workspaceItemSelector(to)).first()
await src.scrollIntoViewIfNeeded()
await dst.scrollIntoViewIfNeeded()
const a = await src.boundingBox()
const b = await dst.boundingBox()
if (!a || !b) throw new Error("Failed to resolve workspace drag bounds")
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2)
await page.mouse.down()
await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 12 })
await page.mouse.up()
}
try {
await openSidebar(page)
await setWorkspacesEnabled(page, rootSlug, true)
for (const _ of [0, 1]) {
const prev = slugFromUrl(page.url())
await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const slug = slugFromUrl(page.url())
return slug.length > 0 && slug !== rootSlug && slug !== prev
},
{ timeout: 45_000 },
)
.toBe(true)
const slug = slugFromUrl(page.url())
const dir = base64Decode(slug)
workspaces.push({ slug, directory: dir })
await openSidebar(page)
}
if (workspaces.length !== 2) throw new Error("Expected two created workspaces")
const a = workspaces[0].slug
const b = workspaces[1].slug
await waitReady(a)
await waitReady(b)
const list = async () => {
const slugs = await listSlugs()
return slugs.filter((s) => s !== rootSlug && (s === a || s === b)).slice(0, 2)
}
await expect
.poll(async () => {
const slugs = await list()
return slugs.length === 2
})
.toBe(true)
const before = await list()
const from = before[1]
const to = before[0]
if (!from || !to) throw new Error("Failed to resolve initial workspace order")
await drag(from, to)
await expect.poll(async () => await list()).toEqual([from, to])
} finally {
await Promise.all(workspaces.map((w) => cleanupTestProject(w.directory)))
}
})
})
+95
View File
@@ -0,0 +1,95 @@
import { test, expect } from "../fixtures"
import type { Page } from "@playwright/test"
import { promptSelector } from "../selectors"
import { withSession } from "../actions"
function contextButton(page: Page) {
return page
.locator('[data-component="button"]')
.filter({ has: page.locator('[data-component="progress-circle"]').first() })
.first()
}
async function seedContextSession(input: { sessionID: string; sdk: Parameters<typeof withSession>[0] }) {
await input.sdk.session.promptAsync({
sessionID: input.sessionID,
noReply: true,
parts: [
{
type: "text",
text: "seed context",
},
],
})
await expect
.poll(async () => {
const messages = await input.sdk.session
.messages({ sessionID: input.sessionID, limit: 1 })
.then((r) => r.data ?? [])
return messages.length
})
.toBeGreaterThan(0)
}
test("context panel can be opened from the prompt", async ({ page, sdk, gotoSession }) => {
const title = `e2e smoke context ${Date.now()}`
await withSession(sdk, title, async (session) => {
await seedContextSession({ sessionID: session.id, sdk })
await gotoSession(session.id)
const trigger = contextButton(page)
await expect(trigger).toBeVisible()
await trigger.click()
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
await expect(tabs.getByRole("tab", { name: "Context" })).toBeVisible()
})
})
test("context panel can be closed from the context tab close action", async ({ page, sdk, gotoSession }) => {
await withSession(sdk, `e2e context toggle ${Date.now()}`, async (session) => {
await seedContextSession({ sessionID: session.id, sdk })
await gotoSession(session.id)
await page.locator(promptSelector).click()
const trigger = contextButton(page)
await expect(trigger).toBeVisible()
await trigger.click()
const tabs = page.locator('[data-component="tabs"][data-variant="normal"]')
const context = tabs.getByRole("tab", { name: "Context" })
await expect(context).toBeVisible()
await page.getByRole("button", { name: "Close tab" }).first().click()
await expect(context).toHaveCount(0)
})
})
test("context panel can open file picker from context actions", async ({ page, sdk, gotoSession }) => {
await withSession(sdk, `e2e context tabs ${Date.now()}`, async (session) => {
await seedContextSession({ sessionID: session.id, sdk })
await gotoSession(session.id)
await page.locator(promptSelector).click()
const trigger = contextButton(page)
await expect(trigger).toBeVisible()
await trigger.click()
await expect(page.getByRole("tab", { name: "Context" })).toBeVisible()
await page.getByRole("button", { name: "Open file" }).first().click()
const dialog = page
.getByRole("dialog")
.filter({ has: page.getByPlaceholder(/search files/i) })
.first()
await expect(dialog).toBeVisible()
await page.keyboard.press("Escape")
await expect(dialog).toHaveCount(0)
})
})
@@ -0,0 +1,43 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { sessionIDFromUrl } from "../actions"
// Regression test for Issue #12453: the synchronous POST /message endpoint holds
// the connection open while the agent works, causing "Failed to fetch" over
// VPN/Tailscale. The fix switches to POST /prompt_async which returns immediately.
test("prompt succeeds when sync message endpoint is unreachable", async ({ page, sdk, gotoSession }) => {
test.setTimeout(120_000)
// Simulate Tailscale/VPN killing the long-lived sync connection
await page.route("**/session/*/message", (route) => route.abort("connectionfailed"))
await gotoSession()
const token = `E2E_ASYNC_${Date.now()}`
await page.locator(promptSelector).click()
await page.keyboard.type(`Reply with exactly: ${token}`)
await page.keyboard.press("Enter")
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
const sessionID = sessionIDFromUrl(page.url())!
try {
// Agent response arrives via SSE despite sync endpoint being dead
await expect
.poll(
async () => {
const messages = await sdk.session.messages({ sessionID, limit: 50 }).then((r) => r.data ?? [])
return messages
.filter((m) => m.info.role === "assistant")
.flatMap((m) => m.parts)
.filter((p) => p.type === "text")
.map((p) => p.text)
.join("\n")
},
{ timeout: 90_000 },
)
.toContain(token)
} finally {
await sdk.session.delete({ sessionID }).catch(() => undefined)
}
})
@@ -1,5 +1,5 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,5 +1,5 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => {
await gotoSession()
@@ -1,10 +1,6 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
function sessionIDFromUrl(url: string) {
const match = /\/session\/([^/?#]+)/.exec(url)
return match?.[1]
}
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { sessionIDFromUrl, withSession } from "../actions"
test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession }) => {
test.setTimeout(120_000)
@@ -48,9 +44,6 @@ test("can send a prompt and receive a reply", async ({ page, sdk, gotoSession })
)
.toContain(token)
const reply = page.locator('[data-slot="session-turn-summary-section"]').filter({ hasText: token }).first()
await expect(reply).toBeVisible({ timeout: 90_000 })
} finally {
page.off("pageerror", onPageError)
await sdk.session.delete({ sessionID }).catch(() => undefined)
+66
View File
@@ -0,0 +1,66 @@
export const promptSelector = '[data-component="prompt-input"]'
export const terminalSelector = '[data-component="terminal"]'
export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]'
export const settingsLanguageSelectSelector = '[data-action="settings-language"]'
export const settingsColorSchemeSelector = '[data-action="settings-color-scheme"]'
export const settingsThemeSelector = '[data-action="settings-theme"]'
export const settingsFontSelector = '[data-action="settings-font"]'
export const settingsNotificationsAgentSelector = '[data-action="settings-notifications-agent"]'
export const settingsNotificationsPermissionsSelector = '[data-action="settings-notifications-permissions"]'
export const settingsNotificationsErrorsSelector = '[data-action="settings-notifications-errors"]'
export const settingsSoundsAgentSelector = '[data-action="settings-sounds-agent"]'
export const settingsSoundsAgentEnabledSelector = '[data-action="settings-sounds-agent-enabled"]'
export const settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
export const settingsSoundsPermissionsEnabledSelector = '[data-action="settings-sounds-permissions-enabled"]'
export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
export const settingsSoundsErrorsEnabledSelector = '[data-action="settings-sounds-errors-enabled"]'
export const settingsUpdatesStartupSelector = '[data-action="settings-updates-startup"]'
export const settingsReleaseNotesSelector = '[data-action="settings-release-notes"]'
export const sidebarNavSelector = '[data-component="sidebar-nav-desktop"]'
export const projectSwitchSelector = (slug: string) =>
`${sidebarNavSelector} [data-action="project-switch"][data-project="${slug}"]`
export const projectCloseHoverSelector = (slug: string) => `[data-action="project-close-hover"][data-project="${slug}"]`
export const projectMenuTriggerSelector = (slug: string) =>
`${sidebarNavSelector} [data-action="project-menu"][data-project="${slug}"]`
export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
export const projectClearNotificationsSelector = (slug: string) =>
`[data-action="project-clear-notifications"][data-project="${slug}"]`
export const projectWorkspacesToggleSelector = (slug: string) =>
`[data-action="project-workspaces-toggle"][data-project="${slug}"]`
export const titlebarRightSelector = "#opencode-titlebar-right"
export const popoverBodySelector = '[data-slot="popover-body"]'
export const dropdownMenuTriggerSelector = '[data-slot="dropdown-menu-trigger"]'
export const dropdownMenuContentSelector = '[data-component="dropdown-menu-content"]'
export const inlineInputSelector = '[data-component="inline-input"]'
export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`
export const workspaceItemSelector = (slug: string) =>
`${sidebarNavSelector} [data-component="workspace-item"][data-workspace="${slug}"]`
export const workspaceMenuTriggerSelector = (slug: string) =>
`${sidebarNavSelector} [data-action="workspace-menu"][data-workspace="${slug}"]`
export const workspaceNewSessionSelector = (slug: string) =>
`${sidebarNavSelector} [data-action="workspace-new-session"][data-workspace="${slug}"]`
export const listItemSelector = '[data-slot="list-item"]'
export const listItemKeyStartsWithSelector = (prefix: string) => `${listItemSelector}[data-key^="${prefix}"]`
export const listItemKeySelector = (key: string) => `${listItemSelector}[data-key="${key}"]`
export const keybindButtonSelector = (id: string) => `[data-keybind-id="${id}"]`
-21
View File
@@ -1,21 +0,0 @@
import { test, expect } from "./fixtures"
import { promptSelector } from "./utils"
test("can open an existing session and type into the prompt", async ({ page, sdk, gotoSession }) => {
const title = `e2e smoke ${Date.now()}`
const created = await sdk.session.create({ title }).then((r) => r.data)
if (!created?.id) throw new Error("Session create did not return an id")
const sessionID = created.id
try {
await gotoSession(sessionID)
const prompt = page.locator(promptSelector)
await prompt.click()
await page.keyboard.type("hello from e2e")
await expect(prompt).toContainText("hello from e2e")
} finally {
await sdk.session.delete({ sessionID }).catch(() => undefined)
}
})
@@ -0,0 +1,233 @@
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
import { withSession } from "../actions"
import { createSdk, modKey } from "../utils"
import { promptSelector } from "../selectors"
async function seedConversation(input: {
page: Page
sdk: ReturnType<typeof createSdk>
sessionID: string
token: string
}) {
const messages = async () =>
await input.sdk.session.messages({ sessionID: input.sessionID, limit: 100 }).then((r) => r.data ?? [])
const seeded = await messages()
const userIDs = new Set(seeded.filter((m) => m.info.role === "user").map((m) => m.info.id))
const prompt = input.page.locator(promptSelector)
await expect(prompt).toBeVisible()
await input.sdk.session.promptAsync({
sessionID: input.sessionID,
noReply: true,
parts: [{ type: "text", text: input.token }],
})
let userMessageID: string | undefined
await expect
.poll(
async () => {
const users = (await messages()).filter(
(m) =>
!userIDs.has(m.info.id) &&
m.info.role === "user" &&
m.parts.filter((p) => p.type === "text").some((p) => p.text.includes(input.token)),
)
if (users.length === 0) return false
const user = users[users.length - 1]
if (!user) return false
userMessageID = user.info.id
return true
},
{ timeout: 90_000, intervals: [250, 500, 1_000] },
)
.toBe(true)
if (!userMessageID) throw new Error("Expected a user message id")
await expect(input.page.locator(`[data-message-id="${userMessageID}"]`).first()).toBeVisible({ timeout: 30_000 })
return { prompt, userMessageID }
}
test("slash undo sets revert and restores prior prompt", async ({ page, withProject }) => {
test.setTimeout(120_000)
const token = `undo_${Date.now()}`
await withProject(async (project) => {
const sdk = createSdk(project.directory)
await withSession(sdk, `e2e undo ${Date.now()}`, async (session) => {
await project.gotoSession(session.id)
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
await seeded.prompt.click()
await page.keyboard.type("/undo")
const undo = page.locator('[data-slash-id="session.undo"]').first()
await expect(undo).toBeVisible()
await page.keyboard.press("Enter")
await expect
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
timeout: 30_000,
})
.toBe(seeded.userMessageID)
await expect(seeded.prompt).toContainText(token)
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`)).toHaveCount(0)
})
})
})
test("slash redo clears revert and restores latest state", async ({ page, withProject }) => {
test.setTimeout(120_000)
const token = `redo_${Date.now()}`
await withProject(async (project) => {
const sdk = createSdk(project.directory)
await withSession(sdk, `e2e redo ${Date.now()}`, async (session) => {
await project.gotoSession(session.id)
const seeded = await seedConversation({ page, sdk, sessionID: session.id, token })
await seeded.prompt.click()
await page.keyboard.type("/undo")
const undo = page.locator('[data-slash-id="session.undo"]').first()
await expect(undo).toBeVisible()
await page.keyboard.press("Enter")
await expect
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
timeout: 30_000,
})
.toBe(seeded.userMessageID)
await seeded.prompt.click()
await page.keyboard.press(`${modKey}+A`)
await page.keyboard.press("Backspace")
await page.keyboard.type("/redo")
const redo = page.locator('[data-slash-id="session.redo"]').first()
await expect(redo).toBeVisible()
await page.keyboard.press("Enter")
await expect
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
timeout: 30_000,
})
.toBeUndefined()
await expect(seeded.prompt).not.toContainText(token)
await expect(page.locator(`[data-message-id="${seeded.userMessageID}"]`).first()).toBeVisible()
})
})
})
test("slash undo/redo traverses multi-step revert stack", async ({ page, withProject }) => {
test.setTimeout(120_000)
const firstToken = `undo_redo_first_${Date.now()}`
const secondToken = `undo_redo_second_${Date.now()}`
await withProject(async (project) => {
const sdk = createSdk(project.directory)
await withSession(sdk, `e2e undo redo stack ${Date.now()}`, async (session) => {
await project.gotoSession(session.id)
const first = await seedConversation({
page,
sdk,
sessionID: session.id,
token: firstToken,
})
const second = await seedConversation({
page,
sdk,
sessionID: session.id,
token: secondToken,
})
expect(first.userMessageID).not.toBe(second.userMessageID)
const firstMessage = page.locator(`[data-message-id="${first.userMessageID}"]`)
const secondMessage = page.locator(`[data-message-id="${second.userMessageID}"]`)
await expect(firstMessage.first()).toBeVisible()
await expect(secondMessage.first()).toBeVisible()
await second.prompt.click()
await page.keyboard.press(`${modKey}+A`)
await page.keyboard.press("Backspace")
await page.keyboard.type("/undo")
const undo = page.locator('[data-slash-id="session.undo"]').first()
await expect(undo).toBeVisible()
await page.keyboard.press("Enter")
await expect
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
timeout: 30_000,
})
.toBe(second.userMessageID)
await expect(firstMessage.first()).toBeVisible()
await expect(secondMessage).toHaveCount(0)
await second.prompt.click()
await page.keyboard.press(`${modKey}+A`)
await page.keyboard.press("Backspace")
await page.keyboard.type("/undo")
await expect(undo).toBeVisible()
await page.keyboard.press("Enter")
await expect
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
timeout: 30_000,
})
.toBe(first.userMessageID)
await expect(firstMessage).toHaveCount(0)
await expect(secondMessage).toHaveCount(0)
await second.prompt.click()
await page.keyboard.press(`${modKey}+A`)
await page.keyboard.press("Backspace")
await page.keyboard.type("/redo")
const redo = page.locator('[data-slash-id="session.redo"]').first()
await expect(redo).toBeVisible()
await page.keyboard.press("Enter")
await expect
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
timeout: 30_000,
})
.toBe(second.userMessageID)
await expect(firstMessage.first()).toBeVisible()
await expect(secondMessage).toHaveCount(0)
await second.prompt.click()
await page.keyboard.press(`${modKey}+A`)
await page.keyboard.press("Backspace")
await page.keyboard.type("/redo")
await expect(redo).toBeVisible()
await page.keyboard.press("Enter")
await expect
.poll(async () => await sdk.session.get({ sessionID: session.id }).then((r) => r.data?.revert?.messageID), {
timeout: 30_000,
})
.toBeUndefined()
await expect(firstMessage.first()).toBeVisible()
await expect(secondMessage.first()).toBeVisible()
})
})
})

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