Compare commits

...
Author SHA1 Message Date
opencode 7659321990 release: v1.4.6 2026-04-15 07:26:23 +00:00
Luke ParkerandGitHub a992d8b733 fix(snapshot): avoid ENAMETOOLONG and improve staging perf via stdin pathspecs (#22560) 2026-04-15 06:43:36 +00:00
Frank ccaa12ee79 sync 2026-04-15 02:22:47 -04:00
opencode-agent[bot] 5687d617a3 chore: generate 2026-04-15 06:08:19 +00:00
Frank 8f1ac2ddf6 Go: list model providers 2026-04-15 02:07:06 -04:00
Frank 1bea2a95a8 Go: qwen 3.5 & 3.6 plus 2026-04-15 02:07:06 -04:00
Frank 6a7ca45ae6 doc: qwen3.5 & 3.6 2026-04-15 02:07:06 -04:00
Brendan AllanandGitHub 8d89c3417b fix: prevent tooltip reopen on trigger click (#22571) 2026-04-15 06:03:29 +00:00
Dax Raad c48a4cc05b docs: use latest release for downloads instead of pinned version 2026-04-15 01:50:22 -04:00
github-actions[bot] df9eafa92c Update VOUCHED list
https://github.com/anomalyco/opencode/issues/22569#issuecomment-4249515283
2026-04-15 05:36:27 +00:00
Brendan AllanandGitHub e24d104e94 fix: update prompt input submit handler (#22566) 2026-04-15 05:32:52 +00:00
DaxGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
be3be32bf1 fix(observability): handle OTEL headers with '=' in value (#22564)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-04-15 01:32:32 -04:00
Brendan AllanandGitHub 66de7bef89 fix: add left padding to session title input (#22556) 2026-04-15 04:35:57 +00:00
opencode-agent[bot]andopencode d06bc3c2ca chore: update nix node_modules hashes 2026-04-15 04:25:29 +00:00
opencode dfc72838d7 release: v1.4.5 2026-04-15 04:25:18 +00:00
Brendan AllanandGitHub 4246368a88 fix(bootstrap): await plugin initialization 2026-04-15 11:40:33 +08:00
Brendan AllanandGitHub 548d9ac726 core: parallelise bootstrap (#22514) 2026-04-15 03:23:28 +00:00
Dax Raad a60fd89d1e ci: ok one more time 2026-04-14 23:22:07 -04:00
Aiden ClineandGitHub d25a7fbb2c chore: bump ai sdk pkgs (#22539) 2026-04-14 22:14:32 -05:00
Dax Raad da0f81d36f ci: remove Tauri desktop builds from release workflow to simplify distribution 2026-04-14 23:14:08 -04:00
Dax Raad 627159acac delete all e2e tests (#22501)
Cherry-picked from ea463e604c
2026-04-14 23:10:25 -04:00
Brendan AllanandGitHub f44aa02e26 fix(desktop): chdir to homedir on macOS to fix ripgrep issues (#22537) 2026-04-15 10:56:22 +08:00
Brendan AllanandGitHub 1ca9804604 fix(desktop): start tauri shell commands from home directory (#22535) 2026-04-15 10:51:53 +08:00
Dax Raad ddad871b46 core: pin downloads to v1.4.3 to ensure users get a tested, stable build instead of potentially unstable latest releases 2026-04-14 22:35:36 -04:00
opencode-agent[bot] d215188e4c chore: generate 2026-04-15 02:31:50 +00:00
Kit LangtonandGitHub f73ff781e7 fix(opencode): export AI SDK telemetry spans (#22526) 2026-04-14 22:30:50 -04:00
opencode-agent[bot] 68a9a47976 chore: update nix node_modules hashes 2026-04-15 01:49:59 +00:00
opencode-agent[bot] fb92bd470c chore: generate 2026-04-15 00:57:20 +00:00
LukeParkerDevandDax Raad 02f8a24e23 Update test.yml 2026-04-14 20:55:42 -04:00
Shoubhit DashandDax Raad 467e5689ec feat(server): extract question handler factory 2026-04-14 20:55:41 -04:00
Shoubhit DashandDax Raad fba752a501 feat(server): extract question httpapi contract 2026-04-14 20:55:39 -04:00
opencode-agent[bot] 87b2a9d749 chore: generate 2026-04-15 00:30:27 +00:00
Frank 8df7ccc304 zen: rate limiter 2026-04-14 20:29:21 -04:00
Brendan AllanandGitHub 2c36bf9490 fix(app): avoid bootstrap error popups during global sync init (#22426) 2026-04-15 08:24:52 +08:00
opencode bddf830083 release: v1.4.4 2026-04-15 00:03:43 +00:00
opencode-agent[bot] 50c1d0a43b chore: update nix node_modules hashes 2026-04-14 23:13:28 +00:00
Frank 60b8041ebb zen: support alibaba cache write 2026-04-14 18:48:00 -04:00
Frank 3b2a2c461d sync zen 2026-04-14 18:37:02 -04:00
Shoubhit DashandGitHub 6706358a6e feat(core): bootstrap packages/server and document extraction plan (#22492) 2026-04-15 04:01:45 +05:30
Shoubhit DashandGitHub f6409759e5 fix: restore instance context in prompt runs (#22498) 2026-04-15 03:59:12 +05:30
Luke ParkerandGitHub f9d99f044d fix(session): keep GitHub Copilot compaction requests valid (#22371) 2026-04-15 08:02:27 +10:00
Caleb NortonandGitHub bbd5faf5cd chore(nix): remove external ripgrep (#22482) 2026-04-14 16:49:44 -05:00
Kit LangtonandGitHub aeb7d99d20 fix(effect): preserve logger context in prompt runs (#22496) 2026-04-14 17:33:44 -04:00
Aiden ClineandGitHub 3695057bee feat: add --sanitize flag to opencode export to strip PII or confidential info (#22489) 2026-04-14 16:24:18 -05:00
opencode-agent[bot] 4ed3afea84 chore: generate 2026-04-14 20:58:35 +00:00
Kit LangtonandGitHub 3cf7c7536b fix(question): restore flat reply sdk shape (#22487) 2026-04-14 16:57:32 -04:00
opencode-agent[bot] 85674f4bfd chore: generate 2026-04-14 19:45:10 +00:00
Kit LangtonandGitHub f2525a63c9 add experimental question HttpApi slice (#22357) 2026-04-14 19:43:49 +00:00
opencode-agent[bot] 8c42d391f5 chore: update nix node_modules hashes 2026-04-14 19:08:59 +00:00
SebastianandGitHub 7f9bf91073 upgrade opentui to 0.1.99 (#22283) 2026-04-14 20:29:56 +02:00
Dax Raad 6ce5c01b1a ignore: v2 experiments 2026-04-14 14:25:38 -04:00
SebastianandGitHub a53fae1511 Fix diff line number contrast for built-in themes (#22464) 2026-04-14 19:59:41 +02:00
Kit LangtonandGitHub 4626458175 fix(mcp): persist immediate oauth connections (#22376) 2026-04-14 13:56:45 -04:00
Goni ZahavyandGitHub 9a5178e4ac fix(cli): handlePluginAuth asks for api key only if authorize method exists (#22475) 2026-04-14 12:53:00 -05:00
Kit LangtonandGitHub 68384613be refactor(session): remove async facade exports (#22471) 2026-04-14 13:45:13 -04:00
Kit LangtonandGitHub 4f967d5bc0 improve bash timeout retry hint (#22390) 2026-04-14 12:55:03 -04:00
Kit LangtonandGitHub ff60859e36 fix(project): reuse runtime in instance boot (#22470) 2026-04-14 12:53:13 -04:00
Kit LangtonandGitHub 020c47a055 refactor(project): remove async facade exports (#22387) 2026-04-14 12:49:20 -04:00
opencode-agent[bot] 64171db173 chore: generate 2026-04-14 16:39:15 +00:00
Kit LangtonandGitHub ad265797ab refactor(share): remove session share async facade exports (#22386) 2026-04-14 12:38:11 -04:00
Aiden ClineandGitHub b1312a3181 core: prevent duplicate user messages in ACP clients (#22468) 2026-04-14 11:37:33 -05:00
RAIT-09andGitHub a8f9f6b705 fix(acp): stop emitting user_message_chunk during session/prompt turn (#21851) 2026-04-14 11:25:00 -05:00
Aiden ClineandGitHub d312c677c5 fix: rm effect logger from processor.ts, use old logger for now instead (#22460) 2026-04-14 10:39:01 -05:00
Shoubhit DashandGitHub 5b60e51c9f fix(opencode): resolve ripgrep worker path in builds (#22436) 2026-04-14 16:39:21 +05:30
opencode-agent[bot] 7cbe1627ec chore: update nix node_modules hashes 2026-04-14 06:58:22 +00:00
Shoubhit DashandGitHub d6840868d4 refactor(ripgrep): use embedded wasm backend (#21703) 2026-04-14 11:56:23 +05:30
Luke ParkerandGitHub 9b2648dd57 build(opencode): shrink single-file executable size (#22362) 2026-04-14 15:49:26 +10:00
Kit LangtonandGitHub f954854232 refactor(instance): remove state helper (#22381) 2026-04-13 22:40:12 -04:00
Kit LangtonandGitHub 6a99079012 kit/env instance state (#22383) 2026-04-13 22:28:16 -04:00
Kit LangtonandGitHub 0a8b6298cd refactor(tui): move config cache to InstanceState (#22378) 2026-04-13 22:24:40 -04:00
Kit LangtonandGitHub f40209bdfb refactor(snapshot): remove async facade exports (#22370) 2026-04-13 21:24:54 -04:00
Kit LangtonandGitHub a2cb4909da refactor(plugin): remove async facade exports (#22367) 2026-04-13 21:24:20 -04:00
Kit LangtonandGitHub 7a05ba47d1 refactor(session): remove compaction async facade exports (#22366) 2026-04-13 21:23:34 -04:00
Kit LangtonandGitHub 36745caa2a refactor(worktree): remove async facade exports (#22369) 2026-04-13 21:23:15 -04:00
c2403d0f15 fix(provider): guard reasoningSummary injection for @ai-sdk/openai-compatible providers (#22352)
Co-authored-by: Nazar Hnatyshen <nazar.hnatyshen@atolls.com>
2026-04-13 20:20:06 -05:00
Aiden ClineandGitHub 34e2429c49 feat: add experimental.compaction.autocontinue hook to disable auto continuing after compaction (#22361) 2026-04-13 20:14:53 -05:00
opencode-agent[bot] 10ba68c772 chore: update nix node_modules hashes 2026-04-14 00:23:25 +00:00
Kit LangtonandGitHub e8471256f2 refactor(session): move llm stream into layer (#22358) 2026-04-13 19:53:30 -04:00
Kit LangtonandGitHub 43b37346b6 feat: add interactive burst to the TUI logo (#22098) 2026-04-13 19:36:28 -04:00
Kit LangtonandGitHub d199648aeb refactor(permission): remove async facade exports (#22342) 2026-04-13 19:33:58 -04:00
Kit LangtonandGitHub a06f40297b fix grep exact file path searches (#22356) 2026-04-13 19:26:50 -04:00
Dax Raad 59c0fc28ee ignore: v2 thoughts 2026-04-13 17:33:34 -04:00
James LongandGitHub b22add292c refactor(core): publish sync events to global event stream (#22347) 2026-04-13 16:51:59 -04:00
Kit LangtonandGitHub 67aaecacac refactor(session): remove revert async facade exports (#22339) 2026-04-13 16:16:13 -04:00
Kit LangtonandGitHub 29c202e6ab refactor(mcp): remove mcp auth async facade exports (#22338) 2026-04-13 15:36:12 -04:00
Kit LangtonandGitHub dcbf11f41a refactor(session): remove summary async facades (#22337) 2026-04-13 15:35:38 -04:00
Kit LangtonandGitHub 14ccff4037 refactor(agent): remove async facade exports (#22341) 2026-04-13 14:54:01 -04:00
Kit LangtonandGitHub 5b8b874732 update effect docs (#22340) 2026-04-13 14:07:59 -04:00
opencode-agent[bot] 1d81c0266c chore: generate 2026-04-13 18:02:12 +00:00
Dax Raad 913120759a session entry 2026-04-13 14:00:49 -04:00
DaxandGitHub 7a6ce05d09 2.0 exploration (#22335) 2026-04-13 13:47:33 -04:00
Kit LangtonandGitHub 1dc69359d5 refactor(mcp): remove async facade exports (#22324) 2026-04-13 13:45:34 -04:00
opencode-agent[bot] 329fcb040b chore: generate 2026-04-13 17:37:41 +00:00
James LongandGitHub bf50d1c028 feat(core): expose workspace adaptors to plugins (#21927) 2026-04-13 13:33:13 -04:00
Kit LangtonandGitHub b8801dbd22 refactor(file): remove async facade exports (#22322) 2026-04-13 13:12:02 -04:00
Kit LangtonandGitHub f7c6943817 refactor(config): remove async facade exports (#22325) 2026-04-13 13:11:05 -04:00
github-actions[bot] 91fe4db27c Update VOUCHED list
https://github.com/anomalyco/opencode/issues/22239#issuecomment-4238224546
2026-04-13 17:06:03 +00:00
Kit LangtonandGitHub 21d7a85e76 refactor(lsp): remove async facade exports (#22321) 2026-04-13 12:47:52 -04:00
Kit LangtonandGitHub 663e798e76 refactor(provider): remove async facade exports (#22320) 2026-04-13 12:40:00 -04:00
Aiden ClineandGitHub 5bc2d2498d test: ensure project and global instructions are loaded (#22317) 2026-04-13 11:34:38 -05:00
Kit LangtonandGitHub c22e34853d refactor(auth): remove async auth facade exports (#22306) 2026-04-13 12:31:43 -04:00
Kit LangtonandGitHub 6825b0bbc7 refactor(pty): remove async facade exports (#22305) 2026-04-13 11:47:05 -04:00
Kit LangtonandGitHub 3644581b55 refactor(file): stream ripgrep search parsing (#22303) 2026-04-13 11:39:37 -04:00
Kit LangtonandGitHub 79cc15335e fix: dispose e2e app runtime (#22316) 2026-04-13 11:36:56 -04:00
Kit LangtonandGitHub ca6200121b refactor: remove vcs async facade exports (#22304) 2026-04-13 11:22:20 -04:00
Kit LangtonandGitHub 7239b38b7f refactor(skill): remove async facade exports (#22308) 2026-04-13 11:18:10 -04:00
Kit LangtonandGitHub 9ae8dc2d01 refactor: remove ToolRegistry runtime facade (#22307) 2026-04-13 11:09:32 -04:00
opencode-agent[bot] 7164662be2 chore: generate 2026-04-13 14:18:05 +00:00
94f71f59a3 core: make InstanceBootstrap into an effect (#22274)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-04-13 10:16:40 -04:00
Kit LangtonandGitHub 3eb6508a64 refactor: share TUI terminal background detection (#22297) 2026-04-13 10:05:37 -04:00
Kit LangtonandGitHub 6fdb8ab90d refactor(file): add ripgrep search service (#22295) 2026-04-13 10:04:32 -04:00
Kit LangtonandGitHub 321bf1f8e1 refactor: finish small effect service adoption cleanups (#22094) 2026-04-13 09:17:13 -04:00
Brendan AllanandGitHub 62bd023086 app: replace parsePatchFiles with parseDiffFromFile (#22270) 2026-04-13 17:19:14 +08:00
Brendan AllanandGitHub cb1a50055c fix(electron): wait until ready before showing the main window (#22262) 2026-04-13 15:17:09 +08:00
opencode-agent[bot] 65e3348232 chore: update nix node_modules hashes 2026-04-13 06:02:50 +00:00
Brendan AllanandGitHub a6b9f0dac1 app: align workspace load more button (#22251) 2026-04-13 13:58:35 +08:00
Brendan AllanandGitHub 34f5bdbc99 app: fix scroll to bottom light mode style (#22250) 2026-04-13 13:55:33 +08:00
Aiden ClineandGitHub 0b4fe14b0a fix: forgot to put alibaba case in last commit (#22249) 2026-04-13 00:39:12 -05:00
Aiden ClineandGitHub 7230cd2683 feat: add alibaba pkg and cache support (#22248) 2026-04-13 00:08:07 -05:00
Aiden ClineandGitHub a915fe74be tweak: adjust session getUsage function to use more up to date LanguageModelUsage instead of LanguageModelV2Usage (#22224) 2026-04-12 21:39:06 -05:00
Brendan AllanandGitHub 26d35583c5 sdk: throw error if response has text/html content type (#21289) 2026-04-13 09:39:53 +08:00
ae17b416b8 fix(cli): auth login now asks for api key in handlePluginAuth (#21641)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-12 20:37:57 -05:00
Aiden ClineandGitHub 8ffadde85c chore: rm git ignored files (#22200) 2026-04-12 15:52:55 -05:00
Dax Raad 3c0ad70653 ci: enable beta branch releases with auto-update support 2026-04-12 14:40:24 -04:00
DaxandGitHub 264418c0cd fix(snapshot): complete gitignore respect for previously tracked files (#22172) 2026-04-12 14:05:46 -04:00
shafdevandGitHub fa2c69f09c fix(opencode): remove spurious scripts and randomField from package.json (#22160) 2026-04-12 13:49:24 -04:00
DaxandGitHub 113304a058 fix(snapshot): respect gitignore for previously tracked files (#22171) 2026-04-12 13:41:50 -04:00
Dax Raad 8c4d49c2bc ci: enable signed Windows builds on beta branch
Allows beta releases to include properly signed Windows CLI executables, ensuring consistent security verification across all release channels.
2026-04-12 13:16:38 -04:00
Dax Raad 2aa6110c6e ignore: exploration 2026-04-12 13:14:46 -04:00
Aiden ClineandGitHub 8b9b9ad31e fix: ensure images read by agent dont count against quota (#22168) 2026-04-12 12:02:39 -05:00
Simon KleeandGitHub 3729fd5706 chore(github): vouch simonklee (#22127) 2026-04-12 11:33:38 +02:00
Aiden ClineandGitHub 74b14a2d4e chore: refactor log.ts, go back to glob but add sort (#22107) 2026-04-11 23:09:19 -05:00
Aiden ClineandGitHub cdb951ec2f feat: make gh copilot use msgs api when available (#22106) 2026-04-11 23:06:35 -05:00
Aiden ClineandGitHub fc01cad2b8 fix: ensure logger cleanup properly orders list before deleting files (#22101) 2026-04-11 22:07:34 -05:00
opencode-agent[bot] c1ddc0ea2d chore: generate 2026-04-12 01:21:17 +00:00
Kit LangtonandGitHub 319b7655b7 refactor(tool): destroy Truncate facade, effectify Tool.define (#22093) 2026-04-11 21:20:12 -04:00
Kit LangtonandGitHub 824c12c01a refactor(file): destroy FileWatcher facade (#22091) 2026-04-11 21:19:12 -04:00
opencode-agent[bot] 17b2900884 chore: generate 2026-04-12 00:58:05 +00:00
Kit LangtonandGitHub 003010bdb6 refactor(question): destroy Question facade (#22092) 2026-04-11 20:57:01 -04:00
Kit LangtonandGitHub 82a4292934 refactor(file): destroy FileTime facade (#22090) 2026-04-11 20:08:55 -04:00
Kit LangtonandGitHub eea4253d67 refactor(session): destroy Instruction facade (#22089) 2026-04-11 20:04:09 -04:00
opencode-agent[bot] 1eacc3c339 chore: generate 2026-04-12 00:03:01 +00:00
Kit LangtonandGitHub 1a509d62a0 refactor(session): destroy SessionRunState facade (#22064) 2026-04-11 20:01:52 -04:00
opencode-agent[bot] 4c4eef46f1 chore: generate 2026-04-11 22:15:53 +00:00
Tommy D. RossiandGitHub d62ec7776e feat: allow session permission updates (#22070) 2026-04-11 17:14:30 -05:00
opencode-agent[bot] cb1e5d9e41 chore: generate 2026-04-11 20:56:22 +00:00
DaxandGitHub ca5f086759 refactor(server): simplify router middleware with next() (#21720) 2026-04-11 16:55:17 -04:00
opencode-agent[bot] 57c40eb7c2 chore: generate 2026-04-11 20:52:52 +00:00
63035f977f fix: enable thinking for zhipuai-coding-plan & prevent Korean IME truncation (#22041)
Co-authored-by: claudianus <claudianus@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 15:51:49 -05:00
opencode-agent[bot] 514d2a36bc chore: update nix node_modules hashes 2026-04-11 19:30:50 +00:00
Aiden ClineandGitHub 0b6fd5f612 chore: bump ai sdk deps (#22005) 2026-04-11 13:45:14 -05:00
Dax Raad 029e7135b7 hide download button 2026-04-11 14:18:58 -04:00
opencode-agent[bot] c43591f8a2 chore: generate 2026-04-11 18:18:40 +00:00
Dax Raad a2c22714cb ignore: exploration 2026-04-11 14:17:22 -04:00
Kit LangtonandGitHub 312f10f797 refactor(account): destroy Account facade (#22068) 2026-04-11 14:16:36 -04:00
opencode-agent[bot] d1f05b0f3a chore: generate 2026-04-11 16:53:40 +00:00
Kit LangtonandGitHub ccb0b320e1 refactor(session): make SystemPrompt a proper Effect Service (#21992) 2026-04-11 12:52:35 -04:00
Kit LangtonandGitHub 5ee7edaf9e refactor(tool): make Tool.Info init effectful (#21989) 2026-04-11 12:33:17 -04:00
opencode-agent[bot] 27190635ea chore: update nix node_modules hashes 2026-04-11 04:45:55 +00:00
opencode-agent[bot] 2e340d976f chore: generate 2026-04-11 03:53:48 +00:00
Kit LangtonandGitHub fe4dfb9f6f refactor(git): remove runtime facade wrappers (#21982) 2026-04-10 23:52:48 -04:00
Kit LangtonandGitHub 5e3dc80999 refactor: collapse command facade (#21981) 2026-04-10 23:52:12 -04:00
Kit LangtonandGitHub d84cc33742 refactor(plugin): return Effect from ToolContext.ask (#21986) 2026-04-10 23:50:50 -04:00
opencode-agent[bot] c92c462148 chore: update nix node_modules hashes 2026-04-11 03:39:49 +00:00
Kit LangtonandGitHub 9ca06e0336 docs(effect): mark SessionTodo migrated (#21987) 2026-04-10 23:35:50 -04:00
opencode-agent[bot] 3b523b32f5 chore: generate 2026-04-11 03:28:30 +00:00
Kit LangtonandGitHub ba3600a515 refactor(session): remove dead updatePartDelta facade (#21985) 2026-04-10 23:27:30 -04:00
Kit LangtonandGitHub 03ce2e5288 refactor(installation): drop facade runtime wrappers (#21984) 2026-04-10 23:26:16 -04:00
Kit LangtonandGitHub 87e23abb10 refactor: remove ProviderAuth facade (#21983) 2026-04-10 23:25:43 -04:00
opencode-agent[bot] 2868000c20 chore: generate 2026-04-11 03:19:50 +00:00
Kit LangtonandGitHub f38f415bf0 refactor: collapse Format facade (#21980) 2026-04-10 23:18:54 -04:00
Kit LangtonandGitHub 4341ab838e refactor(tool): use Session.Service directly in TaskTool (#21975) 2026-04-10 23:18:30 -04:00
Kit LangtonandGitHub cd004cf0b2 refactor(session): eliminate Effect.promise roundtrips for sync MessageV2.stream (#21973) 2026-04-10 23:18:13 -04:00
opencode-agent[bot] 19ae8c88b0 chore: generate 2026-04-11 03:13:03 +00:00
Kit LangtonandGitHub 3dd09147c2 refactor(tool): Tool.Context.metadata returns Effect (#21972) 2026-04-10 23:12:04 -04:00
Kit LangtonandGitHub 9581bf0670 refactor(effect): upgrade opencode to beta.46 context APIs (#21977) 2026-04-10 23:06:28 -04:00
Kit LangtonandGitHub af8aff3788 refactor: make TaskPromptOps effectful (#21971) 2026-04-10 22:57:47 -04:00
opencode-agent[bot] 2a8a59ded9 chore: generate 2026-04-11 02:56:03 +00:00
Kit LangtonandGitHub 5917ac2162 fix: provide EffectLogger.layer to bare Effect.runPromise/runFork calls (#21974) 2026-04-10 22:55:08 -04:00
Brendan AllanandGitHub b6af4d0dc6 refactor(config): pass instance context to containsPath (#21882) 2026-04-11 10:43:40 +08:00
opencode-agent[bot] 577139c626 chore: generate 2026-04-11 02:36:59 +00:00
Kit LangtonandGitHub c5fb6281f0 refactor(tool): Tool.Def.execute returns Effect, rename defineEffect → define (#21961) 2026-04-10 22:36:02 -04:00
Kit LangtonandGitHub f99812443c refactor: destroy SessionStatus facade (#21968) 2026-04-10 22:16:53 -04:00
opencode-agent[bot] b898c6d0ea chore: generate 2026-04-11 02:04:02 +00:00
Kit LangtonandGitHub 9e7045eaec refactor: destroy ShareNext facade (#21965) 2026-04-10 22:03:06 -04:00
Kit LangtonandGitHub a17ac02061 refactor: extract LSP diagnostic report formatter (#21964) 2026-04-10 22:00:56 -04:00
opencode-agent[bot] 57f9397677 chore: generate 2026-04-11 01:48:25 +00:00
Kit LangtonandGitHub a4c686025c refactor: destroy Todo facade (#21962) 2026-04-10 21:47:28 -04:00
Kit LangtonandGitHub face879100 fix: disable default Effect console logger (#21963) 2026-04-10 21:27:24 -04:00
opencode-agent[bot] 605559b165 chore: generate 2026-04-11 01:22:01 +00:00
Kit LangtonandGitHub 5cd4c6eb22 refactor: destroy Storage facades (#21956) 2026-04-10 21:21:02 -04:00
Kit LangtonandGitHub 40358d60a0 refactor: add Effect logger for motel observability (#21954) 2026-04-10 21:10:58 -04:00
Aiden ClineandGitHub 96c1c0363d chore: rm unnecessary test (now we use effect) and the test is flaky (#21959) 2026-04-10 19:59:33 -05:00
Aiden ClineandGitHub 33819932ec tweak: rm processor .trim calls (#21958) 2026-04-10 19:47:08 -05:00
Kit LangtonandGitHub 5d6fe01465 convert skill tool to Tool.defineEffect (#21936) 2026-04-10 19:49:53 -04:00
Kit LangtonandGitHub cf27a73397 feat: add AppRuntime for unified service composition (#21953) 2026-04-10 19:46:52 -04:00
opencode-agent[bot] f2c492a8e6 chore: generate 2026-04-10 23:43:20 +00:00
Kit LangtonandGitHub 0556774097 refactor(tool): convert apply_patch to Tool.defineEffect (#21938) 2026-04-10 19:42:14 -04:00
Kit LangtonandGitHub d9d5a0615e refactor: break SessionPrompt/TaskTool cycle via ctx injection (#21948) 2026-04-10 19:36:13 -04:00
Kit LangtonandGitHub d72ddd71fa refactor(tool): convert grep tool to Tool.defineEffect (#21937) 2026-04-10 19:20:00 -04:00
opencode-agent[bot] fb26308bc7 chore: generate 2026-04-10 21:12:22 +00:00
Kit LangtonandGitHub b41fa8e318 refactor: convert edit tool to Tool.defineEffect (#21904) 2026-04-10 17:10:28 -04:00
opencode-agent[bot] 57b2e64345 chore: generate 2026-04-10 21:01:33 +00:00
Frank 346b3e1b8d zen: nemotron doc 2026-04-10 16:58:38 -04:00
Kit LangtonandGitHub b139bc2ef3 refactor(tool): convert write tool to Tool.defineEffect (#21901) 2026-04-10 16:57:12 -04:00
opencode-agent[bot] 378b8ca241 chore: generate 2026-04-10 19:40:10 +00:00
Kit LangtonandGitHub f63bdc8e08 convert list tool to Tool.defineEffect (#21899) 2026-04-10 15:38:52 -04:00
Aiden ClineandGitHub ce26120205 tweak: make it so disabling uv or ruff fmters disables both (#21921) 2026-04-10 13:30:30 -05:00
opencode-agent[bot] d2d5d84d1e chore: generate 2026-04-10 17:57:46 +00:00
Kit LangtonandGitHub 847f1d99c9 convert glob tool to Tool.defineEffect (#21897) 2026-04-10 13:56:42 -04:00
opencode-agent[bot] 59d08683ea chore: generate 2026-04-10 17:27:31 +00:00
Kit LangtonandGitHub f7514d9eca refactor(tool): convert bash to defineEffect with ChildProcessSpawner (#21895) 2026-04-10 13:26:31 -04:00
James LongandGitHub 180ded6a27 rector(core,tui): handle workspace state in project context, add workspace status, improve ui (#21896) 2026-04-10 13:03:20 -04:00
Kit LangtonandGitHub bf601628db refactor(tool): convert codesearch tool internals to Effect (#21811) 2026-04-10 11:49:20 -04:00
opencode-agent[bot] 00e39d2114 chore: generate 2026-04-10 15:31:39 +00:00
Kit LangtonandGitHub 46b74e0873 refactor(tool): convert websearch tool internals to Effect (#21810) 2026-04-10 11:30:38 -04:00
opencode-agent[bot] aedc4e964f chore: generate 2026-04-10 14:51:27 +00:00
Kit LangtonandGitHub e83404367c refactor(tool): convert webfetch tool internals to Effect (#21809) 2026-04-10 10:50:13 -04:00
James LongandGitHub 42206da1f8 refactor(tui): switch to global events and start passing workspace param (#21719) 2026-04-10 10:47:27 -04:00
Kit LangtonandGitHub 44f38193c0 refactor(tool): convert plan tool internals to Effect (#21807) 2026-04-10 10:38:46 -04:00
opencode-agent[bot] 9a6b455bfe chore: generate 2026-04-10 14:08:27 +00:00
Kit LangtonandGitHub 8063e0b5c6 refactor(tool): convert lsp tool internals to Effect (#21806) 2026-04-10 10:07:19 -04:00
Kit LangtonandGitHub 157c5d77f8 refactor(tool): convert question tool internals to Effect (#21808) 2026-04-10 09:42:06 -04:00
Aiden ClineandGitHub ce19c051be fix: ts lsp (#21827) 2026-04-10 00:15:45 -05:00
Kit LangtonandGitHub 91786d2fc1 refactor(effect): use Git service in file and storage (#21803) 2026-04-09 22:49:36 -04:00
Kit LangtonandGitHub eca11ca71a refactor(effect): use SessionRevert service in prompt (#21796) 2026-04-09 22:28:11 -04:00
Kit LangtonandGitHub 17bd16667c refactor(effect): move tool descriptions into registry (#21795) 2026-04-09 22:20:27 -04:00
Kit LangtonandGitHub 16c60c9ee7 refactor(session): extract sharing orchestration (#21759) 2026-04-09 21:47:48 -04:00
Dax Raad 0970b102e1 Merge remote-tracking branch 'origin/dev' into dev 2026-04-09 21:35:09 -04:00
Dax Raad 04074d3f4a core: enable prod channel to use shared production database
Ensures users on the prod channel have their data persisted to the same
database as latest and beta channels, preventing data fragmentation
across different release channels.
2026-04-09 21:34:52 -04:00
Luke Parkerandopencode b16ee08fd5 ci use node 24 in test workflow fixing random ECONNRESET (#21782) 2026-04-10 01:00:21 +00:00
Luke Parkerandopencode 98874a09f7 fix windows e2e backend not stopping on sigterm waiting 10s for no reason (#21781) 2026-04-10 01:00:21 +00:00
opencode 877be7e8e0 release: v1.4.3 2026-04-10 01:00:12 +00:00
Dax Raad eac50f9151 ci: prevent beta branch builds from triggering production release steps
Skip Windows and Linux code signing, along with artifact downloads for
the beta branch to ensure beta builds don't go through production
release processes.
2026-04-09 17:06:53 -04:00
Dax Raad 1a902b291c ci: skip winget publish on beta and ensure finalize always runs
Beta releases no longer trigger unnecessary Winget submissions, and release
finalization now completes even when some build artifacts are missing.
2026-04-09 16:34:05 -04:00
opencode-agent[bot] bbe4a04f9f chore: generate 2026-04-09 20:29:48 +00:00
Kit LangtonandGitHub b2f621b897 refactor(session): inline init route orchestration (#21754) 2026-04-09 16:28:42 -04:00
7202b3a325 fix: ensure that openai oauth works for agent create cmd, use temporary hack (#21749)
Co-authored-by: OpeOginni <brightoginni123@gmail.com>
2026-04-09 15:25:59 -05:00
opencode-agent[bot] 35b44df94a chore: generate 2026-04-09 20:05:05 +00:00
Kit LangtonandGitHub 10441efad1 refactor(effect): extract session run state service (#21744) 2026-04-09 16:03:40 -04:00
Kit LangtonandGitHub 3199383eef fix: finalize interrupted bash via tool result path (#21724) 2026-04-09 15:20:28 -04:00
Kit LangtonandGitHub 9f54115c5d refactor: remove unused runtime facade exports (#21731) 2026-04-09 14:52:06 -04:00
Kit LangtonandGitHub 2ecc6ae65f fix(effect): suspend agent default layer construction (#21732) 2026-04-09 14:32:41 -04:00
Simon KleeandGitHub 02b32e1ba7 Revert "opencode: lazy-load top-level CLI commands" (#21726) 2026-04-09 20:03:48 +02:00
Kit LangtonandGitHub 34b9792654 delete unused withALS method (#21723) 2026-04-09 13:52:58 -04:00
Simon Klee 537160dbc0 opencode: lazy-load top-level CLI commands
The CLI imports every top-level command before argument parsing has
decided which handler will run. This makes simple invocations pay for
the full command graph up front and slows down the default startup path.

Parse the root argv first and load only the command module that matches
the selected top-level command. Keep falling back to the default TUI
path for non-command positionals, and preserve root help, version and
completion handling
2026-04-09 19:42:25 +02:00
Aiden ClineandGitHub b0600664ab feat: add support for fast modes for claude and gpt models (that support it) (#21706) 2026-04-09 12:06:26 -05:00
Kit LangtonandGitHub 581a7692ff fix(tui): restore hidden session scrollbar default (#20947) 2026-04-09 13:01:08 -04:00
opencode-agent[bot] f73e4d5d31 chore: generate 2026-04-09 16:47:02 +00:00
a7743e6467 feat(mcp): add OAuth redirect URI configuration for MCP servers (#21385)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-09 11:45:52 -05:00
opencode 5d3dba666c release: v1.4.2 2026-04-09 16:24:43 +00:00
DaxandGitHub bd53b651a3 refactor: fix tool call state handling and clean up imports (#21709) 2026-04-09 11:56:19 -04:00
Kit LangtonandGitHub 46da801f30 refactor(effect): drop shell abort signals from runner (#21599) 2026-04-09 10:54:26 -04:00
Kit LangtonandGitHub 58a99916bb fix: preserve text part timing in session processor (#21691) 2026-04-09 10:05:14 -04:00
Kit LangtonandGitHub c29392d085 fix: preserve interrupted bash output in tool results (#21598) 2026-04-09 10:03:26 -04:00
Brendan AllanandGitHub 46f243fea7 app: remove min loading duration (#21655) 2026-04-09 16:29:46 +08:00
opencode 847fc9d268 release: v1.4.1 2026-04-09 07:12:17 +00:00
489f57974d feat: add opencode go upsell modal when limits are hit (#21583)
Co-authored-by: Frank <frank@anoma.ly>
2026-04-09 02:16:29 -04:00
opencode-agent[bot] 3fc3974cbc chore: update nix node_modules hashes 2026-04-09 06:03:26 +00:00
opencode-agent[bot] ca57248246 chore: generate 2026-04-09 05:19:52 +00:00
ee23043d64 Remove CLI from electron app (#17803)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-04-09 13:18:46 +08:00
Cho HyeonJongandGitHub 9c1c061b84 fix(lsp): remove CMakeLists.txt and Makefile from clangd root markers (#21466) 2026-04-08 23:10:06 -05:00
opencode-agent[bot] d82b163e56 chore: update nix node_modules hashes 2026-04-09 02:27:34 +00:00
cd8e8a9928 feat(llm): integrate GitLab DWS tool approval with permission system (#19955)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-08 20:39:33 -05:00
Kit LangtonandGitHub 8bdcc22541 refactor(effect): inline session processor interrupt cleanup (#21593) 2026-04-08 21:19:01 -04:00
Kit LangtonandGitHub 2bdd279467 fix: propagate abort signal to inline read tool (#21584) 2026-04-08 21:07:55 -04:00
OpeOginniandGitHub 51535d8ef3 fix(app): skip url password setting for same-origin server and web app (#19923) 2026-04-09 07:13:10 +08:00
Kit LangtonandGitHub 38f8714c09 refactor(effect): build task tool from agent services (#21017) 2026-04-08 19:02:19 -04:00
Aiden ClineandGitHub 4961d72c0f tweak: separate ModelsDev.Model and Config model schemas (#21561) 2026-04-08 15:55:14 -05:00
Aiden ClineandGitHub 00cb8839ae fix: dont show invalid variants for BP (#21555) 2026-04-08 14:52:34 -05:00
Adam 689b1a4b3a fix(app): diff list normalization 2026-04-08 14:02:23 -05:00
Adam d98be39344 fix(app): patch tool diff rendering 2026-04-08 13:49:16 -05:00
Aiden ClineandGitHub 039c60170d fix: ensure that /providers list and shell endpoints are correctly typed in sdk and openapi schema (#21543) 2026-04-08 12:56:15 -05:00
Aiden ClineGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
cd87d4f9d3 test: update webfetch test (#21398)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-04-08 12:25:02 -05:00
Brendan AllanandGitHub 988c9894f2 ui: fix sticky session diffs header (#21486) 2026-04-08 17:01:52 +08:00
Kit LangtonGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
ae614d919f fix(tui): simplify console org display (#21339)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-04-07 21:03:24 -04:00
opencode-agent[bot]andopencode 65cde7f494 chore: update nix node_modules hashes 2026-04-08 00:32:40 +00:00
opencode 98325dcdc6 release: v1.4.0 2026-04-08 00:32:31 +00:00
opencode-agent[bot] 0788a535e2 chore: generate 2026-04-07 23:49:25 +00:00
b7fab49b64 refactor(snapshot): store unified patches in file diffs (#21244)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-04-07 19:48:23 -04:00
DaxandGitHub 463318486f core: refactor tool system to remove agent context from initialization (#21052) 2026-04-07 19:48:12 -04:00
Frank 7afb517a1a go: glm5.1 2026-04-07 17:55:09 -04:00
Frank c589724729 zen: glm5.1 doc 2026-04-07 17:46:11 -04:00
Frank 9385714373 zen: glm5.1 doc 2026-04-07 17:21:24 -04:00
DaxandGitHub c90fc6a486 feat(opencode): add OTLP observability support (#21387) 2026-04-07 17:02:55 -04:00
Aiden ClineandGitHub bc1840b196 fix(opencode): clear webfetch timeouts on failed fetches (#21378) 2026-04-07 20:46:02 +00:00
Kyle AltendorfandGitHub 095aeba0a7 test: disable GPG signing in test fixtures (#20386) 2026-04-07 15:26:01 -05:00
e945436b6f feat(tui): allow variant_list keybind for the "Switch model variant" command (#21185)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-07 14:46:04 -05:00
opencode-agent[bot] 6bfa82de65 chore: generate 2026-04-07 18:33:24 +00:00
Kit LangtonandGitHub d83fe4b540 fix(opencode): improve console login transport errors (#21350) 2026-04-07 18:31:53 +00:00
Aiden ClineandGitHub 81bdffc81c fix: ensure the alibaba provider errors are retried (#21355) 2026-04-07 17:46:01 +00:00
2549a38a71 fix(tui): use sentence case for theme mode command palette items (#21192)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-07 12:25:13 -05:00
James LongandGitHub 5d48e7bd44 refactor(core): support multiple event streams in worker and remove workspaces from plugin api (#21348) 2026-04-07 13:22:34 -04:00
AdamandGitHub ec8b9810b4 feat(app): better subagent experience (#20708) 2026-04-07 11:06:23 -05:00
Adam 65318a80f7 chore: update web stats 2026-04-07 11:02:37 -05:00
opencode-agent[bot] 6a5aae9a84 chore: generate 2026-04-07 14:14:07 +00:00
DaxandGitHub 1f94c48bdd fix(opencode): keep user message variants scoped to model (#21332) 2026-04-07 10:12:53 -04:00
Frank 01c5eb679c go: support coupon 2026-04-07 10:09:00 -04:00
Shoubhit DashandGitHub 41612b3dbe Move auto-accept permissions to settings (#21308) 2026-04-07 11:00:13 +00:00
Shoubhit DashandGitHub c2d2ca3522 style(app): redesign jump-to-bottom button per figma spec (#21313) 2026-04-07 10:47:53 +00:00
Shoubhit DashandGitHub 3a1ec27feb feat(app): show full names on composer attachment chips (#21306) 2026-04-07 10:15:22 +00:00
3c96bf8468 feat(opencode): Add PDF attachment Drag and Drop (#16926)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-04-07 04:39:59 +00:00
opencode-agent[bot] 3ea6413407 chore: update nix node_modules hashes 2026-04-07 04:38:49 +00:00
Aiden ClineandGitHub 885df8eb54 feat: add --dangerously-skip-permissions flag to opencode run (#21266) 2026-04-06 23:30:05 -05:00
Frank f4975ef32a go: add mimo 2026-04-07 00:16:34 -04:00
James LongandGitHub 37883a9f3a refactor(core): add full http proxy and change workspace adaptor interface (#21239) 2026-04-06 23:19:55 -04:00
Aiden ClineandGitHub 3c31d04666 chore: bump anthropic ai sdk pkg, delete patch (#21247) 2026-04-06 20:28:32 -05:00
opencode-agent[bot] e64548fb4d chore: update nix node_modules hashes 2026-04-07 00:30:30 +00:00
Aiden ClineandGitHub 31f6f43cfc chore: remove ai-sdk/provider-utils patch and update pkg (#21245) 2026-04-06 23:53:27 +00:00
opencode-agent[bot] 090ad8290e chore: update nix node_modules hashes 2026-04-06 23:42:39 +00:00
Aiden ClineandGitHub d1258ac19c fix: bump openrouter ai sdk pkg to fix openrouter issues (#21242) 2026-04-06 17:56:11 -05:00
Aiden ClineandGitHub 48c1b6b338 tweak: move the max token exclusions to plugins @rekram1-node (#21225) 2026-04-06 17:43:58 -05:00
Aiden ClineandGitHub 40e4cd27a1 tweak: adjust chat.params hook to allow altering of the maxOutputTokens (#21220) 2026-04-06 18:13:30 +00:00
Aiden ClineandGitHub 5a6d10cd53 tweak: ensure copilot anthropic models have same reasoning effort model as copilot cli, also fix qwen incorrectly having variants (#21212) 2026-04-06 18:12:43 +00:00
opencode-agent[bot] 527b51477d chore: update nix node_modules hashes 2026-04-06 18:08:04 +00:00
DaxGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>Luke ParkerAdamBrendan Allan
535343bf56 refactor(server): replace Bun serve with Hono node adapters (#18335)
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-04-06 13:24:55 -04:00
SebastianandGitHub 4394e42615 upgrade opentui to 0.1.97 (#21137) 2026-04-06 18:42:05 +02:00
2e4c43c1cf refactor: replace Bun.serve with Node http.createServer in OAuth handlers (#18327)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-04-06 12:17:29 -04:00
MCandGitHub 965c751522 docs: update Cloudflare provider setup to reflect /connect prompt flow (#20589) 2026-04-06 10:50:24 -05:00
opencode-agent[bot] 24bdd3c9fb chore: generate 2026-04-06 13:51:36 +00:00
Derek BarreraandGitHub 01f0319192 fix(lsp): MEMORY LEAK: ensure typescript server uses native project config (#19953) 2026-04-06 09:50:36 -04:00
opencode 517e6c9aa4 release: v1.3.17 2026-04-06 07:39:18 +00:00
Luke ParkerandGitHub a4a9ea4ab0 fix(tui): revert kitty keyboard events workaround on windows (#20180) 2026-04-06 07:04:50 +00:00
eaa272ef7f fix: show clear error when Cloudflare provider env vars are missing (#20399)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-04-06 05:26:04 +00:00
Frank 70b636a360 zen: normalize ipv6 2026-04-06 00:32:55 -04:00
Frank a8fd0159be zen: remove header check 2026-04-05 23:51:37 -04:00
opencode 342436dfc4 release: v1.3.16 2026-04-06 03:44:46 +00:00
Luke ParkerandGitHub 77a462c930 fix(tui): default Ctrl+Z to undo on Windows (#21138) 2026-04-06 02:38:35 +00:00
9965d385de fix: pass both 'openai' and 'azure' providerOptions keys for @ai-sdk/azure (#20272)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-06 02:34:53 +00:00
George HarkerandGitHub f0f1e51c5c fix(core): implement proper configOptions for acp (#21134) 2026-04-05 21:29:34 -05:00
Gautier DI FOLCOandGitHub 4712c18a58 feat(tui): make the mouse disablable (#6824, #7926) (#13748) 2026-04-05 21:14:11 -05:00
opencode-agent[bot] 9e156ea168 chore: update nix node_modules hashes 2026-04-06 01:18:03 +00:00
Luke ParkerandGitHub 68f4aa220e fix(plugin): parse package specifiers with npm-package-arg and sanitize win32 cache paths (#21135) 2026-04-06 00:26:40 +00:00
Aiden ClineandGitHub 3a0e00dd7f tweak: add newline between <content> and first line of read tool output to prevent confusion (#21070) 2026-04-05 04:55:22 +00:00
Frank 66b4e5e020 doc: udpate doc 2026-04-05 00:35:40 -04:00
Aiden ClineandGitHub 8b8d4fa066 test: add regression test for double counting bug (#21053) 2026-04-04 16:40:28 -07:00
opencode-agent[bot] 6253ef0c27 chore: generate 2026-04-04 23:26:37 +00:00
Kit LangtonandGitHub c6ebc7ff7c fix(tui): only show org switch affordances when useful (#21054) 2026-04-04 19:25:37 -04:00
opencode-agent[bot] 985663620f chore: generate 2026-04-04 23:07:01 +00:00
Kit LangtonandGitHub c796b9a19e refactor(effect): move read tool onto defineEffect (#21016) 2026-04-04 19:06:00 -04:00
Kit LangtonandGitHub 6ea108a03b feat(tui): show console-managed providers (#20956) 2026-04-04 19:05:45 -04:00
Aiden ClineandGitHub 280eb16e77 fix: ensure reasoning tokens arent double counted when calculating usage (#21047) 2026-04-04 15:29:53 -07:00
opencode 930e94a3ea release: v1.3.15 2026-04-04 20:54:20 +00:00
DaxandGitHub 629e866ff0 fix(npm): Arborist reify fails on compiled binary — Bun pre-resolves node-gyp path at build time (#21040) 2026-04-04 16:27:20 -04:00
c08fa5675f refactor: remove redundant Kimi skill section (#20393)
Co-authored-by: dongyuxin <dongyuxin@dev.dongyuxin.msh-dev.svc.cluster.local>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-04 18:32:26 +00:00
opencode cc50b778eb release: v1.3.14 2026-04-04 18:32:17 +00:00
Kit LangtonandGitHub 00fa68b3a7 fix(ci): create JUnit output dirs before tests (#20959) 2026-04-03 22:47:20 -04:00
288eb044cb perf(opencode): batch snapshot diffFull blob reads (#20752)
Co-authored-by: Nate Williams <50088025+natewill@users.noreply.github.com>
2026-04-04 01:05:23 +00:00
59ca4543d8 refactor(provider): stop custom loaders using facades (#20776)
Co-authored-by: luanweslley77 <213105503+luanweslley77@users.noreply.github.com>
2026-04-04 00:24:24 +00:00
opencode-agent[bot] 650d0dbe54 chore: generate 2026-04-03 22:55:05 +00:00
SebastianandGitHub a5ec741cff notes on v2 (#20941) 2026-04-04 00:54:03 +02:00
Aiden ClineandGitHub fff98636f7 chore: rm models snapshot (#20929) 2026-04-03 19:44:50 +00:00
Kit LangtonandGitHub c72642dd35 test(ci): publish unit reports in actions (#20547) 2026-04-03 16:12:01 +00:00
f2d4ced8ea refactor(effect): build todowrite tool from Todo service (#20789)
Co-authored-by: Juan Pablo Carranza Hurtado <52012198+jpcarranza94@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 12:05:40 -04:00
Adam ae7e2eb3fb chore(app): remove queued follow-ups for now 2026-04-03 10:56:40 -05:00
Adam a32ffaba35 fix(app): show correct submit icon when typing follow up 2026-04-03 10:51:07 -05:00
opencode-agent[bot] a4e75a0794 chore: generate 2026-04-03 14:56:44 +00:00
Shoubhit DashandGitHub 35350b1d25 feat: restore git-backed review modes (#20845) 2026-04-03 09:54:57 -05:00
Shoubhit DashandGitHub 263dcf75b5 fix: restore prompt focus after footer selection (#20841) 2026-04-03 14:36:33 +00:00
Kit LangtonandGitHub 7994dce0f2 refactor(effect): resolve built tools through the registry (#20787) 2026-04-03 10:31:00 -04:00
Shoubhit DashandGitHub fbfa148e4e fix(app): hide default session timestamps (#20892) 2026-04-03 13:51:54 +00:00
9d57f21f9f feat(ui): redesign modified files section in session turn (#20348)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2026-04-03 08:32:53 -05:00
Shoubhit DashandGitHub 3deee3a02b fix(session): disable todo dock auto-scroll (#20840) 2026-04-03 08:32:30 -05:00
Shoubhit DashandGitHub 2002f08f2e fix(prompt): unmount model controls in shell mode (#20886) 2026-04-03 13:16:26 +00:00
Shoubhit DashandGitHub c307505f8b fix(session): delay jump-to-bottom button (#20853) 2026-04-03 08:12:01 -05:00
Nate WilliamsandGitHub 6359d00fb4 fix(core): fix restoring earlier messages in a reverted chain (#20780) 2026-04-03 18:23:00 +05:30
Brendan AllanandGitHub b969066a20 electron: better menus (#20878) 2026-04-03 12:41:30 +00:00
opencode-agent[bot] 500dcfc586 chore: update nix node_modules hashes 2026-04-03 03:53:46 +00:00
Luke ParkerandGitHub 7b8dc8065e fix(sdk): handle Windows opencode spawn and shutdown (#20772) 2026-04-03 13:18:50 +10:00
Kevin FlansburgandGitHub e89527c9f0 feat: Send x-session-affinity and x-parent-session-id headers (#20744) 2026-04-02 22:09:53 -05:00
DaxandGitHub aa2239d5de add automatic heap snapshots for high-memory cli processes (#20788) 2026-04-03 02:34:33 +00:00
opencode-agent[bot] 8daeacc989 chore: generate 2026-04-03 02:10:50 +00:00
81d3ac3bf0 fix: prevent Tool.define() wrapper accumulation on object-defined tools (#16952)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 22:09:53 -04:00
Luke ParkerandGitHub eb6f1dada8 fix: call models.dev once instead of twice on start (#20765) 2026-04-03 11:26:53 +10:00
Kit LangtonandGitHub 8e9e79d276 refactor(share): effectify share next (#20596) 2026-04-03 00:56:56 +00:00
Aiden ClineandGitHub 38014fe448 fix: rm dynamic part from bash tool description again to restore cache hits across projects (#20771) 2026-04-03 00:16:40 +00:00
Kit LangtonandGitHub 8942fc21aa refactor(effect): prune unused facades (#20748) 2026-04-02 20:15:09 -04:00
7f45943a9e fix(opencode): honor model limit.input overrides (#16306)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-03 00:13:10 +00:00
SebastianandGitHub 6e1400fc45 dialog aware prompt cursor (#20753) 2026-04-02 23:20:56 +02:00
opencode-agent[bot] bf26c08d51 chore: update nix node_modules hashes 2026-04-02 20:47:24 +00:00
SebastianandGitHub 29f7dc073b Adds TUI prompt traits, refs, and plugin slots (#20741) 2026-04-02 22:11:17 +02:00
Kit LangtonandGitHub 5e1b513527 refactor(todo): effectify session todo (#20595) 2026-04-02 19:11:23 +00:00
Kit LangtonandGitHub f549fde874 test(app): emit junit artifacts for playwright (#20732) 2026-04-02 15:07:46 -04:00
Kit LangtonandGitHub 6dfb30448c refactor(app): unexport internal e2e helpers (#20730) 2026-04-02 18:53:51 +00:00
Kit LangtonandGitHub b5b5f7e019 test(opencode): remove temporary e2e url repro (#20729) 2026-04-02 14:35:21 -04:00
Kit LangtonandGitHub ae7b49b034 docs(effect): refresh migration status (#20665) 2026-04-02 14:33:58 -04:00
opencode-agent[bot] f151c660b1 chore: update nix node_modules hashes 2026-04-02 18:31:14 +00:00
Kit LangtonandGitHub c3ef69c866 test(app): add a golden path for mocked e2e prompts (#20593) 2026-04-02 18:17:28 +00:00
opencode-agent[bot] 363891126c chore: generate 2026-04-02 17:58:00 +00:00
1989704abe feat(acp): Add messageID and emit user_message_chunk on prompt/command (#18625)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-02 12:56:56 -05:00
opencode-agent[bot] f0a9ebfed4 chore: generate 2026-04-02 17:54:07 +00:00
7e32f80d82 feat: add macOS managed preferences support for enterprise MDM deployments (#19178)
Co-authored-by: Lenny Vaknine <lvaknine@gitlab.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-02 12:52:49 -05:00
Brendan AllanandGitHub 966d9cfa41 electron: add basic context menu for inspect element (#20723) 2026-04-02 17:41:26 +00:00
Frank 92e820fdc8 go: add mimo 2026-04-02 12:59:04 -04:00
Brendan AllanandGitHub c4b3971548 app: unify auto scroll ref handling (#20716) 2026-04-02 16:44:52 +00:00
DaxandGitHub 3faabdadb7 refactor(format): update formatter interface to return command from enabled() (#20703) 2026-04-02 12:22:01 -04:00
JackandGitHub 93a139315c Add MiMo-V2 models to Go UI and docs (#20709) 2026-04-02 12:01:04 -04:00
Aiden ClineandGitHub 10ca1ace6b tweak: add abort signal timeout to the github copilot model fetch to prevent infinite blocking (#20705) 2026-04-02 10:53:08 -05:00
c3dfd08ba8 fix(format): use biome format instead of check to prevent import removal (#20545)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-02 10:52:52 -05:00
Aiden ClineandGitHub 510a1e8140 ignore: fix typecheck in dev (#20702) 2026-04-02 15:38:30 +00:00
opencode-agent[bot] 159ede2d5c chore: generate 2026-04-02 15:19:26 +00:00
Noam BresslerandGitHub 291a857fb8 feat: add optional messageID to ShellInput (#20657) 2026-04-02 10:18:16 -05:00
opencode-agent[bot] 57a5236e71 chore: generate 2026-04-02 15:01:45 +00:00
Aiden ClineandGitHub 23c8656080 refactor: split up models.dev and config model definitions to prevent coupling (#20605) 2026-04-02 10:00:43 -05:00
opencode-agent[bot] ec3ae17e4d chore: update nix node_modules hashes 2026-04-02 10:23:59 +00:00
Brendan AllanandGitHub 69d047ae7d cleanup event listeners with solid-primitives/event-listener (#20619) 2026-04-02 09:40:03 +00:00
Brendan AllanandGitHub 327f62526a use solid-primitives/resize-observer across web code (#20613) 2026-04-02 17:24:10 +08:00
Shoubhit DashandGitHub d540d363a7 refactor: simplify solid reactivity across app and web (#20497) 2026-04-02 17:14:05 +08:00
Frank db93891373 zen: friendly trial ended message 2026-04-02 03:15:35 -04:00
Brendan AllanandGitHub 0f488996b3 fix(node): set OPENCODE_CHANNEL during build (#20616) 2026-04-02 06:05:36 +00:00
opencode-agent[bot] a6f524ca08 chore: update nix node_modules hashes 2026-04-02 04:47:27 +00:00
Frank 811c7e2494 cli: update usage exceeded error 2026-04-02 00:25:23 -04:00
opencode-agent[bot] ebaa99aba2 chore: generate 2026-04-02 04:06:47 +00:00
dpuyosaandGitHub d66e6dc25f feat(opencode): Add Venice AI package as dependency (#20570) 2026-04-01 23:05:49 -05:00
Kit LangtonandGitHub 336d28f112 fix(cli): restore colored help logo (#20592) 2026-04-02 03:21:07 +00:00
Kit LangtonandGitHub 916afb5220 refactor(account): share token freshness helper (#20591) 2026-04-02 02:57:45 +00:00
5daf2fa7f0 fix(session): compaction agent responds in same language as conversation (#20581)
Co-authored-by: Aaron Zhu <aaron@Aarons-MacBook-Air.local>
2026-04-01 21:44:16 -05:00
733a3bd031 fix(core): prevent agent loop from stopping after tool calls with OpenAI-compatible providers (#14973)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-04-01 21:34:01 -05:00
Kit LangtonandGitHub 2e8e278441 fix(cli): use simple logo in CLI (#20585) 2026-04-02 02:27:09 +00:00
Kit LangtonandGitHub 0bae38c062 refactor(instruction): migrate to Effect service pattern (#20542) 2026-04-01 22:22:51 -04:00
Kit LangtonandGitHub a09b086729 test(app): block real llm calls in e2e prompts (#20579) 2026-04-01 22:22:43 -04:00
Aiden ClineandGitHub df1c6c9e8d tui: add consent dialog when sharing for the first time (#20525) 2026-04-02 01:58:57 +00:00
opencode-agent[bot] 789d86f7b0 chore: generate 2026-04-02 01:56:34 +00:00
Kit LangtonandGitHub e148b318b7 fix(build): replace require() with dynamic import() in cross-spawn-spawner (#20580) 2026-04-01 21:55:35 -04:00
0cad775427 chore: add User-Agent headers for Cloudflare providers (#20538)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-01 20:02:17 -05:00
Kit LangtonandGitHub 00d6841f84 fix(account): refresh console tokens before expiry (#20558) 2026-04-02 00:25:24 +00:00
SebastianandGitHub 8a8f7b3e90 flock npm.add (#20557) 2026-04-02 00:21:26 +00:00
Kit LangtonandGitHub c526caae7b fix: show model display name in message footer and transcript (#20539) 2026-04-02 00:17:38 +00:00
Kit LangtonandGitHub b1c07488bd refactor(revert): yield SessionSummary.Service directly (#20541) 2026-04-01 20:10:59 -04:00
Kit LangtonandGitHub 92f8e03160 fix(test): use effect helper in snapshot race test (#20567) 2026-04-01 20:05:47 -04:00
SebastianandGitHub f6fd43e574 Refactor plugin/config loading, add theme-only plugin package support (#20556) 2026-04-01 23:50:22 +00:00
opencode-agent[bot] 854484babf chore: generate 2026-04-01 23:49:44 +00:00
Kit LangtonandGitHub e4ff1ea778 refactor(bash): use Effect ChildProcess for bash tool execution (#20496) 2026-04-01 19:48:47 -04:00
Kit LangtonandGitHub 26fb6b8788 refactor: add Effect-returning versions of MessageV2 functions (#20374) 2026-04-01 19:48:36 -04:00
opencode-agent[bot] 4214ae205d chore: generate 2026-04-01 23:48:30 +00:00
Kit LangtonandGitHub d9d4f895bc fix(test): auto-acknowledge tool-result follow-ups in mock LLM server (#20528) 2026-04-01 23:47:26 +00:00
Kit LangtonandGitHub 48db7cf07a fix(opencode): batch snapshot revert without reordering (#20564) 2026-04-01 23:46:06 +00:00
Luke ParkerandGitHub 802d165572 chore(tui): clean up scroll config follow-up (#20561) 2026-04-02 09:36:49 +10:00
Luke ParkerandGitHub f7f41dc3a0 fix(tui): apply scroll configuration uniformly across all scrollboxes (#14735) 2026-04-02 09:15:19 +10:00
Aiden ClineandGitHub 1fcfb69bf7 feat: add new provider plugin hook for resolving models and sync models from github models endpoint (falls back to models.dev) (#20533) 2026-04-01 23:04:14 +00:00
Luke ParkerandGitHub fa96cb9c6e Fix selection expansion by retaining focused input selections during global key events (#20205) 2026-04-02 08:43:40 +10:00
SebastianandGitHub cc30bfc94b resolve subpath only packages for plugins (#20555) 2026-04-01 22:14:36 +00:00
880c0a7477 fix: normalize filepath in FileTime to prevent Windows path mismatch (#20367)
Co-authored-by: JosXa <info@josxa.dev>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-04-02 07:45:50 +10:00
Frank eabf3caeb9 zen: sync 2026-04-01 17:41:04 -04:00
DaxGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>Brendan AllanAiden Cline
c9326fc199 refactor: replace BunProc with Npm module using @npmcli/arborist (#18308)
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-04-01 21:01:37 +00:00
Frank d7481f4593 wip: zen 2026-04-01 14:17:31 -04:00
Kit LangtonandGitHub f3f728ec27 test(app): fix isolated backend follow-ups (#20513) 2026-04-01 17:43:19 +00:00
Kit LangtonandGitHub c619caefdd fix(account): coalesce concurrent console token refreshes (#20503) 2026-04-01 13:16:35 -04:00
Kit LangtonandGitHub c559af51ce test(app): migrate more e2e suites to isolated backend (#20505) 2026-04-01 13:15:42 -04:00
github-actions[bot] d1e0a4640c Update VOUCHED list
https://github.com/anomalyco/opencode/issues/20482#issuecomment-4171492178
2026-04-01 16:50:21 +00:00
opencode-agent[bot] f9e71ec515 chore: update nix node_modules hashes 2026-04-01 16:47:33 +00:00
opencode-agent[bot] ef538c9707 chore: generate 2026-04-01 16:14:37 +00:00
Kit LangtonandGitHub 2f405daa98 refactor: use Effect services instead of async facades in provider, auth, and file (#20480) 2026-04-01 16:13:13 +00:00
Kit LangtonandGitHub a9c85b7c27 refactor(shell): use Effect ChildProcess for shell command execution (#20494) 2026-04-01 12:07:57 -04:00
Shoubhit DashandGitHub 897d83c589 refactor(init): tighten AGENTS guidance (#20422) 2026-04-01 21:37:25 +05:30
opencode-agent[bot] 0a125e5d4d chore: generate 2026-04-01 15:59:28 +00:00
Kit LangtonandGitHub 38d2276592 test(e2e): isolate prompt tests with per-worker backend (#20464) 2026-04-01 15:58:11 +00:00
Dax Raad d58004a864 fall back to first agent if last used agent is not available 2026-04-01 11:09:29 -04:00
Kit LangtonandGitHub 5fd833aa18 refactor: standardize InstanceState variable name to state (#20267) 2026-04-01 10:39:43 -04:00
Shoubhit DashandGitHub 44f83015cd perf(review): defer offscreen diff mounts (#20469) 2026-04-01 19:29:12 +05:30
Kit LangtonandGitHub 9a1c9ae15a test(app): route prompt e2e through mock llm (#20383) 2026-04-01 08:28:38 -04:00
Shoubhit DashandGitHub a3a6cf1c07 feat(comments): support file mentions (#20447) 2026-04-01 16:11:57 +05:30
Shoubhit DashandGitHub 47a676111a fix(session): add keyboard support to question dock (#20439) 2026-04-01 15:47:15 +05:30
Brendan AllanandGitHub 1df5ad470a app: try to hide autofill popups in prompt input (#20197) 2026-04-01 08:43:03 +00:00
Brendan AllanandGitHub 506dd75818 electron: port mergeShellEnv logic from tauri (#20192) 2026-04-01 07:01:44 +00:00
Kit LangtonandGitHub c8ecd64022 test(app): add mock llm e2e fixture (#20375) 2026-03-31 21:24:39 -04:00
opencode-agent[bot] ca376a4cff chore: update nix node_modules hashes 2026-04-01 01:15:51 +00:00
Kit LangtonandGitHub 7532d99e5b test: finish HTTP mock processor coverage (#20372) 2026-04-01 00:45:42 +00:00
Kit Langtonandopencode 181b5f6236 refactor(prompt): use Provider service in effect layers (#20167) 2026-04-01 00:44:15 +00:00
opencode 6314f09c14 release: v1.3.13 2026-04-01 00:44:06 +00:00
SebastianandGitHub 4b4b7832aa upgrade opentui to 0.1.95 (#20369) 2026-04-01 01:53:05 +02:00
opencode-agent[bot] 4280307013 chore: update nix node_modules hashes 2026-03-31 23:19:18 +00:00
opencode-agent[bot] 9b09a7e766 chore: generate 2026-03-31 23:15:56 +00:00
Kit LangtonandGitHub 3fc0367b93 refactor(session): effectify SessionRevert service (#20143) 2026-03-31 19:14:49 -04:00
Kit LangtonandGitHub 954a6ca88e refactor(session): effectify SessionSummary service (#20142) 2026-03-31 19:14:45 -04:00
Kit LangtonandGitHub 0c03a3ee10 test: migrate prompt tests to HTTP mock LLM server (#20304) 2026-03-31 19:14:32 -04:00
github-actions[bot]andopencode 53330a518f Update VOUCHED list
https://github.com/anomalyco/opencode/issues/20333#issuecomment-4166038038
2026-03-31 22:35:10 +00:00
opencode 892bdebaac release: v1.3.12 2026-03-31 22:35:01 +00:00
SebastianandGitHub 18121300f3 upgrade opentui to 0.1.94 (#20357) 2026-03-31 23:54:13 +02:00
github-actions[bot] d6d4446f46 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/20342#issuecomment-4165277636
2026-03-31 20:24:07 +00:00
Major HaydenandGitHub 26cc924ea2 feat: enable prompt caching and cache token tracking for google-vertex-anthropic (#20266)
Signed-off-by: Major Hayden <major@mhtx.net>
2026-03-31 15:16:14 -05:00
Aiden ClineandGitHub 4dd866d5c4 fix: rm exclusion of ai-sdk/azure in transform.ts, when we migrated to v6 the ai sdk changed the key for ai-sdk/azure so the exclusion is no longer needed (#20326) 2026-03-31 14:57:15 -05:00
opencode beab4cc2c2 release: v1.3.11 2026-03-31 19:55:41 +00:00
DaxandGitHub 567a91191a refactor(session): simplify LLM stream by replacing queue with fromAsyncIterable (#20324) 2026-03-31 15:27:51 -04:00
Aiden ClineandGitHub 434d82bbe2 test: update model test fixture (#20182) 2026-03-31 16:20:01 +00:00
Aiden ClineandGitHub 2929774acb chore: rm harcoded model definition from codex plugin (#20294) 2026-03-31 11:13:11 -05:00
Adam 6e61a46a84 chore: skip 2 tests 2026-03-31 10:56:06 -05:00
2daf4b805a feat: add a dedicated system prompt for Kimi models (#20259)
Co-authored-by: dongyuxin <dongyuxin@dev.dongyuxin.msh-dev.svc.cluster.local>
2026-03-31 17:44:17 +02:00
opencode-agent[bot] 7342e650c0 chore: update nix node_modules hashes 2026-03-31 15:33:12 +00:00
Adam 8c2e2ecc95 chore: e2e model 2026-03-31 10:14:26 -05:00
SebastianandGitHub 25a2b739e6 warn only and ignore plugins without entrypoints, default config via exports (#20284) 2026-03-31 17:14:03 +02:00
Adam 85c16926c4 chore: use paid zen model in e2e 2026-03-31 10:06:44 -05:00
SebastianandGitHub 2e78fdec43 ensure pinned plugin versions and do not run package scripts on install (#20248) 2026-03-31 16:59:43 +02:00
SebastianandGitHub 1fcb920eb4 upgrade opentui to 0.1.93 (#19950) 2026-03-31 16:50:23 +02:00
opencode b1e89c344b release: v1.3.10 2026-03-31 13:31:37 +00:00
DaxandGitHub befbedacdc fix(session): subagents not being clickable (#20263) 2026-03-31 08:58:46 -04:00
Frank 2cc738fb17 wip: zen 2026-03-31 00:07:56 -04:00
opencode-agent[bot] 71b20698bb chore: generate 2026-03-31 01:57:41 +00:00
Kit LangtonandGitHub 3df18dcde1 refactor(provider): effectify Provider service (#20160) 2026-03-30 21:56:43 -04:00
Kit LangtonandGitHub a898c2ea3a refactor(storage): effectify Storage service (#20132) 2026-03-31 01:16:02 +00:00
Kit LangtonandGitHub bf777298c8 fix(theme): darken muted text in catppuccin tui themes (#20161) 2026-03-30 21:06:05 -04:00
Luke ParkerandGitHub 93fad99f7f smarter changelog (#20138) 2026-03-31 00:05:46 +00:00
opencode 057848deb8 release: v1.3.9 2026-03-30 23:43:58 +00:00
Luke ParkerandGitHub 1de06452d3 fix(plugin): properly resolve entrypoints without leading dot (#20140) 2026-03-31 09:21:17 +10:00
Frank 58f60629a1 wip: zen 2026-03-30 19:04:42 -04:00
Frank 39a47c9b8c wip: zen 2026-03-30 18:50:09 -04:00
opencode-agent[bot] ea88044f2e chore: generate 2026-03-30 21:49:45 +00:00
Kit LangtonandGitHub e6f6f7aff1 refactor: replace Filesystem util with AppFileSystem service (#20127) 2026-03-30 21:48:28 +00:00
opencode 48e97b47af release: v1.3.8 2026-03-30 21:35:10 +00:00
opencode-agent[bot] fe120e3cbf chore: update nix node_modules hashes 2026-03-30 20:46:10 +00:00
Frank f2dd774660 zen: qwen3.6 plus 2026-03-30 16:43:55 -04:00
Frank e7ff0f17c8 zen: qwen3.6 plus 2026-03-30 16:32:22 -04:00
Kit LangtonandGitHub 2ed756c72c fix(session): restore busy route handling and add regression coverage (#20125) 2026-03-30 20:30:34 +00:00
Frank 054f4be185 zen: add safety identifier 2026-03-30 16:28:40 -04:00
e3e1e9af50 fix(Opencode): Bump ai-sdk/xai to 3.0.75 to fix tool calls (#20123)
Co-authored-by: Jaaneek <jankiewiczmilosz@gmail.com>
2026-03-30 15:11:21 -05:00
opencode-agent[bot] c8389cf96d chore: generate 2026-03-30 20:08:09 +00:00
Kit LangtonandGitHub c5442d418d refactor(session): effectify SessionPrompt service (#19483) 2026-03-30 20:06:51 +00:00
SebastianandGitHub fa95a61c4e Refactor into plugin loader and do not enforce (#20112) 2026-03-30 20:36:21 +02:00
Aiden ClineandGitHub 9f3c2bd861 fix: agent value passed to chat.params and chat.headers hooks was not a string (#19996) 2026-03-30 16:32:27 +00:00
AdamandGitHub c2f78224ae chore(app): cleanup (#20062) 2026-03-30 08:50:42 -05:00
SebastianandGitHub 14f9e21d5c pluggable home footer (#20057) 2026-03-30 14:33:01 +02:00
SebastianandGitHub 8e4bab5181 update plugin themes when plugin was updated (#20052) 2026-03-30 13:51:07 +02:00
JackandGitHub 3c32013eb1 fix: preserve image attachments when selecting slash commands (#19771) 2026-03-30 17:11:34 +08:00
opencode 47d2ab120a release: v1.3.7 2026-03-30 06:06:12 +00:00
Luke ParkerandGitHub 186af2723d make variant modal less annoying (#19998) 2026-03-30 15:42:38 +10:00
Luke ParkerandGitHub 6926fe1c74 fix: stabilize release changelog generation (#19987) 2026-03-30 04:37:02 +00:00
Chris YangandGitHub ee018d5c82 docs: rename patch tool to apply_patch and clarify apply_patch behavior (#19979) 2026-03-29 23:01:57 -05:00
opencode-agent[bot] 0465579d6b chore: update nix node_modules hashes 2026-03-30 03:53:11 +00:00
Knut ZuidemaandGitHub 196a03caff fix: discourage _noop tool call during LiteLLM compaction (#18539) 2026-03-29 22:48:17 -05:00
Luke ParkerandGitHub b234370080 feat(windows): add first-class pwsh/powershell support (#16069) 2026-03-30 13:10:01 +10:00
SebastianandGitHub 5d2dc8888c theme colors for dialog textarea placeholders (#19939) 2026-03-29 21:37:46 +02:00
SebastianandGitHub 0b1018f6dd plugins installs should preserve jsonc comments (#19938) 2026-03-29 21:15:03 +02:00
Aiden Clineandopencode afb6abff73 fix: ensure OPENCODE_DISABLE_CLAUDE_CODE_PROMPT is respected for project lvl CLAUDE.md (#19924) 2026-03-29 18:02:29 +00:00
opencode e7f94f9b9a release: v1.3.6 2026-03-29 18:02:20 +00:00
72c77d0e7b fix(session): fix token usage double-counting w/ anthropic & bedrock due to AI SDK v6 upgrade (#19758)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-03-29 12:40:10 -05:00
Ariane EmoryandGitHub 5c15755a10 docs: add question tool to available permissions list (#19922) 2026-03-29 17:35:52 +00:00
Frank 3a4bfeb5b5 wip: zen 2026-03-29 13:06:00 -04:00
Frank 1037c72d99 wip: zen 2026-03-29 12:47:53 -04:00
Kit LangtonandGitHub ba00e9a993 Fix variant dialog filtering (#19917) 2026-03-29 16:43:13 +00:00
Frank 963dad75ef ci: fix 2026-03-29 12:41:52 -04:00
Frank 7e9b721e97 wip: zen 2026-03-29 12:04:20 -04:00
Luke Parkerandopencode a5b1dc081d test: add regression coverage for sync plugin hooks (#19589) 2026-03-29 03:43:00 +00:00
opencode 0bc2f99f2d release: v1.3.5 2026-03-29 03:42:52 +00:00
Aiden ClineandGitHub 55895d0663 core: fix plugin hooks to properly handle async operations ensuring plugins can execute async code without errors (#19586) 2026-03-29 03:15:41 +00:00
Aiden ClineandGitHub 72cb9dfa31 tweak: adjust gpt prompt to be more minimal, fix file reference annoyances (#19585) 2026-03-28 21:32:40 -05:00
opencode f0a9075fdf release: v1.3.4 2026-03-29 01:00:44 +00:00
Luke ParkerandGitHub fee1e25ab4 ci: cancel stale nix-hashes runs (#19571) 2026-03-29 10:39:02 +10:00
Frank a94ac5aa2c zen: ZDR policy 2026-03-28 20:16:52 -04:00
Frank 62ac45a9c9 wip: zen 2026-03-28 20:16:51 -04:00
Frank f7c2ef876f wip: zen 2026-03-28 20:16:51 -04:00
opencode-agent[bot] 6639f92739 chore: update nix node_modules hashes 2026-03-29 00:07:30 +00:00
Aiden ClineandGitHub 36aeb32159 ignore: kill todo (#19566) 2026-03-29 00:06:32 +00:00
Caleb NortonandGitHub ff37d7c2df fix: nix embedded web-ui support (#19561) 2026-03-28 18:39:22 -05:00
Caleb NortonandGitHub 4f96eb239f fix: respect semver build identifiers for nix (#11915) 2026-03-28 18:39:03 -05:00
SebastianandGitHub 38af99dcb4 prompt slot (#19563) 2026-03-29 00:27:27 +01:00
opencode-agent[bot] 772059acb5 chore: update nix node_modules hashes 2026-03-28 20:46:03 +00:00
Vladimir GlafirovandGitHub 1f290fc1ba fix: update opencode-gitlab-auth to 2.0.1 (#19552) 2026-03-28 20:10:06 +00:00
SebastianandGitHub 77d4f99497 use theme color for prompt placeholder (#19535) 2026-03-28 17:20:37 +01:00
DaxandGitHub aa2d753e7e feat: dialog variant menu and subagent improvements (#19537) 2026-03-28 16:16:00 +00:00
Kit LangtonandGitHub 860531c275 refactor(session): effectify session processor (#19485) 2026-03-28 12:09:47 -04:00
DaxandGitHub 2b86b36c8c feat: open dialog for model variant selection instead of cycling (#19534) 2026-03-28 15:37:20 +00:00
SebastianandGitHub 8ac2fbbd12 kv theme before default fallback (#19523) 2026-03-28 15:54:54 +01:00
opencode-agent[bot] 26382c6216 chore: update nix node_modules hashes 2026-03-28 14:52:13 +00:00
opencode-agent[bot] 0981b8eb71 chore: generate 2026-03-28 14:26:45 +00:00
Kit LangtonandGitHub aa9ed001d3 refactor(file): use AppFileSystem instead of raw Filesystem (#19458) 2026-03-28 10:25:46 -04:00
SebastianandGitHub 6086072567 upgrade opentui to 0.1.91 (#19440) 2026-03-28 15:10:59 +01:00
Dax Raad 6c14ea1d22 tweak(session): add top spacing and remove obsolete docs prompt 2026-03-28 01:56:11 -04:00
DaxandGitHub c3a9ec4a99 fix: restore subagent footer and fix style guide violations (#19491) 2026-03-28 01:46:29 -04:00
DaxandGitHub 41b0d03f6a feat: add model variant selection dialog (#19488) 2026-03-28 01:21:28 -04:00
DaxandGitHub 81eb6e670b refactor(prompt): remove variant cycle display from footer (#19489) 2026-03-28 01:21:16 -04:00
DaxandGitHub 8446719b13 refactor(session): move context into prompt footer (#19486) 2026-03-28 01:05:18 -04:00
Aiden ClineandGitHub 15a8c22a26 tweak: adjust bash tool description to increase cache hit rates between projects (#19487) 2026-03-27 23:53:36 -05:00
opencode-agent[bot] 48326e8d9c chore: update nix node_modules hashes 2026-03-28 04:20:56 +00:00
Kit LangtonandGitHub 43bc5551e8 update effect to 4.0.0-beta.42 (#19484) 2026-03-27 23:25:05 -04:00
AdamandGitHub f736116967 fix(app): more startup efficiency (#19454) 2026-03-28 01:26:57 +00:00
Luke ParkerandGitHub 82fc493520 feat(ci): use Azure Artifact Signing for Windows releases (#15201) 2026-03-28 11:20:19 +10:00
Kit LangtonandGitHub 2145d97f18 refactor(session): effectify SessionCompaction service (#19459) 2026-03-28 01:09:14 +00:00
SebastianandGitHub f3997d8082 Single target plugin entrypoints (#19467) 2026-03-27 23:44:46 +00:00
opencode-agent[bot] 02b19bc3d7 chore: generate 2026-03-27 21:38:08 +00:00
Kit LangtonandGitHub 5cd54ec345 refactor(format): use ChildProcessSpawner instead of Process.spawn (#19457) 2026-03-27 17:37:07 -04:00
opencode-agent[bot] c8909908f5 chore: update nix node_modules hashes 2026-03-27 21:11:06 +00:00
James LongandGitHub 4b9660b211 refactor(core): move more responsibility to workspace routing (#19455) 2026-03-27 16:33:56 -04:00
Kit LangtonandGitHub e5f0e813b6 refactor(session): effectify Session service (#19449) 2026-03-27 16:25:47 -04:00
Aiden ClineandGitHub c33d9996f0 feat: AI SDK v6 support (#18433) 2026-03-27 15:24:30 -05:00
SebastianandGitHub 7a7643c86a no theme override in dev (#19456) 2026-03-27 20:21:15 +00:00
Aiden ClineandGitHub 6f5b70e681 tweak: add additional overflow error patterns (#19446) 2026-03-27 15:19:51 -05:00
SebastianandGitHub ff13524a53 fix flaky plugin tests (no mock.module for bun) (#19445) 2026-03-27 20:55:03 +01:00
Kit LangtonandGitHub e973bbf54a fix(app): default file tree to closed with minimum width (#19426) 2026-03-27 14:11:50 -04:00
Kit LangtonandGitHub d36b38e4a6 fix(desktop-electron): match dev dock icon inset on macOS (#19429) 2026-03-27 17:32:05 +00:00
Burak Yigit KayaandGitHub bdd7829c68 fix(app): resize layout viewport when mobile keyboard appears (#15841) 2026-03-27 11:39:13 -05:00
Shoubhit DashandGitHub a93374c48f fix(ui): make streamed markdown feel more continuous (#19404) 2026-03-27 22:06:47 +05:30
Adam af2ccc94eb chore(app): more spacing controls 2026-03-27 11:22:28 -05:00
James LongandGitHub a76be695c7 refactor(core): split out instance and route through workspaces (#19335) 2026-03-27 11:51:21 -04:00
Kit LangtonandGitHub e528ed5d86 effectify Plugin service internals (#19365) 2026-03-27 15:20:11 +00:00
opencode-agent[bot] bb8d2cdd10 chore: update nix node_modules hashes 2026-03-27 14:45:53 +00:00
Kit LangtonandGitHub decb5e68ee effectify Skill service internals (#19364) 2026-03-27 10:15:51 -04:00
opencode-agent[bot] 21023337fa chore: generate 2026-03-27 14:01:28 +00:00
SebastianandGitHub 6274b0677c tui plugins (#19347) 2026-03-27 15:00:26 +01:00
opencode-agent[bot] d8ad8338f5 chore: generate 2026-03-27 13:53:59 +00:00
Kit LangtonandGitHub 7b44918149 refactor(tool-registry): yield Config/Plugin services, use Effect.forEach (#19363) 2026-03-27 09:53:00 -04:00
Shoubhit DashandGitHub d2bfa92e74 fix(app): persist queued followups across project switches (#19421) 2026-03-27 12:02:09 +00:00
opencode-agent[bot] 3fb60d05e5 chore: update nix node_modules hashes 2026-03-27 08:25:19 +00:00
Shoubhit DashandGitHub d341499684 fix(ui): keep partial markdown readable while responses stream (#19403) 2026-03-27 07:46:47 +00:00
Kit LangtonandGitHub 771525270a fix(opencode): ignore generated models snapshot files (#19362) 2026-03-27 02:21:17 +00:00
Kit LangtonandGitHub e96eead32e refactor(vcs): replace async git() with ChildProcessSpawner (#19361) 2026-03-27 02:14:46 +00:00
opencode-agent[bot] b242a8d8e4 chore: generate 2026-03-27 01:47:36 +00:00
Kit LangtonandGitHub 9c6f1edfd7 refactor(effect): yield services instead of promise facades (#19325) 2026-03-26 21:46:38 -04:00
Luke ParkerandGitHub ef7d1f7efa fix: web ui bundle build on windows (#19337) 2026-03-26 22:14:20 +00:00
Shoubhit DashandGitHub b7a06e1939 fix(ui): reduce markdown jank while responses stream (#19304) 2026-03-26 14:43:30 -05:00
Adam 311ba4179a fix(app): remove fork session button 2026-03-26 14:34:01 -05:00
Adam ad3b350672 fix(app): default shell tool to collapsed 2026-03-26 14:20:30 -05:00
opencode-agent[bot] 590523dcd1 chore: generate 2026-03-26 19:03:22 +00:00
AdamandGitHub b8fb75a94a fix(app): don't bundle fonts (#19329) 2026-03-26 14:02:01 -05:00
opencode-agent[bot] 98a31e30cc chore: update nix node_modules hashes 2026-03-26 18:45:12 +00:00
opencode-agent[bot] c333e914ee chore: generate 2026-03-26 18:42:59 +00:00
AdamandGitHub c7760b433b fix(app): more startup perf (#19288) 2026-03-26 13:41:22 -05:00
Kit LangtonandGitHub 2e6ac8ff49 fix(mcp): close transport on failed/timed-out connections (#19200) 2026-03-26 14:41:00 -04:00
Kit LangtonandGitHub 1ebc92fd36 refactor(config): use cachedInvalidateWithTTL, bump effect to beta.37 (#19322) 2026-03-26 18:05:47 +00:00
opencode-agent[bot] 9f94bdb496 chore: generate 2026-03-26 17:08:33 +00:00
Kit LangtonandGitHub 28f5176ffd effectify Config service (#19139) 2026-03-26 13:07:01 -04:00
James LongandGitHub 38450443b1 feat(core): remove workspace server, WorkspaceContext, start work towards better routing (#19316) 2026-03-26 12:30:26 -04:00
Aiden ClineandGitHub da1d37274f feat: add gpt prompt so non codex gpt models have their own system prompt modeled after codex cli (#19220) 2026-03-26 15:57:38 +00:00
opencode-agent[bot] 17e8f577d6 chore: generate 2026-03-26 15:49:53 +00:00
Kit LangtonandGitHub c7d23098d1 refactor(lsp): effectify LSP service with InstanceState (#19150) 2026-03-26 11:48:36 -04:00
Dax Raad bcf18edde4 changelog ci tweaks 2026-03-26 11:13:13 -04:00
opencode-agent[bot]andopencode 9a2482ac09 chore: generate 2026-03-26 15:05:29 +00:00
opencode 54443bfb7e release: v1.3.3 2026-03-26 15:05:21 +00:00
ec20efc11a feat: embed WebUI in binary with proxy flags (#19299)
Co-authored-by: BlankParticle <blankparticle@gmail.com>
2026-03-26 14:43:56 +00:00
DaxandGitHub 83ed1c4414 tui: bypass local SSE event streaming in worker (#19183) 2026-03-26 14:41:36 +00:00
opencode-agent[bot] 1d363fa19f chore: generate 2026-03-26 14:16:07 +00:00
Shoubhit DashandGitHub 1b028d0632 revert: roll back git-backed review modes (#19295) 2026-03-26 10:14:57 -04:00
Aiden ClineandGitHub d500a8432a fix: ensure enterprise url is set properly during auth flow (#19212) 2026-03-25 23:59:53 -05:00
Frank 2d502d6ffe go: do not respect disabled zen models 2026-03-26 00:47:23 -04:00
Frank 2ad190e482 wip: zen 2026-03-26 00:47:23 -04:00
Frank 16742af7f3 wip: zen 2026-03-26 00:47:23 -04:00
opencode-agent[bot] 652313e036 chore: update nix node_modules hashes 2026-03-26 02:14:16 +00:00
Luke ParkerandGitHub 1a4a6eabe2 fix(opencode): image paste on Windows Terminal 1.25+ with kitty keyboard (#17674) 2026-03-26 12:00:38 +10:00
Kit LangtonandGitHub ba244a6e62 test: restore 5 workers on Windows e2e (#19188) 2026-03-25 21:28:18 -04:00
Vladimir GlafirovandGitHub 7cb690d7e5 fix: bump gitlab-ai-provider to 5.3.3 for DWS tool approval support (#19185) 2026-03-26 01:17:51 +00:00
opencode-agent[bot] 31ad6e85ba chore: generate 2026-03-26 00:56:34 +00:00
Kit LangtonandGitHub ea04b23745 skill: use Effect.cached for load deduplication (#19165) 2026-03-26 00:55:43 +00:00
Aiden ClineandGitHub 05c3cfb2aa ignore: update disavowed list (#19184) 2026-03-26 00:20:43 +00:00
Kit LangtonandGitHub f54e4b60cc file: use Effect.cached for scan deduplication (#19164) 2026-03-25 20:19:58 -04:00
Kit LangtonandGitHub 97c15a087d effectify Bus service: migrate to Effect PubSub + InstanceState (#18579) 2026-03-25 20:19:24 -04:00
Kit LangtonandGitHub b90de755f9 fix+refactor(mcp): lifecycle tests, cancelPending fix, Effect migration (#19042) 2026-03-25 20:15:05 -04:00
Kit LangtonandGitHub 8864fdce2f fix: Windows e2e stability (CrossSpawnSpawner, snapshot isolation, session race guards) (#19163) 2026-03-25 19:49:14 -04:00
AdamandGitHub 5179b87aef fix(app): agent normalization (#19169) 2026-03-25 20:56:10 +00:00
Ariane EmoryandGitHub 66a56551be fix(task): respect agent permission config for todowrite tool (#19125) 2026-03-25 15:18:52 -05:00
7123aad5a8 fix(opencode): classify ZlibError from Bun fetch as retryable instead of unknown (#19104)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-25 13:08:40 -05:00
Adam d6fc5f414b chore: storybook tweaks 2026-03-25 11:30:41 -05:00
Aiden ClineandGitHub 77fc88c8ad chore: remove dead code for todoread tool (#19128)
publish-github-action / publish (push) Has been cancelled
2026-03-25 16:21:42 +00:00
Adam cafc2b204b chore: cleanup 2026-03-25 11:15:34 -05:00
opencode-agent[bot] 36709aae5f chore: update nix node_modules hashes 2026-03-25 16:08:30 +00:00
opencode-agent[bot] fac0dd8862 chore: generate 2026-03-25 15:40:53 +00:00
Shoubhit DashandGitHub 73e107250d feat: restore git-backed review modes with effectful git service (#18900) 2026-03-25 21:09:53 +05:30
Adam b746aec493 chore: storybook tweaks 2026-03-25 10:20:19 -05:00
Aiden ClineandGitHub ad40b65b0b chore: bump modelcontextprotocol/sdk to 1.27.1 (#19064) 2026-03-25 10:00:39 -05:00
opencode-agent[bot] 971383661a chore: generate 2026-03-25 14:48:44 +00:00
James LongandGitHub b0017bf1b9 feat(core): initial implementation of syncing (#17814) 2026-03-25 10:47:40 -04:00
Adam 0c0c6f3bdb chore(app): markdown playground in storyboard 2026-03-25 09:14:35 -05:00
Adam b480a38d31 chore(app): markdown playground in storyboard 2026-03-25 09:14:35 -05:00
Adam 4167e25c7e fix(app): opencode web server url 2026-03-25 06:41:00 -05:00
Adam 1041ae91d1 Reapply "fix(app): startup efficiency"
This reverts commit 898456a25c.
2026-03-25 06:25:57 -05:00
Adam 898456a25c Revert "fix(app): startup efficiency" 2026-03-25 06:25:05 -05:00
Adam 53d0b58ebf fix(app): hash inline script for csp 2026-03-25 05:59:06 -05:00
Adam 2b0baf97bd Reapply "fix(app): more startup efficiency (#18985)"
This reverts commit cbe1337f24.
2026-03-25 05:59:06 -05:00
Adam 0dbfefa080 Reapply "fix(app): startup efficiency (#18854)"
This reverts commit a379eb3867.
2026-03-25 05:59:05 -05:00
Shoubhit DashandGitHub d1c49ba210 fix(app): move message navigation off cmd+arrow (#18728) 2026-03-25 05:24:55 -05:00
Brendan AllanandGitHub 3ea72aec21 app: pre-warm project globalSync state when navigate project via keybind (#19088) 2026-03-25 17:32:49 +08:00
Brendan AllanandGitHub 9717383823 electron: remove file extension from electron-store wrapper (#19082) 2026-03-25 07:57:27 +00:00
Brendan AllanandGitHub 5d9e780029 electron: add createDirectory to open directory picker (#19071) 2026-03-25 06:25:51 +00:00
Luke ParkerandGitHub aa11fa865d fix: unblock beta conflict recovery (#19068) 2026-03-25 06:14:38 +00:00
Luke ParkerandGitHub 9a64bdb539 fix: beta resolver typecheck + build smoke check (#19060) 2026-03-25 05:45:30 +00:00
Aiden ClineandGitHub 71693cc24b tweak: only spawn lsp servers for files in current instance (or cwd if instance is global) (#19058) 2026-03-25 00:31:29 -05:00
Luke ParkerandGitHub 700f57112a fix: provide merge context to beta conflict resolver (#19055) 2026-03-25 04:45:37 +00:00
DaxandGitHub 0a80ef4278 fix(opencode): avoid snapshotting files over 2MB (#19043) 2026-03-25 04:43:48 +00:00
Dax Raad 4f9667c4bb Change issue close reason from not_planned to completed 2026-03-24 23:55:10 -04:00
Dax Raad be142b00bd Process issues sequentially to avoid rate limits 2026-03-24 23:54:27 -04:00
Dax Raad 45c2573979 Fix close-issues workflow permissions
- Add contents: read permission for checkout
- Use github.token instead of secrets.GITHUB_TOKEN
2026-03-24 23:51:46 -04:00
Dax Raad 79e9d19019 Add close-issues script and GitHub Action
- Create script/github/close-issues.ts to close stale issues after 60 days
- Add GitHub Action workflow to run daily at 2 AM
- Remove old stale-issues workflow to avoid conflicts
2026-03-24 23:50:35 -04:00
Dax Raad 958a80cc05 fix: increase operations-per-run to 1000 and pin stale action to v10.2.0
The stale-issues workflow was hitting the default 30 operations limit,
preventing it from processing all 2900+ issues/PRs. Increased to 1000
to handle the full backlog. Also pinned to exact v10.2.0 for reproducibility.
2026-03-24 23:38:15 -04:00
Kit LangtonandGitHub 4647aa80ac effectify Worktree service (#18679) 2026-03-24 20:26:21 -04:00
Adam a379eb3867 Revert "fix(app): startup efficiency (#18854)"
This reverts commit 546748a461.
2026-03-24 18:36:37 -05:00
Adam cbe1337f24 Revert "fix(app): more startup efficiency (#18985)"
This reverts commit 98b3340cee.
2026-03-24 18:36:25 -05:00
Kit LangtonandGitHub 50f6aa3763 fix(opencode): skip typechecking generated models snapshot (#19018) 2026-03-24 19:11:45 -04:00
opencode 0dcdf5f529 release: v1.3.2 2026-03-24 22:50:35 +00:00
Dax Raad 4586b41ffd change model for changelog 2026-03-24 18:25:52 -04:00
Dax Raad 35884defd8 ci 2026-03-24 18:24:34 -04:00
DaxandGitHub 15dc33d1a3 feat(tui): add heap snapshot functionality for TUI and server (#19028) 2026-03-24 18:20:11 -04:00
opencode-agent[bot]andopencode 1398674e53 chore: update nix node_modules hashes 2026-03-24 22:07:32 +00:00
Jay Vandopencode afc4c831eb tweak: use theme tokens for debug bar surface 2026-03-24 22:07:32 +00:00
opencode ec64ceabec release: v1.3.1 2026-03-24 22:07:24 +00:00
DaxandGitHub 56644be95a fix(core): restore SIGHUP exit handler (#16057) (#18527) 2026-03-24 17:42:58 -04:00
Kamil JopekandGitHub 00d3b831fc feat: add Poe OAuth auth plugin (#18477) 2026-03-24 16:17:47 -05:00
AdamandGitHub b848b7ebae fix(app): session timeline jumping on scroll (#18993) 2026-03-24 13:51:09 -05:00
opencode-agent[bot] e837dcc1c5 chore: generate 2026-03-24 18:43:20 +00:00
Nicholas HansenandGitHub 024979f3fd feat(bedrock): Add token caching for any amazon-bedrock provider (#18959) 2026-03-24 13:42:20 -05:00
opencode-agent[bot] bc608fb081 chore: update nix node_modules hashes 2026-03-24 18:40:28 +00:00
Adam 9838f56a6f fix(app): sidebar ux 2026-03-24 13:35:20 -05:00
AdamandGitHub 98b3340cee fix(app): more startup efficiency (#18985) 2026-03-24 13:23:41 -05:00
5e684c6e80 chore: effectify agent.ts (#18971)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-03-24 18:15:23 +00:00
Caleb NortonandGitHub 2c1d8a90d5 fix: nix hash update parsing... again (#18989) 2026-03-24 13:06:46 -05:00
opencode-agent[bot] 8994cbfc0f chore: generate 2026-03-24 18:05:43 +00:00
Adam 42a773481e fix(app): sidebar truncation 2026-03-24 13:04:32 -05:00
Kit LangtonandGitHub 539b01f20f effectify Project service (#18808) 2026-03-24 14:04:22 -04:00
814a515a8a fix: improve plugin system robustness — agent/command resolution, async errors, hook timing, two-phase init (#18280)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-24 12:50:55 -05:00
235a82aea9 chore: update flake.lock (#18976)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-24 12:50:25 -05:00
Vladimir GlafirovandGitHub 9330bc5339 fix: route GitLab Duo Workflow system prompt via flowConfig (#18928) 2026-03-24 12:33:18 -05:00
Caleb NortonandGitHub 1238d1f61a fix: nix hash update parsing (#18979) 2026-03-24 12:32:48 -05:00
opencode-agent[bot] 1d3232b388 chore: generate 2026-03-24 17:05:02 +00:00
Kit LangtonandGitHub 5c1bb5de86 fix: remove flaky cross-spawn spawner tests (#18977) 2026-03-24 13:04:04 -04:00
JackandGitHub 7c5ed771c3 fix: update Feishu community links for zh locales (#18975) 2026-03-25 01:03:01 +08:00
opencode-agent[bot] 31c4a4fb47 chore: update nix node_modules hashes 2026-03-24 16:43:24 +00:00
Caleb NortonandGitHub 037077285a fix: better nix hash detection (#18957) 2026-03-24 11:30:39 -05:00
Kit LangtonandGitHub 41c77ccb33 fix: restore cross-spawn behavior for effect child processes (#18798) 2026-03-24 10:35:24 -04:00
AdamandGitHub 546748a461 fix(app): startup efficiency (#18854) 2026-03-24 09:10:24 -05:00
Burak Yigit KayaandGitHub c9c93eac00 fix(ui): eliminate N+1 reactive subscriptions in SessionTurn (#18924) 2026-03-24 09:02:22 -05:00
Burak Yigit KayaandGitHub 3f1a4abe6d fix(app): use optional chaining for model.current() in ProviderIcon (#18927) 2026-03-24 09:01:58 -05:00
Burak Yigit KayaandGitHub 431e0586ad fix(app): filter non-renderable part types from browser store (#18926) 2026-03-24 09:01:25 -05:00
Shoubhit DashandGitHub fde201c286 fix(app): stop terminal autofocus on shortcuts (#18931) 2026-03-24 11:16:16 +00:00
SebastianandGitHub d3debc191f manually lock/unlock theme mode (#18905) 2026-03-24 10:00:19 +01:00
Frank 34f43fff89 sync 2026-03-24 01:00:20 -04:00
opencode-agent[bot] 49623aa519 chore: update nix node_modules hashes 2026-03-24 03:14:22 +00:00
Vladimir GlafirovandGitHub f1340472ec chore: bump gitlab-ai-provider to 5.3.1 for GPT-5.4 model support (#18849) 2026-03-23 22:00:36 -05:00
Frank a8b28826a0 wip: zen 2026-03-23 22:24:58 -04:00
Frank a03a2b6eab Zen: adjust cache tokens 2026-03-23 20:33:11 -04:00
SebastianandGitHub ad78b79b8a use renderer theme mode to switch dark/light mode (#18851) 2026-03-24 00:32:48 +01:00
opencode-agent[bot] 9a006d8700 chore: generate 2026-03-23 17:12:55 +00:00
Kit LangtonandGitHub 3a0bf2f39f fix console account URL handling (#18809) 2026-03-23 13:11:38 -04:00
Frank b556979634 ci: fix 2026-03-23 12:47:42 -04:00
Aiden ClineandGitHub 691644eeeb tweak: add back setting user agent in requests (#18795) 2026-03-23 15:34:59 +00:00
Abhishek KeshriandGitHub 4aebaaf067 feat(tui): add syntax highlighting for kotlin, hcl, lua, toml (#18198) 2026-03-23 16:15:24 +01:00
David HillandGitHub 77b3b46788 tui: keep file tree open at its minimum resized width (#18777) 2026-03-23 20:06:43 +08:00
Brendan AllanandGitHub 36dfe1646b fix(app): only navigate prompt history when input is empty (#18775) 2026-03-23 11:48:34 +00:00
opencode-agent[bot] 6926dc26d1 chore: update nix node_modules hashes 2026-03-23 10:52:56 +00:00
opencode-agent[bot] eb74e4a6d2 chore: update nix node_modules hashes 2026-03-23 10:37:23 +00:00
opencode-agent[bot] 85d8e143bf chore: generate 2026-03-23 10:35:30 +00:00
Brendan AllanandGitHub 8e1b53b32c fix(app): handle session busy state better (#18758) 2026-03-23 10:34:32 +00:00
Brendan AllanandGitHub 0a7dfc03ee fix(app): lift up project hover state to layout (#18732) 2026-03-23 08:58:20 +00:00
Brendan AllanandGitHub 4c27e7fc64 electron: more robust sidecar kill handling (#18742) 2026-03-23 08:44:23 +00:00
Shoubhit DashandGitHub 0f5626d2e4 fix(app): prefer cmd+k for command palette (#18731) 2026-03-23 08:00:24 +00:00
Shoubhit DashandGitHub 5ea95451dd fix(app): prevent stale session hover preview on refocus (#18727) 2026-03-23 07:25:30 +00:00
Shoubhit DashandGitHub 9239d877b9 fix(app): batch multi-file prompt attachments (#18722) 2026-03-23 06:44:17 +00:00
github-actions[bot] fc68c24433 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/18718#issuecomment-4108322776
2026-03-23 06:28:47 +00:00
Luke ParkerandGitHub db9619dad6 Add 'write' role to vouch manage action (#18718) 2026-03-23 06:27:35 +00:00
James LongandGitHub 84d9b38873 fix(core): fix file watcher test (#18698) 2026-03-23 03:35:17 +00:00
opencode-agent[bot] 8035c3435b chore: update nix node_modules hashes 2026-03-23 01:03:20 +00:00
SebastianandGitHub 71e7603d71 Upgrade opentui to 0.1.90 (#18551) 2026-03-23 01:45:34 +01:00
David HillandGitHub 40e49c5b49 tui: keep patch tool counts visible with long filenames (#18678) 2026-03-23 00:45:11 +00:00
Luke ParkerandGitHub afe9b97274 fix(app): restore keyboard project switching in open sidebar (#18682) 2026-03-23 00:39:46 +00:00
opencode-agent[bot] 3b3549902d chore: update nix node_modules hashes 2026-03-23 00:29:45 +00:00
David HillandGitHub e9a9c75c1f tweak(ui): fix padding bottom on the context tab (#18680) 2026-03-23 00:23:45 +00:00
2b171828b0 tui: prevent project avatar popover flicker when switching projects (#18660)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-03-23 10:20:49 +10:00
Luke ParkerandGitHub 8dd817023a chore: bump Bun to 1.3.11 (#18144) 2026-03-23 10:19:21 +10:00
Dax Raad 0d6c601365 changelog slash command 2026-03-22 19:45:23 -04:00
opencode-agent[bot]andopencode 5460bf9989 chore: generate 2026-03-22 23:32:09 +00:00
opencode eb3bfffad4 release: v1.3.0 2026-03-22 23:32:01 +00:00
DaxandGitHub e2d03ce38c feat: interactive update flow for non-patch releases (#18662) 2026-03-22 23:12:40 +00:00
David HillandGitHub 32f9dc6383 fix(ui): stop auto close of sidebar on resize (#18647) 2026-03-23 08:53:12 +10:00
c529529f84 fix(app): terminal rename from context menu (#18263)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-03-22 04:39:51 +00:00
Kit LangtonandGitHub 13bac9c91a effectify Pty service (#18572) 2026-03-22 01:17:13 +00:00
Kit LangtonandGitHub fe53af4819 effectify ToolRegistry service (#18571) 2026-03-22 01:06:01 +00:00
opencode-agent[bot] e82c5a9a28 chore: generate 2026-03-22 00:51:19 +00:00
Kit LangtonandGitHub 3236f228fb effectify Plugin service (#18570) 2026-03-22 00:50:22 +00:00
Kit LangtonandGitHub 0e0e7a4a4b effectify Command service (#18568) 2026-03-22 00:21:41 +00:00
Kit LangtonandGitHub 10a3d6c54e effectify SessionStatus service (#18565) 2026-03-21 23:55:07 +00:00
opencode-agent[bot] 832b8e252e chore: update nix node_modules hashes 2026-03-21 19:17:12 +00:00
SebastianandGitHub 040f551c57 Upgrade opentui to 0.1.88 (#18079) 2026-03-21 19:00:42 +00:00
Protocol ZeroandGitHub cc818f8032 fix(provider): only set thinkingConfig for models with reasoning capability (#18283) 2026-03-21 11:57:52 -05:00
opencode-agent[bot] d5337b41f4 chore: update nix node_modules hashes 2026-03-21 15:47:15 +00:00
opencode-agent[bot] 9f7a76d6c0 chore: generate 2026-03-21 15:34:05 +00:00
Brendan AllanandGitHub 6a16db4b92 app: manage mutation loading states with tanstack query (#18501) 2026-03-21 23:33:04 +08:00
Brendan AllanandGitHub 9ad6588f3e app: allow navigating projects with keybinds (#18502) 2026-03-21 22:13:09 +08:00
opencode-agent[bot] fb6bf0b35e chore: generate 2026-03-21 13:12:54 +00:00
Dax Raad f80343b875 fix annotation 2026-03-21 09:11:15 -04:00
Frank 9b805e1cc4 wip: zen 2026-03-21 04:07:51 -04:00
opencode-agent[bot] 2e0d5d2308 chore: generate 2026-03-21 04:52:23 +00:00
Kit LangtonandGitHub 38e0dc9ccd Move service state into InstanceState, flatten service facades (#18483) 2026-03-21 04:51:35 +00:00
opencode-agent[bot] 40aeaa120d chore: generate 2026-03-21 03:11:28 +00:00
Kit LangtonandGitHub 6a64177589 fix(zen): emit cost chunk in client-facing format, not upstream format (#16817) 2026-03-20 23:10:34 -04:00
Dax Raad 5dc47905a9 allow customizing DB location 2026-03-20 22:49:55 -04:00
Aiden ClineandGitHub dc0044882c ignore: add danieljoshuanazareth to disavow list (#18476) 2026-03-20 21:34:07 -04:00
github-actions[bot] 45ae7dc653 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/18464#issuecomment-4101766628
2026-03-21 01:28:40 +00:00
Dax Raad 129fe1e350 ci 2026-03-20 17:00:05 -04:00
Kit LangtonandGitHub 214a6c6cf1 fix: switch consumers to service imports to break bundle cycles (#18438) 2026-03-20 20:55:46 +00:00
Dax Raad 3f249aba6d commit and push 2026-03-20 16:36:19 -04:00
Dax Raad 5c6ec1caac fix question cross out 2026-03-20 15:50:04 -04:00
Kit LangtonandGitHub 24f9df5463 fix: update stale account url/email on re-login (#18426) 2026-03-20 14:50:01 -04:00
opencode-agent[bot] 12b8e1c2be chore: generate 2026-03-20 18:38:08 +00:00
Kit LangtonandGitHub d70099b059 fix: apply Layer.fresh at instance service definition site (#18418) 2026-03-20 14:37:12 -04:00
opencode-agent[bot] ce845a0b1b chore: update nix node_modules hashes 2026-03-20 18:16:17 +00:00
Vladimir GlafirovandGitHub 05d3e65f76 feat: enable GitLab Agent Platform with workflow model discovery (#18014) 2026-03-20 12:55:22 -05:00
opencode-agent[bot] 51618e9cef chore: generate 2026-03-20 16:11:21 +00:00
Kit LangtonandGitHub e78944e9a4 effectify Installation service, drop Effect suffix from namespaces (#18266) 2026-03-20 12:10:33 -04:00
Aiden ClineandGitHub bfdc38e421 tweak: adjust codex plugin logic so that codex instruction isn't always added (oauth plan no longer enforces instruction whitelisting) (#18337) 2026-03-20 10:37:47 -05:00
83023e4f0f docs: add Cloudflare Workers AI provider (#18322)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-20 10:10:22 -05:00
Brendan AllanandGitHub d0a57305ef app: file type filter on desktop + multiple files (#18403) 2026-03-20 15:02:07 +00:00
Shoubhit DashandGitHub 27a70ad70f fix(app): align review file comments with diff comments (#18406) 2026-03-20 14:44:18 +00:00
Luke ParkerandGitHub 0bbf26a1ce deslopity deslopity (#18343) 2026-03-20 05:24:27 +00:00
opencode-agent[bot] 83cdb4de64 chore: update nix node_modules hashes 2026-03-20 05:14:32 +00:00
Brendan AllanandGitHub 4989632245 patch solid to try fix memo undefined under transition bug (#18338) 2026-03-20 14:58:35 +10:00
Luke ParkerandGitHub d460614cd7 fix: lots of desktop stability, better e2e error logging (#18300) 2026-03-20 00:12:06 -04:00
Luke ParkerandGitHub 7866dbcfcc fix: avoid truncate permission import cycle (#18292) 2026-03-19 23:52:04 -04:00
opencode-agent[bot] e71a21e0a8 chore: update nix node_modules hashes 2026-03-20 02:21:29 +00:00
DaxandGitHub 1071aca91f fix: miscellaneous small fixes (#18328) 2026-03-19 22:20:29 -04:00
b3d0446d13 feat: switch xai provider to responses API (#18175)
Co-authored-by: Jaaneek <jankiewiczmilosz@gmail.com>
2026-03-19 21:09:49 -05:00
opencode-agent[bot] 949191ab74 chore: update nix node_modules hashes 2026-03-20 01:36:22 +00:00
DaxandGitHub 92cd908fb5 feat: add Node.js entry point and build script (#18324) 2026-03-19 21:35:07 -04:00
DaxandGitHub 6fcc970def fix: include cache bin directory in which() lookups (#18320) 2026-03-19 21:21:55 -04:00
DaxandGitHub 52a7a04ad8 refactor: replace Bun shell execution with portable Process utilities (#18318) 2026-03-19 21:17:06 -04:00
DaxandGitHub 37b8662a9d refactor: abstract SQLite behind runtime-conditional #db import (#18316) 2026-03-19 21:15:35 -04:00
DaxandGitHub ddcb32ae0b refactor(tui): replace Bun-specific APIs with portable alternatives (#18304) 2026-03-19 19:32:59 -04:00
Frank 2c056c90da doc: update translator to gpt model 2026-03-19 19:07:02 -04:00
DaxandGitHub 812d1bb32a chore: inline tool descriptions, remove separate .txt files (#18303) 2026-03-19 19:02:42 -04:00
Frank 9a58c43ef4 go: upi translation 2026-03-19 18:54:32 -04:00
DaxandGitHub 63585db6a7 refactor: replace Bun.sleep with node:timers/promises sleep (#18301) 2026-03-19 18:50:40 -04:00
Frank bd44489ada go: upi payment 2026-03-19 18:44:24 -04:00
opencode-agent[bot] a6ef9e9206 chore: generate 2026-03-19 20:21:10 +00:00
Kit LangtonandGitHub 6e09a1d904 fix(account): handle pending console login polling (#18281) 2026-03-19 16:18:28 -04:00
opencode-agent[bot] 4f21757e0d chore: generate 2026-03-19 20:05:33 +00:00
2dbcd79fd2 fix: stabilize agent and skill ordering in prompt descriptions (#18261)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-19 15:04:03 -05:00
James LongandGitHub 48a7f0fd93 Fix base64Decode import in workspaces.spec.ts (#18274) 2026-03-19 15:38:54 -04:00
James LongandGitHub d69962b0f7 fix(core): disable chunk timeout by default (#18264) 2026-03-19 14:30:08 -04:00
opencode-agent[bot] a6f23cb08e chore: generate 2026-03-19 17:52:50 +00:00
James LongandGitHub 0540751897 fix(core): use a queue to process events in event routes (#18259) 2026-03-19 13:51:14 -04:00
opencode-agent[bot] baa204193c chore: generate 2026-03-19 16:16:29 +00:00
Aiden ClineandGitHub aeece6166b ignore: revert 3 commits that broke dev branch (#18260) 2026-03-19 11:15:07 -05:00
Kit LangtonandGitHub 0d7e62a532 fix forked prompt attachments losing file parts (#17815) 2026-03-19 12:00:32 -04:00
Shoubhit DashandGitHub 41aa254db4 fix(app): show review on the empty session route (#18251) 2026-03-19 19:51:32 +05:30
opencode-agent[bot] d178d8249f chore: generate 2026-03-19 14:00:32 +00:00
Shoubhit DashandGitHub e6f5214779 feat: add git-backed session review modes (#17961) 2026-03-19 19:29:29 +05:30
84f60d97a0 app: fix workspace flicker when switching directories (#18207)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-03-19 19:29:14 +05:30
Brendan Allan cbf4b68fee electron: lazily read updater enabled 2026-03-19 21:47:47 +08:00
bd4527b4f2 fix(desktop): remote server switching (#17214)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-03-19 13:32:11 +00:00
f4a9fe29a3 fix(app): ignore repeated Enter submits in prompt input (#18148)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-03-19 11:53:44 +00:00
opencode-agent[bot] 5a0bfa7061 chore: generate 2026-03-19 04:46:39 +00:00
DaxandGitHub 1ac1a0287c anthropic legal requests (#18186) 2026-03-19 04:45:24 +00:00
Aiden ClineandGitHub 8e09e8c612 feat: integrate multistep auth flows into desktop app (#18103) 2026-03-19 13:47:51 +10:00
Kit LangtonandGitHub 84e62fc662 fix(session): preserve tagged error messages (#18165) 2026-03-18 20:36:53 -04:00
Frank a7ea93528b zen: add mimo pro/omni models 2026-03-18 20:28:41 -04:00
opencode-agent[bot] d90e3a2833 chore: update nix node_modules hashes 2026-03-19 00:08:27 +00:00
opencode-agent[bot] 1c74c2741a chore: update nix node_modules hashes 2026-03-19 00:07:30 +00:00
Luke ParkerandGitHub 5d2f8d77f9 fix: restore recent test regressions and upgrade effect beta (#18158) 2026-03-19 09:54:01 +10:00
Kit LangtonandGitHub 81be544981 feat(filesystem): add AppFileSystem service, migrate Snapshot (#18138) 2026-03-18 19:52:43 -04:00
opencode-agent[bot] 773c1192dc chore: generate 2026-03-18 23:45:03 +00:00
Kit LangtonandGitHub 5ddfe4ada5 chore: type Provider.list() as Record<ProviderID, Info>, delete dead code (#18123) 2026-03-18 19:43:12 -04:00
opencode-agent[bot] a93d98bd94 chore: update nix node_modules hashes 2026-03-18 23:03:55 +00:00
Luke ParkerandGitHub 54ed87d53c fix(windows): use cross-spawn for shim-backed commands (#18010) 2026-03-19 08:49:16 +10:00
Aiden ClineandGitHub 8ee939c741 tweak: remove unnecessary parts from the fallback system prompt (#18140) 2026-03-18 17:27:47 -05:00
Frank 1b0096bf61 docs: update go models 2026-03-18 17:48:39 -04:00
Frank 8006c29db3 fix: docs 2026-03-18 16:30:33 -04:00
Frank 3f1c96a0bb zen: minimax m2.7 2026-03-18 14:30:55 -04:00
Frank 3558deba4a zen: minimax m2.7 2026-03-18 14:15:00 -04:00
opencode-agent[bot] c3ddc85cca chore: generate 2026-03-18 17:37:40 +00:00
Kit LangtonandGitHub a800583aea refactor(effect): unify service namespaces and align naming (#18093) 2026-03-18 13:34:36 -04:00
Aiden ClineandGitHub 171e69c2fc feat: integrate support for multi step auth flows for providers that require additional questions (#18035) 2026-03-18 11:36:19 -05:00
Aiden ClineandGitHub 822bb7b336 tweak: update gpt subscription model list (#18101) 2026-03-18 10:51:39 -05:00
Frank 47cf267c23 zen: fix routing non OC traffic 2026-03-18 10:59:56 -04:00
OpeOginniandGitHub 976aae7e42 fix(desktop): fix error handling by adding errorName function to identify NotFoundError rather than statusCode (#17591) 2026-03-18 19:27:56 +05:30
David HillandGitHub 0ca51eebcf tweak(ui): theme overrides (#17958) 2026-03-18 19:26:32 +05:30
David Hill 3256886e25 tui: make the title bar search easier to scan without a redundant icon 2026-03-18 13:52:57 +00:00
David Hill d2194f6dde Revert "tui: clean up search button in session header by removing magnifying glass icon and excess padding"
This reverts commit bfd4787fcd.
2026-03-18 13:52:36 +00:00
David Hill bfd4787fcd tui: clean up search button in session header by removing magnifying glass icon and excess padding 2026-03-18 13:49:58 +00:00
opencode-agent[bot] 58dce0148a chore: generate 2026-03-18 12:58:46 +00:00
OpeOginniandGitHub 79635b8b41 docs(cli): update experimental TY LSP flag description for clarity across multiple languages (#14770) 2026-03-18 18:27:42 +05:30
Brendan Allan 331dacf9db app: remove debug text 2026-03-18 17:02:23 +08:00
Brendan Allan 4ba7d3b406 app: replace autoselect effects with single resource 2026-03-18 17:01:38 +08:00
Brendan Allan a43783a6d4 app: initialize command catalog more efficiently
cuts down load times by like 30-50%
2026-03-18 14:46:53 +08:00
Frank 37c5295111 zen: gpt 5.4 mini and nano 2026-03-18 02:12:30 -04:00
56102ff642 fix(core): detect vLLM context overflow errors (#17763)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-17 23:52:16 -05:00
Frank 1b86c27fb8 wip: zen 2026-03-17 23:31:14 -04:00
Frank fe43bdb699 wip: zen 2026-03-17 22:50:54 -04:00
a849a17e93 feat(enterprise): contact form now pushes to salesforce 🙄 (#17964)
Co-authored-by: slickstef11 <stefan@wundergraph.com>
Co-authored-by: Frank <frank@anoma.ly>
2026-03-17 22:43:43 -04:00
opencode-agent[bot] 0292f1b559 chore: generate 2026-03-18 02:01:02 +00:00
Kit LangtonandGitHub 5dfe86dcb1 refactor(truncation): effectify TruncateService, delete Scheduler (#17957) 2026-03-17 21:59:54 -04:00
Ariane EmoryandGitHub 4b4dd2b882 fix: Add apply_patch to EDIT_TOOLS filter (#18009) 2026-03-17 20:11:42 -05:00
opencode-agent[bot] bc949af623 chore: generate 2026-03-18 01:05:16 +00:00
Kit LangtonandGitHub 9e7c136de7 refactor(snapshot): effectify SnapshotService (#17878) 2026-03-17 21:04:16 -04:00
Kit LangtonandGitHub fee3c196c5 add prompt schema validation debug logs (#17812) 2026-03-17 19:18:16 -04:00
Frank 6c047391bb wip: zen 2026-03-17 19:06:22 -04:00
Frank 350df0b261 zen: add missing model lab names 2026-03-17 18:41:38 -04:00
Frank fbabc97c4c zen: error logging 2026-03-17 16:53:10 -04:00
7daea69e13 tweak(ui): add an empty state to the sidebar when no projects (#17971)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-03-17 19:54:14 +00:00
Frank 0772a95918 wip: zen 2026-03-17 15:00:44 -04:00
Frank dadddc9c8c zen: deprecate gemini 3 pro 2026-03-17 12:31:54 -04:00
OpeOginniandGitHub 6708c3f6cf docs: mark tools config as deprecated (#17951) 2026-03-17 10:07:35 -05:00
Shoubhit DashandGitHub ba22976568 fix: inline review comment submit and layout (#17948) 2026-03-17 19:54:20 +05:30
Brendan Allan 0afeaea21f app: inherit owner when creating prompt session 2026-03-17 19:47:06 +08:00
opencode-agent[bot] b07b5a9b7f chore: generate 2026-03-17 11:15:35 +00:00
Luke ParkerandGitHub dbbe931a18 fix(app): avoid prompt tooltip Switch on startup (#17857) 2026-03-17 06:14:30 -05:00
opencode-agent[bot] e14e874e51 chore: generate 2026-03-17 03:47:33 +00:00
544315dff7 docs: add describe annotation to snapshot config field (#17861)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-16 22:46:09 -05:00
Aiden ClineandGitHub f13da808ff chore: denounce ai spammer (#17901) 2026-03-16 22:38:15 -05:00
Luke ParkerandGitHub e416e59ea6 test(app): deflake slash terminal toggle flow (#17881) 2026-03-17 12:55:58 +10:00
Luke ParkerandGitHub cb69501098 test(opencode): deflake file and tool timing (#17859) 2026-03-17 00:49:04 +00:00
Kyle AltendorfandGitHub a64f604d54 fix(tui): check for selected text instead of any selection in dialog escape handler (#16779) 2026-03-17 10:25:03 +10:00
opencode-agent[bot] d7093abf61 chore: update nix node_modules hashes 2026-03-17 00:05:19 +00:00
opencode-agent[bot] 60af447908 chore: update nix node_modules hashes 2026-03-16 23:54:30 +00:00
opencode-agent[bot] 1cdc558ac0 chore: generate 2026-03-16 23:52:10 +00:00
Kit LangtonandGitHub 3849822769 refactor(skill): effectify SkillService as scoped service (#17849) 2026-03-16 23:51:07 +00:00
AbigailJixiangyuyuandGitHub e9a17e4480 fix(windows): restore /editor support on Windows (#17146) 2026-03-17 08:11:02 +10:00
Aiden ClineandGitHub 68809365df fix: github copilot enterprise integration (#17847) 2026-03-16 17:05:14 -05:00
opencode-agent[bot] 8da511dfa8 chore: generate 2026-03-16 20:19:50 +00:00
Kit LangtonandGitHub 69381f6aea refactor(file): effectify FileService as scoped service (#17845) 2026-03-16 16:18:39 -04:00
opencode-agent[bot] df6508530f chore: generate 2026-03-16 19:59:49 +00:00
Kit LangtonandGitHub 335356280c refactor(format): effectify FormatService as scoped service (#17675) 2026-03-16 15:58:36 -04:00
opencode-agent[bot] 03d84f49c2 chore: generate 2026-03-16 18:24:21 +00:00
Kit LangtonandGitHub 2cbdf04ec9 refactor(file-time): effectify FileTimeService with Semaphore locks (#17835) 2026-03-16 18:23:13 +00:00
opencode-agent[bot] 410fbd8a00 chore: generate 2026-03-16 18:00:18 +00:00
Kit LangtonandGitHub e5cbecf17c fix+refactor(vcs): fix HEAD filter bug and effectify VcsService (#17829) 2026-03-16 13:59:11 -04:00
opencode-agent[bot] ca3af5dc6a chore: generate 2026-03-16 17:19:44 +00:00
Kit LangtonandGitHub 9e740d9947 stack: effectify-file-watcher-service (#17827) 2026-03-16 13:18:40 -04:00
opencode-agent[bot] d4694d058c chore: generate 2026-03-16 16:56:12 +00:00
Kit LangtonandGitHub 469c3a4204 refactor(instance): move scoped services to LayerMap (#17544) 2026-03-16 12:55:14 -04:00
DSandGitHub 4cb29967f6 fix(opencode): apply message transforms during compaction (#17823) 2026-03-16 11:32:53 -05:00
Johannes LoherandGitHub e718db624f fix(core): consider code: context_length_exceeded as context overflow in API call errors (#17748) 2026-03-16 10:25:24 -05:00
Michal ŠlesárandGitHub 15b27e0d18 fix(app): agent switch should not reset thinking level (#17470) 2026-03-16 19:43:29 +05:30
Kit LangtonandGitHub c523aac586 fix(cli): scope active org labels to the active account (#16957) 2026-03-16 10:01:42 -04:00
Shoubhit DashandGitHub 51fcd04a70 Wrap question option descriptions instead of truncating (#17782) 2026-03-16 11:29:18 +00:00
Luke ParkerandGitHub 4d7cbdcbef fix(ci): workaround by using hoisted Bun linker on Windows (#17751) 2026-03-16 08:39:06 +00:00
Luke ParkerandGitHub 59c530cc6c fix(opencode): teach Kit's test what an ID is (#17745) 2026-03-16 18:08:27 +10:00
c2ca1494e5 fix(opencode): preserve prompt tool enables with empty agent permissions (#17064)
Co-authored-by: jquense <jquense@ramp.com>
2026-03-15 23:01:46 -05:00
opencode 4ee426ba54 release: v1.2.27 2026-03-16 02:33:48 +00:00
Aiden ClineandGitHub 510374207d fix: vcs watcher if statement (#17673) 2026-03-15 19:20:39 -05:00
Erik EngervallandGitHub aedbecedf7 docs: Add opencode-firecrawl to ecosystem documentation (#17672) 2026-03-15 18:56:24 -05:00
Frank 9c00669927 zen: update claude prices 2026-03-15 10:54:40 -04:00
David HillandGitHub b9f6b40e3a tweak(ui): remove open label (#17512) 2026-03-15 08:25:40 -05:00
Orlando AscanioandGitHub ad06d8f496 docs(es): fix Spanish intro page translation, grammar, and terminology (#17563) 2026-03-15 08:22:32 -05:00
Kit LangtonandGitHub 2fc06c5a17 chore(permission): delete legacy permission module (#17534) 2026-03-14 20:56:52 -05:00
Kit LangtonandGitHub 52877d8765 fix(question): clean up pending entry on abort (#17533) 2026-03-15 00:49:49 +00:00
SebastianandGitHub 8f957b8f90 remove sighup exit (#17254) 2026-03-15 00:52:28 +01:00
opencode-agent[bot] 0befa1e57e chore: generate 2026-03-14 18:29:06 +00:00
Kit LangtonandGitHub f015154314 refactor(permission): effectify PermissionNext + fix InstanceState ALS bug (#17511) 2026-03-14 18:28:00 +00:00
Shoubhit DashandGitHub 689d9e14ea fix(app): handle multiline web paste in prompt composer (#17509) 2026-03-14 22:51:45 +05:30
Kit LangtonandGitHub 66e8c57ed1 refactor(schema): inline branded ID schemas (#17504) 2026-03-14 16:14:46 +00:00
opencode-agent[bot] b698f14e55 chore: generate 2026-03-14 15:59:01 +00:00
Kit LangtonandGitHub cec1255b36 refactor(question): effectify QuestionService (#17432) 2026-03-14 11:58:00 -04:00
Aiden ClineandGitHub 88226f3061 tweak: ensure that compaction message is tracked as agent initiated (#17431) 2026-03-14 10:46:24 -05:00
James LongandGitHub 8c53b2b470 fix(core): increase default chunk timeout from 2 min to 5 min (#17490) 2026-03-14 14:27:06 +00:00
Marcus SchiesserandGitHub f2d3a4c70f fix(ui): prevent long filenames from overlapping actions (#17151) 2026-03-13 16:17:15 -05:00
4b9b86b544 fix(opencode): lost sessions across worktrees and orphan branches (#16389)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-13 15:51:55 -04:00
David Hillandopencode f54abe58cf tui: update compaction status message to use Session instead of History across all languages
The compaction message now correctly indicates the current session was compacted rather than the entire history, making it clearer to users which conversation data was optimized.
2026-03-13 16:33:01 +00:00
opencode d954026dd8 release: v1.2.26 2026-03-13 16:32:53 +00:00
AdamandGitHub 4ad8116ce3 fix(app): model selection persist by session (#17348) 2026-03-13 11:05:08 -05:00
David HillandGitHub 5c7088338c fix(app): polish prompt composer controls (#17388) 2026-03-13 10:48:10 -05:00
Adam 389daa03df fix(app): sidebar sync 2026-03-13 10:47:45 -05:00
David HillandAdam 1cbe7b0854 tweak(ui): use new-session icon in sidebar buttons 2026-03-13 10:18:08 -05:00
David HillandAdam 050d71bcf9 fix(app): avoid clipping new session during sidebar anim 2026-03-13 10:18:03 -05:00
David HillandAdam ffde837e83 fix(app): animate titlebar controls on sidebar open 2026-03-13 10:17:56 -05:00
David HillandGitHub 536abea2e2 fix(app): restore sidebar dash and sync session spinner colors (#17384) 2026-03-13 10:08:23 -05:00
Kit LangtonandGitHub c7a52b6a2d feat(schema): scaffold effect-to-zod bridge (#17273) 2026-03-13 10:59:57 -04:00
AdamandGitHub c4ccb50c37 fix(app): fork should copy prompt into new session (#17375) 2026-03-13 09:59:11 -05:00
JackandGitHub 5aaf1ddfb7 fix(ui): force wasm highlighter for markdown code blocks (#17373) 2026-03-13 09:57:14 -05:00
David HillandGitHub f5f07310e0 fix(app): sidebar spacing + session list spinner transition (#17355) 2026-03-13 09:19:02 -05:00
AdamandGitHub c9e9dbeee1 fix(app): terminal cloning without retry (#17354) 2026-03-13 08:56:48 -05:00
Adam b88b323049 fix(app): scroll falls behind prompt input 2026-03-13 08:39:42 -05:00
Adam 6653f868ae fix(app): tooltip quirks 2026-03-13 08:38:32 -05:00
Adam af29d91dca fix(app): todo spacing 2026-03-13 07:43:50 -05:00
AdamandGitHub 1a3735b619 fix(app): better optimistic prompt submit (#17337) 2026-03-13 07:38:03 -05:00
Shoubhit DashandGitHub d4ae13f2a0 fix(opencode): serialize config bun installs (#17342) 2026-03-13 17:58:00 +05:30
Adam f4804dac85 fix(app): oc-2 went too dark 2026-03-13 07:25:42 -05:00
AdamandGitHub 843f188aaa fix(app): support text attachments (#17335) 2026-03-13 06:58:24 -05:00
AdamandGitHub 05cb3c87ca chore(app): i18n sync (#17283) 2026-03-13 06:48:38 -05:00
AdamandGitHub 270cb0b8b4 chore: cleanup (#17284) 2026-03-13 06:27:58 -05:00
Shoubhit DashandGitHub 46ba9c8170 perf(app): use cursor session history loading (#17329) 2026-03-13 16:43:41 +05:30
David HillandGitHub 80f91d3fd9 Remove prompt mode toggle (#17216) 2026-03-13 05:38:34 -05:00
opencode-agent[bot] a564231caf chore: generate 2026-03-13 10:19:52 +00:00
Shoubhit DashandGitHub 9457493696 perf(server): paginate session history (#17134) 2026-03-13 15:48:43 +05:30
AdamandGitHub ff748b82ca fix(app): simplify themes (#17274) 2026-03-13 10:12:58 +00:00
Frank 9fafa57562 go: upi pay 2026-03-13 03:24:33 -04:00
Aiden ClineandGitHub f8475649da chore: cleanup migrate from global code (#17292) 2026-03-12 23:54:11 -05:00
b94e110a4c fix(opencode): sessions lost after git init in existing project (#16814)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-12 23:18:59 -05:00
Luke ParkerandGitHub f0bba10b12 fix(e2e): fail fast on config dependency installs (#17280) 2026-03-13 14:08:51 +10:00
Adam d961981e25 fix(app): list item background colors 2026-03-12 22:23:51 -05:00
AdamandGitHub 5576662200 feat(app): missing themes (#17275) 2026-03-12 22:21:02 -05:00
Tom RyderandGitHub 4a2a046d79 fix: filter empty content blocks for Bedrock provider (#14586) 2026-03-12 22:13:09 -05:00
opencode-agent[bot] 8f8c74cfb8 chore: generate 2026-03-13 02:33:45 +00:00
Kit LangtonandGitHub 092f654f63 fix(cli): hide console command from help output (#17277) 2026-03-12 22:22:16 -04:00
Luke ParkerandGitHub 96b1d8f639 fix(app): stabilize todo dock e2e with composer probe (#17267) 2026-03-13 12:21:50 +10:00
opencode-agent[bot] dcb17c6a67 chore: generate 2026-03-13 02:10:27 +00:00
Kit LangtonandGitHub dd68b85f58 refactor(provider): effectify ProviderAuthService (#17227) 2026-03-13 02:08:37 +00:00
Brendan AllanandGitHub 84df96eaef desktop: multi-window support in electron (#17155) 2026-03-13 09:18:27 +08:00
Kit LangtonandGitHub d9dd33aeeb feat(cli): add console account subcommands (#17265) 2026-03-13 00:56:40 +00:00
Kit LangtonandGitHub 0a281c7390 refactor(auth): effectify AuthService (#17212) 2026-03-12 20:43:24 -04:00
Aiden ClineandGitHub 3016efba47 tweak: rm openrouter warning (#17259) 2026-03-12 19:42:31 -05:00
Luke ParkerandGitHub 3998df8112 fix(app): increase CI e2e workers (#17263) 2026-03-13 10:15:34 +10:00
Kit LangtonandGitHub 7066e2a25e reorder provider list in providers login (#17262) 2026-03-13 00:09:30 +00:00
Adam c173988aaa feat(app): interruption state 2026-03-12 19:07:23 -05:00
Luke ParkerandGitHub 268855dc5a fix(ci): keep test runs on dev (#17260) 2026-03-13 09:54:34 +10:00
opencode bfb736e94a release: v1.2.25 2026-03-12 23:34:11 +00:00
Frank df8464f89c zen: handle cache key 2026-03-12 19:10:58 -04:00
Adam 3ea387f364 fix(app): sidebar re-rendering too often 2026-03-12 16:27:15 -05:00
Adam 9d3c42c8c4 fix(app): task error state 2026-03-12 16:25:49 -05:00
Adam f2cad046e6 fix(app): message loading 2026-03-12 16:25:48 -05:00
Aiden ClineandGitHub d722026a8d fix: if server password exists, use basic auth for plugin client by default (#17213) 2026-03-12 15:41:46 -05:00
AdamandGitHub 42a5af6c8f feat(app): follow-up behavior (#17233) 2026-03-12 20:17:36 +00:00
Adam f0542fae7a fix(app): optimistic revert/restore 2026-03-12 15:04:27 -05:00
Adam 02c75821a8 feat(app): AMOLED theme 2026-03-12 14:11:22 -05:00
Adam 3ba9ab2c0a fix(app): not loading message nav 2026-03-12 13:33:17 -05:00
David HillandGitHub 184732fc20 fix(app): titlebar cleanup (#17206) 2026-03-12 18:26:50 +00:00
Frank b66222baf7 zen: fix nemotron issue 2026-03-12 14:17:36 -04:00
AdamandGitHub dce7eceb28 chore: cleanup (#17197) 2026-03-12 11:32:05 -05:00
AdamandGitHub 0e077f7483 feat: session load perf (#17186) 2026-03-12 11:31:52 -05:00
AdamandGitHub 776e7a9c15 feat(app): better themes (#16889) 2026-03-12 10:35:23 -05:00
opencode-agent[bot] c455d41876 chore: update nix node_modules hashes 2026-03-12 15:07:54 +00:00
Aiden ClineandGitHub a776a3ee12 fix: non openai azure models that use completions endpoints (#17128) 2026-03-12 10:05:00 -05:00
Kit LangtonandGitHub 64fb9233bf refactor(import): use .parse() at boundaries instead of manual .make() (#17106) 2026-03-12 10:52:20 -04:00
opencode-agent[bot] 3533f33ecb chore: generate 2026-03-12 14:49:22 +00:00
Kit LangtonandGitHub 1cb7df7159 refactor(provider): flow branded ProviderID/ModelID through internal signatures (#17182) 2026-03-12 14:48:17 +00:00
Wang SiyuanandGitHub a4f8d66a9b docs: clarify subagent session navigation keybinds (#16455) 2026-03-12 09:45:46 -05:00
AdamandGitHub 12efbbfa4c chore: cleanup (#17184) 2026-03-12 08:52:51 -05:00
max tomashevskyandGitHub 13402529ce fix(web): fix broken mobile sidebar (in workflows mode) sizing issue by adding flex-1 (#17055) 2026-03-12 08:46:41 -05:00
Adam fc678ef36c fix(app): terminal animation 2026-03-12 08:43:25 -05:00
Adam 03cd891ea9 chore: cleanup 2026-03-12 08:41:22 -05:00
opencode-agent[bot] 6314d741e7 chore: generate 2026-03-12 13:28:46 +00:00
Kit LangtonandGitHub c45467964c feat(id): brand ProviderID and ModelID (#17110) 2026-03-12 09:27:52 -04:00
Adam 2eeba53b07 fix(app): sidebar quirks 2026-03-12 07:51:31 -05:00
AdamandGitHub d4107d51f1 chore: cleanup (#17115) 2026-03-12 12:26:43 +00:00
opencode-agent[bot] d8fbe0af01 chore: update nix node_modules hashes 2026-03-12 08:26:49 +00:00
Brendan AllanandGitHub b76ead3fe8 refactor(desktop): rework default server initialization and connection handling (#16965) 2026-03-12 08:10:52 +00:00
opencode-agent[bot] 51835ecf90 chore: generate 2026-03-12 07:36:35 +00:00
Luke ParkerandGitHub 328c6de80d Fix terminal e2e flakiness with a real terminal driver (#17144) 2026-03-12 17:35:26 +10:00
OpeOginniandGitHub c9c0318e0e fix(desktop): set default WebSocket username and prevent repeated calling of terminal spawn properly closing the terminal (#17061) 2026-03-12 14:48:44 +08:00
d481f64bde fix(electron): theme Windows titlebar overlay (#16843)
Co-authored-by: Brendan Allan <brendonovich@outlook.com>
2026-03-12 16:38:56 +10:00
Luke ParkerandGitHub 54e7baa6cf fix(desktop-electron): fix resource loading under file:// protocol (#17125) 2026-03-12 12:19:44 +08:00
opencode-agent[bot] 1d7fcd40b4 chore: generate 2026-03-12 03:56:04 +00:00
Luke ParkerandGitHub db7bafe917 fix(app): guard comment accessor in message timeline (#17126) 2026-03-12 13:55:16 +10:00
Dax Raad b1ef501207 Merge remote-tracking branch 'origin/dev' into dev 2026-03-11 23:24:38 -04:00
Dax Raad 9fb12a906e core: remove external sourcemap generation to reduce build artifacts 2026-03-11 23:24:26 -04:00
Luke ParkerandGitHub fafbc29316 fix(ci): use dynamic bun cache path for cross-platform support (#17120) 2026-03-12 03:19:28 +00:00
opencode-agent[bot] 7b0def4b81 chore: generate 2026-03-12 02:04:26 +00:00
Luke ParkerandGitHub 1d9c83b576 fix(e2e): re-focus prompt after terminal opens in slash-terminal test (#17113) 2026-03-12 12:03:38 +10:00
opencode-agent[bot] 2c825c3223 chore: generate 2026-03-12 01:50:44 +00:00
Kit LangtonandGitHub 2a4dedc210 feat(id): brand PermissionID, PtyID, QuestionID, and ToolID (#17042) 2026-03-12 01:49:57 +00:00
opencode-agent[bot] b0bca6342e chore: generate 2026-03-12 00:26:05 +00:00
Luke ParkerandGitHub 547eb7676d feat(windows): add arm64 release targets for cli and desktop (#16696) 2026-03-12 00:25:09 +00:00
opencode-agent[bot] 83f083ee0d chore: generate 2026-03-11 23:41:43 +00:00
Kit LangtonandGitHub 090f636354 feat(id): brand PartID through Drizzle and Zod schemas (#16966) 2026-03-11 23:40:50 +00:00
opencode-agent[bot] d26c6f80e1 chore: generate 2026-03-11 23:31:07 +00:00
Kit LangtonandGitHub 16a6d6feba feat(id): brand WorkspaceID through Drizzle and Zod schemas (#16964) 2026-03-11 23:30:17 +00:00
f1c3a44190 fix: resolve symlinks in Instance cache to prevent duplicate contexts (#16651)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-03-11 23:26:54 +00:00
opencode-agent[bot] 34fa5de9c5 chore: generate 2026-03-11 23:17:42 +00:00
Kit LangtonandGitHub cb67465675 feat(id): brand SessionID through Drizzle and Zod schemas (#16953) 2026-03-11 23:16:56 +00:00
Frank 4e73473119 wip: zen 2026-03-11 19:00:05 -04:00
Frank cc18fa599c wip: zen 2026-03-11 18:50:49 -04:00
Frank aa81c1c4cb docs: go pricing 2026-03-11 18:09:41 -04:00
Frank 8569fc1f0e docs: zen update models 2026-03-11 18:09:41 -04:00
Frank 78de287bcc wip: zen 2026-03-11 18:09:41 -04:00
Frank bbc7052c7a go: dashboard design 2026-03-11 18:09:41 -04:00
Frank 502d6db6d0 go: first month discount 2026-03-11 18:09:41 -04:00
Frank 0b0ad5de99 zen: update discount copy on lander 2026-03-11 18:09:41 -04:00
Frank 9e6c4a01aa zen: add alipay for adding balance 2026-03-11 18:09:41 -04:00
Frank 4a81df190c zen: add alipay for go sub 2026-03-11 18:09:41 -04:00
Frank 75cae81f75 zen: add Go page 2026-03-11 18:09:41 -04:00
Frank ed3bb3ea8f zen: add usage section 2026-03-11 18:09:40 -04:00
Frank fac23a1afc zen: update usage graph on landing page 2026-03-11 18:09:40 -04:00
Frank f89696509e zen: update header 2026-03-11 18:09:40 -04:00
Dax Raad 604ab1bde1 core: restore plugin serverUrl getter so plugins can connect to local server 2026-03-11 17:41:51 -04:00
AdamandGitHub fbd9b7cf4f feat(app): restore to message and fork session (#17092) 2026-03-11 21:34:48 +00:00
Adam 58f45ae22b chore: skip test 2026-03-11 16:21:04 -05:00
Noam BresslerandGitHub 440405dbdd fix: re-enable snapshot in acp (#14918) 2026-03-11 16:18:40 -05:00
Adam a1cda29012 chore: fix test 2026-03-11 16:11:02 -05:00
Aiden ClineandGitHub f96e2d4222 tweak: adjust skill presentation to be a little less token heavy (#17098) 2026-03-11 16:03:15 -05:00
Adam 387ab78bf6 chore: fix test 2026-03-11 16:02:11 -05:00
Kit LangtonandGitHub dbc00aa8e0 feat(id): brand ProjectID through Drizzle and Zod schemas (#16948) 2026-03-11 16:44:26 -04:00
Adam c37f7b9d99 fix(app): todos not clearing 2026-03-11 14:42:34 -05:00
Chris YangandGitHub cf7ca9b2f7 fix(app): skip editor reconcile during IME composition (#17041) 2026-03-11 13:40:06 -05:00
Kit LangtonandGitHub 981c7b9e37 refactor(account): tighten effect-based account flows (#17072) 2026-03-11 18:18:58 +00:00
Johannes LoherandGitHub 2aae0d3493 fix(core): read stdout and stderr in PackageRegistry.info before waiting for the process to exit (#16998) 2026-03-11 13:10:45 -05:00
AdamandGitHub bcc0d19867 chore(app): simplify review pane (#17066) 2026-03-11 12:24:51 -05:00
9c585bb58b docs(providers): clarify npm choice for chat vs responses APIs (#16974)
Co-authored-by: wangxinxin <xinxin.wang@pharmbrain.com>
2026-03-11 10:35:16 -05:00
Aiden ClineandGitHub 0f6bc8ae71 tweak: adjust way skills are presented to agent to increase likelyhood of skill invocations. (#17053) 2026-03-11 10:24:55 -05:00
Shoubhit DashandGitHub 7291e28273 perf(app): trim session render work (#16987) 2026-03-11 18:19:17 +05:30
FilipandGitHub db57fe6193 fix(app): make error tool card respect settings (#17005) 2026-03-11 14:52:33 +05:30
Brendan Allan 802416639b ci: setup node in tauri build 2026-03-11 16:09:17 +08:00
opencode-agent[bot] 7ec398d855 chore: generate 2026-03-11 03:34:02 +00:00
4ab35d2c5c fix(electron): hide Windows background consoles (#16842)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-03-11 13:33:06 +10:00
b4ae030fc2 fix: add GOOGLE_VERTEX_LOCATION env var support for Vertex AI (#16922)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-10 22:32:39 -05:00
0843964eb3 feat(web): use Feishu for Chinese community links (#16908)
Co-authored-by: Frank <frank@anoma.ly>
2026-03-11 11:07:13 +08:00
Kit LangtonandGitHub a1b06d63c9 fix(account): resilient orgs fetch (#16944) 2026-03-11 00:39:07 +00:00
Dax Raad 1b6820bab5 sync 2026-03-10 20:13:56 -04:00
Adam 89bf199c07 chore(app): fix tests 2026-03-10 19:03:44 -05:00
Aiden ClineandGitHub 5acfdd1c5d chore: kill old copilot 403 message that was used for old plugin migration (#16904) 2026-03-10 16:20:41 -05:00
Dax Raad 556703f8ab ci: cancel duplicate workflow runs and add read permissions
- Add concurrency settings to cancel outdated runs when new commits are pushed
- Add contents: read permission for security hardening
- Remove redundant required job that checked test results
2026-03-10 17:17:11 -04:00
Frank 6b9f8fb9b3 zen: raise limit 2026-03-10 15:22:02 -04:00
f77e5cf8fb feat(ui): restyle Card and improve tool error cards (#16888)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-03-10 13:52:32 -05:00
Frank e6cdc21f2d zen: raise limit 2026-03-10 14:40:18 -04:00
Dax Raad 1fe8d4d7ad ci: exclude draft PRs from beta labeling process to prevent unfinished work from being included in releases 2026-03-10 14:08:55 -04:00
Dax Raad e44320980d ci: install setuptools to prevent Python distutils errors during dependency installation 2026-03-10 14:08:38 -04:00
Adam f5d7fe3072 chore: cleanup 2026-03-10 13:00:14 -05:00
Adam 835a27cf51 fix(app): terminal jank 2026-03-10 13:00:14 -05:00
Adam 85afaaa13d fix(app): terminal focus issues and jank 2026-03-10 13:00:14 -05:00
opencode-agent[bot] 490615169e chore: update nix node_modules hashes 2026-03-10 17:14:55 +00:00
DaxandGitHub bb232247d0 Fix ESM imports for @opencode-ai/plugin (#16916) 2026-03-10 13:11:36 -04:00
opencode-agent[bot] 94c128f73b chore: generate 2026-03-10 16:56:30 +00:00
613562f504 core: make account login upgrades safe while adding multi-account workspace auth (#15487)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 12:53:47 -04:00
AdamandGitHub 9c4325bcf8 fix(core): don't permit access to system directories (#16891) 2026-03-10 11:32:05 -05:00
Aiden ClineandGitHub ad08fd57df chore: rekram1-node is no longer on vacation (#16905) 2026-03-10 10:27:04 -05:00
opencode-agent[bot] 54ba59d3e1 chore: generate 2026-03-10 15:14:46 +00:00
James LongandGitHub a4330a225d feat(core): allow passing workspaceID into session create endpoint (#16798) 2026-03-10 11:12:51 -04:00
James LongandGitHub 69ddc91c35 fix(core): a chunk timeout when processing llm stream (#16366) 2026-03-10 11:12:14 -04:00
James LongandGitHub 4c4aed5a87 fix(core): make worktrees read the project id from local workspace (#16795) 2026-03-10 11:11:28 -04:00
opencode-agent[bot] 5a40158abf chore: generate 2026-03-10 15:07:35 +00:00
4dce485854 fix(opencode): add thinking variants support for SAP AI provider (#14958)
Co-authored-by: Test <test@test.com>
Co-authored-by: Stephen Collings <stevoland@gmail.com>
2026-03-10 10:05:45 -05:00
Adam 5ec5d1dace chore(app): debug window 2026-03-10 07:05:54 -05:00
opencode-agent[bot] d2c765e2b3 chore: generate 2026-03-10 11:01:22 +00:00
bhaktatejas922andGitHub d036c57d59 docs: update opencode-morph-plugin in all language ecosystem pages (#16869) 2026-03-10 06:00:13 -05:00
opencode-agent[bot] e7493e2204 chore: update nix node_modules hashes 2026-03-10 10:16:15 +00:00
SebastianandGitHub 3500bf64b8 upgrade opentui to v0.1.87 (#16772) 2026-03-10 11:03:05 +01:00
opencode-agent[bot] 4f982ddb94 chore: generate 2026-03-10 02:02:18 +00:00
ff3bb7424d fix(mcp): fix OAuth auto-connect failing on first connection (#15547)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-09 21:01:19 -05:00
Dax Raad 89d6f60d25 refactor(server): extract createApp function for server initialization
- Replace Server.App() with Server.Default() for internal server access
- Extract server app creation into Server.createApp(opts) for testability
- Move CORS whitelist from module-level variable to function parameter
- Update all tests to use Server.Default() instead of Server.App()
2026-03-09 17:13:52 -04:00
Adam ee18c9976e chore(app): dev stats 2026-03-09 15:57:24 -05:00
Adam 794532928f fix(app): terminal state corruption 2026-03-09 15:28:35 -05:00
Adam 7b773c65ec chore: cleanup 2026-03-09 15:28:35 -05:00
Adam e53aa79dc6 chore: cleanup 2026-03-09 15:28:35 -05:00
opencode-agent[bot] d9a97249c0 chore: generate 2026-03-09 20:15:31 +00:00
James LongandGitHub 86cef16940 fix(core): put workspace routing behind OPENCODE_EXPERIMENTAL_WORKSPACES flag (#16775) 2026-03-09 16:14:19 -04:00
opencode-agent[bot] ce38997c76 chore: update nix node_modules hashes 2026-03-09 19:51:58 +00:00
opencode-agent[bot] 7e10c728d4 chore: update nix node_modules hashes 2026-03-09 19:49:01 +00:00
bhaktatejas922andGitHub 3627c67cf2 docs: update opencode-morph-fast-apply to opencode-morph-plugin in ecosystem (#16634) 2026-03-09 14:39:06 -05:00
opencode-agent[bot] 2518fd81f6 chore: generate 2026-03-09 19:31:33 +00:00
Dax Raad 39ef7fc90e Merge remote-tracking branch 'origin/dev' into dev 2026-03-09 15:30:15 -04:00
Dax Raad 37ae0a4051 refactor: replace bun semver with npm semver package 2026-03-09 15:29:55 -04:00
Kyle AltendorfandGitHub b312928e9f fix(tui): wait for model store before auto-submitting --prompt (#7476) 2026-03-09 14:22:38 -05:00
DaxandGitHub 2f2856e20a refactor(opencode): replace Bun shell in core flows (#16286) 2026-03-09 15:19:50 -04:00
Dax Raad 831eb6881b refactor: change pathToFileURL imports from bun to url module 2026-03-09 14:52:25 -04:00
James LongandGitHub f20ee2fad2 fix(tui): handle error when creating a session (#16767) 2026-03-09 12:13:32 -04:00
Stephen Collingsandopencode 8b9710e56c fix: Multiple jdtls LSPs eating memory in java monorepos (#12123) 2026-03-09 16:09:43 +00:00
opencode c6262f9d40 release: v1.2.24 2026-03-09 16:09:34 +00:00
Adam b749fa90f2 fix(app): scroll jitter/loop 2026-03-09 10:44:02 -05:00
Dax Raad 8a51cbd253 core: prevent accidental edits to migration files by restricting agent access 2026-03-09 11:25:58 -04:00
David HillandGitHub 399b8f0701 fix(app): session title turn spinner (#16764) 2026-03-09 09:46:15 -05:00
FilipandGitHub 3742e42fdf fix(app): dismiss toast notifications when questions or permissions a… (#16758) 2026-03-09 09:36:57 -05:00
Karan HandaandGitHub 0388ec6862 fix(storybook): add ci build workflow (#16760) 2026-03-09 09:33:19 -05:00
James LongandGitHub 366b8a8034 feat(tui): add initial support for workspaces into the tui (#16230) 2026-03-09 10:28:04 -04:00
ef9bc4ec9e feat(gitlab): send context-1m-2025-08-07 beta header to enable 1M context window (#16153)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-03-09 09:22:00 -05:00
JackandGitHub 5838b58913 add copilot gpt-5.4 xhigh support (#16294) 2026-03-09 22:07:12 +08:00
opencode 2712244ad3 release: v1.2.23 2026-03-09 13:50:43 +00:00
Adam 6388cbaf92 fix(app): remove oc-1 theme 2026-03-09 08:25:41 -05:00
David Hill 5cc61e1b53 tui: fix sidebar workspace container sizing by adding box-border class to prevent content overflow issues 2026-03-09 13:05:43 +00:00
Adam 0243be86a7 fix(app): don't animate review panel in/out 2026-03-09 07:49:11 -05:00
opencode-agent[bot] 9154cd64e7 chore: update nix node_modules hashes 2026-03-09 12:46:47 +00:00
AdamandGitHub c71d1bde5e revert(app): "STUPID SEXY TIMELINE (#16420)" (#16745) 2026-03-09 07:36:39 -05:00
Luke ParkerandGitHub f27ef595f6 fix(app): sanitize workspace store filenames on Windows (#16703) 2026-03-09 20:26:53 +10:00
Yihui KhuuandGitHub 34328828ae fix(app): fix issue with scroll jumping when pressing escape in comment text area (#15374) 2026-03-09 15:29:24 +05:30
18fb19da3b fix(opencode): pass missing auth headers in run --attach (#16097)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-03-09 07:32:13 +00:00
opencode-agent[bot] 849e1ac543 docs(i18n): sync locale docs from english changes 2026-03-09 02:08:46 +00:00
Ariane EmoryandGitHub 656a8d8f55 docs: add session_child_first keybinding to documentation (#16631) 2026-03-08 21:03:52 -05:00
AdamandGitHub b976f339e8 feat(app): generate color palettes (#16232) 2026-03-08 19:28:58 -05:00
Dax Raad 7d7837e5b6 disable fallback to free nano for small model 2026-03-08 19:27:15 -04:00
opencode 1db292f4df release: v1.2.22 2026-03-08 22:34:59 +00:00
SebastianandGitHub 49a3a9fe36 guard tui exit (#16640) 2026-03-08 23:14:41 +01:00
Luke ParkerandGitHub e51ed460a6 fix(tui): canonicalize cwd after chdir (#16641) 2026-03-09 07:57:48 +10:00
David Hill d15c2ce349 tui: fix sidebar background color when collapsed
When the sidebar was collapsed (not on mobile), the background color was showing as the stronger variant instead of matching the base background. This fixes the hover state detection so users see a consistent lighter background when the sidebar is in collapsed mode.
2026-03-08 13:34:56 +00:00
David Hill 5cc4bb4089 app: suppress hover when opening project menu or right-clicking to prevent flickering 2026-03-08 13:31:18 +00:00
Shoubhit DashandGitHub 6e9e027886 fix: trim retained desktop terminal buffers (#16583) 2026-03-08 07:50:04 -05:00
opencode-agent[bot] f9a3d129a4 chore: update nix node_modules hashes 2026-03-08 12:25:35 +00:00
Adam c53d1d3ad8 fix(app): less auto-expand/collapse 2026-03-08 07:11:15 -05:00
Adam f386137fba chore: refactoring ui hooks 2026-03-08 07:11:15 -05:00
Adam c797b60069 fix(app): messages not loading reliably 2026-03-08 07:11:15 -05:00
Shoubhit DashandGitHub a139e9297d fix: prune and evict stale app session caches (#16584) 2026-03-08 07:10:00 -05:00
Shoubhit DashandGitHub 050f99ec54 test: make process cwd check cross-platform (#16594) 2026-03-08 06:56:45 -05:00
Roy BruschiniandGitHub 23ed652901 docs(zen.mdx): correct Italian grammar and punctuation errors (#16590) 2026-03-08 16:40:06 +05:30
13a68f3de3 fix(opencode): avoid TTY corruption from double cleanup (#16565)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-03-08 13:55:33 +05:30
fdad35aaa7 fix(tui): fix broken /mcp toggling (#16431)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-03-08 13:31:09 +05:30
DaxandGitHub a2ce4eb650 test: remove unused Ripgrep.search coverage (#16554) 2026-03-07 21:40:57 -05:00
David Hill 8fa04986cf Revert "tui: dock auto-accept after thinking and move Add file to bottom-left"
This reverts commit 69cb49f7cc.
2026-03-08 01:31:09 +00:00
David Hill a5710ed3e1 Revert "tui: keep model + thinking selectors beside Add file"
This reverts commit 426dcfa3b0.
2026-03-08 01:31:06 +00:00
David Hill 2efdc9df93 Revert "tui: add more editor bottom padding for prompt controls"
This reverts commit 981353793d.
2026-03-08 01:31:03 +00:00
David Hill 0c245886fe Revert "tui: expose auto-accept as a permissions select"
This reverts commit 12d862dbd3.
2026-03-08 01:31:00 +00:00
David Hill f03288b411 Revert "tui: use text-base color for prompt selects"
This reverts commit 207ebf4b8c.
2026-03-08 01:30:55 +00:00
David Hill 09388c98f3 Revert "tui: remove prompt model/thinking/permissions selectors on dev so the composer stays simple"
This reverts commit ae25c1e7b7.
2026-03-08 01:27:45 +00:00
David Hill ae25c1e7b7 tui: remove prompt model/thinking/permissions selectors on dev so the composer stays simple 2026-03-08 01:21:45 +00:00
David Hill 0813c14cc6 tui: restore new-session logo on dev so users recognize OpenCode immediately 2026-03-08 01:18:42 +00:00
David Hill b5151c421f tui: revert new-session logo on dev so this UI change only ships with auto-accept-permissions 2026-03-08 01:10:52 +00:00
David Hill e66fd079db tui: add opencode logo to new session screen so users can immediately identify the app when starting a fresh session 2026-03-08 00:59:03 +00:00
David Hill 207ebf4b8c tui: use text-base color for prompt selects
Select triggers in the composer now use the normal text color so model/thinking/permissions controls read consistently with the rest of the input UI.
2026-03-08 00:53:57 +00:00
David Hill 12d862dbd3 tui: expose auto-accept as a permissions select
Lets people explicitly choose between normal permission prompts and auto-accept while composing, without relying on an ambiguous icon state.
2026-03-08 00:53:57 +00:00
David Hill 981353793d tui: add more editor bottom padding for prompt controls
Gives typed text more breathing room above the Add file/model/thinking row so the controls don’t visually crowd what you’re writing.
2026-03-08 00:53:57 +00:00
David Hill 426dcfa3b0 tui: keep model + thinking selectors beside Add file
People change models and thinking settings while composing, so keeping those controls next to the Add file button avoids hunting in the footer and reduces context switching mid-message.
2026-03-08 00:53:57 +00:00
David Hill 69cb49f7cc tui: dock auto-accept after thinking and move Add file to bottom-left
Auto-accept now lives in the footer dock beside the thinking control so it stays easy to find without crowding the text box.

The Add file button moves to the bottom-left of the editor and the input gets a bit more bottom padding so the control row doesn’t overlap what you’re typing.
2026-03-08 00:53:57 +00:00
Dax Raad e30678a088 test: normalize ripgrep path assertion on windows 2026-03-07 19:47:57 -05:00
opencode-agent[bot] 771b29a857 chore: generate 2026-03-08 00:31:35 +00:00
Dax Raad e6d1aae33a test: lock in process, ripgrep, and installation helpers 2026-03-07 19:30:32 -05:00
David Hill 9dc8ac4734 tui: revert prompt control docking
Restore the previous prompt control layout after the dock/position changes made the composer feel less familiar.

This brings auto-accept back to its prior spot and returns Add file to the previous placement.
2026-03-08 00:17:28 +00:00
David Hill fdd037ba20 tui: dock auto-accept after thinking and move Add file to bottom-left
Auto-accept now lives in the footer dock beside the thinking control so it stays easy to find without crowding the text box.

The Add file button moves to the bottom-left of the editor and the input gets a bit more bottom padding so the control row doesn’t overlap what you’re typing.
2026-03-08 00:08:37 +00:00
Dax Raad 523f792b48 core: update database path test to verify correct channel-based filename
The test now validates that the database file is named according to the current installation channel (latest/beta get 'opencode.db', others get sanitized names). This ensures users' data is stored in the correct location based on their update channel.
2026-03-07 18:53:29 -05:00
Dax Raad 2230c3c401 core: allow beta channel to share database with stable channel 2026-03-07 18:53:29 -05:00
David Hill 1b494e5087 tui: balance titlebar columns so center content doesn't get squeezed by long side content 2026-03-07 23:50:23 +00:00
David Hill 9c43893a0f tui: align numeric displays consistently across tool outputs and diff counters using tabular numerals 2026-03-07 23:49:10 +00:00
David Hill 6dfe19b445 tui: center empty states vertically in session view and improve review panel messaging for projects without version control 2026-03-07 23:45:16 +00:00
Dax Raad a965a06259 core: add OPENCODE_SKIP_MIGRATIONS flag to bypass database migrations
Allows users to skip automatic database migrations by setting the
OPENCODE_SKIP_MIGRATIONS environment variable. Useful for testing
scenarios or when manually managing database state.
2026-03-07 16:17:00 -05:00
Frank 0654f28c72 zen: fix graph legend 2026-03-07 14:28:36 -05:00
Adam a32b76dee0 fix(app): review panel transition 2026-03-07 13:27:44 -06:00
opencode a52d640c8c release: v1.2.21 2026-03-07 18:00:39 +00:00
Karan HandaandGitHub 218869cf45 fix(storybook): restore build by mocking useLocation (#16472) 2026-03-07 09:55:43 -06:00
Eric GuoandGitHub e99d7a4292 fix(app): text-shimmer undefined length (#16475) 2026-03-07 09:53:32 -06:00
SANGWOO PARKandGitHub f0beb38f91 fix(app): guard session-header current() against undefined when options is empty (#16478) 2026-03-07 09:51:21 -06:00
FilipandGitHub 66fcab7b08 fix(app): preserve file tree tab on reopen + fix e2e test regressions (#16482) 2026-03-07 09:47:45 -06:00
641e1781a2 tui: remove close button from project hover popover (#16403)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-03-07 07:00:58 -06:00
Adam 490b95efe7 fix(app): new session uses agent model/variant 2026-03-07 07:00:38 -06:00
Adam ba1edea0ab fix(app): model sticks to session 2026-03-07 06:57:00 -06:00
Adam 73c9b685a7 fix(app): all panels transition 2026-03-07 06:48:37 -06:00
Adam 99d8aab0ac fix(app): can't scroll files 2026-03-07 06:47:11 -06:00
Adam 7dd6369952 fix(app): task agent title 2026-03-07 06:03:30 -06:00
Adam 06f60af1e9 chore: update web stats 2026-03-07 05:47:47 -06:00
Adam 66d0beba6f fix(app): fix max-width on timeline 2026-03-07 05:45:30 -06:00
David HillandGitHub 6b99dd50b6 tui: align session empty states (#16412) 2026-03-07 05:39:43 -06:00
opencode-agent[bot] c53c9d4e4e chore: generate 2026-03-07 11:26:12 +00:00
Kit LangtonandGitHub bbd0f3a252 STUPID SEXY TIMELINE (#16420) 2026-03-07 05:25:22 -06:00
Luke ParkerandGitHub b7e208b4f1 test(app): share workspace slug wait helper across e2e specs (#16446) 2026-03-07 07:48:30 +00:00
be9b4d1bcd fix(opencode): preserve original line endings in 'edit' tool (#9443)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-03-07 07:42:54 +00:00
Nate WilliamsandGitHub 5b5b791d75 fix(tui): fix broken /export toggling (#16443) 2026-03-07 12:52:17 +05:30
Luke ParkerandGitHub 0b7a5b1e7b test(app): abort sessions and wait for idle before e2e cleanup (#16439) 2026-03-07 16:33:12 +10:00
Karan HandaandGitHub 28bb16ca2a fix(config): point GitHub PR search tool at current repository (#16441) 2026-03-07 05:53:28 +00:00
Luke ParkerandGitHub 8a95be492d fix(windows): git path resolution for modified files across Git Bash, MSYS2, and Cygwin (#16422) 2026-03-07 15:42:14 +10:00
opencode-agent[bot] c42c5a0cc6 chore: generate 2026-03-07 04:54:05 +00:00
kikuchanandGitHub b2c2478d9d fix(pty): pty session handle leak (#15599) 2026-03-07 14:53:11 +10:00
Luke ParkerandGitHub 1a9af8acb6 feat(desktop): show skill issue when snapshotting is off (#16432) 2026-03-07 04:52:04 +00:00
Luke ParkerandGitHub 4c7fe60493 fix(opencode): sanitize preview database filenames (#16430) 2026-03-07 04:34:29 +00:00
opencode-agent[bot] c108f304c6 chore: update nix node_modules hashes 2026-03-07 02:35:17 +00:00
David Hill 2b8acfa0e2 app: fix portal positioning for sidebar menus and tooltips by removing conditional mount logic 2026-03-07 00:30:51 +00:00
Jay V b83282b940 docs: update legal policies for 2026 terms refresh 2026-03-06 18:31:02 -05:00
FilipandGitHub c4fd677785 tests(app): e2e tests part 67 (#16406) 2026-03-06 17:08:59 -06:00
opencode-agent[bot] 770cb66628 chore: generate 2026-03-06 22:34:25 +00:00
b0bc3d87f5 feat(app): sidebar reveal animation, hover peek overlay, and weaker dividers (#16374)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-03-06 16:33:34 -06:00
James LongandGitHub a2634337b8 fix(core): log stack trace when schema validation fails (#16401) 2026-03-06 17:04:22 -05:00
Dax Raad 7417c869fc fix issue with migration 2026-03-06 16:44:49 -05:00
091cf25de8 fix(app): better review/filetree empty states (#16221)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-03-06 15:39:31 -06:00
Adam 7a071eff5c chore: fix test 2026-03-06 15:27:49 -06:00
opencode-agent[bot] 7da24ebf5d chore: generate 2026-03-06 19:40:36 +00:00
Shoubhit DashandGitHub d6e0f47361 feat: add project git init api (#16383) 2026-03-06 13:39:50 -06:00
Alexandre Reyes MartinsandGitHub 95385eb652 fix(app): enable Safari autocorrect in normal mode, disable in shell mode (#15563) 2026-03-06 22:36:27 +05:30
Adam a71b11caca fix(app): stale keyed show errors 2026-03-06 11:03:37 -06:00
Hoshiumi ArataandGitHub e9568999c3 fix(ui): prevent unwanted key events during composition in LineCommentEditor (#16361) 2026-03-06 11:01:13 -06:00
Dax Raad 5e699c9426 chore(storage): update drizzle and channel db handling 2026-03-06 10:58:19 -05:00
Adam e0ca52ed1f fix(app): part type gate 2026-03-06 09:56:02 -06:00
Shoubhit DashandGitHub 1d9dcd2a27 share: speed up share loads (#16165) 2026-03-06 06:49:15 -06:00
Adam eeeb21ff86 Revert "fix(app): stale read error"
This reverts commit 152df2428d.
2026-03-06 05:52:48 -06:00
Adam 2094e8b255 Revert "fix(app): stale keyed show errors"
This reverts commit 7665b8e30d.
2026-03-06 05:52:47 -06:00
opencode-agent[bot] e1cf761d29 chore: generate 2026-03-06 11:48:31 +00:00
Kirill TregubovandGitHub f64bb91257 fix(app): add english to locale matchers (#16280) 2026-03-06 05:47:39 -06:00
DandGitHub eb9eb5e334 feat: Add Vietnamese README and update all language navigation links … (#16322) 2026-03-06 05:46:53 -06:00
opencode-agent[bot] d4d1292a0e chore: generate 2026-03-06 10:50:00 +00:00
Luis Felipe Cordeiro SenaandGitHub b7605add58 fix(app): enable auto-accept keybind regardless of permission config (#16259) 2026-03-06 04:49:12 -06:00
opencode 6c7d968c44 release: v1.2.20 2026-03-06 07:29:34 +00:00
DaxandGitHub 326c70184d fix: restore Bun stdin reads for prompt input (#16300) 2026-03-06 02:08:10 -05:00
Luke ParkerandGitHub aec6ca71fa fix(git): stop leaking fsmonitor daemons e.g. 60GB+ of commited memory after running tests (#16249) 2026-03-06 15:42:08 +10:00
opencode-agent[bot] c04da45be5 chore: update nix node_modules hashes 2026-03-06 05:31:01 +00:00
DaxandGitHub 74effa8eec refactor(opencode): replace Bun.which with npm which (#15012) 2026-03-06 05:18:29 +00:00
opencode cb411248bf release: v1.2.19 2026-03-06 04:29:10 +00:00
SadıkandGitHub 46d7d2fdc0 feat: add "gpt-5.4" to codex allowed models list (#16274) 2026-03-05 23:03:01 -05:00
Dax Raad d68afcaa55 refactor: replace Bun.stderr and Bun.color with Node.js equivalents 2026-03-05 22:20:16 -05:00
Dax Raad bf35a865ba refactor: replace Bun.connect with net.createConnection 2026-03-05 22:17:08 -05:00
Dax Raad 6733a5a822 fix: use sha1 for hash instead of unsupported xxhash3-xxh64 2026-03-05 22:12:10 -05:00
Dax Raad 7e28098365 refactor: use node:stream/consumers for stdin reading 2026-03-05 22:08:50 -05:00
Dax Raad ae5c9ed3dd refactor: replace Bun.stdin.text with Node.js stream reading 2026-03-05 22:04:20 -05:00
Dax Raad a9bf1c0505 refactor: replace Bun.hash with Hash.fast using xxhash3-xxh64 2026-03-05 22:03:24 -05:00
Dax Raad dad248832d refactor: replace Bun.write with Filesystem.write in config files 2026-03-05 21:59:20 -05:00
Dax Raad 6e89d3e597 refactor: replace Bun.write/file with Filesystem utilities in snapshot 2026-03-05 21:56:41 -05:00
DaxandGitHub 3ebba02d04 refactor: replace Bun.sleep with node timers (#15013)
publish-github-action / publish (push) Has been cancelled
2026-03-05 21:54:06 -05:00
FilipandGitHub cf425d114e fix(app): stale show (#16236) 2026-03-05 18:23:48 -06:00
David Hill 39691e5174 tui: remove keyboard shortcut tooltips from new session and new workspace buttons in the sidebar 2026-03-05 23:44:00 +00:00
Frank adaee66364 zen: gpt 5.4 pro 2026-03-05 18:39:17 -05:00
Frank a6978167ae ci: fix 2026-03-05 15:46:17 -05:00
Frank 80c36c788c zen: gpt5.3 codex spark 2026-03-05 15:40:51 -05:00
JunandGitHub 76cdc668e8 fix(console): follow-up for #13108 docs/en routing and locale cookie sync (#13608) 2026-03-05 14:31:38 -06:00
1463 changed files with 189183 additions and 87933 deletions
+11
View File
@@ -10,6 +10,10 @@
adamdotdevin
-agusbasari29 AI PR slop
ariane-emory
-atharvau AI review spamming literally every PR
-borealbytes
-danieljoshuanazareth
-danieljoshuanazareth
edemaine
-florianleibert
fwang
@@ -17,7 +21,14 @@ iamdavidhill
jayair
kitlangton
kommander
-opencode2026
-opencodeengineer bot that spams issues
r44vc0rp
rekram1-node
-ricardo-m-l
-robinmordasiewicz
shantur
simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr
-toastythebot
+26 -9
View File
@@ -3,14 +3,6 @@ description: "Setup Bun with caching and install dependencies"
runs:
using: "composite"
steps:
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Get baseline download URL
id: bun-url
shell: bash
@@ -31,6 +23,31 @@ runs:
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
bun-download-url: ${{ steps.bun-url.outputs.url }}
- name: Get cache directory
id: cache
shell: bash
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install setuptools for distutils compatibility
run: python3 -m pip install setuptools || pip install setuptools || true
shell: bash
- name: Install dependencies
run: bun install
run: |
# Workaround for patched peer variants
# e.g. ./patches/ for standard-openapi
# https://github.com/oven-sh/bun/issues/28147
if [ "$RUNNER_OS" = "Windows" ]; then
bun install --linker hoisted
else
bun install
fi
shell: bash
+24
View File
@@ -0,0 +1,24 @@
name: close-issues
on:
schedule:
- cron: "0 2 * * *" # Daily at 2:00 AM
workflow_dispatch:
jobs:
close:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Close stale issues
env:
GITHUB_TOKEN: ${{ github.token }}
run: bun script/github/close-issues.ts
+1 -1
View File
@@ -11,7 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
deploy:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
+3 -2
View File
@@ -9,7 +9,8 @@ on:
jobs:
sync-locales:
if: github.actor != 'opencode-agent[bot]'
if: false
#if: github.actor != 'opencode-agent[bot]'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
@@ -34,7 +35,7 @@ jobs:
- 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)
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- ':(glob)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"
+5 -1
View File
@@ -17,6 +17,10 @@ on:
- "patches/**"
- ".github/workflows/nix-hashes.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Native runners required: bun install cross-compilation flags (--os/--cpu)
# do not produce byte-identical node_modules as native installs.
@@ -56,7 +60,7 @@ jobs:
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)"
HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)"
if [ -z "$HASH" ]; then
echo "::error::Failed to compute hash for ${SYSTEM}"
+196 -2
View File
@@ -98,15 +98,129 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: opencode-cli
path: packages/opencode/dist
path: |
packages/opencode/dist/opencode-darwin*
packages/opencode/dist/opencode-linux*
- uses: actions/upload-artifact@v4
with:
name: opencode-cli-windows
path: packages/opencode/dist/opencode-windows*
outputs:
version: ${{ needs.version.outputs.version }}
sign-cli-windows:
needs:
- build-cli
- version
runs-on: blacksmith-4vcpu-windows-2025
if: github.repository == 'anomalyco/opencode'
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v4
with:
name: opencode-cli-windows
path: packages/opencode/dist
- 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: Azure login
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: azure/artifact-signing-action@v1
with:
endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
files: |
${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Verify Windows CLI signatures
shell: pwsh
run: |
$files = @(
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
)
foreach ($file in $files) {
$sig = Get-AuthenticodeSignature $file
if ($sig.Status -ne "Valid") {
throw "Invalid signature for ${file}: $($sig.Status)"
}
}
- name: Repack Windows CLI archives
working-directory: packages/opencode/dist
shell: pwsh
run: |
Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
- name: Upload signed Windows CLI release assets
if: needs.version.outputs.release != ''
shell: pwsh
env:
GH_TOKEN: ${{ steps.committer.outputs.token }}
run: |
gh release upload "v${{ needs.version.outputs.version }}" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
"${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
--clobber `
--repo "${{ needs.version.outputs.repo }}"
- uses: actions/upload-artifact@v4
with:
name: opencode-cli-signed-windows
path: |
packages/opencode/dist/opencode-windows-arm64
packages/opencode/dist/opencode-windows-x64
packages/opencode/dist/opencode-windows-x64-baseline
build-tauri:
needs:
- build-cli
- version
continue-on-error: false
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
strategy:
fail-fast: false
matrix:
@@ -115,6 +229,9 @@ jobs:
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: windows-2025
target: aarch64-pc-windows-msvc
- host: blacksmith-4vcpu-windows-2025
target: x86_64-pc-windows-msvc
- host: blacksmith-4vcpu-ubuntu-2404
@@ -149,6 +266,18 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Azure login
if: runner.os == 'Windows'
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Cache apt packages
if: contains(matrix.settings.host, 'ubuntu')
uses: actions/cache@v4
@@ -183,6 +312,7 @@ jobs:
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -239,11 +369,35 @@ jobs:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
- name: Verify signed Windows desktop artifacts
if: runner.os == 'Windows'
shell: pwsh
run: |
$files = @(
"${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
)
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
foreach ($file in $files) {
$sig = Get-AuthenticodeSignature $file
if ($sig.Status -ne "Valid") {
throw "Invalid signature for ${file}: $($sig.Status)"
}
}
build-electron:
needs:
- build-cli
- version
if: github.repository == 'anomalyco/opencode'
continue-on-error: false
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
strategy:
fail-fast: false
matrix:
@@ -254,6 +408,10 @@ jobs:
- host: macos-latest
target: aarch64-apple-darwin
platform_flag: --mac --arm64
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: "windows-2025"
target: aarch64-pc-windows-msvc
platform_flag: --win --arm64
- host: "blacksmith-4vcpu-windows-2025"
target: x86_64-pc-windows-msvc
platform_flag: --win
@@ -264,7 +422,6 @@ jobs:
target: aarch64-unknown-linux-gnu
platform_flag: --linux
runs-on: ${{ matrix.settings.host }}
# if: github.ref_name == 'beta'
steps:
- uses: actions/checkout@v3
@@ -281,6 +438,14 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: Azure login
if: runner.os == 'Windows'
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: actions/setup-node@v4
with:
node-version: "24"
@@ -315,6 +480,7 @@ jobs:
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -347,6 +513,22 @@ jobs:
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
- name: Verify signed Windows Electron artifacts
if: runner.os == 'Windows'
shell: pwsh
run: |
$files = @()
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
foreach ($file in $files | Select-Object -Unique) {
$sig = Get-AuthenticodeSignature $file
if ($sig.Status -ne "Valid") {
throw "Invalid signature for ${file}: $($sig.Status)"
}
}
- uses: actions/upload-artifact@v4
with:
name: opencode-electron-${{ matrix.settings.target }}
@@ -362,8 +544,10 @@ jobs:
needs:
- version
- build-cli
- sign-cli-windows
- build-tauri
- build-electron
if: always() && !failure() && !cancelled()
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
@@ -400,6 +584,16 @@ jobs:
name: opencode-cli
path: packages/opencode/dist
- uses: actions/download-artifact@v4
with:
name: opencode-cli-windows
path: packages/opencode/dist
- uses: actions/download-artifact@v4
with:
name: opencode-cli-signed-windows
path: packages/opencode/dist
- uses: actions/download-artifact@v4
if: needs.version.outputs.release
with:
-54
View File
@@ -1,54 +0,0 @@
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
-33
View File
@@ -1,33 +0,0 @@
name: stale-issues
on:
schedule:
- cron: "30 1 * * *" # Daily at 1:30 AM
workflow_dispatch:
env:
DAYS_BEFORE_STALE: 90
DAYS_BEFORE_CLOSE: 7
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v10
with:
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
stale-issue-label: "stale"
close-issue-message: |
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
Feel free to reopen if you still need this!
stale-issue-message: |
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
remove-stale-when-updated: true
exempt-issue-labels: "pinned,security,feature-request,on-hold"
start-date: "2025-12-27"
+38
View File
@@ -0,0 +1,38 @@
name: storybook
on:
push:
branches: [dev]
paths:
- ".github/workflows/storybook.yml"
- "package.json"
- "bun.lock"
- "packages/storybook/**"
- "packages/ui/**"
pull_request:
branches: [dev]
paths:
- ".github/workflows/storybook.yml"
- "package.json"
- "bun.lock"
- "packages/storybook/**"
- "packages/ui/**"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: storybook build
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Build Storybook
run: bun --cwd packages/storybook build
+81 -23
View File
@@ -6,6 +6,20 @@ on:
- dev
pull_request:
workflow_dispatch:
concurrency:
# Keep every run on dev so cancelled checks do not pollute the default branch
# commit history. PRs and other branches still share a group and cancel stale runs.
group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
cancel-in-progress: true
permissions:
contents: read
checks: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
unit:
name: unit (${{ matrix.settings.name }})
@@ -27,6 +41,11 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: ./.github/actions/setup-bun
@@ -35,25 +54,53 @@ jobs:
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
- name: Cache Turbo
uses: actions/cache@v4
with:
path: node_modules/.cache/turbo
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-
turbo-${{ runner.os }}-
- name: Run unit tests
run: bun turbo test
run: bun turbo test:ci
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
- name: Publish unit reports
if: always()
uses: mikepenz/action-junit-report@v6
with:
report_paths: packages/*/.artifacts/unit/junit.xml
check_name: "unit results (${{ matrix.settings.name }})"
detailed_summary: true
include_time_in_summary: true
fail_on_failure: false
- name: Upload unit artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }}
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
path: packages/*/.artifacts/unit/junit.xml
e2e:
name: e2e (${{ matrix.settings.name }})
needs: unit
strategy:
fail-fast: false
matrix:
settings:
- name: linux
host: blacksmith-4vcpu-ubuntu-2404
playwright: bunx playwright install --with-deps
- name: windows
host: blacksmith-4vcpu-windows-2025
playwright: bunx playwright install
runs-on: ${{ matrix.settings.host }}
env:
PLAYWRIGHT_BROWSERS_PATH: 0
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
defaults:
run:
shell: bash
@@ -63,41 +110,52 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Install Playwright browsers
- name: Read Playwright version
id: playwright-version
run: |
version=$(node -e 'console.log(require("./package.json").workspaces.catalog["@playwright/test"])')
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.playwright-browsers
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
- name: Install Playwright system dependencies
if: runner.os == 'Linux'
working-directory: packages/app
run: ${{ matrix.settings.playwright }}
run: bunx playwright install-deps chromium
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: packages/app
run: bunx playwright install chromium
- name: Run app e2e tests
run: bun --cwd packages/app test:e2e:local
env:
CI: true
PLAYWRIGHT_JUNIT_OUTPUT: e2e/junit-${{ matrix.settings.name }}.xml
timeout-minutes: 30
- name: Upload Playwright artifacts
if: failure()
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
if-no-files-found: ignore
retention-days: 7
path: |
packages/app/e2e/junit-*.xml
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"
+1 -1
View File
@@ -33,6 +33,6 @@ jobs:
with:
issue-id: ${{ github.event.issue.number }}
comment-id: ${{ github.event.comment.id }}
roles: admin,maintain
roles: admin,maintain,write
env:
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
+2 -1
View File
@@ -17,7 +17,7 @@ ts-dist
/result
refs
Session.vim
opencode.json
/opencode.json
a.out
target
.scripts
@@ -25,6 +25,7 @@ target
# Local dev files
opencode-dev
UPCOMING_CHANGELOG.md
logs/
*.bun-build
tsconfig.tsbuildinfo
+6 -2
View File
@@ -1,3 +1,7 @@
plans/
bun.lock
node_modules
plans
package.json
bun.lock
.gitignore
package-lock.json
references/
-34
View File
@@ -1,34 +0,0 @@
---
description: ALWAYS use this when writing docs
color: "#38A3EE"
---
You are an expert technical documentation writer
You are not verbose
Use a relaxed and friendly tone
The title of the page should be a word or a 2-3 word phrase
The description should be one short line, should not start with "The", should
avoid repeating the title of the page, should be 5-10 words long
Chunks of text should not be more than 2 sentences long
Each section is separated by a divider of 3 dashes
The section titles are short with only the first letter of the word capitalized
The section titles are in the imperative mood
The section titles should not repeat the term used in the page title, for
example, if the page title is "Models", avoid using a section title like "Add
new models". This might be unavoidable in some cases, but try to avoid it.
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
For JS or TS code snippets remove trailing semicolons and any trailing commas
that might not be needed.
If you are making a commit prefix the commit message with `docs:`
+1 -1
View File
@@ -1,7 +1,7 @@
---
description: Translate content for a specified locale while preserving technical terms
mode: subagent
model: opencode/gemini-3-pro
model: opencode/gpt-5.4
---
You are a professional translator and localization specialist.
+46
View File
@@ -0,0 +1,46 @@
---
model: opencode/gpt-5.4
---
Create `UPCOMING_CHANGELOG.md` from the structured changelog input below.
If `UPCOMING_CHANGELOG.md` already exists, ignore its current contents completely.
Do not preserve, merge, or reuse text from the existing file.
The input already contains the exact commit range since the last non-draft release.
The commits are already filtered to the release-relevant packages and grouped into
the release sections. Do not fetch GitHub releases, PRs, or build your own commit list.
The input may also include a `## Community Contributors Input` section.
Before writing any entry you keep, inspect the real diff with
`git show --stat --format='' <hash>` or `git show --format='' <hash>` so you can
understand the actual code changes and not just the commit message (they may be misleading).
Do not use `git log` or author metadata when deciding attribution.
Rules:
- Write the final file with sections in this order:
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
- Only include sections that have at least one notable entry
- Keep one bullet per commit you keep
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
- Start each bullet with a capital letter
- Prefer what changed for users over what code changed internally
- Do not copy raw commit prefixes like `fix:` or `feat:` or trailing PR numbers like `(#123)`
- Community attribution is deterministic: only preserve an existing `(@username)` suffix from the changelog input
- If an input bullet has no `(@username)` suffix, do not add one
- Never add a new `(@username)` suffix from `git show`, commit authors, names, or email addresses
- If no notable entries remain and there is no contributor block, write exactly `No notable changes.`
- If no notable entries remain but there is a contributor block, omit all release sections and return only the contributor block
- If the input contains `## Community Contributors Input`, append the block below that heading to the end of the final file verbatim
- Do not add, remove, rewrite, or reorder contributor names or commit titles in that block
- Do not derive the thank-you section from the main summary bullets
- Do not include the heading `## Community Contributors Input` in the final file
- Focus on writing the least words to get your point across - users will skim read the changelog, so we should be precise
**Importantly, the changelog is for users (who are at least slightly technical), they may use the TUI, Desktop, SDK, Plugins and so forth. Be thorough in understanding flow on effects may not be immediately apparent. e.g. a package upgrade looks internal but may patch a bug. Or a refactor may also stabilise some race condition that fixes bugs for users. The PR title/body + commit message will give you the authors context, usually containing the outcome not just technical detail**
<changelog_input>
!`bun script/raw-changelog.ts $ARGUMENTS`
</changelog_input>
+5
View File
@@ -5,6 +5,11 @@
"options": {},
},
},
"permission": {
"edit": {
"packages/opencode/migration/*": "deny",
},
},
"mcp": {},
"tools": {
"github-triage": false,
+223
View File
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord10",
"light": "nord9"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord6",
"light": "nord0"
},
"textMuted": {
"dark": "#8B95A7",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord2",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "#8B95A7",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "#8B95A7",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#36413C",
"light": "#E6EBE7"
},
"diffRemovedBg": {
"dark": "#43393D",
"light": "#ECE6E8"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#303A35",
"light": "#DDE4DF"
},
"diffRemovedLineNumberBg": {
"dark": "#3C3336",
"light": "#E4DDE0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "#8B95A7",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "#8B95A7",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "#8B95A7",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
+937
View File
@@ -0,0 +1,937 @@
/** @jsxImportSource @opentui/solid */
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
import { RGBA, VignetteEffect } from "@opentui/core"
import type {
TuiKeybindSet,
TuiPlugin,
TuiPluginApi,
TuiPluginMeta,
TuiPluginModule,
TuiSlotPlugin,
} from "@opencode-ai/plugin/tui"
const tabs = ["overview", "counter", "help"]
const bind = {
modal: "ctrl+shift+m",
screen: "ctrl+shift+o",
home: "escape,ctrl+h",
left: "left,h",
right: "right,l",
up: "up,k",
down: "down,j",
alert: "a",
confirm: "c",
prompt: "p",
select: "s",
modal_accept: "enter,return",
modal_close: "escape",
dialog_close: "escape",
local: "x",
local_push: "enter,return",
local_close: "q,backspace",
host: "z",
}
const pick = (value: unknown, fallback: string) => {
if (typeof value !== "string") return fallback
if (!value.trim()) return fallback
return value
}
const num = (value: unknown, fallback: number) => {
if (typeof value !== "number") return fallback
return value
}
const rec = (value: unknown) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return
return Object.fromEntries(Object.entries(value))
}
type Cfg = {
label: string
route: string
vignette: number
keybinds: Record<string, unknown> | undefined
}
type Route = {
modal: string
screen: string
}
type State = {
tab: number
count: number
source: string
note: string
selected: string
local: number
}
const cfg = (options: Record<string, unknown> | undefined) => {
return {
label: pick(options?.label, "smoke"),
route: pick(options?.route, "workspace-smoke"),
vignette: Math.max(0, num(options?.vignette, 0.35)),
keybinds: rec(options?.keybinds),
}
}
const names = (input: Cfg) => {
return {
modal: `${input.route}.modal`,
screen: `${input.route}.screen`,
}
}
type Keys = TuiKeybindSet
const ui = {
panel: "#1d1d1d",
border: "#4a4a4a",
text: "#f0f0f0",
muted: "#a5a5a5",
accent: "#5f87ff",
}
type Color = RGBA | string
const ink = (map: Record<string, unknown>, name: string, fallback: string): Color => {
const value = map[name]
if (typeof value === "string") return value
if (value instanceof RGBA) return value
return fallback
}
const look = (map: Record<string, unknown>) => {
return {
panel: ink(map, "backgroundPanel", ui.panel),
border: ink(map, "border", ui.border),
text: ink(map, "text", ui.text),
muted: ink(map, "textMuted", ui.muted),
accent: ink(map, "primary", ui.accent),
selected: ink(map, "selectedListItemText", ui.text),
}
}
const tone = (api: TuiPluginApi) => {
return look(api.theme.current)
}
type Skin = {
panel: Color
border: Color
text: Color
muted: Color
accent: Color
selected: Color
}
const Btn = (props: { txt: string; run: () => void; skin: Skin; on?: boolean }) => {
return (
<box
onMouseUp={() => {
props.run()
}}
backgroundColor={props.on ? props.skin.accent : props.skin.border}
paddingLeft={1}
paddingRight={1}
>
<text fg={props.on ? props.skin.selected : props.skin.text}>{props.txt}</text>
</box>
)
}
const parse = (params: Record<string, unknown> | undefined) => {
const tab = typeof params?.tab === "number" ? params.tab : 0
const count = typeof params?.count === "number" ? params.count : 0
const source = typeof params?.source === "string" ? params.source : "unknown"
const note = typeof params?.note === "string" ? params.note : ""
const selected = typeof params?.selected === "string" ? params.selected : ""
const local = typeof params?.local === "number" ? params.local : 0
return {
tab: Math.max(0, Math.min(tab, tabs.length - 1)),
count,
source,
note,
selected,
local: Math.max(0, local),
}
}
const current = (api: TuiPluginApi, route: Route) => {
const value = api.route.current
const ok = Object.values(route).includes(value.name)
if (!ok) return parse(undefined)
if (!("params" in value)) return parse(undefined)
return parse(value.params)
}
const opts = [
{
title: "Overview",
value: 0,
description: "Switch to overview tab",
},
{
title: "Counter",
value: 1,
description: "Switch to counter tab",
},
{
title: "Help",
value: 2,
description: "Switch to help tab",
},
]
const host = (api: TuiPluginApi, input: Cfg, skin: Skin) => {
api.ui.dialog.setSize("medium")
api.ui.dialog.replace(() => (
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
<text fg={skin.text}>
<b>{input.label} host overlay</b>
</text>
<text fg={skin.muted}>Using api.ui.dialog stack with built-in backdrop</text>
<text fg={skin.muted}>esc closes · depth {api.ui.dialog.depth}</text>
<box flexDirection="row" gap={1}>
<Btn txt="close" run={() => api.ui.dialog.clear()} skin={skin} on />
</box>
</box>
))
}
const warn = (api: TuiPluginApi, route: Route, value: State) => {
const DialogAlert = api.ui.DialogAlert
api.ui.dialog.setSize("medium")
api.ui.dialog.replace(() => (
<DialogAlert
title="Smoke alert"
message="Testing built-in alert dialog"
onConfirm={() => api.route.navigate(route.screen, { ...value, source: "alert" })}
/>
))
}
const check = (api: TuiPluginApi, route: Route, value: State) => {
const DialogConfirm = api.ui.DialogConfirm
api.ui.dialog.setSize("medium")
api.ui.dialog.replace(() => (
<DialogConfirm
title="Smoke confirm"
message="Apply +1 to counter?"
onConfirm={() => api.route.navigate(route.screen, { ...value, count: value.count + 1, source: "confirm" })}
onCancel={() => api.route.navigate(route.screen, { ...value, source: "confirm-cancel" })}
/>
))
}
const entry = (api: TuiPluginApi, route: Route, value: State) => {
const DialogPrompt = api.ui.DialogPrompt
api.ui.dialog.setSize("medium")
api.ui.dialog.replace(() => (
<DialogPrompt
title="Smoke prompt"
value={value.note}
onConfirm={(note) => {
api.ui.dialog.clear()
api.route.navigate(route.screen, { ...value, note, source: "prompt" })
}}
onCancel={() => {
api.ui.dialog.clear()
api.route.navigate(route.screen, value)
}}
/>
))
}
const picker = (api: TuiPluginApi, route: Route, value: State) => {
const DialogSelect = api.ui.DialogSelect
api.ui.dialog.setSize("medium")
api.ui.dialog.replace(() => (
<DialogSelect
title="Smoke select"
options={opts}
current={value.tab}
onSelect={(item) => {
api.ui.dialog.clear()
api.route.navigate(route.screen, {
...value,
tab: typeof item.value === "number" ? item.value : value.tab,
selected: item.title,
source: "select",
})
}}
/>
))
}
const Screen = (props: {
api: TuiPluginApi
input: Cfg
route: Route
keys: Keys
meta: TuiPluginMeta
params?: Record<string, unknown>
}) => {
const dim = useTerminalDimensions()
const value = parse(props.params)
const skin = tone(props.api)
const set = (local: number, base?: State) => {
const next = base ?? current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, local: Math.max(0, local), source: "local" })
}
const push = (base?: State) => {
const next = base ?? current(props.api, props.route)
set(next.local + 1, next)
}
const open = () => {
const next = current(props.api, props.route)
if (next.local > 0) return
set(1, next)
}
const pop = (base?: State) => {
const next = base ?? current(props.api, props.route)
const local = Math.max(0, next.local - 1)
set(local, next)
}
const show = () => {
setTimeout(() => {
open()
}, 0)
}
useKeyboard((evt) => {
if (props.api.route.current.name !== props.route.screen) return
const next = current(props.api, props.route)
if (props.api.ui.dialog.open) {
if (props.keys.match("dialog_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.ui.dialog.clear()
return
}
return
}
if (next.local > 0) {
if (evt.name === "escape" || props.keys.match("local_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
pop(next)
return
}
if (props.keys.match("local_push", evt)) {
evt.preventDefault()
evt.stopPropagation()
push(next)
return
}
return
}
if (props.keys.match("home", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate("home")
return
}
if (props.keys.match("left", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
return
}
if (props.keys.match("right", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
return
}
if (props.keys.match("up", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
return
}
if (props.keys.match("down", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
return
}
if (props.keys.match("modal", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.modal, next)
return
}
if (props.keys.match("local", evt)) {
evt.preventDefault()
evt.stopPropagation()
open()
return
}
if (props.keys.match("host", evt)) {
evt.preventDefault()
evt.stopPropagation()
host(props.api, props.input, skin)
return
}
if (props.keys.match("alert", evt)) {
evt.preventDefault()
evt.stopPropagation()
warn(props.api, props.route, next)
return
}
if (props.keys.match("confirm", evt)) {
evt.preventDefault()
evt.stopPropagation()
check(props.api, props.route, next)
return
}
if (props.keys.match("prompt", evt)) {
evt.preventDefault()
evt.stopPropagation()
entry(props.api, props.route, next)
return
}
if (props.keys.match("select", evt)) {
evt.preventDefault()
evt.stopPropagation()
picker(props.api, props.route, next)
}
})
return (
<box width={dim().width} height={dim().height} backgroundColor={skin.panel} position="relative">
<box
flexDirection="column"
width="100%"
height="100%"
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
>
<box flexDirection="row" justifyContent="space-between" paddingBottom={1}>
<text fg={skin.text}>
<b>{props.input.label} screen</b>
<span style={{ fg: skin.muted }}> plugin route</span>
</text>
<text fg={skin.muted}>{props.keys.print("home")} home</text>
</box>
<box flexDirection="row" gap={1} paddingBottom={1}>
{tabs.map((item, i) => {
const on = value.tab === i
return (
<Btn
txt={item}
run={() => props.api.route.navigate(props.route.screen, { ...value, tab: i })}
skin={skin}
on={on}
/>
)
})}
</box>
<box
border
borderColor={skin.border}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
flexGrow={1}
>
{value.tab === 0 ? (
<box flexDirection="column" gap={1}>
<text fg={skin.text}>Route: {props.route.screen}</text>
<text fg={skin.muted}>plugin state: {props.meta.state}</text>
<text fg={skin.muted}>
first: {props.meta.state === "first" ? "yes" : "no"} · updated:{" "}
{props.meta.state === "updated" ? "yes" : "no"} · loads: {props.meta.load_count}
</text>
<text fg={skin.muted}>plugin source: {props.meta.source}</text>
<text fg={skin.muted}>source: {value.source}</text>
<text fg={skin.muted}>note: {value.note || "(none)"}</text>
<text fg={skin.muted}>selected: {value.selected || "(none)"}</text>
<text fg={skin.muted}>local stack depth: {value.local}</text>
<text fg={skin.muted}>host stack open: {props.api.ui.dialog.open ? "yes" : "no"}</text>
</box>
) : null}
{value.tab === 1 ? (
<box flexDirection="column" gap={1}>
<text fg={skin.text}>Counter: {value.count}</text>
<text fg={skin.muted}>
{props.keys.print("up")} / {props.keys.print("down")} change value
</text>
</box>
) : null}
{value.tab === 2 ? (
<box flexDirection="column" gap={1}>
<text fg={skin.muted}>
{props.keys.print("modal")} modal | {props.keys.print("alert")} alert | {props.keys.print("confirm")}{" "}
confirm | {props.keys.print("prompt")} prompt | {props.keys.print("select")} select
</text>
<text fg={skin.muted}>
{props.keys.print("local")} local stack | {props.keys.print("host")} host stack
</text>
<text fg={skin.muted}>
local open: {props.keys.print("local_push")} push nested · esc or {props.keys.print("local_close")}{" "}
close
</text>
<text fg={skin.muted}>{props.keys.print("home")} returns home</text>
</box>
) : null}
</box>
<box flexDirection="row" gap={1} paddingTop={1}>
<Btn txt="go home" run={() => props.api.route.navigate("home")} skin={skin} />
<Btn txt="modal" run={() => props.api.route.navigate(props.route.modal, value)} skin={skin} on />
<Btn txt="local overlay" run={show} skin={skin} />
<Btn txt="host overlay" run={() => host(props.api, props.input, skin)} skin={skin} />
<Btn txt="alert" run={() => warn(props.api, props.route, value)} skin={skin} />
<Btn txt="confirm" run={() => check(props.api, props.route, value)} skin={skin} />
<Btn txt="prompt" run={() => entry(props.api, props.route, value)} skin={skin} />
<Btn txt="select" run={() => picker(props.api, props.route, value)} skin={skin} />
</box>
</box>
<box
visible={value.local > 0}
width={dim().width}
height={dim().height}
alignItems="center"
position="absolute"
zIndex={3000}
paddingTop={dim().height / 4}
left={0}
top={0}
backgroundColor={RGBA.fromInts(0, 0, 0, 160)}
onMouseUp={() => {
pop()
}}
>
<box
onMouseUp={(evt) => {
evt.stopPropagation()
}}
width={60}
maxWidth={dim().width - 2}
backgroundColor={skin.panel}
border
borderColor={skin.border}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
gap={1}
flexDirection="column"
>
<text fg={skin.text}>
<b>{props.input.label} local overlay</b>
</text>
<text fg={skin.muted}>Plugin-owned stack depth: {value.local}</text>
<text fg={skin.muted}>
{props.keys.print("local_push")} push nested · {props.keys.print("local_close")} pop/close
</text>
<box flexDirection="row" gap={1}>
<Btn txt="push" run={push} skin={skin} on />
<Btn txt="pop" run={pop} skin={skin} />
</box>
</box>
</box>
</box>
)
}
const Modal = (props: {
api: TuiPluginApi
input: Cfg
route: Route
keys: Keys
params?: Record<string, unknown>
}) => {
const Dialog = props.api.ui.Dialog
const value = parse(props.params)
const skin = tone(props.api)
useKeyboard((evt) => {
if (props.api.route.current.name !== props.route.modal) return
if (props.keys.match("modal_accept", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...value, source: "modal" })
return
}
if (props.keys.match("modal_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate("home")
}
})
return (
<box width="100%" height="100%" backgroundColor={skin.panel}>
<Dialog onClose={() => props.api.route.navigate("home")}>
<box paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1} flexDirection="column">
<text fg={skin.text}>
<b>{props.input.label} modal</b>
</text>
<text fg={skin.muted}>{props.keys.print("modal")} modal command</text>
<text fg={skin.muted}>{props.keys.print("screen")} screen command</text>
<text fg={skin.muted}>
{props.keys.print("modal_accept")} opens screen · {props.keys.print("modal_close")} closes
</text>
<box flexDirection="row" gap={1}>
<Btn
txt="open screen"
run={() => props.api.route.navigate(props.route.screen, { ...value, source: "modal" })}
skin={skin}
on
/>
<Btn txt="cancel" run={() => props.api.route.navigate("home")} skin={skin} />
</box>
</box>
</Dialog>
</box>
)
}
const home = (api: TuiPluginApi, input: Cfg) => ({
slots: {
home_logo(ctx) {
const map = ctx.theme.current
const skin = look(map)
const art = [
" $$\\",
" $$ |",
" $$$$$$$\\ $$$$$$\\$$$$\\ $$$$$$\\ $$ | $$\\ $$$$$$\\",
"$$ _____|$$ _$$ _$$\\ $$ __$$\\ $$ | $$ |$$ __$$\\",
"\\$$$$$$\\ $$ / $$ / $$ |$$ / $$ |$$$$$$ / $$$$$$$$ |",
" \\____$$\\ $$ | $$ | $$ |$$ | $$ |$$ _$$< $$ ____|",
"$$$$$$$ |$$ | $$ | $$ |\\$$$$$$ |$$ | \\$$\\ \\$$$$$$$\\",
"\\_______/ \\__| \\__| \\__| \\______/ \\__| \\__| \\_______|",
]
const fill = [
skin.accent,
skin.muted,
ink(map, "info", ui.accent),
skin.text,
ink(map, "success", ui.accent),
ink(map, "warning", ui.accent),
ink(map, "secondary", ui.accent),
ink(map, "error", ui.accent),
]
return (
<box flexDirection="column">
{art.map((line, i) => (
<text fg={fill[i]}>{line}</text>
))}
</box>
)
},
home_prompt(ctx, value) {
const skin = look(ctx.theme.current)
type Prompt = (props: {
workspaceID?: string
visible?: boolean
disabled?: boolean
onSubmit?: () => void
hint?: JSX.Element
right?: JSX.Element
showPlaceholder?: boolean
placeholders?: {
normal?: string[]
shell?: string[]
}
}) => JSX.Element
type Slot = (
props: { name: string; mode?: unknown; children?: JSX.Element } & Record<string, unknown>,
) => JSX.Element | null
const ui = api.ui as TuiPluginApi["ui"] & { Prompt: Prompt; Slot: Slot }
const Prompt = ui.Prompt
const Slot = ui.Slot
const normal = [
`[SMOKE] route check for ${input.label}`,
"[SMOKE] confirm home_prompt slot override",
"[SMOKE] verify prompt-right slot passthrough",
]
const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"]
const hint = (
<box flexShrink={0} flexDirection="row" gap={1}>
<text fg={skin.muted}>
<span style={{ fg: skin.accent }}></span> smoke home prompt
</text>
</box>
)
return (
<Prompt
workspaceID={value.workspace_id}
hint={hint}
right={
<box flexDirection="row" gap={1}>
<Slot name="home_prompt_right" workspace_id={value.workspace_id} />
<Slot name="smoke_prompt_right" workspace_id={value.workspace_id} label={input.label} />
</box>
}
placeholders={{ normal, shell }}
/>
)
},
home_prompt_right(ctx, value) {
const skin = look(ctx.theme.current)
const id = value.workspace_id?.slice(0, 8) ?? "none"
return (
<text fg={skin.muted}>
<span style={{ fg: skin.accent }}>{input.label}</span> home:{id}
</text>
)
},
session_prompt_right(ctx, value) {
const skin = look(ctx.theme.current)
return (
<text fg={skin.muted}>
<span style={{ fg: skin.accent }}>{input.label}</span> session:{value.session_id.slice(0, 8)}
</text>
)
},
smoke_prompt_right(ctx, value) {
const skin = look(ctx.theme.current)
const id = typeof value.workspace_id === "string" ? value.workspace_id.slice(0, 8) : "none"
const label = typeof value.label === "string" ? value.label : input.label
return (
<text fg={skin.muted}>
<span style={{ fg: skin.accent }}>{label}</span> custom:{id}
</text>
)
},
home_bottom(ctx) {
const skin = look(ctx.theme.current)
const text = "extra content in the unified home bottom slot"
return (
<box width="100%" maxWidth={75} alignItems="center" paddingTop={1} flexShrink={0} gap={1}>
<box
border
borderColor={skin.border}
backgroundColor={skin.panel}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
width="100%"
>
<text fg={skin.muted}>
<span style={{ fg: skin.accent }}>{input.label}</span> {text}
</text>
</box>
</box>
)
},
},
})
const block = (input: Cfg, order: number, title: string, text: string): TuiSlotPlugin => ({
order,
slots: {
sidebar_content(ctx, value) {
const skin = look(ctx.theme.current)
return (
<box
border
borderColor={skin.border}
backgroundColor={skin.panel}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
flexDirection="column"
gap={1}
>
<text fg={skin.accent}>
<b>{title}</b>
</text>
<text fg={skin.text}>{text}</text>
<text fg={skin.muted}>
{input.label} order {order} · session {value.session_id.slice(0, 8)}
</text>
</box>
)
},
},
})
const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
home(api, input),
block(input, 50, "Smoke above", "renders above internal sidebar blocks"),
block(input, 250, "Smoke between", "renders between internal sidebar blocks"),
block(input, 650, "Smoke below", "renders below internal sidebar blocks"),
]
const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => {
const route = names(input)
api.command.register(() => [
{
title: `${input.label} modal`,
value: "plugin.smoke.modal",
keybind: keys.get("modal"),
category: "Plugin",
slash: {
name: "smoke",
},
onSelect: () => {
api.route.navigate(route.modal, { source: "command" })
},
},
{
title: `${input.label} screen`,
value: "plugin.smoke.screen",
keybind: keys.get("screen"),
category: "Plugin",
slash: {
name: "smoke-screen",
},
onSelect: () => {
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
},
},
{
title: `${input.label} alert dialog`,
value: "plugin.smoke.alert",
category: "Plugin",
slash: {
name: "smoke-alert",
},
onSelect: () => {
warn(api, route, current(api, route))
},
},
{
title: `${input.label} confirm dialog`,
value: "plugin.smoke.confirm",
category: "Plugin",
slash: {
name: "smoke-confirm",
},
onSelect: () => {
check(api, route, current(api, route))
},
},
{
title: `${input.label} prompt dialog`,
value: "plugin.smoke.prompt",
category: "Plugin",
slash: {
name: "smoke-prompt",
},
onSelect: () => {
entry(api, route, current(api, route))
},
},
{
title: `${input.label} select dialog`,
value: "plugin.smoke.select",
category: "Plugin",
slash: {
name: "smoke-select",
},
onSelect: () => {
picker(api, route, current(api, route))
},
},
{
title: `${input.label} host overlay`,
value: "plugin.smoke.host",
category: "Plugin",
slash: {
name: "smoke-host",
},
onSelect: () => {
host(api, input, tone(api))
},
},
{
title: `${input.label} go home`,
value: "plugin.smoke.home",
category: "Plugin",
enabled: api.route.current.name !== "home",
onSelect: () => {
api.route.navigate("home")
},
},
{
title: `${input.label} toast`,
value: "plugin.smoke.toast",
category: "Plugin",
onSelect: () => {
api.ui.toast({
variant: "info",
title: "Smoke",
message: "Plugin toast works",
duration: 2000,
})
},
},
])
}
const tui: TuiPlugin = async (api, options, meta) => {
if (options?.enabled === false) return
await api.theme.install("./smoke-theme.json")
api.theme.set("smoke-theme")
const value = cfg(options ?? undefined)
const route = names(value)
const keys = api.keybind.create(bind, value.keybinds)
const fx = new VignetteEffect(value.vignette)
const post = fx.apply.bind(fx)
api.renderer.addPostProcessFn(post)
api.lifecycle.onDispose(() => {
api.renderer.removePostProcessFn(post)
})
api.route.register([
{
name: route.screen,
render: ({ params }) => <Screen api={api} input={value} route={route} keys={keys} meta={meta} params={params} />,
},
{
name: route.modal,
render: ({ params }) => <Modal api={api} input={value} route={route} keys={keys} params={params} />,
},
])
reg(api, value, keys)
for (const item of slot(api, value)) {
api.slots.register(item)
}
}
const plugin: TuiPluginModule & { id: string } = {
id: "tui-smoke",
tui,
}
export default plugin
+21
View File
@@ -0,0 +1,21 @@
---
name: effect
description: Answer questions about the Effect framework
---
# Effect
This codebase uses Effect, a framework for writing typescript.
## How to Answer Effect Questions
1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
`.opencode/references/effect-smol` in this project NOT the skill folder.
2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
3. Provide responses based on the actual Effect source code and documentation
## Guidelines
- Always use the explore agent with the cloned repository when answering Effect-related questions
- Reference specific files and patterns found in the Effect codebase
- Do not answer from memory - always verify against the source
+1
View File
@@ -0,0 +1 @@
smoke-theme.json
+2 -2
View File
@@ -116,8 +116,8 @@
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
"dark": "#abafb7",
"light": "textMuted"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
+10 -3
View File
@@ -1,7 +1,5 @@
/// <reference path="../env.d.ts" />
import { tool } from "@opencode-ai/plugin"
import DESCRIPTION from "./github-pr-search.txt"
async function githubFetch(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`https://api.github.com${endpoint}`, {
...options,
@@ -24,7 +22,16 @@ interface PR {
}
export default tool({
description: DESCRIPTION,
description: `Use this tool to search GitHub pull requests by title and description.
This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including:
- PR number and title
- Author
- State (open/closed/merged)
- Labels
- Description snippet
Use the query parameter to search for keywords that might appear in PR titles or descriptions.`,
args: {
query: tool.schema.string().describe("Search query for PR titles and descriptions"),
limit: tool.schema.number().describe("Maximum number of results to return").default(10),
-10
View File
@@ -1,10 +0,0 @@
Use this tool to search GitHub pull requests by title and description.
This tool searches PRs in the sst/opencode repository and returns LLM-friendly results including:
- PR number and title
- Author
- State (open/closed/merged)
- Labels
- Description snippet
Use the query parameter to search for keywords that might appear in PR titles or descriptions.
+13 -16
View File
@@ -1,20 +1,10 @@
/// <reference path="../env.d.ts" />
import { tool } from "@opencode-ai/plugin"
import DESCRIPTION from "./github-triage.txt"
const TEAM = {
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
zen: ["fwang", "MrMushrooooom"],
tui: [
"thdxr",
"kommander",
// "rekram1-node" (on vacation)
],
core: [
"thdxr",
// "rekram1-node", (on vacation)
"jlongster",
],
tui: ["thdxr", "kommander", "rekram1-node"],
core: ["thdxr", "rekram1-node", "jlongster"],
docs: ["R44VC0RP"],
windows: ["Hona"],
} as const
@@ -48,9 +38,17 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
}
export default tool({
description: DESCRIPTION,
description: `Use this tool to assign and/or label a GitHub issue.
Choose labels and assignee using the current triage policy and ownership rules.
Pick the most fitting labels for the issue and assign one owner.
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
args: {
assignee: tool.schema.enum(ASSIGNEES as [string, ...string[]]).describe("The username of the assignee"),
assignee: tool.schema
.enum(ASSIGNEES as [string, ...string[]])
.describe("The username of the assignee")
.default("rekram1-node"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
.describe("The labels(s) to add to the issue")
@@ -73,8 +71,7 @@ export default tool({
results.push("Dropped label: nix (issue does not mention nix)")
}
// const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
const assignee = web ? pick(TEAM.desktop) : args.assignee
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
if (labels.includes("zen") && !zen) {
throw new Error("Only add the zen label when issue title/body contains 'zen'")
-8
View File
@@ -1,8 +0,0 @@
Use this tool to assign and/or label a GitHub issue.
Choose labels and assignee using the current triage policy and ownership rules.
Pick the most fitting labels for the issue and assign one owner.
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
(Note: rekram1-node is on vacation, do not assign issues to him.)
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://opencode.ai/tui.json",
"plugin": [
[
"./plugins/tui-smoke.tsx",
{
"enabled": false,
"label": "workspace",
"keybinds": {
"modal": "ctrl+alt+m",
"screen": "ctrl+alt+o",
"home": "escape,ctrl+shift+h",
"dialog_close": "escape,q"
}
}
]
]
}
@@ -1,5 +0,0 @@
github-policies:
runners:
allowed_groups:
- "GitHub Actions"
- "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"
+4
View File
@@ -122,3 +122,7 @@ const table = sqliteTable("session", {
- 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`.
## Type Checking
- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
+2 -1
View File
@@ -35,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+2 -1
View File
@@ -35,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+2 -1
View File
@@ -35,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+2 -1
View File
@@ -35,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+2 -1
View File
@@ -35,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+3 -1
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+2 -1
View File
@@ -35,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+141
View File
@@ -0,0 +1,141 @@
<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">Trợ lý lập trình AI mã nguồn mở.</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> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Cài đặt
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Các trình quản lý gói (Package managers)
npm i -g opencode-ai@latest # hoặc bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS và Linux (khuyên dùng, luôn cập nhật)
brew install opencode # macOS và Linux (công thức brew chính thức, ít cập nhật hơn)
sudo pacman -S opencode # Arch Linux (Bản ổn định)
paru -S opencode-bin # Arch Linux (Bản mới nhất từ AUR)
mise use -g opencode # Mọi hệ điều hành
nix run nixpkgs#opencode # hoặc github:anomalyco/opencode cho nhánh dev mới nhất
```
> [!TIP]
> Hãy xóa các phiên bản cũ hơn 0.1.x trước khi cài đặt.
### Ứng dụng Desktop (BETA)
OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download).
| Nền tảng | Tải xuống |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, hoặc AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Thư mục cài đặt
Tập lệnh cài đặt tuân theo thứ tự ưu tiên sau cho đường dẫn cài đặt:
1. `$OPENCODE_INSTALL_DIR` - Thư mục cài đặt tùy chỉnh
2. `$XDG_BIN_DIR` - Đường dẫn tuân thủ XDG Base Directory Specification
3. `$HOME/bin` - Thư mục nhị phân tiêu chuẩn của người dùng (nếu tồn tại hoặc có thể tạo)
4. `$HOME/.opencode/bin` - Mặc định dự phòng
```bash
# Ví dụ
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents (Đại diện)
OpenCode bao gồm hai agent được tích hợp sẵn mà bạn có thể chuyển đổi bằng phím `Tab`.
- **build** - Agent mặc định, có toàn quyền truy cập cho công việc lập trình
- **plan** - Agent chỉ đọc dùng để phân tích và khám phá mã nguồn
- Mặc định từ chối việc chỉnh sửa tệp
- Hỏi quyền trước khi chạy các lệnh bash
- Lý tưởng để khám phá các codebase lạ hoặc lên kế hoạch thay đổi
Ngoài ra còn có một subagent **general** dùng cho các tìm kiếm phức tạp và tác vụ nhiều bước.
Agent này được sử dụng nội bộ và có thể gọi bằng cách dùng `@general` trong tin nhắn.
Tìm hiểu thêm về [agents](https://opencode.ai/docs/agents).
### Tài liệu
Để biết thêm thông tin về cách cấu hình OpenCode, [**hãy truy cập tài liệu của chúng tôi**](https://opencode.ai/docs).
### Đóng góp
Nếu bạn muốn đóng góp cho OpenCode, vui lòng đọc [tài liệu hướng dẫn đóng góp](./CONTRIBUTING.md) trước khi gửi pull request.
### Xây dựng trên nền tảng OpenCode
Nếu bạn đang làm việc trên một dự án liên quan đến OpenCode và sử dụng "opencode" như một phần của tên dự án, ví dụ "opencode-dashboard" hoặc "opencode-mobile", vui lòng thêm một ghi chú vào README của bạn để làm rõ rằng dự án đó không được xây dựng bởi đội ngũ OpenCode và không liên kết với chúng tôi dưới bất kỳ hình thức nào.
### Các câu hỏi thường gặp (FAQ)
#### OpenCode khác biệt thế nào so với Claude Code?
Về mặt tính năng, nó rất giống Claude Code. Dưới đây là những điểm khác biệt chính:
- 100% mã nguồn mở
- Không bị ràng buộc với bất kỳ nhà cung cấp nào. Mặc dù chúng tôi khuyên dùng các mô hình được cung cấp qua [OpenCode Zen](https://opencode.ai/zen), OpenCode có thể được sử dụng với Claude, OpenAI, Google, hoặc thậm chí các mô hình chạy cục bộ. Khi các mô hình phát triển, khoảng cách giữa chúng sẽ thu hẹp lại và giá cả sẽ giảm, vì vậy việc không phụ thuộc vào nhà cung cấp là rất quan trọng.
- Hỗ trợ LSP ngay từ đầu
- Tập trung vào TUI (Giao diện người dùng dòng lệnh). OpenCode được xây dựng bởi những người dùng neovim và đội ngũ tạo ra [terminal.shop](https://terminal.shop); chúng tôi sẽ đẩy giới hạn của những gì có thể làm được trên terminal lên mức tối đa.
- Kiến trúc client/server. Chẳng hạn, điều này cho phép OpenCode chạy trên máy tính của bạn trong khi bạn điều khiển nó từ xa qua một ứng dụng di động, nghĩa là frontend TUI chỉ là một trong những client có thể dùng.
---
**Tham gia cộng đồng của chúng tôi** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
+4 -2
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -135,4 +137,4 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
---
**加入我们的社区** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
+4 -2
View File
@@ -27,6 +27,7 @@
<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> |
@@ -34,7 +35,8 @@
<a href="README.tr.md">Türkçe</a> |
<a href="README.uk.md">Українська</a> |
<a href="README.bn.md">বাংলা</a> |
<a href="README.gr.md">Ελληνικά</a>
<a href="README.gr.md">Ελληνικά</a> |
<a href="README.vi.md">Tiếng Việt</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -135,4 +137,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
---
**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
+1410 -854
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": 1772091128,
"narHash": "sha256-TnrYykX8Mf/Ugtkix6V+PjW7miU2yClA6uqWl/v6KWM=",
"lastModified": 1773909469,
"narHash": "sha256-vglVrLfHjFIzIdV9A27Ugul6rh3I1qHbbitGW7dk420=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3f0336406035444b4a24b942788334af5f906259",
"rev": "7149c06513f335be57f26fcbbbe34afda923882b",
"type": "github"
},
"original": {
+2 -2
View File
@@ -8,6 +8,7 @@ import type { Context as GitHubContext } from "@actions/github/lib/context"
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { spawn } from "node:child_process"
import { setTimeout as sleep } from "node:timers/promises"
type GitHubAuthor = {
login: string
@@ -281,7 +282,7 @@ async function assertOpencodeConnected() {
connected = true
break
} catch (e) {}
await Bun.sleep(300)
await sleep(300)
} while (retry++ < 30)
if (!connected) {
@@ -495,7 +496,6 @@ async function subscribeSessionEvents() {
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", "\x1b[33m\x1b[1m"],
todoread: ["Todo", "\x1b[33m\x1b[1m"],
bash: ["Bash", "\x1b[31m\x1b[1m"],
edit: ["Edit", "\x1b[32m\x1b[1m"],
glob: ["Glob", "\x1b[34m\x1b[1m"],
+23
View File
@@ -103,6 +103,18 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
const zenLiteProduct = new stripe.Product("ZenLite", {
name: "OpenCode Go",
})
const zenLiteCouponFirstMonth50 = new stripe.Coupon("ZenLiteCouponFirstMonth50", {
name: "First month 50% off",
percentOff: 50,
appliesToProducts: [zenLiteProduct.id],
duration: "once",
})
const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100", {
name: "First month 100% off",
percentOff: 100,
appliesToProducts: [zenLiteProduct.id],
duration: "once",
})
const zenLitePrice = new stripe.Price("ZenLitePrice", {
product: zenLiteProduct.id,
currency: "usd",
@@ -116,6 +128,9 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
properties: {
product: zenLiteProduct.id,
price: zenLitePrice.id,
priceInr: 92900,
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
},
})
@@ -194,6 +209,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")
const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID")
const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET")
const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
handler: "packages/console/function/src/log-processor.ts",
link: [new sst.Secret("HONEYCOMB_API_KEY")],
@@ -212,8 +231,12 @@ new sst.cloudflare.x.SolidStart("Console", {
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
AWS_SES_SECRET_ACCESS_KEY,
SALESFORCE_CLIENT_ID,
SALESFORCE_CLIENT_SECRET,
SALESFORCE_INSTANCE_URL,
ZEN_BLACK_PRICE,
ZEN_LITE_PRICE,
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
new sst.Secret("ZEN_LIMITS"),
new sst.Secret("ZEN_SESSION_SECRET"),
...ZEN_MODELS,
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-v83hWzYVg/g4zJiBpGsQ71wTdndPk3BQVZ2mjMApUIQ=",
"aarch64-linux": "sha256-inpMwkQqwBFP2wL8w/pTOP7q3fg1aOqvE0wgzVd3/B8=",
"aarch64-darwin": "sha256-r42LGrQWqDyIy62mBSU5Nf3M22dJ3NNo7mjN/1h8d8Y=",
"x86_64-darwin": "sha256-J6XrrdK5qBK3sQBQOO/B3ZluOnsAf5f65l4q/K1nDTI="
"x86_64-linux": "sha256-gdS7MkWGeVO0qLs0HKD156YE0uCk5vWeYjKu4JR1Apw=",
"aarch64-linux": "sha256-tF4pyVqzbrvdkRG23Fot37FCg8guRZkcU738fHPr/OQ=",
"aarch64-darwin": "sha256-FugTWzGMb2ktAbNwQvWRM3GWOb5RTR++8EocDDrQMLc=",
"x86_64-darwin": "sha256-jpe6EiwKr+CS00cn0eHwcDluO4LvO3t/5l/LcFBBKP0="
}
}
+2 -1
View File
@@ -20,7 +20,7 @@ let
in
stdenvNoCC.mkDerivation {
pname = "opencode-node_modules";
version = "${packageJson.version}-${rev}";
version = "${packageJson.version}+${lib.replaceString "-" "." rev}";
src = lib.fileset.toSource {
root = ../.;
@@ -54,6 +54,7 @@ stdenvNoCC.mkDerivation {
--filter '!./' \
--filter './packages/opencode' \
--filter './packages/desktop' \
--filter './packages/app' \
--frozen-lockfile \
--ignore-scripts \
--no-progress
+21 -18
View File
@@ -3,10 +3,10 @@
stdenvNoCC,
callPackage,
bun,
nodejs,
sysctl,
makeBinaryWrapper,
models-dev,
ripgrep,
installShellFiles,
versionCheckHook,
writableTmpDirAsHomeHook,
@@ -19,6 +19,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
nativeBuildInputs = [
bun
nodejs # for patchShebangs node_modules
installShellFiles
makeBinaryWrapper
models-dev
@@ -29,6 +30,8 @@ stdenvNoCC.mkDerivation (finalAttrs: {
runHook preConfigure
cp -R ${finalAttrs.node_modules}/. .
patchShebangs node_modules
patchShebangs packages/*/node_modules
runHook postConfigure
'';
@@ -48,25 +51,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
runHook postBuild
'';
installPhase = ''
runHook preInstall
installPhase =
''
runHook preInstall
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
install -Dm644 schema.json $out/share/opencode/schema.json
wrapProgram $out/bin/opencode \
--prefix PATH : ${
lib.makeBinPath (
[
ripgrep
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
install -Dm644 schema.json $out/share/opencode/schema.json
''
# bun runs sysctl to detect if dunning on rosetta2
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
wrapProgram $out/bin/opencode \
--prefix PATH : ${
lib.makeBinPath [
sysctl
]
# bun runs sysctl to detect if dunning on rosetta2
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
)
}
runHook postInstall
'';
}
''
+ ''
runHook postInstall
'';
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
# trick yargs into also generating zsh completions
+20 -8
View File
@@ -4,13 +4,15 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.10",
"packageManager": "bun@1.3.11",
"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",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook",
"typecheck": "bun turbo typecheck",
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
"prepare": "husky",
"random": "echo 'Random script'",
"hello": "echo 'Hello World!'",
@@ -24,7 +26,10 @@
"packages/slack"
],
"catalog": {
"@types/bun": "1.3.9",
"@effect/opentelemetry": "4.0.0-beta.48",
"@effect/platform-node": "4.0.0-beta.48",
"@types/bun": "1.3.11",
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
@@ -41,16 +46,19 @@
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.12-a5629fb",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
"ai": "5.0.124",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.48",
"ai": "6.0.158",
"cross-spawn": "7.0.6",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"marked": "17.0.1",
"marked-shiki": "1.2.1",
"@playwright/test": "1.51.0",
"remend": "1.3.0",
"@playwright/test": "1.59.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
@@ -64,7 +72,8 @@
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"solid-js": "1.9.10",
"vite-plugin-solid": "2.11.10"
"vite-plugin-solid": "2.11.10",
"@lydell/node-pty": "1.2.0-beta.10"
}
},
"devDependencies": {
@@ -84,6 +93,7 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"heap-snapshot-toolkit": "1.1.3",
"typescript": "catalog:"
},
"repository": {
@@ -97,9 +107,11 @@
},
"trustedDependencies": [
"esbuild",
"node-pty",
"protobufjs",
"tree-sitter",
"tree-sitter-bash",
"tree-sitter-powershell",
"web-tree-sitter",
"electron"
],
@@ -109,6 +121,6 @@
},
"patchedDependencies": {
"@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"
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
}
}
+2 -3
View File
@@ -31,11 +31,10 @@ Your app is ready to be deployed!
## E2E Testing
Playwright starts the Vite dev server automatically via `webServer`, and UI tests need an opencode backend (defaults to `localhost:4096`).
Use the local runner to create a temp sandbox, seed data, and run the tests.
Playwright starts the Vite dev server automatically via `webServer`, and UI tests expect an opencode backend at `localhost:4096` by default.
```bash
bunx playwright install
bunx playwright install chromium
bun run test:e2e:local
bun run test:e2e:local -- --grep "settings"
```
-176
View File
@@ -1,176 +0,0 @@
# 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.
-578
View File
@@ -1,578 +0,0 @@
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(".scroll-view__viewport").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)
}
}
const seedSystem = [
"You are seeding deterministic e2e UI state.",
"Follow the user's instruction exactly.",
"When asked to call a tool, call exactly that tool exactly once with the exact JSON input.",
"Do not call any extra tools.",
].join(" ")
const wait = async <T>(input: { probe: () => Promise<T | undefined>; timeout?: number }) => {
const timeout = input.timeout ?? 30_000
const end = Date.now() + timeout
while (Date.now() < end) {
const value = await input.probe()
if (value !== undefined) return value
await new Promise((resolve) => setTimeout(resolve, 250))
}
}
const seed = async <T>(input: {
sessionID: string
prompt: string
sdk: ReturnType<typeof createSdk>
probe: () => Promise<T | undefined>
timeout?: number
attempts?: number
}) => {
for (let i = 0; i < (input.attempts ?? 2); i++) {
await input.sdk.session.promptAsync({
sessionID: input.sessionID,
agent: "build",
system: seedSystem,
parts: [{ type: "text", text: input.prompt }],
})
const value = await wait({ probe: input.probe, timeout: input.timeout })
if (value !== undefined) return value
}
}
export async function seedSessionQuestion(
sdk: ReturnType<typeof createSdk>,
input: {
sessionID: string
questions: Array<{
header: string
question: string
options: Array<{ label: string; description: string }>
multiple?: boolean
custom?: boolean
}>
},
) {
const first = input.questions[0]
if (!first) throw new Error("Question seed requires at least one question")
const text = [
"Your only valid response is one question tool call.",
`Use this JSON input: ${JSON.stringify({ questions: input.questions })}`,
"Do not output plain text.",
"After calling the tool, wait for the user response.",
].join("\n")
const result = await seed({
sdk,
sessionID: input.sessionID,
prompt: text,
timeout: 30_000,
probe: async () => {
const list = await sdk.question.list().then((x) => x.data ?? [])
return list.find((item) => item.sessionID === input.sessionID && item.questions[0]?.header === first.header)
},
})
if (!result) throw new Error("Timed out seeding question request")
return { id: result.id }
}
export async function seedSessionPermission(
sdk: ReturnType<typeof createSdk>,
input: {
sessionID: string
permission: string
patterns: string[]
description?: string
},
) {
const text = [
"Your only valid response is one bash tool call.",
`Use this JSON input: ${JSON.stringify({
command: input.patterns[0] ? `ls ${JSON.stringify(input.patterns[0])}` : "pwd",
workdir: "/",
description: input.description ?? `seed ${input.permission} permission request`,
})}`,
"Do not output plain text.",
].join("\n")
const result = await seed({
sdk,
sessionID: input.sessionID,
prompt: text,
timeout: 30_000,
probe: async () => {
const list = await sdk.permission.list().then((x) => x.data ?? [])
return list.find((item) => item.sessionID === input.sessionID)
},
})
if (!result) throw new Error("Timed out seeding permission request")
return { id: result.id }
}
export async function seedSessionTodos(
sdk: ReturnType<typeof createSdk>,
input: {
sessionID: string
todos: Array<{ content: string; status: string; priority: string }>
},
) {
const text = [
"Your only valid response is one todowrite tool call.",
`Use this JSON input: ${JSON.stringify({ todos: input.todos })}`,
"Do not output plain text.",
].join("\n")
const target = JSON.stringify(input.todos)
const result = await seed({
sdk,
sessionID: input.sessionID,
prompt: text,
timeout: 30_000,
probe: async () => {
const todos = await sdk.session.todo({ sessionID: input.sessionID }).then((x) => x.data ?? [])
if (JSON.stringify(todos) !== target) return
return true
},
})
if (!result) throw new Error("Timed out seeding todos")
return true
}
export async function clearSessionDockSeed(sdk: ReturnType<typeof createSdk>, sessionID: string) {
const [questions, permissions] = await Promise.all([
sdk.question.list().then((x) => x.data ?? []),
sdk.permission.list().then((x) => x.data ?? []),
])
await Promise.all([
...questions
.filter((item) => item.sessionID === sessionID)
.map((item) => sdk.question.reject({ requestID: item.id }).catch(() => undefined)),
...permissions
.filter((item) => item.sessionID === sessionID)
.map((item) => sdk.permission.reply({ requestID: item.id, reply: "reject" }).catch(() => undefined)),
])
return true
}
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
}
-21
View File
@@ -1,21 +0,0 @@
import { test, expect } from "../fixtures"
import { serverName } from "../utils"
test("home renders and shows core entrypoints", async ({ page }) => {
await page.goto("/")
await expect(page.getByRole("button", { name: "Open project" }).first()).toBeVisible()
await expect(page.getByRole("button", { name: serverName })).toBeVisible()
})
test("server picker dialog opens from home", async ({ page }) => {
await page.goto("/")
const trigger = page.getByRole("button", { name: serverName })
await expect(trigger).toBeVisible()
await trigger.click()
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByRole("textbox").first()).toBeVisible()
})
-10
View File
@@ -1,10 +0,0 @@
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))
await expect(page).toHaveURL(new RegExp(`/${slug}/session`))
await expect(page.locator(promptSelector)).toBeVisible()
})
-11
View File
@@ -1,11 +0,0 @@
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,55 +0,0 @@
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"
test("can set a default server on web", async ({ page, gotoSession }) => {
await page.addInitScript((key: string) => {
try {
localStorage.removeItem(key)
} catch {
return
}
}, DEFAULT_SERVER_URL_KEY)
await gotoSession()
const status = page.getByRole("button", { name: "Status" })
await expect(status).toBeVisible()
const popover = page.locator('[data-component="popover-content"]').filter({ hasText: "Manage servers" })
const ensurePopoverOpen = async () => {
if (await popover.isVisible()) return
await status.click()
await expect(popover).toBeVisible()
}
await ensurePopoverOpen()
await popover.getByRole("button", { name: "Manage servers" }).click()
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first()
await expect(row).toBeVisible()
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 closeDialog(page, dialog)
await ensurePopoverOpen()
const serverRow = popover.locator("button").filter({ hasText: serverName }).first()
await expect(serverRow).toBeVisible()
await expect(serverRow.getByText("Default", { exact: true })).toBeVisible()
})
-16
View File
@@ -1,16 +0,0 @@
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")
})
})
@@ -1,124 +0,0 @@
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()
})
})
})
@@ -1,15 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("ctrl+l focuses the prompt", async ({ page, gotoSession }) => {
await gotoSession()
const prompt = page.locator(promptSelector)
await expect(prompt).toBeVisible()
await page.locator("main").click({ position: { x: 5, y: 5 } })
await expect(prompt).not.toBeFocused()
await page.keyboard.press("Control+L")
await expect(prompt).toBeFocused()
})
-31
View File
@@ -1,31 +0,0 @@
import { test, expect } from "../fixtures"
import { modKey } from "../utils"
const expanded = async (el: { getAttribute: (name: string) => Promise<string | null> }) => {
const value = await el.getAttribute("aria-expanded")
if (value !== "true" && value !== "false") throw new Error(`Expected aria-expanded to be true|false, got: ${value}`)
return value === "true"
}
test("review panel can be toggled via keybind", async ({ page, gotoSession }) => {
await gotoSession()
const treeToggle = page.getByRole("button", { name: "Toggle file tree" }).first()
await expect(treeToggle).toBeVisible()
if (await expanded(treeToggle)) await treeToggle.click()
await expect(treeToggle).toHaveAttribute("aria-expanded", "false")
const reviewToggle = page.getByRole("button", { name: "Toggle review" }).first()
await expect(reviewToggle).toBeVisible()
if (await expanded(reviewToggle)) await reviewToggle.click()
await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
await expect(page.locator("#review-panel")).toHaveCount(0)
await page.keyboard.press(`${modKey}+Shift+R`)
await expect(reviewToggle).toHaveAttribute("aria-expanded", "true")
await expect(page.locator("#review-panel")).toBeVisible()
await page.keyboard.press(`${modKey}+Shift+R`)
await expect(reviewToggle).toHaveAttribute("aria-expanded", "false")
await expect(page.locator("#review-panel")).toHaveCount(0)
})
@@ -1,32 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { modKey } from "../utils"
test("mod+w closes the active file tab", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/open")
await expect(page.locator('[data-slash-id="file.open"]').first()).toBeVisible()
await page.keyboard.press("Enter")
const dialog = page
.getByRole("dialog")
.filter({ has: page.getByPlaceholder(/search files/i) })
.first()
await expect(dialog).toBeVisible()
await dialog.getByRole("textbox").first().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 tab = page.getByRole("tab", { name: "package.json" }).first()
await expect(tab).toBeVisible()
await tab.click()
await expect(tab).toHaveAttribute("aria-selected", "true")
await page.keyboard.press(`${modKey}+W`)
await expect(page.getByRole("tab", { name: "package.json" })).toHaveCount(0)
})
-31
View File
@@ -1,31 +0,0 @@
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
@@ -1,49 +0,0 @@
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 viewer = page.locator('[data-component="file"][data-mode="text"]').first()
await expect(viewer).toBeVisible()
await expect(viewer).toContainText("export default function FileTree")
})
-156
View File
@@ -1,156 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { modKey } from "../utils"
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 viewer = page.locator('[data-component="file"][data-mode="text"]').first()
await expect(viewer).toBeVisible()
await expect(viewer.getByText(/"name"\s*:\s*"@opencode-ai\/app"/)).toBeVisible()
})
test("cmd+f opens text viewer search while prompt is focused", 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 viewer = page.locator('[data-component="file"][data-mode="text"]').first()
await expect(viewer).toBeVisible()
await page.locator(promptSelector).click()
await page.keyboard.press(`${modKey}+f`)
const findInput = page.getByPlaceholder("Find")
await expect(findInput).toBeVisible()
await expect(findInput).toBeFocused()
})
test("cmd+f opens text viewer search while prompt is not focused", 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 viewer = page.locator('[data-component="file"][data-mode="text"]').first()
await expect(viewer).toBeVisible()
await viewer.click()
await page.keyboard.press(`${modKey}+f`)
const findInput = page.getByPlaceholder("Find")
await expect(findInput).toBeVisible()
await expect(findInput).toBeFocused()
})
-87
View File
@@ -1,87 +0,0 @@
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 = {
directory: string
slug: string
}
export const test = base.extend<TestFixtures, WorkerFixtures>({
directory: [
async ({}, use) => {
const directory = await getWorktree()
await use(directory)
},
{ scope: "worker" },
],
slug: [
async ({ directory }, use) => {
await use(dirSlug(directory))
},
{ scope: "worker" },
],
sdk: async ({ directory }, use) => {
await use(createSdk(directory))
},
gotoSession: async ({ page, directory }, use) => {
await seedStorage(page, { directory })
const gotoSession = async (sessionID?: string) => {
await page.goto(sessionPath(directory, sessionID))
await expect(page.locator(promptSelector)).toBeVisible()
}
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,48 +0,0 @@
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()
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
const command = page.locator('[data-slash-id="model.choose"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
const input = dialog.getByRole("textbox").first()
const selected = dialog.locator('[data-slot="list-item"][data-selected="true"]').first()
await expect(selected).toBeVisible()
const other = dialog.locator('[data-slot="list-item"]:not([data-selected="true"])').first()
const target = (await other.count()) > 0 ? other : selected
const key = await target.getAttribute("data-key")
if (!key) throw new Error("Failed to resolve model key from list item")
const model = key.split(":").slice(1).join(":")
await input.fill(model)
await clickListItem(dialog, { key })
await expect(dialog).toHaveCount(0)
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const dialogAgain = page.getByRole("dialog")
await expect(dialogAgain).toBeVisible()
await expect(dialogAgain.locator(`[data-slot="list-item"][data-key="${key}"][data-selected="true"]`)).toBeVisible()
})
@@ -1,61 +0,0 @@
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()
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
const command = page.locator('[data-slash-id="model.choose"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const picker = page.getByRole("dialog")
await expect(picker).toBeVisible()
const target = picker.locator('[data-slot="list-item"]').first()
await expect(target).toBeVisible()
const key = await target.getAttribute("data-key")
if (!key) throw new Error("Failed to resolve model key from list item")
const name = (await target.locator("span").first().innerText()).trim()
if (!name) throw new Error("Failed to resolve model name from list item")
await page.keyboard.press("Escape")
await expect(picker).toHaveCount(0)
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Models" }).click()
const search = settings.getByPlaceholder("Search models")
await expect(search).toBeVisible()
await search.fill(name)
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
const input = toggle.locator('[data-slot="switch-input"]')
await expect(toggle).toBeVisible()
await expect(input).toHaveAttribute("aria-checked", "true")
await toggle.locator('[data-slot="switch-control"]').click()
await expect(input).toHaveAttribute("aria-checked", "false")
await closeDialog(page, settings)
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const pickerAgain = page.getByRole("dialog")
await expect(pickerAgain).toBeVisible()
await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
await page.keyboard.press("Escape")
await expect(pickerAgain).toHaveCount(0)
})
@@ -1,53 +0,0 @@
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)
})
})
@@ -1,72 +0,0 @@
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)
}
})
@@ -1,143 +0,0 @@
import { base64Decode } from "@opencode-ai/util/encode"
import { test, expect } from "../fixtures"
import {
defocus,
createTestProject,
cleanupTestProject,
openSidebar,
setWorkspacesEnabled,
sessionIDFromUrl,
} from "../actions"
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
import { createSdk, dirSlug, sessionPath } from "../utils"
function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
}
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)
}
})
test("switching back to a project opens the latest workspace session", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const other = await createTestProject()
const otherSlug = dirSlug(other)
let rootDir: string | undefined
let workspaceDir: string | undefined
let sessionID: string | undefined
try {
await withProject(
async ({ directory, slug }) => {
rootDir = directory
await defocus(page)
await openSidebar(page)
await setWorkspacesEnabled(page, slug, true)
await page.getByRole("button", { name: "New workspace" }).first().click()
await expect
.poll(
() => {
const next = slugFromUrl(page.url())
if (!next) return ""
if (next === slug) return ""
return next
},
{ timeout: 45_000 },
)
.not.toBe("")
const workspaceSlug = slugFromUrl(page.url())
workspaceDir = base64Decode(workspaceSlug)
if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`)
await openSidebar(page)
const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
await expect(workspace).toBeVisible()
await workspace.hover()
const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first()
await expect(newSession).toBeVisible()
await newSession.click({ force: true })
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))
// Create a session by sending a prompt
const prompt = page.locator(promptSelector)
await expect(prompt).toBeVisible()
await prompt.fill("test")
await page.keyboard.press("Enter")
// Wait for the URL to update with the new session ID
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 15_000 }).not.toBe("")
const created = sessionIDFromUrl(page.url())
if (!created) throw new Error(`Failed to get session ID from url: ${page.url()}`)
sessionID = created
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
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 rootButton = page.locator(projectSwitchSelector(slug)).first()
await expect(rootButton).toBeVisible()
await rootButton.click()
await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created)
await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
},
{ extra: [other] },
)
} finally {
if (sessionID) {
const id = sessionID
const dirs = [rootDir, workspaceDir].filter((x): x is string => !!x)
await Promise.all(
dirs.map((directory) =>
createSdk(directory)
.session.delete({ sessionID: id })
.catch(() => undefined),
),
)
}
if (workspaceDir) {
await cleanupTestProject(workspaceDir)
}
await cleanupTestProject(other)
}
})
@@ -1,144 +0,0 @@
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)))
}
})
})
@@ -1,409 +0,0 @@
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
@@ -1,95 +0,0 @@
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)
})
})
@@ -1,43 +0,0 @@
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,22 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("dropping text/plain file: uri inserts a file pill", async ({ page, gotoSession }) => {
await gotoSession()
const prompt = page.locator(promptSelector)
await prompt.click()
const path = process.platform === "win32" ? "C:\\opencode-e2e-drop.txt" : "/tmp/opencode-e2e-drop.txt"
const dt = await page.evaluateHandle((text) => {
const dt = new DataTransfer()
dt.setData("text/plain", text)
return dt
}, `file:${path}`)
await page.dispatchEvent("body", "drop", { dataTransfer: dt })
const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
await expect(pill).toBeVisible()
await expect(pill).toHaveAttribute("data-path", path)
})
@@ -1,30 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("dropping an image file adds an attachment", async ({ page, gotoSession }) => {
await gotoSession()
const prompt = page.locator(promptSelector)
await prompt.click()
const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3+4uQAAAAASUVORK5CYII="
const dt = await page.evaluateHandle((b64) => {
const dt = new DataTransfer()
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))
const file = new File([bytes], "drop.png", { type: "image/png" })
dt.items.add(file)
return dt
}, png)
await page.dispatchEvent("body", "drop", { dataTransfer: dt })
const img = page.locator('img[alt="drop.png"]').first()
await expect(img).toBeVisible()
const remove = page.getByRole("button", { name: "Remove attachment" }).first()
await expect(remove).toBeVisible()
await img.hover()
await remove.click()
await expect(page.locator('img[alt="drop.png"]')).toHaveCount(0)
})
@@ -1,26 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("smoke @mention inserts file pill token", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
const sep = process.platform === "win32" ? "\\" : "/"
const file = ["packages", "app", "package.json"].join(sep)
const filePattern = /packages[\\/]+app[\\/]+\s*package\.json/
await page.keyboard.type(`@${file}`)
const suggestion = page.getByRole("button", { name: filePattern }).first()
await expect(suggestion).toBeVisible()
await suggestion.hover()
await page.keyboard.press("Tab")
const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
await expect(pill).toBeVisible()
await expect(pill).toHaveAttribute("data-path", filePattern)
await page.keyboard.type(" ok")
await expect(page.locator(promptSelector)).toContainText("ok")
})
@@ -1,18 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("shift+enter inserts a newline without submitting", async ({ page, gotoSession }) => {
await gotoSession()
await expect(page).toHaveURL(/\/session\/?$/)
const prompt = page.locator(promptSelector)
await prompt.click()
await page.keyboard.type("line one")
await page.keyboard.press("Shift+Enter")
await page.keyboard.type("line two")
await expect(page).toHaveURL(/\/session\/?$/)
await expect(prompt).toContainText("line one")
await expect(prompt).toContainText("line two")
})
@@ -1,22 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
test("smoke /open opens file picker dialog", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/open")
const command = page.locator('[data-slash-id="file.open"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
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)
})
@@ -1,23 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector, terminalSelector } from "../selectors"
test("/terminal toggles the terminal panel", async ({ page, gotoSession }) => {
await gotoSession()
const prompt = page.locator(promptSelector)
const terminal = page.locator(terminalSelector)
await expect(terminal).not.toBeVisible()
await prompt.click()
await page.keyboard.type("/terminal")
await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible()
await page.keyboard.press("Enter")
await expect(terminal).toBeVisible()
await prompt.click()
await page.keyboard.type("/terminal")
await expect(page.locator('[data-slash-id="terminal.toggle"]').first()).toBeVisible()
await page.keyboard.press("Enter")
await expect(terminal).not.toBeVisible()
})
-55
View File
@@ -1,55 +0,0 @@
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)
const pageErrors: string[] = []
const onPageError = (err: Error) => {
pageErrors.push(err.message)
}
page.on("pageerror", onPageError)
await gotoSession()
const token = `E2E_OK_${Date.now()}`
const prompt = page.locator(promptSelector)
await prompt.click()
await page.keyboard.type(`Reply with exactly: ${token}`)
await page.keyboard.press("Enter")
await expect(page).toHaveURL(/\/session\/[^/?#]+/, { timeout: 30_000 })
const sessionID = (() => {
const id = sessionIDFromUrl(page.url())
if (!id) throw new Error(`Failed to parse session id from url: ${page.url()}`)
return id
})()
try {
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 {
page.off("pageerror", onPageError)
await sdk.session.delete({ sessionID }).catch(() => undefined)
}
if (pageErrors.length > 0) {
throw new Error(`Page error(s):\n${pageErrors.join("\n")}`)
}
})
-73
View File
@@ -1,73 +0,0 @@
export const promptSelector = '[data-component="prompt-input"]'
export const terminalSelector = '[data-component="terminal"]'
export const sessionComposerDockSelector = '[data-component="session-prompt-dock"]'
export const questionDockSelector = '[data-component="dock-prompt"][data-kind="question"]'
export const permissionDockSelector = '[data-component="dock-prompt"][data-kind="permission"]'
export const permissionRejectSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(1)`
export const permissionAllowAlwaysSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(2)`
export const permissionAllowOnceSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(3)`
export const sessionTodoDockSelector = '[data-component="session-todo-dock"]'
export const sessionTodoToggleSelector = '[data-action="session-todo-toggle"]'
export const sessionTodoToggleButtonSelector = '[data-action="session-todo-toggle-button"]'
export const sessionTodoListSelector = '[data-slot="session-todo-list"]'
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 settingsSoundsPermissionsSelector = '[data-action="settings-sounds-permissions"]'
export const settingsSoundsErrorsSelector = '[data-action="settings-sounds-errors"]'
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}"]`
@@ -1,425 +0,0 @@
import { test, expect } from "../fixtures"
import { clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions"
import {
permissionDockSelector,
promptSelector,
questionDockSelector,
sessionComposerDockSelector,
sessionTodoDockSelector,
sessionTodoListSelector,
sessionTodoToggleButtonSelector,
} from "../selectors"
type Sdk = Parameters<typeof clearSessionDockSeed>[0]
type PermissionRule = { permission: string; pattern: string; action: "allow" | "deny" | "ask" }
async function withDockSession<T>(
sdk: Sdk,
title: string,
fn: (session: { id: string; title: string }) => Promise<T>,
opts?: { permission?: PermissionRule[] },
) {
const session = await sdk.session
.create(opts?.permission ? { title, permission: opts.permission } : { title })
.then((r) => r.data)
if (!session?.id) throw new Error("Session create did not return an id")
try {
return await fn(session)
} finally {
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
}
}
test.setTimeout(120_000)
async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>) {
try {
return await fn()
} finally {
await clearSessionDockSeed(sdk, sessionID).catch(() => undefined)
}
}
async function clearPermissionDock(page: any, label: RegExp) {
const dock = page.locator(permissionDockSelector)
for (let i = 0; i < 3; i++) {
const count = await dock.count()
if (count === 0) return
await dock.getByRole("button", { name: label }).click()
await page.waitForTimeout(150)
}
}
async function setAutoAccept(page: any, enabled: boolean) {
const button = page.locator('[data-action="prompt-permissions"]').first()
await expect(button).toBeVisible()
const pressed = (await button.getAttribute("aria-pressed")) === "true"
if (pressed === enabled) return
await button.click()
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
}
async function withMockPermission<T>(
page: any,
request: {
id: string
sessionID: string
permission: string
patterns: string[]
metadata?: Record<string, unknown>
always?: string[]
},
opts: { child?: any } | undefined,
fn: () => Promise<T>,
) {
let pending = [
{
...request,
always: request.always ?? ["*"],
metadata: request.metadata ?? {},
},
]
const list = async (route: any) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(pending),
})
}
const reply = async (route: any) => {
const url = new URL(route.request().url())
const id = url.pathname.split("/").pop()
pending = pending.filter((item) => item.id !== id)
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(true),
})
}
await page.route("**/permission", list)
await page.route("**/session/*/permissions/*", reply)
const sessionList = opts?.child
? async (route: any) => {
const res = await route.fetch()
const json = await res.json()
const list = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
if (Array.isArray(list) && !list.some((item) => item?.id === opts.child?.id)) list.push(opts.child)
await route.fulfill({
status: res.status(),
headers: res.headers(),
contentType: "application/json",
body: JSON.stringify(json),
})
}
: undefined
if (sessionList) await page.route("**/session?*", sessionList)
try {
return await fn()
} finally {
await page.unroute("**/permission", list)
await page.unroute("**/session/*/permissions/*", reply)
if (sessionList) await page.unroute("**/session?*", sessionList)
}
}
test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock default", async (session) => {
await gotoSession(session.id)
await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
await expect(page.locator(promptSelector)).toBeVisible()
await expect(page.locator(questionDockSelector)).toHaveCount(0)
await expect(page.locator(permissionDockSelector)).toHaveCount(0)
await page.locator(promptSelector).click()
await expect(page.locator(promptSelector)).toBeFocused()
})
})
test("auto-accept toggle works before first submit", async ({ page, gotoSession }) => {
await gotoSession()
const button = page.locator('[data-action="prompt-permissions"]').first()
await expect(button).toBeVisible()
await expect(button).toHaveAttribute("aria-pressed", "false")
await setAutoAccept(page, true)
await setAutoAccept(page, false)
})
test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock question", async (session) => {
await withDockSeed(sdk, session.id, async () => {
await gotoSession(session.id)
await seedSessionQuestion(sdk, {
sessionID: session.id,
questions: [
{
header: "Need input",
question: "Pick one option",
options: [
{ label: "Continue", description: "Continue now" },
{ label: "Stop", description: "Stop here" },
],
},
],
})
const dock = page.locator(questionDockSelector)
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await dock.locator('[data-slot="question-option"]').first().click()
await dock.getByRole("button", { name: /submit/i }).click()
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
})
})
})
test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock permission once", async (session) => {
await gotoSession(session.id)
await setAutoAccept(page, false)
await withMockPermission(
page,
{
id: "per_e2e_once",
sessionID: session.id,
permission: "bash",
patterns: ["/tmp/opencode-e2e-perm-once"],
metadata: { description: "Need permission for command" },
},
undefined,
async () => {
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await clearPermissionDock(page, /allow once/i)
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
},
)
})
})
test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock permission reject", async (session) => {
await gotoSession(session.id)
await setAutoAccept(page, false)
await withMockPermission(
page,
{
id: "per_e2e_reject",
sessionID: session.id,
permission: "bash",
patterns: ["/tmp/opencode-e2e-perm-reject"],
},
undefined,
async () => {
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await clearPermissionDock(page, /deny/i)
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
},
)
})
})
test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock permission always", async (session) => {
await gotoSession(session.id)
await setAutoAccept(page, false)
await withMockPermission(
page,
{
id: "per_e2e_always",
sessionID: session.id,
permission: "bash",
patterns: ["/tmp/opencode-e2e-perm-always"],
metadata: { description: "Need permission for command" },
},
undefined,
async () => {
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await clearPermissionDock(page, /allow always/i)
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
},
)
})
})
test("child session question request blocks parent dock and unblocks after submit", async ({
page,
sdk,
gotoSession,
}) => {
await withDockSession(sdk, "e2e composer dock child question parent", async (session) => {
await gotoSession(session.id)
const child = await sdk.session
.create({
title: "e2e composer dock child question",
parentID: session.id,
})
.then((r) => r.data)
if (!child?.id) throw new Error("Child session create did not return an id")
try {
await withDockSeed(sdk, child.id, async () => {
await seedSessionQuestion(sdk, {
sessionID: child.id,
questions: [
{
header: "Child input",
question: "Pick one child option",
options: [
{ label: "Continue", description: "Continue child" },
{ label: "Stop", description: "Stop child" },
],
},
],
})
const dock = page.locator(questionDockSelector)
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await dock.locator('[data-slot="question-option"]').first().click()
await dock.getByRole("button", { name: /submit/i }).click()
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
})
} finally {
await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
}
})
})
test("child session permission request blocks parent dock and supports allow once", async ({
page,
sdk,
gotoSession,
}) => {
await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => {
await gotoSession(session.id)
await setAutoAccept(page, false)
const child = await sdk.session
.create({
title: "e2e composer dock child permission",
parentID: session.id,
})
.then((r) => r.data)
if (!child?.id) throw new Error("Child session create did not return an id")
try {
await withMockPermission(
page,
{
id: "per_e2e_child",
sessionID: child.id,
permission: "bash",
patterns: ["/tmp/opencode-e2e-perm-child"],
metadata: { description: "Need child permission" },
},
{ child },
async () => {
await page.goto(page.url())
const dock = page.locator(permissionDockSelector)
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await clearPermissionDock(page, /allow once/i)
await page.goto(page.url())
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
await expect(page.locator(promptSelector)).toBeVisible()
},
)
} finally {
await sdk.session.delete({ sessionID: child.id }).catch(() => undefined)
}
})
})
test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock todo", async (session) => {
await withDockSeed(sdk, session.id, async () => {
await gotoSession(session.id)
await seedSessionTodos(sdk, {
sessionID: session.id,
todos: [
{ content: "first task", status: "pending", priority: "high" },
{ content: "second task", status: "in_progress", priority: "medium" },
],
})
await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(sessionTodoListSelector)).toBeVisible()
await page.locator(sessionTodoToggleButtonSelector).click()
await expect(page.locator(sessionTodoListSelector)).toBeHidden()
await page.locator(sessionTodoToggleButtonSelector).click()
await expect(page.locator(sessionTodoListSelector)).toBeVisible()
await seedSessionTodos(sdk, {
sessionID: session.id,
todos: [
{ content: "first task", status: "completed", priority: "high" },
{ content: "second task", status: "cancelled", priority: "medium" },
],
})
await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(0)
})
})
})
test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSession }) => {
await withDockSession(sdk, "e2e composer dock keyboard", async (session) => {
await withDockSeed(sdk, session.id, async () => {
await gotoSession(session.id)
await seedSessionQuestion(sdk, {
sessionID: session.id,
questions: [
{
header: "Need input",
question: "Pick one option",
options: [{ label: "Continue", description: "Continue now" }],
},
],
})
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(1)
await expect(page.locator(promptSelector)).toHaveCount(0)
await page.locator("main").click({ position: { x: 5, y: 5 } })
await page.keyboard.type("abc")
await expect(page.locator(promptSelector)).toHaveCount(0)
})
})
})
@@ -1,233 +0,0 @@
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()
})
})
})
-174
View File
@@ -1,174 +0,0 @@
import { test, expect } from "../fixtures"
import {
openSidebar,
openSessionMoreMenu,
clickMenuItem,
confirmDialog,
openSharePopover,
withSession,
} from "../actions"
import { sessionItemSelector, inlineInputSelector } from "../selectors"
const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
type Sdk = Parameters<typeof withSession>[0]
async function seedMessage(sdk: Sdk, sessionID: string) {
await sdk.session.promptAsync({
sessionID,
noReply: true,
parts: [{ type: "text", text: "e2e seed" }],
})
await expect
.poll(
async () => {
const messages = await sdk.session.messages({ sessionID, limit: 1 }).then((r) => r.data ?? [])
return messages.length
},
{ timeout: 30_000 },
)
.toBeGreaterThan(0)
}
test("session can be renamed via header menu", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
const originalTitle = `e2e rename test ${stamp}`
const renamedTitle = `e2e renamed ${stamp}`
await withSession(sdk, originalTitle, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id)
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(originalTitle)
const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /rename/i)
const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
await expect(input).toBeVisible()
await expect(input).toBeFocused()
await input.fill(renamedTitle)
await expect(input).toHaveValue(renamedTitle)
await input.press("Enter")
await expect
.poll(
async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.title
},
{ timeout: 30_000 },
)
.toBe(renamedTitle)
await expect(page.getByRole("heading", { level: 1 }).first()).toHaveText(renamedTitle)
})
})
test("session can be archived via header menu", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
const title = `e2e archive test ${stamp}`
await withSession(sdk, title, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id)
const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /archive/i)
await expect
.poll(
async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.time?.archived
},
{ timeout: 30_000 },
)
.not.toBeUndefined()
await openSidebar(page)
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
})
})
test("session can be deleted via header menu", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
const title = `e2e delete test ${stamp}`
await withSession(sdk, title, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id)
const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /delete/i)
await confirmDialog(page, /delete/i)
await expect
.poll(
async () => {
const data = await sdk.session
.get({ sessionID: session.id })
.then((r) => r.data)
.catch(() => undefined)
return data?.id
},
{ timeout: 30_000 },
)
.toBeUndefined()
await openSidebar(page)
await expect(page.locator(sessionItemSelector(session.id))).toHaveCount(0)
})
})
test("session can be shared and unshared via header button", async ({ page, sdk, gotoSession }) => {
test.skip(shareDisabled, "Share is disabled in this environment (OPENCODE_DISABLE_SHARE).")
const stamp = Date.now()
const title = `e2e share test ${stamp}`
await withSession(sdk, title, async (session) => {
await seedMessage(sdk, session.id)
await gotoSession(session.id)
const shared = await openSharePopover(page)
const publish = shared.popoverBody.getByRole("button", { name: "Publish" }).first()
await expect(publish).toBeVisible({ timeout: 30_000 })
await publish.click()
await expect(shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()).toBeVisible({
timeout: 30_000,
})
await expect
.poll(
async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.share?.url || undefined
},
{ timeout: 30_000 },
)
.not.toBeUndefined()
const unpublish = shared.popoverBody.getByRole("button", { name: "Unpublish" }).first()
await expect(unpublish).toBeVisible({ timeout: 30_000 })
await unpublish.click()
await expect(shared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
timeout: 30_000,
})
await expect
.poll(
async () => {
const data = await sdk.session.get({ sessionID: session.id }).then((r) => r.data)
return data?.share?.url || undefined
},
{ timeout: 30_000 },
)
.toBeUndefined()
const unshared = await openSharePopover(page)
await expect(unshared.popoverBody.getByRole("button", { name: "Publish" }).first()).toBeVisible({
timeout: 30_000,
})
})
})
@@ -1,392 +0,0 @@
import { test, expect } from "../fixtures"
import { openSettings, closeDialog, withSession } from "../actions"
import { keybindButtonSelector, terminalSelector } from "../selectors"
import { modKey } from "../utils"
test("changing sidebar toggle keybind works", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle")).first()
await expect(keybindButton).toBeVisible()
const initialKeybind = await keybindButton.textContent()
expect(initialKeybind).toContain("B")
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+Shift+KeyH`)
await page.waitForTimeout(100)
const newKeybind = await keybindButton.textContent()
expect(newKeybind).toContain("H")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["sidebar.toggle"]).toBe("mod+shift+h")
await closeDialog(page, dialog)
const main = page.locator("main")
const initialClasses = (await main.getAttribute("class")) ?? ""
const initiallyClosed = initialClasses.includes("xl:border-l")
await page.keyboard.press(`${modKey}+Shift+H`)
await page.waitForTimeout(100)
const afterToggleClasses = (await main.getAttribute("class")) ?? ""
const afterToggleClosed = afterToggleClasses.includes("xl:border-l")
expect(afterToggleClosed).toBe(!initiallyClosed)
await page.keyboard.press(`${modKey}+Shift+H`)
await page.waitForTimeout(100)
const finalClasses = (await main.getAttribute("class")) ?? ""
const finalClosed = finalClasses.includes("xl:border-l")
expect(finalClosed).toBe(initiallyClosed)
})
test("sidebar toggle keybind guards against shortcut conflicts", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
await expect(keybindButton).toBeVisible()
const initialKeybind = await keybindButton.textContent()
expect(initialKeybind).toContain("B")
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+Shift+KeyP`)
await page.waitForTimeout(100)
const toast = page.locator('[data-component="toast"]').last()
await expect(toast).toBeVisible()
await expect(toast).toContainText(/already/i)
await keybindButton.click()
await expect(keybindButton).toContainText("B")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined()
await closeDialog(page, dialog)
})
test("resetting all keybinds to defaults works", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ keybinds: { "sidebar.toggle": "mod+shift+x" } }))
})
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
await expect(keybindButton).toBeVisible()
const customKeybind = await keybindButton.textContent()
expect(customKeybind).toContain("X")
const resetButton = dialog.getByRole("button", { name: "Reset to defaults" })
await expect(resetButton).toBeVisible()
await expect(resetButton).toBeEnabled()
await resetButton.click()
await page.waitForTimeout(100)
const restoredKeybind = await keybindButton.textContent()
expect(restoredKeybind).toContain("B")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["sidebar.toggle"]).toBeUndefined()
await closeDialog(page, dialog)
})
test("clearing a keybind works", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("sidebar.toggle"))
await expect(keybindButton).toBeVisible()
const initialKeybind = await keybindButton.textContent()
expect(initialKeybind).toContain("B")
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press("Delete")
await page.waitForTimeout(100)
const clearedKeybind = await keybindButton.textContent()
expect(clearedKeybind).toMatch(/unassigned|press/i)
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["sidebar.toggle"]).toBe("none")
await closeDialog(page, dialog)
await page.keyboard.press(`${modKey}+B`)
await page.waitForTimeout(100)
const stillOnSession = page.url().includes("/session")
expect(stillOnSession).toBe(true)
})
test("changing settings open keybind works", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("settings.open"))
await expect(keybindButton).toBeVisible()
const initialKeybind = await keybindButton.textContent()
expect(initialKeybind).toContain(",")
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+Slash`)
await page.waitForTimeout(100)
const newKeybind = await keybindButton.textContent()
expect(newKeybind).toContain("/")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["settings.open"]).toBe("mod+/")
await closeDialog(page, dialog)
const settingsDialog = page.getByRole("dialog")
await expect(settingsDialog).toHaveCount(0)
await page.keyboard.press(`${modKey}+Slash`)
await page.waitForTimeout(100)
await expect(settingsDialog).toBeVisible()
await closeDialog(page, settingsDialog)
})
test("changing new session keybind works", async ({ page, sdk, gotoSession }) => {
await withSession(sdk, "test session for keybind", async (session) => {
await gotoSession(session.id)
const initialUrl = page.url()
expect(initialUrl).toContain(`/session/${session.id}`)
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("session.new"))
await expect(keybindButton).toBeVisible()
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+Shift+KeyN`)
await page.waitForTimeout(100)
const newKeybind = await keybindButton.textContent()
expect(newKeybind).toContain("N")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["session.new"]).toBe("mod+shift+n")
await closeDialog(page, dialog)
await page.keyboard.press(`${modKey}+Shift+N`)
await page.waitForTimeout(200)
const newUrl = page.url()
expect(newUrl).toMatch(/\/session\/?$/)
expect(newUrl).not.toContain(session.id)
})
})
test("changing file open keybind works", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("file.open"))
await expect(keybindButton).toBeVisible()
const initialKeybind = await keybindButton.textContent()
expect(initialKeybind).toContain("P")
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+Shift+KeyF`)
await page.waitForTimeout(100)
const newKeybind = await keybindButton.textContent()
expect(newKeybind).toContain("F")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["file.open"]).toBe("mod+shift+f")
await closeDialog(page, dialog)
const filePickerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder(/search files/i) })
await expect(filePickerDialog).toHaveCount(0)
await page.keyboard.press(`${modKey}+Shift+F`)
await page.waitForTimeout(100)
await expect(filePickerDialog).toBeVisible()
await page.keyboard.press("Escape")
await expect(filePickerDialog).toHaveCount(0)
})
test("changing terminal toggle keybind works", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle"))
await expect(keybindButton).toBeVisible()
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+KeyY`)
await page.waitForTimeout(100)
const newKeybind = await keybindButton.textContent()
expect(newKeybind).toContain("Y")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["terminal.toggle"]).toBe("mod+y")
await closeDialog(page, dialog)
const terminal = page.locator(terminalSelector)
await expect(terminal).not.toBeVisible()
await page.keyboard.press(`${modKey}+Y`)
await expect(terminal).toBeVisible()
await page.keyboard.press(`${modKey}+Y`)
await expect(terminal).not.toBeVisible()
})
test("terminal toggle keybind persists after reload", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("terminal.toggle"))
await expect(keybindButton).toBeVisible()
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+Shift+KeyY`)
await page.waitForTimeout(100)
await expect(keybindButton).toContainText("Y")
await closeDialog(page, dialog)
await page.reload()
await expect
.poll(async () => {
return await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
if (!raw) return
const parsed = JSON.parse(raw)
return parsed?.keybinds?.["terminal.toggle"]
})
})
.toBe("mod+shift+y")
const reloaded = await openSettings(page)
await reloaded.getByRole("tab", { name: "Shortcuts" }).click()
const reloadedKeybind = reloaded.locator(keybindButtonSelector("terminal.toggle")).first()
await expect(reloadedKeybind).toContainText("Y")
await closeDialog(page, reloaded)
})
test("changing command palette keybind works", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
const keybindButton = dialog.locator(keybindButtonSelector("command.palette"))
await expect(keybindButton).toBeVisible()
const initialKeybind = await keybindButton.textContent()
expect(initialKeybind).toContain("P")
await keybindButton.click()
await expect(keybindButton).toHaveText(/press/i)
await page.keyboard.press(`${modKey}+Shift+KeyK`)
await page.waitForTimeout(100)
const newKeybind = await keybindButton.textContent()
expect(newKeybind).toContain("K")
const stored = await page.evaluate(() => {
const raw = localStorage.getItem("settings.v3")
return raw ? JSON.parse(raw) : null
})
expect(stored?.keybinds?.["command.palette"]).toBe("mod+shift+k")
await closeDialog(page, dialog)
const palette = page.getByRole("dialog").filter({ has: page.getByRole("textbox").first() })
await expect(palette).toHaveCount(0)
await page.keyboard.press(`${modKey}+Shift+K`)
await page.waitForTimeout(100)
await expect(palette).toBeVisible()
await expect(palette.getByRole("textbox").first()).toBeVisible()
await page.keyboard.press("Escape")
await expect(palette).toHaveCount(0)
})
@@ -1,122 +0,0 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"
import { closeDialog, openSettings } from "../actions"
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
const command = page.locator('[data-slash-id="model.choose"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const picker = page.getByRole("dialog")
await expect(picker).toBeVisible()
const target = picker.locator('[data-slot="list-item"]').first()
await expect(target).toBeVisible()
const key = await target.getAttribute("data-key")
if (!key) throw new Error("Failed to resolve model key from list item")
const name = (await target.locator("span").first().innerText()).trim()
if (!name) throw new Error("Failed to resolve model name from list item")
await page.keyboard.press("Escape")
await expect(picker).toHaveCount(0)
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Models" }).click()
const search = settings.getByPlaceholder("Search models")
await expect(search).toBeVisible()
await search.fill(name)
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
const input = toggle.locator('[data-slot="switch-input"]')
await expect(toggle).toBeVisible()
await expect(input).toHaveAttribute("aria-checked", "true")
await toggle.locator('[data-slot="switch-control"]').click()
await expect(input).toHaveAttribute("aria-checked", "false")
await closeDialog(page, settings)
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const pickerAgain = page.getByRole("dialog")
await expect(pickerAgain).toBeVisible()
await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
await page.keyboard.press("Escape")
await expect(pickerAgain).toHaveCount(0)
})
test("showing a hidden model restores it to the model picker", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
const command = page.locator('[data-slash-id="model.choose"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const picker = page.getByRole("dialog")
await expect(picker).toBeVisible()
const target = picker.locator('[data-slot="list-item"]').first()
await expect(target).toBeVisible()
const key = await target.getAttribute("data-key")
if (!key) throw new Error("Failed to resolve model key from list item")
const name = (await target.locator("span").first().innerText()).trim()
if (!name) throw new Error("Failed to resolve model name from list item")
await page.keyboard.press("Escape")
await expect(picker).toHaveCount(0)
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Models" }).click()
const search = settings.getByPlaceholder("Search models")
await expect(search).toBeVisible()
await search.fill(name)
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
const input = toggle.locator('[data-slot="switch-input"]')
await expect(toggle).toBeVisible()
await expect(input).toHaveAttribute("aria-checked", "true")
await toggle.locator('[data-slot="switch-control"]').click()
await expect(input).toHaveAttribute("aria-checked", "false")
await toggle.locator('[data-slot="switch-control"]').click()
await expect(input).toHaveAttribute("aria-checked", "true")
await closeDialog(page, settings)
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const pickerAgain = page.getByRole("dialog")
await expect(pickerAgain).toBeVisible()
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toBeVisible()
await page.keyboard.press("Escape")
await expect(pickerAgain).toHaveCount(0)
})
@@ -1,136 +0,0 @@
import { test, expect } from "../fixtures"
import { closeDialog, openSettings } from "../actions"
test("custom provider form can be filled and validates input", async ({ page, gotoSession }) => {
await gotoSession()
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Providers" }).click()
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
await expect(customProviderSection).toBeVisible()
const connectButton = customProviderSection.getByRole("button", { name: "Connect" })
await connectButton.click()
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
await expect(providerDialog).toBeVisible()
await providerDialog.getByLabel("Provider ID").fill("test-provider")
await providerDialog.getByLabel("Display name").fill("Test Provider")
await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/fake")
await providerDialog.getByLabel("API key").fill("fake-key")
await providerDialog.getByPlaceholder("model-id").first().fill("test-model")
await providerDialog.getByPlaceholder("Display Name").first().fill("Test Model")
await expect(providerDialog.getByRole("textbox", { name: "Provider ID" })).toHaveValue("test-provider")
await expect(providerDialog.getByRole("textbox", { name: "Display name" })).toHaveValue("Test Provider")
await expect(providerDialog.getByRole("textbox", { name: "Base URL" })).toHaveValue("http://localhost:9999/fake")
await expect(providerDialog.getByRole("textbox", { name: "API key" })).toHaveValue("fake-key")
await expect(providerDialog.getByPlaceholder("model-id").first()).toHaveValue("test-model")
await expect(providerDialog.getByPlaceholder("Display Name").first()).toHaveValue("Test Model")
await page.keyboard.press("Escape")
await expect(providerDialog).toHaveCount(0)
await closeDialog(page, settings)
})
test("custom provider form shows validation errors", async ({ page, gotoSession }) => {
await gotoSession()
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Providers" }).click()
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
await customProviderSection.getByRole("button", { name: "Connect" }).click()
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
await expect(providerDialog).toBeVisible()
await providerDialog.getByLabel("Provider ID").fill("invalid provider id")
await providerDialog.getByLabel("Base URL").fill("not-a-url")
await providerDialog.getByRole("button", { name: /submit|save/i }).click()
await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /lowercase/i })).toBeVisible()
await expect(providerDialog.locator('[data-slot="input-error"]').filter({ hasText: /http/i })).toBeVisible()
await page.keyboard.press("Escape")
await expect(providerDialog).toHaveCount(0)
await closeDialog(page, settings)
})
test("custom provider form can add and remove models", async ({ page, gotoSession }) => {
await gotoSession()
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Providers" }).click()
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
await customProviderSection.getByRole("button", { name: "Connect" }).click()
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
await expect(providerDialog).toBeVisible()
await providerDialog.getByLabel("Provider ID").fill("multi-model-test")
await providerDialog.getByLabel("Display name").fill("Multi Model Test")
await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/multi")
await providerDialog.getByPlaceholder("model-id").first().fill("model-1")
await providerDialog.getByPlaceholder("Display Name").first().fill("Model 1")
const idInputsBefore = await providerDialog.getByPlaceholder("model-id").count()
await providerDialog.getByRole("button", { name: "Add model" }).click()
const idInputsAfter = await providerDialog.getByPlaceholder("model-id").count()
expect(idInputsAfter).toBe(idInputsBefore + 1)
await providerDialog.getByPlaceholder("model-id").nth(1).fill("model-2")
await providerDialog.getByPlaceholder("Display Name").nth(1).fill("Model 2")
await expect(providerDialog.getByPlaceholder("model-id").nth(1)).toHaveValue("model-2")
await expect(providerDialog.getByPlaceholder("Display Name").nth(1)).toHaveValue("Model 2")
await page.keyboard.press("Escape")
await expect(providerDialog).toHaveCount(0)
await closeDialog(page, settings)
})
test("custom provider form can add and remove headers", async ({ page, gotoSession }) => {
await gotoSession()
const settings = await openSettings(page)
await settings.getByRole("tab", { name: "Providers" }).click()
const customProviderSection = settings.locator('[data-component="custom-provider-section"]')
await customProviderSection.getByRole("button", { name: "Connect" }).click()
const providerDialog = page.getByRole("dialog").filter({ has: page.getByText("Custom provider") })
await expect(providerDialog).toBeVisible()
await providerDialog.getByLabel("Provider ID").fill("header-test")
await providerDialog.getByLabel("Display name").fill("Header Test")
await providerDialog.getByLabel("Base URL").fill("http://localhost:9999/headers")
await providerDialog.getByPlaceholder("model-id").first().fill("model-x")
await providerDialog.getByPlaceholder("Display Name").first().fill("Model X")
const headerInputsBefore = await providerDialog.getByPlaceholder("Header-Name").count()
await providerDialog.getByRole("button", { name: "Add header" }).click()
const headerInputsAfter = await providerDialog.getByPlaceholder("Header-Name").count()
expect(headerInputsAfter).toBe(headerInputsBefore + 1)
await providerDialog.getByPlaceholder("Header-Name").first().fill("Authorization")
await providerDialog.getByPlaceholder("value").first().fill("Bearer token123")
await expect(providerDialog.getByPlaceholder("Header-Name").first()).toHaveValue("Authorization")
await expect(providerDialog.getByPlaceholder("value").first()).toHaveValue("Bearer token123")
await page.keyboard.press("Escape")
await expect(providerDialog).toHaveCount(0)
await closeDialog(page, settings)
})
-476
View File
@@ -1,476 +0,0 @@
import { test, expect, settingsKey } from "../fixtures"
import { closeDialog, openSettings } from "../actions"
import {
settingsColorSchemeSelector,
settingsFontSelector,
settingsLanguageSelectSelector,
settingsNotificationsAgentSelector,
settingsNotificationsErrorsSelector,
settingsNotificationsPermissionsSelector,
settingsReleaseNotesSelector,
settingsSoundsAgentSelector,
settingsSoundsErrorsSelector,
settingsSoundsPermissionsSelector,
settingsThemeSelector,
settingsUpdatesStartupSelector,
} from "../selectors"
test("smoke settings dialog opens, switches tabs, closes", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
await dialog.getByRole("tab", { name: "Shortcuts" }).click()
await expect(dialog.getByRole("button", { name: "Reset to defaults" })).toBeVisible()
await expect(dialog.getByPlaceholder("Search shortcuts")).toBeVisible()
await closeDialog(page, dialog)
})
test("changing language updates settings labels", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
})
await gotoSession()
const dialog = await openSettings(page)
const heading = dialog.getByRole("heading", { level: 2 })
await expect(heading).toHaveText("General")
const select = dialog.locator(settingsLanguageSelectSelector)
await expect(select).toBeVisible()
await select.locator('[data-slot="select-select-trigger"]').click()
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Deutsch" }).click()
await expect(heading).toHaveText("Allgemein")
await select.locator('[data-slot="select-select-trigger"]').click()
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "English" }).click()
await expect(heading).toHaveText("General")
})
test("changing color scheme persists in localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const select = dialog.locator(settingsColorSchemeSelector)
await expect(select).toBeVisible()
await select.locator('[data-slot="select-select-trigger"]').click()
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
const colorScheme = await page.evaluate(() => {
return document.documentElement.getAttribute("data-color-scheme")
})
expect(colorScheme).toBe("dark")
await select.locator('[data-slot="select-select-trigger"]').click()
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Light" }).click()
const lightColorScheme = await page.evaluate(() => {
return document.documentElement.getAttribute("data-color-scheme")
})
expect(lightColorScheme).toBe("light")
})
test("changing theme persists in localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const select = dialog.locator(settingsThemeSelector)
await expect(select).toBeVisible()
await select.locator('[data-slot="select-select-trigger"]').click()
const items = page.locator('[data-slot="select-select-item"]')
const count = await items.count()
expect(count).toBeGreaterThan(1)
const firstTheme = await items.nth(1).locator('[data-slot="select-select-item-label"]').textContent()
expect(firstTheme).toBeTruthy()
await items.nth(1).click()
await page.keyboard.press("Escape")
const storedThemeId = await page.evaluate(() => {
return localStorage.getItem("opencode-theme-id")
})
expect(storedThemeId).not.toBeNull()
expect(storedThemeId).not.toBe("oc-1")
const dataTheme = await page.evaluate(() => {
return document.documentElement.getAttribute("data-theme")
})
expect(dataTheme).toBe(storedThemeId)
})
test("changing font persists in localStorage and updates CSS variable", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const select = dialog.locator(settingsFontSelector)
await expect(select).toBeVisible()
const initialFontFamily = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
})
expect(initialFontFamily).toContain("IBM Plex Mono")
await select.locator('[data-slot="select-select-trigger"]').click()
const items = page.locator('[data-slot="select-select-item"]')
await items.nth(2).click()
await page.waitForTimeout(100)
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.appearance?.font).not.toBe("ibm-plex-mono")
const newFontFamily = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono")
})
expect(newFontFamily).not.toBe(initialFontFamily)
})
test("color scheme and font rehydrate after reload", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const colorSchemeSelect = dialog.locator(settingsColorSchemeSelector)
await expect(colorSchemeSelect).toBeVisible()
await colorSchemeSelect.locator('[data-slot="select-select-trigger"]').click()
await page.locator('[data-slot="select-select-item"]').filter({ hasText: "Dark" }).click()
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
const fontSelect = dialog.locator(settingsFontSelector)
await expect(fontSelect).toBeVisible()
const initialFontFamily = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
})
const initialSettings = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
const currentFont =
(await fontSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
await fontSelect.locator('[data-slot="select-select-trigger"]').click()
const fontItems = page.locator('[data-slot="select-select-item"]')
expect(await fontItems.count()).toBeGreaterThan(1)
if (currentFont) {
await fontItems.filter({ hasNotText: currentFont }).first().click()
}
if (!currentFont) {
await fontItems.nth(1).click()
}
await expect
.poll(async () => {
return await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
})
.toMatchObject({
appearance: {
font: expect.any(String),
},
})
const updatedSettings = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
const updatedFontFamily = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
})
expect(updatedFontFamily).not.toBe(initialFontFamily)
expect(updatedSettings?.appearance?.font).not.toBe(initialSettings?.appearance?.font)
await closeDialog(page, dialog)
await page.reload()
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
await expect
.poll(async () => {
return await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
})
.toMatchObject({
appearance: {
font: updatedSettings?.appearance?.font,
},
})
const rehydratedSettings = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
await expect
.poll(async () => {
return await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
})
})
.not.toBe(initialFontFamily)
const rehydratedFontFamily = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue("--font-family-mono").trim()
})
expect(rehydratedFontFamily).not.toBe(initialFontFamily)
expect(rehydratedSettings?.appearance?.font).toBe(updatedSettings?.appearance?.font)
})
test("toggling notification agent switch updates localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const switchContainer = dialog.locator(settingsNotificationsAgentSelector)
await expect(switchContainer).toBeVisible()
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(initialState).toBe(true)
await switchContainer.locator('[data-slot="switch-control"]').click()
await page.waitForTimeout(100)
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(newState).toBe(false)
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.notifications?.agent).toBe(false)
})
test("toggling notification permissions switch updates localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const switchContainer = dialog.locator(settingsNotificationsPermissionsSelector)
await expect(switchContainer).toBeVisible()
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(initialState).toBe(true)
await switchContainer.locator('[data-slot="switch-control"]').click()
await page.waitForTimeout(100)
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(newState).toBe(false)
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.notifications?.permissions).toBe(false)
})
test("toggling notification errors switch updates localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const switchContainer = dialog.locator(settingsNotificationsErrorsSelector)
await expect(switchContainer).toBeVisible()
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(initialState).toBe(false)
await switchContainer.locator('[data-slot="switch-control"]').click()
await page.waitForTimeout(100)
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(newState).toBe(true)
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.notifications?.errors).toBe(true)
})
test("changing sound agent selection persists in localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const select = dialog.locator(settingsSoundsAgentSelector)
await expect(select).toBeVisible()
await select.locator('[data-slot="select-select-trigger"]').click()
const items = page.locator('[data-slot="select-select-item"]')
await items.nth(2).click()
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.sounds?.agent).not.toBe("staplebops-01")
})
test("selecting none disables agent sound", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const select = dialog.locator(settingsSoundsAgentSelector)
const trigger = select.locator('[data-slot="select-select-trigger"]')
await expect(select).toBeVisible()
await expect(trigger).toBeEnabled()
await trigger.click()
const items = page.locator('[data-slot="select-select-item"]')
await expect(items.first()).toBeVisible()
await items.first().click()
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.sounds?.agentEnabled).toBe(false)
})
test("changing permissions and errors sounds updates localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const permissionsSelect = dialog.locator(settingsSoundsPermissionsSelector)
const errorsSelect = dialog.locator(settingsSoundsErrorsSelector)
await expect(permissionsSelect).toBeVisible()
await expect(errorsSelect).toBeVisible()
const initial = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
const permissionsCurrent =
(await permissionsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
await permissionsSelect.locator('[data-slot="select-select-trigger"]').click()
const permissionItems = page.locator('[data-slot="select-select-item"]')
expect(await permissionItems.count()).toBeGreaterThan(1)
if (permissionsCurrent) {
await permissionItems.filter({ hasNotText: permissionsCurrent }).first().click()
}
if (!permissionsCurrent) {
await permissionItems.nth(1).click()
}
const errorsCurrent =
(await errorsSelect.locator('[data-slot="select-select-trigger-value"]').textContent())?.trim() ?? ""
await errorsSelect.locator('[data-slot="select-select-trigger"]').click()
const errorItems = page.locator('[data-slot="select-select-item"]')
expect(await errorItems.count()).toBeGreaterThan(1)
if (errorsCurrent) {
await errorItems.filter({ hasNotText: errorsCurrent }).first().click()
}
if (!errorsCurrent) {
await errorItems.nth(1).click()
}
await expect
.poll(async () => {
return await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
})
.toMatchObject({
sounds: {
permissions: expect.any(String),
errors: expect.any(String),
},
})
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.sounds?.permissions).not.toBe(initial?.sounds?.permissions)
expect(stored?.sounds?.errors).not.toBe(initial?.sounds?.errors)
})
test("toggling updates startup switch updates localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const switchContainer = dialog.locator(settingsUpdatesStartupSelector)
await expect(switchContainer).toBeVisible()
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
const isDisabled = await toggleInput.evaluate((el: HTMLInputElement) => el.disabled)
if (isDisabled) {
test.skip()
return
}
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(initialState).toBe(true)
await switchContainer.locator('[data-slot="switch-control"]').click()
await page.waitForTimeout(100)
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(newState).toBe(false)
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.updates?.startup).toBe(false)
})
test("toggling release notes switch updates localStorage", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = await openSettings(page)
const switchContainer = dialog.locator(settingsReleaseNotesSelector)
await expect(switchContainer).toBeVisible()
const toggleInput = switchContainer.locator('[data-slot="switch-input"]')
const initialState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(initialState).toBe(true)
await switchContainer.locator('[data-slot="switch-control"]').click()
await page.waitForTimeout(100)
const newState = await toggleInput.evaluate((el: HTMLInputElement) => el.checked)
expect(newState).toBe(false)
const stored = await page.evaluate((key) => {
const raw = localStorage.getItem(key)
return raw ? JSON.parse(raw) : null
}, settingsKey)
expect(stored?.general?.releaseNotes).toBe(false)
})

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