Compare commits

...
Author SHA1 Message Date
Kit Langton 28fa040e5d refactor(file): drop redundant gen wrappers 2026-04-13 13:06:32 -04:00
Kit Langton c97ac8f71c test(file): provide file service in tests 2026-04-13 13:06:32 -04:00
Kit Langton 908fa7d058 refactor(file): remove async facade exports 2026-04-13 13:06:32 -04:00
Kit Langton 21d7a85e76 refactor(lsp): remove async facade exports (#22321) 2026-04-13 12:47:52 -04:00
Kit Langton 663e798e76 refactor(provider): remove async facade exports (#22320) 2026-04-13 12:40:00 -04:00
Aiden Cline 5bc2d2498d test: ensure project and global instructions are loaded (#22317) 2026-04-13 11:34:38 -05:00
Kit Langton c22e34853d refactor(auth): remove async auth facade exports (#22306) 2026-04-13 12:31:43 -04:00
Kit Langton 6825b0bbc7 refactor(pty): remove async facade exports (#22305) 2026-04-13 11:47:05 -04:00
Kit Langton 3644581b55 refactor(file): stream ripgrep search parsing (#22303) 2026-04-13 11:39:37 -04:00
Kit Langton 79cc15335e fix: dispose e2e app runtime (#22316) 2026-04-13 11:36:56 -04:00
Kit Langton ca6200121b refactor: remove vcs async facade exports (#22304) 2026-04-13 11:22:20 -04:00
Kit Langton 7239b38b7f refactor(skill): remove async facade exports (#22308) 2026-04-13 11:18:10 -04:00
Kit Langton 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
Brendan AllanandKit Langton 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 Langton 3eb6508a64 refactor: share TUI terminal background detection (#22297) 2026-04-13 10:05:37 -04:00
Kit Langton 6fdb8ab90d refactor(file): add ripgrep search service (#22295) 2026-04-13 10:04:32 -04:00
Kit Langton 321bf1f8e1 refactor: finish small effect service adoption cleanups (#22094) 2026-04-13 09:17:13 -04:00
Brendan Allan 62bd023086 app: replace parsePatchFiles with parseDiffFromFile (#22270) 2026-04-13 17:19:14 +08:00
Brendan Allan 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 Allan a6b9f0dac1 app: align workspace load more button (#22251) 2026-04-13 13:58:35 +08:00
Brendan Allan 34f5bdbc99 app: fix scroll to bottom light mode style (#22250) 2026-04-13 13:55:33 +08:00
Aiden Cline 0b4fe14b0a fix: forgot to put alibaba case in last commit (#22249) 2026-04-13 00:39:12 -05:00
Aiden Cline 7230cd2683 feat: add alibaba pkg and cache support (#22248) 2026-04-13 00:08:07 -05:00
Aiden Cline 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 Allan 26d35583c5 sdk: throw error if response has text/html content type (#21289) 2026-04-13 09:39:53 +08:00
Goni ZahavyandAiden Cline 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 Cline 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
Dax 264418c0cd fix(snapshot): complete gitignore respect for previously tracked files (#22172) 2026-04-12 14:05:46 -04:00
shafdev fa2c69f09c fix(opencode): remove spurious scripts and randomField from package.json (#22160) 2026-04-12 13:49:24 -04:00
Dax 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 Cline 8b9b9ad31e fix: ensure images read by agent dont count against quota (#22168) 2026-04-12 12:02:39 -05:00
Simon Klee 3729fd5706 chore(github): vouch simonklee (#22127) 2026-04-12 11:33:38 +02:00
Aiden Cline 74b14a2d4e chore: refactor log.ts, go back to glob but add sort (#22107) 2026-04-11 23:09:19 -05:00
Aiden Cline cdb951ec2f feat: make gh copilot use msgs api when available (#22106) 2026-04-11 23:06:35 -05:00
Aiden Cline 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 Langton 319b7655b7 refactor(tool): destroy Truncate facade, effectify Tool.define (#22093) 2026-04-11 21:20:12 -04:00
Kit Langton 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 Langton 003010bdb6 refactor(question): destroy Question facade (#22092) 2026-04-11 20:57:01 -04:00
Kit Langton 82a4292934 refactor(file): destroy FileTime facade (#22090) 2026-04-11 20:08:55 -04:00
Kit Langton 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 Langton 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. Rossi 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
Dax 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 Cline 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 Langton 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 Langton ccb0b320e1 refactor(session): make SystemPrompt a proper Effect Service (#21992) 2026-04-11 12:52:35 -04:00
Kit Langton 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 Langton fe4dfb9f6f refactor(git): remove runtime facade wrappers (#21982) 2026-04-10 23:52:48 -04:00
Kit Langton 5e3dc80999 refactor: collapse command facade (#21981) 2026-04-10 23:52:12 -04:00
Kit Langton 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 Langton 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 Langton ba3600a515 refactor(session): remove dead updatePartDelta facade (#21985) 2026-04-10 23:27:30 -04:00
Kit Langton 03ce2e5288 refactor(installation): drop facade runtime wrappers (#21984) 2026-04-10 23:26:16 -04:00
Kit Langton 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 Langton f38f415bf0 refactor: collapse Format facade (#21980) 2026-04-10 23:18:54 -04:00
Kit Langton 4341ab838e refactor(tool): use Session.Service directly in TaskTool (#21975) 2026-04-10 23:18:30 -04:00
Kit Langton 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 Langton 3dd09147c2 refactor(tool): Tool.Context.metadata returns Effect (#21972) 2026-04-10 23:12:04 -04:00
Kit Langton 9581bf0670 refactor(effect): upgrade opencode to beta.46 context APIs (#21977) 2026-04-10 23:06:28 -04:00
Kit Langton 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 Langton 5917ac2162 fix: provide EffectLogger.layer to bare Effect.runPromise/runFork calls (#21974) 2026-04-10 22:55:08 -04:00
Brendan Allan 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 Langton c5fb6281f0 refactor(tool): Tool.Def.execute returns Effect, rename defineEffect → define (#21961) 2026-04-10 22:36:02 -04:00
Kit Langton 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 Langton 9e7045eaec refactor: destroy ShareNext facade (#21965) 2026-04-10 22:03:06 -04:00
Kit Langton 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 Langton a4c686025c refactor: destroy Todo facade (#21962) 2026-04-10 21:47:28 -04:00
Kit Langton 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 Langton 5cd4c6eb22 refactor: destroy Storage facades (#21956) 2026-04-10 21:21:02 -04:00
Kit Langton 40358d60a0 refactor: add Effect logger for motel observability (#21954) 2026-04-10 21:10:58 -04:00
Aiden Cline 96c1c0363d chore: rm unnecessary test (now we use effect) and the test is flaky (#21959) 2026-04-10 19:59:33 -05:00
Aiden Cline 33819932ec tweak: rm processor .trim calls (#21958) 2026-04-10 19:47:08 -05:00
Kit Langton 5d6fe01465 convert skill tool to Tool.defineEffect (#21936) 2026-04-10 19:49:53 -04:00
Kit Langton 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 Langton 0556774097 refactor(tool): convert apply_patch to Tool.defineEffect (#21938) 2026-04-10 19:42:14 -04:00
Kit Langton d9d5a0615e refactor: break SessionPrompt/TaskTool cycle via ctx injection (#21948) 2026-04-10 19:36:13 -04:00
Kit Langton 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 Langton 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 Langton 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 Langton f63bdc8e08 convert list tool to Tool.defineEffect (#21899) 2026-04-10 15:38:52 -04:00
Aiden Cline 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 Langton 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 Langton f7514d9eca refactor(tool): convert bash to defineEffect with ChildProcessSpawner (#21895) 2026-04-10 13:26:31 -04:00
James Long 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 Langton 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 Langton 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 Langton e83404367c refactor(tool): convert webfetch tool internals to Effect (#21809) 2026-04-10 10:50:13 -04:00
James Long 42206da1f8 refactor(tui): switch to global events and start passing workspace param (#21719) 2026-04-10 10:47:27 -04:00
Kit Langton 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 Langton 8063e0b5c6 refactor(tool): convert lsp tool internals to Effect (#21806) 2026-04-10 10:07:19 -04:00
Kit Langton 157c5d77f8 refactor(tool): convert question tool internals to Effect (#21808) 2026-04-10 09:42:06 -04:00
Aiden Cline ce19c051be fix: ts lsp (#21827) 2026-04-10 00:15:45 -05:00
Kit Langton 91786d2fc1 refactor(effect): use Git service in file and storage (#21803) 2026-04-09 22:49:36 -04:00
Kit Langton eca11ca71a refactor(effect): use SessionRevert service in prompt (#21796) 2026-04-09 22:28:11 -04:00
Kit Langton 17bd16667c refactor(effect): move tool descriptions into registry (#21795) 2026-04-09 22:20:27 -04:00
Kit Langton 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 Parker b16ee08fd5 ci use node 24 in test workflow fixing random ECONNRESET (#21782) 2026-04-10 01:00:21 +00:00
Luke Parker 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 Langton b2f621b897 refactor(session): inline init route orchestration (#21754) 2026-04-09 16:28:42 -04:00
Aiden ClineandOpeOginni 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 Langton 10441efad1 refactor(effect): extract session run state service (#21744) 2026-04-09 16:03:40 -04:00
Kit Langton 3199383eef fix: finalize interrupted bash via tool result path (#21724) 2026-04-09 15:20:28 -04:00
Kit Langton 9f54115c5d refactor: remove unused runtime facade exports (#21731) 2026-04-09 14:52:06 -04:00
Kit Langton 2ecc6ae65f fix(effect): suspend agent default layer construction (#21732) 2026-04-09 14:32:41 -04:00
Simon Klee 02b32e1ba7 Revert "opencode: lazy-load top-level CLI commands" (#21726) 2026-04-09 20:03:48 +02:00
Kit Langton 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 Cline 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 Langton 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
Aleksandr LossenkoandAiden Cline 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
Dax bd53b651a3 refactor: fix tool call state handling and clean up imports (#21709) 2026-04-09 11:56:19 -04:00
Kit Langton 46da801f30 refactor(effect): drop shell abort signals from runner (#21599) 2026-04-09 10:54:26 -04:00
Kit Langton 58a99916bb fix: preserve text part timing in session processor (#21691) 2026-04-09 10:05:14 -04:00
Kit Langton c29392d085 fix: preserve interrupted bash output in tool results (#21598) 2026-04-09 10:03:26 -04:00
Brendan Allan 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
Aiden ClineandFrank 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
Brendan AllanandLukeParkerDev 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 HyeonJong 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
Vladimir GlafirovandAiden Cline 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 Langton 8bdcc22541 refactor(effect): inline session processor interrupt cleanup (#21593) 2026-04-08 21:19:01 -04:00
Kit Langton 2bdd279467 fix: propagate abort signal to inline read tool (#21584) 2026-04-08 21:07:55 -04:00
OpeOginni 51535d8ef3 fix(app): skip url password setting for same-origin server and web app (#19923) 2026-04-09 07:13:10 +08:00
Kit Langton 38f8714c09 refactor(effect): build task tool from agent services (#21017) 2026-04-08 19:02:19 -04:00
Aiden Cline 4961d72c0f tweak: separate ModelsDev.Model and Config model schemas (#21561) 2026-04-08 15:55:14 -05:00
Aiden Cline 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
343 changed files with 15029 additions and 10841 deletions
+1
View File
@@ -26,6 +26,7 @@ kommander
r44vc0rp r44vc0rp
rekram1-node rekram1-node
-robinmordasiewicz -robinmordasiewicz
simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team -spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr thdxr
-toastythebot -toastythebot
+2 -1
View File
@@ -389,6 +389,7 @@ jobs:
needs: needs:
- build-cli - build-cli
- version - version
if: github.repository == 'anomalyco/opencode'
continue-on-error: false continue-on-error: false
env: env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
@@ -421,7 +422,6 @@ jobs:
target: aarch64-unknown-linux-gnu target: aarch64-unknown-linux-gnu
platform_flag: --linux platform_flag: --linux
runs-on: ${{ matrix.settings.host }} runs-on: ${{ matrix.settings.host }}
# if: github.ref_name == 'beta'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -547,6 +547,7 @@ jobs:
- sign-cli-windows - sign-cli-windows
- build-tauri - build-tauri
- build-electron - build-electron
if: always() && !failure() && !cancelled()
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
+13
View File
@@ -17,6 +17,9 @@ permissions:
contents: read contents: read
checks: write checks: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs: jobs:
unit: unit:
name: unit (${{ matrix.settings.name }}) name: unit (${{ matrix.settings.name }})
@@ -38,6 +41,11 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun - name: Setup Bun
uses: ./.github/actions/setup-bun uses: ./.github/actions/setup-bun
@@ -102,6 +110,11 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun - name: Setup Bun
uses: ./.github/actions/setup-bun uses: ./.github/actions/setup-bun
+89 -54
View File
@@ -27,7 +27,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -81,7 +81,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -115,7 +115,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -142,7 +142,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -166,7 +166,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -190,7 +190,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -223,14 +223,8 @@
}, },
"packages/desktop-electron": { "packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@solidjs/meta": "catalog:",
"@solidjs/router": "0.15.4",
"effect": "catalog:", "effect": "catalog:",
"electron-context-menu": "4.1.2", "electron-context-menu": "4.1.2",
"electron-log": "^5", "electron-log": "^5",
@@ -238,24 +232,41 @@
"electron-updater": "^6", "electron-updater": "^6",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"marked": "^15", "marked": "^15",
"solid-js": "catalog:",
"tree-kill": "^1.2.2",
}, },
"devDependencies": { "devDependencies": {
"@actions/artifact": "4.0.0", "@actions/artifact": "4.0.0",
"@lydell/node-pty": "catalog:",
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@solidjs/meta": "catalog:",
"@solidjs/router": "0.15.4",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"@valibot/to-json-schema": "1.6.0",
"electron": "40.4.1", "electron": "40.4.1",
"electron-builder": "^26", "electron-builder": "^26",
"electron-vite": "^5", "electron-vite": "^5",
"solid-js": "catalog:",
"sury": "11.0.0-alpha.4",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "catalog:", "vite": "catalog:",
"zod-openapi": "5.4.6",
},
"optionalDependencies": {
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.10",
"@lydell/node-pty-darwin-x64": "1.2.0-beta.10",
"@lydell/node-pty-linux-arm64": "1.2.0-beta.10",
"@lydell/node-pty-linux-x64": "1.2.0-beta.10",
"@lydell/node-pty-win32-arm64": "1.2.0-beta.10",
"@lydell/node-pty-win32-x64": "1.2.0-beta.10",
}, },
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@@ -284,7 +295,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -300,7 +311,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.4.0", "version": "1.4.3",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -308,7 +319,8 @@
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1", "@agentclientprotocol/sdk": "0.16.1",
"@ai-sdk/amazon-bedrock": "4.0.83", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.93",
"@ai-sdk/anthropic": "3.0.67", "@ai-sdk/anthropic": "3.0.67",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
@@ -320,7 +332,7 @@
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
"@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/openai-compatible": "2.0.41",
"@ai-sdk/perplexity": "3.0.26", "@ai-sdk/perplexity": "3.0.26",
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/provider-utils": "4.0.23",
@@ -330,13 +342,12 @@
"@aws-sdk/credential-providers": "3.993.0", "@aws-sdk/credential-providers": "3.993.0",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3", "@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11", "@hono/node-server": "1.19.11",
"@hono/node-ws": "1.3.0", "@hono/node-ws": "1.3.0",
"@hono/standard-validator": "0.1.5", "@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:", "@hono/zod-validator": "catalog:",
"@lydell/node-pty": "1.2.0-beta.10", "@lydell/node-pty": "catalog:",
"@modelcontextprotocol/sdk": "1.27.1", "@modelcontextprotocol/sdk": "1.27.1",
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@octokit/graphql": "9.0.2", "@octokit/graphql": "9.0.2",
@@ -346,7 +357,7 @@
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "2.4.2", "@openrouter/ai-sdk-provider": "2.5.1",
"@opentui/core": "0.1.97", "@opentui/core": "0.1.97",
"@opentui/solid": "0.1.97", "@opentui/solid": "0.1.97",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
@@ -367,7 +378,7 @@
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.0.0", "gitlab-ai-provider": "6.4.2",
"glob": "13.0.5", "glob": "13.0.5",
"google-auth-library": "10.5.0", "google-auth-library": "10.5.0",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
@@ -402,7 +413,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.4", "@babel/core": "7.28.4",
"@effect/language-service": "0.79.0", "@effect/language-service": "0.84.2",
"@octokit/webhooks-types": "7.6.1", "@octokit/webhooks-types": "7.6.1",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -436,9 +447,10 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
"zod": "catalog:", "zod": "catalog:",
}, },
"devDependencies": { "devDependencies": {
@@ -470,7 +482,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -485,7 +497,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -520,7 +532,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -569,7 +581,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -580,7 +592,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -630,9 +642,10 @@
}, },
"catalog": { "catalog": {
"@cloudflare/workers-types": "4.20251008.0", "@cloudflare/workers-types": "4.20251008.0",
"@effect/platform-node": "4.0.0-beta.43", "@effect/platform-node": "4.0.0-beta.46",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@lydell/node-pty": "1.2.0-beta.10",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806", "@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.1.0-beta.18", "@pierre/diffs": "1.1.0-beta.18",
@@ -650,13 +663,13 @@
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@typescript/native-preview": "7.0.0-dev.20251207.1", "@typescript/native-preview": "7.0.0-dev.20251207.1",
"ai": "6.0.149", "ai": "6.0.158",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"diff": "8.0.2", "diff": "8.0.2",
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.43", "effect": "4.0.0-beta.46",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
@@ -695,7 +708,9 @@
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="], "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="], "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
@@ -1013,11 +1028,11 @@
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="], "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.43", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.43", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43", "ioredis": "^5.7.0" } }, "sha512-Uq6E1rjaIpjHauzjwoB2HzAg3battYt2Boy8XO50GoHiWCXKE6WapYZ0/AnaBx5v5qg2sOfqpuiLsUf9ZgxOkA=="], "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.46", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.46", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46", "ioredis": "^5.7.0" } }, "sha512-6AFRKjJO95dFl5lK/YnJi04uePjQDFi3+K1aXwcz/EfVlRwJ4+lg5O4vbievfKL/hnfcShVp3/eXnNS9tvlMZQ=="],
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.43", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43" } }, "sha512-A9q0GEb61pYcQ06Dr6gXj1nKlDI3KHsar1sk3qb1ZY+kVSR64tBAylI8zGon23KY+NPtTUj/sEIToB7jc3Qt5w=="], "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.46", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-Yzci82XbZ1W3tuiownsJawrJZTGeTrTZKLD0uxdBWCBzlVyqDwoSwRwO5qh33DurJj9B7iS8MDf14fpGRBPNGQ=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
@@ -1149,8 +1164,6 @@
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="], "@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
@@ -1527,7 +1540,7 @@
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.4.2", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uRQZ4da77gru1I7/lNGJhKbqEIY7o/sPsLlbCM97VY9muGDjM/TaJzuwqIviqKTtXLzF0WDj5qBAi6FhxjvlSg=="], "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -2313,6 +2326,8 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
@@ -2371,7 +2386,7 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@6.0.149", "", { "dependencies": { "@ai-sdk/gateway": "3.0.91", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3asRb/m3ZGH7H4+VTuTgj8eQYJZ9IJUmV0ljLslY92mQp6Zj+NVn4SmFj0TBr2Y/wFBWC3xgn++47tSGOXxdbw=="], "ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
@@ -2875,7 +2890,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="], "effect": ["effect@4.0.0-beta.46", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-3f6gXvvUMtEueCRY0tU76Vq2Pej1SAwwE+s0Owd5nD53yS5n4RZhUA1rlCGFuSbQFA225pGy8vO72+lpvu7u5A=="],
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
@@ -3165,7 +3180,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.0.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-683GcJdrer/GhnljkbVcGsndCEhvGB8f9fUdCxQBlkuyt8rzf0G9DpSh+iMBYp9HpcSvYmYG0Qv5ks9dLrNxwQ=="], "gitlab-ai-provider": ["gitlab-ai-provider@6.4.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Wyw6uslCuipBOr/NYwAtpgXEUJj68iJY5aekad2DjePN99JetKVQBqkLgAy9PZp2EA4OuscfRQu9qKIBN/evNw=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
@@ -4577,6 +4592,8 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="],
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
@@ -4655,8 +4672,6 @@
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], "traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="], "tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
"tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="], "tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
@@ -4811,6 +4826,8 @@
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
"valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="],
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
@@ -4967,6 +4984,8 @@
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
"zod-openapi": ["zod-openapi@5.4.6", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
@@ -4987,7 +5006,13 @@
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="], "@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
@@ -5263,10 +5288,6 @@
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"@gitlab/gitlab-ai-provider/openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
@@ -5491,6 +5512,10 @@
"@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.11", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="], "@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.11", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="],
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
@@ -5531,7 +5556,9 @@
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-J39Dh6Gyg6HjG3A7OFKnJMp3QyZ3Eex+XDiX8aFBdRwwZm3jGWaMhkCxQPH7yiQ9kRiErZwHXX/Oexx4SyGGGA=="], "ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="], "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
@@ -5747,6 +5774,8 @@
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="], "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], "opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
@@ -5929,8 +5958,6 @@
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -6417,12 +6444,18 @@
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -6795,6 +6828,8 @@
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-85wpU1oCWbthPleNIOj5d5AOuuYZ6rM7gMLZR6YJ2WU=", "x86_64-linux": "sha256-g29OM3dy+sZ3ioTs8zjQOK1N+KnNr9ptP9xtdPcdr64=",
"aarch64-linux": "sha256-C3A56SDQGJquCpIRj2JhIzr4A7N4cc9lxtEjl8bXDeM=", "aarch64-linux": "sha256-Iu91KwDcV5omkf4Ngny1aYpyCkPLjuoWOVUDOJUhW1k=",
"aarch64-darwin": "sha256-/Ij3qhGRrcLlMfl9uEacDNnGK5URxhctuQFBW4Njrog=", "aarch64-darwin": "sha256-bk3G6m+Yo60Ea3Kyglc37QZf5Vm7MLMFcxemjc7HnL0=",
"x86_64-darwin": "sha256-10sOPuN4eZ75orw4FI8ztCq1+AKS2e8aAfg3Z6Yn56w=" "x86_64-darwin": "sha256-y3hooQw13Z3Cu0KFfXYdpkTEeKTyuKd+a/jsXHQLdqA="
} }
} }
+5 -4
View File
@@ -26,7 +26,7 @@
"packages/slack" "packages/slack"
], ],
"catalog": { "catalog": {
"@effect/platform-node": "4.0.0-beta.43", "@effect/platform-node": "4.0.0-beta.46",
"@types/bun": "1.3.11", "@types/bun": "1.3.11",
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@@ -47,8 +47,8 @@
"dompurify": "3.3.1", "dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.43", "effect": "4.0.0-beta.46",
"ai": "6.0.149", "ai": "6.0.158",
"cross-spawn": "7.0.6", "cross-spawn": "7.0.6",
"hono": "4.10.7", "hono": "4.10.7",
"hono-openapi": "1.1.2", "hono-openapi": "1.1.2",
@@ -71,7 +71,8 @@
"@solidjs/router": "0.15.4", "@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"solid-js": "1.9.10", "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": { "devDependencies": {
+7 -3
View File
@@ -44,8 +44,12 @@ async function waitForHealth(url: string, probe = "/global/health") {
throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`) throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
} }
function done(proc: ReturnType<typeof spawn>) {
return proc.exitCode !== null || proc.signalCode !== null
}
async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) { async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
if (proc.exitCode !== null) return if (done(proc)) return
await Promise.race([ await Promise.race([
new Promise<void>((resolve) => proc.once("exit", () => resolve())), new Promise<void>((resolve) => proc.once("exit", () => resolve())),
new Promise<void>((resolve) => setTimeout(resolve, timeout)), new Promise<void>((resolve) => setTimeout(resolve, timeout)),
@@ -123,11 +127,11 @@ export async function startBackend(label: string, input?: { llmUrl?: string }):
return { return {
url, url,
async stop() { async stop() {
if (proc.exitCode === null) { if (!done(proc)) {
proc.kill("SIGTERM") proc.kill("SIGTERM")
await waitExit(proc) await waitExit(proc)
} }
if (proc.exitCode === null) { if (!done(proc)) {
proc.kill("SIGKILL") proc.kill("SIGKILL")
await waitExit(proc) await waitExit(proc)
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.4.0", "version": "1.4.3",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
-1
View File
@@ -182,7 +182,6 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
if (checkMode() === "background" || type === "http") return false if (checkMode() === "background" || type === "http") return false
} }
}).pipe( }).pipe(
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }), Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
Effect.ensuring(Effect.sync(() => setCheckMode("background"))), Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
Effect.runPromise, Effect.runPromise,
+7 -2
View File
@@ -174,6 +174,7 @@ export const Terminal = (props: TerminalProps) => {
const auth = server.current?.http const auth = server.current?.http
const username = auth?.username ?? "opencode" const username = auth?.username ?? "opencode"
const password = auth?.password ?? "" const password = auth?.password ?? ""
const sameOrigin = new URL(url, location.href).origin === location.origin
let container!: HTMLDivElement let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"]) const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
const id = local.pty.id const id = local.pty.id
@@ -519,8 +520,12 @@ export const Terminal = (props: TerminalProps) => {
next.searchParams.set("directory", directory) next.searchParams.set("directory", directory)
next.searchParams.set("cursor", String(seek)) next.searchParams.set("cursor", String(seek))
next.protocol = next.protocol === "https:" ? "wss:" : "ws:" next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
next.username = username if (!sameOrigin && password) {
next.password = password next.searchParams.set("auth_token", btoa(`${username}:${password}`))
// For same-origin requests, let the browser reuse the page's existing auth.
next.username = username
next.password = password
}
const socket = new WebSocket(next) const socket = new WebSocket(next)
socket.binaryType = "arraybuffer" socket.binaryType = "arraybuffer"
@@ -14,6 +14,7 @@ import type {
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim" import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache" import { dropSessionCaches } from "./session-cache"
import { diffs as list, message as clean } from "@/utils/diffs"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
@@ -162,7 +163,7 @@ export function applyDirectoryEvent(input: {
} }
case "session.diff": { case "session.diff": {
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] } const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" })) input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
break break
} }
case "todo.updated": { case "todo.updated": {
@@ -177,7 +178,7 @@ export function applyDirectoryEvent(input: {
break break
} }
case "message.updated": { case "message.updated": {
const info = (event.properties as { info: Message }).info const info = clean((event.properties as { info: Message }).info)
const messages = input.store.message[info.sessionID] const messages = input.store.message[info.sessionID]
if (!messages) { if (!messages) {
input.setStore("message", info.sessionID, [info]) input.setStore("message", info.sessionID, [info])
+3 -2
View File
@@ -13,6 +13,7 @@ import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client" import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache" import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
import { diffs as list, message as clean } from "@/utils/diffs"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
@@ -300,7 +301,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }), input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
) )
const items = (messages.data ?? []).filter((x) => !!x?.info?.id) const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id)) const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id))
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) })) const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
return { return {
@@ -509,7 +510,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return runInflight(inflightDiff, key, () => return runInflight(inflightDiff, key, () =>
retry(() => client.session.diff({ sessionID })).then((diff) => { retry(() => client.session.diff({ sessionID })).then((diff) => {
if (!tracked(directory, sessionID)) return if (!tracked(directory, sessionID)) return
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" })) setStore("session_diff", sessionID, reconcile(list(diff.data), { key: "file" }))
}), }),
) )
}, },
@@ -274,7 +274,7 @@ const WorkspaceSessionList = (props: {
<div class="relative w-full py-1"> <div class="relative w-full py-1">
<Button <Button
variant="ghost" variant="ghost"
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10" class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
size="large" size="large"
onClick={(e: MouseEvent) => { onClick={(e: MouseEvent) => {
props.loadMore() props.loadMore()
+7 -10
View File
@@ -58,6 +58,7 @@ import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionCommands } from "@/pages/session/use-session-commands"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { Identifier } from "@/utils/id" import { Identifier } from "@/utils/id"
import { diffs as list } from "@/utils/diffs"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { extractPromptFromParts } from "@/utils/prompt" import { extractPromptFromParts } from "@/utils/prompt"
import { same } from "@/utils/same" import { same } from "@/utils/same"
@@ -430,7 +431,7 @@ export default function Page() {
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const isChildSession = createMemo(() => !!info()?.parentID) const isChildSession = createMemo(() => !!info()?.parentID)
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length)) const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
const hasSessionReview = createMemo(() => sessionCount() > 0) const hasSessionReview = createMemo(() => sessionCount() > 0)
const canReview = createMemo(() => !!sync.project) const canReview = createMemo(() => !!sync.project)
@@ -611,7 +612,7 @@ export default function Page() {
.diff({ mode }) .diff({ mode })
.then((result) => { .then((result) => {
if (vcsRun.get(mode) !== run) return if (vcsRun.get(mode) !== run) return
setVcs("diff", mode, result.data ?? []) setVcs("diff", mode, list(result.data))
setVcs("ready", mode, true) setVcs("ready", mode, true)
}) })
.catch((error) => { .catch((error) => {
@@ -649,7 +650,7 @@ export default function Page() {
return open return open
}, desktopReviewOpen()) }, desktopReviewOpen())
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? []) const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs))
const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git") const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git")
const changesOptions = createMemo<ChangeMode[]>(() => { const changesOptions = createMemo<ChangeMode[]>(() => {
const list: ChangeMode[] = [] const list: ChangeMode[] = []
@@ -669,15 +670,11 @@ export default function Page() {
if (store.changes === "git" || store.changes === "branch") return store.changes if (store.changes === "git" || store.changes === "branch") return store.changes
}) })
const reviewDiffs = createMemo(() => { const reviewDiffs = createMemo(() => {
if (store.changes === "git") return vcs.diff.git if (store.changes === "git") return list(vcs.diff.git)
if (store.changes === "branch") return vcs.diff.branch if (store.changes === "branch") return list(vcs.diff.branch)
return turnDiffs() return turnDiffs()
}) })
const reviewCount = createMemo(() => { const reviewCount = createMemo(() => reviewDiffs().length)
if (store.changes === "git") return vcs.diff.git.length
if (store.changes === "branch") return vcs.diff.branch.length
return turnDiffs().length
})
const hasReview = createMemo(() => reviewCount() > 0) const hasReview = createMemo(() => reviewCount() > 0)
const reviewReady = createMemo(() => { const reviewReady = createMemo(() => {
if (store.changes === "git") return vcs.ready.git if (store.changes === "git") return vcs.ready.git
@@ -642,10 +642,10 @@ export function MessageTimeline(props: {
onClick={props.onResumeScroll} onClick={props.onResumeScroll}
> >
<div <div
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-[var(--gray-dark-7)] bg-[color-mix(in_srgb,var(--gray-dark-3)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--gray-dark-8)] [--icon-base:var(--gray-dark-10)] group-hover:[--icon-base:var(--gray-dark-11)]" class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-border-weaker-base bg-[color-mix(in_srgb,var(--surface-raised-stronger-non-alpha)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--border-weak-base)] group-hover:[--icon-base:var(--icon-hover)]"
style={{ style={{
"box-shadow": "box-shadow":
"0 51px 60px 0 rgba(0,0,0,0.13), 0 15.375px 18.088px 0 rgba(0,0,0,0.19), 0 6.386px 7.513px 0 rgba(0,0,0,0.25), 0 2.31px 2.717px 0 rgba(0,0,0,0.38)", "0 51px 60px 0 rgba(0,0,0,0.10), 0 15px 18px 0 rgba(0,0,0,0.12), 0 6.386px 7.513px 0 rgba(0,0,0,0.12), 0 2.31px 2.717px 0 rgba(0,0,0,0.20)",
}} }}
> >
<Icon name="arrow-down-to-line" size="small" /> <Icon name="arrow-down-to-line" size="small" />
+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, test } from "bun:test"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
import { diffs, message } from "./diffs"
const item = {
file: "src/app.ts",
patch: "@@ -1 +1 @@\n-old\n+new\n",
additions: 1,
deletions: 1,
status: "modified",
} satisfies SnapshotFileDiff
describe("diffs", () => {
test("keeps valid arrays", () => {
expect(diffs([item])).toEqual([item])
})
test("wraps a single diff object", () => {
expect(diffs(item)).toEqual([item])
})
test("reads keyed diff objects", () => {
expect(diffs({ a: item })).toEqual([item])
})
test("drops invalid entries", () => {
expect(
diffs([
item,
{ file: "src/bad.ts", additions: 1, deletions: 1 },
{ patch: item.patch, additions: 1, deletions: 1 },
]),
).toEqual([item])
})
})
describe("message", () => {
test("normalizes user summaries with object diffs", () => {
const input = {
id: "msg_1",
sessionID: "ses_1",
role: "user",
time: { created: 1 },
agent: "build",
model: { providerID: "openai", modelID: "gpt-5" },
summary: {
title: "Edit",
diffs: { a: item },
},
} as unknown as Message
expect(message(input)).toMatchObject({
summary: {
title: "Edit",
diffs: [item],
},
})
})
test("drops invalid user summaries", () => {
const input = {
id: "msg_1",
sessionID: "ses_1",
role: "user",
time: { created: 1 },
agent: "build",
model: { providerID: "openai", modelID: "gpt-5" },
summary: true,
} as unknown as Message
expect(message(input)).toMatchObject({ summary: undefined })
})
})
+49
View File
@@ -0,0 +1,49 @@
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
type Diff = SnapshotFileDiff | VcsFileDiff
function diff(value: unknown): value is Diff {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
if (!("file" in value) || typeof value.file !== "string") return false
if (!("patch" in value) || typeof value.patch !== "string") return false
if (!("additions" in value) || typeof value.additions !== "number") return false
if (!("deletions" in value) || typeof value.deletions !== "number") return false
if (!("status" in value) || value.status === undefined) return true
return value.status === "added" || value.status === "deleted" || value.status === "modified"
}
function object(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value)
}
export function diffs(value: unknown): Diff[] {
if (Array.isArray(value) && value.every(diff)) return value
if (Array.isArray(value)) return value.filter(diff)
if (diff(value)) return [value]
if (!object(value)) return []
return Object.values(value).filter(diff)
}
export function message(value: Message): Message {
if (value.role !== "user") return value
const raw = value.summary as unknown
if (raw === undefined) return value
if (!object(raw)) return { ...value, summary: undefined }
const title = typeof raw.title === "string" ? raw.title : undefined
const body = typeof raw.body === "string" ? raw.body : undefined
const next = diffs(raw.diffs)
if (title === raw.title && body === raw.body && next === raw.diffs) return value
return {
...value,
summary: {
...(title === undefined ? {} : { title }),
...(body === undefined ? {} : { body }),
diffs: next,
},
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.4.0", "version": "1.4.3",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -316,7 +316,8 @@
/* Download Hero Section */ /* Download Hero Section */
[data-component="download-hero"] { [data-component="download-hero"] {
display: grid; /* display: grid; */
display: none;
grid-template-columns: 260px 1fr; grid-template-columns: 260px 1fr;
gap: 4rem; gap: 4rem;
padding-bottom: 2rem; padding-bottom: 2rem;
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.4.0", "version": "1.4.3",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.4.0", "version": "1.4.3",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.4.0", "version": "1.4.3",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -34,11 +34,6 @@ const getBase = (): Configuration => ({
}, },
files: ["out/**/*", "resources/**/*"], files: ["out/**/*", "resources/**/*"],
extraResources: [ extraResources: [
{
from: "resources/",
to: "",
filter: ["opencode-cli*"],
},
{ {
from: "native/", from: "native/",
to: "native/", to: "native/",
@@ -1,5 +1,6 @@
import { defineConfig } from "electron-vite" import { defineConfig } from "electron-vite"
import appPlugin from "@opencode-ai/app/vite" import appPlugin from "@opencode-ai/app/vite"
import * as fs from "node:fs/promises"
const channel = (() => { const channel = (() => {
const raw = process.env.OPENCODE_CHANNEL const raw = process.env.OPENCODE_CHANNEL
@@ -7,6 +8,10 @@ const channel = (() => {
return "dev" return "dev"
})() })()
const OPENCODE_SERVER_DIST = "../opencode/dist/node"
const nodePtyPkg = `@lydell/node-pty-${process.platform}-${process.arch}`
export default defineConfig({ export default defineConfig({
main: { main: {
define: { define: {
@@ -16,7 +21,33 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
input: { index: "src/main/index.ts" }, input: { index: "src/main/index.ts" },
}, },
externalizeDeps: { include: [nodePtyPkg] },
}, },
plugins: [
{
name: "opencode:node-pty-narrower",
enforce: "pre",
resolveId(s) {
if (s === "@lydell/node-pty") return nodePtyPkg
},
},
{
name: "opencode:virtual-server-module",
enforce: "pre",
resolveId(id) {
if (id === "virtual:opencode-server") return this.resolve(`${OPENCODE_SERVER_DIST}/node.js`)
},
},
{
name: "opencode:copy-server-assets",
async writeBundle() {
for (const l of await fs.readdir(OPENCODE_SERVER_DIST)) {
if (!l.endsWith(".wasm")) continue
await fs.writeFile(`./out/main/chunks/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`))
}
},
},
],
}, },
preload: { preload: {
build: { build: {
+23 -12
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"private": true, "private": true,
"version": "1.4.0", "version": "1.4.3",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://opencode.ai", "homepage": "https://opencode.ai",
@@ -13,7 +13,7 @@
"typecheck": "tsgo -b", "typecheck": "tsgo -b",
"predev": "bun ./scripts/predev.ts", "predev": "bun ./scripts/predev.ts",
"dev": "electron-vite dev", "dev": "electron-vite dev",
"prebuild": "bun ./scripts/copy-icons.ts", "prebuild": "bun ./scripts/prebuild.ts",
"build": "electron-vite build", "build": "electron-vite build",
"preview": "electron-vite preview", "preview": "electron-vite preview",
"package": "electron-builder --config electron-builder.config.ts", "package": "electron-builder --config electron-builder.config.ts",
@@ -24,31 +24,42 @@
}, },
"main": "./out/main/index.js", "main": "./out/main/index.js",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@solidjs/meta": "catalog:",
"@solidjs/router": "0.15.4",
"effect": "catalog:", "effect": "catalog:",
"electron-context-menu": "4.1.2", "electron-context-menu": "4.1.2",
"electron-log": "^5", "electron-log": "^5",
"electron-store": "^10", "electron-store": "^10",
"electron-updater": "^6", "electron-updater": "^6",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"marked": "^15", "marked": "^15"
"solid-js": "catalog:",
"tree-kill": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {
"@actions/artifact": "4.0.0", "@actions/artifact": "4.0.0",
"@lydell/node-pty": "catalog:",
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@solidjs/meta": "catalog:",
"@solidjs/router": "0.15.4",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
"@valibot/to-json-schema": "1.6.0",
"electron": "40.4.1", "electron": "40.4.1",
"electron-builder": "^26", "electron-builder": "^26",
"electron-vite": "^5", "electron-vite": "^5",
"solid-js": "catalog:",
"sury": "11.0.0-alpha.4",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "catalog:" "vite": "catalog:",
"zod-openapi": "5.4.6"
},
"optionalDependencies": {
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.10",
"@lydell/node-pty-darwin-x64": "1.2.0-beta.10",
"@lydell/node-pty-linux-arm64": "1.2.0-beta.10",
"@lydell/node-pty-linux-x64": "1.2.0-beta.10",
"@lydell/node-pty-win32-arm64": "1.2.0-beta.10",
"@lydell/node-pty-win32-x64": "1.2.0-beta.10"
} }
} }
@@ -0,0 +1,9 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { resolveChannel } from "./utils"
const channel = resolveChannel()
await $`bun ./scripts/copy-icons.ts ${channel}`
await $`cd ../opencode && bun script/build-node.ts`
+1 -13
View File
@@ -1,17 +1,5 @@
import { $ } from "bun" import { $ } from "bun"
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}` await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
const RUST_TARGET = Bun.env.RUST_TARGET await $`cd ../opencode && bun script/build-node.ts`
const sidecarConfig = getCurrentSidecar(RUST_TARGET)
const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`)
await (sidecarConfig.ocBinary.includes("-baseline")
? $`cd ../opencode && bun run build --single --baseline`
: $`cd ../opencode && bun run build --single`)
await copyBinaryToSidecarFolder(binaryPath, RUST_TARGET)
+1 -17
View File
@@ -1,25 +1,9 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { $ } from "bun"
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import { copyBinaryToSidecarFolder, getCurrentSidecar, resolveChannel, windowsify } from "./utils"
const channel = resolveChannel() await import("./prebuild")
await $`bun ./scripts/copy-icons.ts ${channel}`
const pkg = await Bun.file("./package.json").json() const pkg = await Bun.file("./package.json").json()
pkg.version = Script.version pkg.version = Script.version
await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n") await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
console.log(`Updated package.json version to ${Script.version}`) console.log(`Updated package.json version to ${Script.version}`)
const sidecarConfig = getCurrentSidecar()
const artifact = process.env.OPENCODE_CLI_ARTIFACT ?? "opencode-cli"
const dir = "resources/opencode-binaries"
await $`mkdir -p ${dir}`
await $`gh run download ${process.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir)
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
await $`rm -rf ${dir}`
-283
View File
@@ -1,283 +0,0 @@
import { execFileSync, spawn } from "node:child_process"
import { EventEmitter } from "node:events"
import { chmodSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { dirname, join } from "node:path"
import readline from "node:readline"
import { fileURLToPath } from "node:url"
import { app } from "electron"
import treeKill from "tree-kill"
import { WSL_ENABLED_KEY } from "./constants"
import { getUserShell, loadShellEnv, mergeShellEnv } from "./shell-env"
import { store } from "./store"
const CLI_INSTALL_DIR = ".opencode/bin"
const CLI_BINARY_NAME = "opencode"
export type ServerConfig = {
hostname?: string
port?: number
}
export type Config = {
server?: ServerConfig
}
export type TerminatedPayload = { code: number | null; signal: number | null }
export type CommandEvent =
| { type: "stdout"; value: string }
| { type: "stderr"; value: string }
| { type: "error"; value: string }
| { type: "terminated"; value: TerminatedPayload }
| { type: "sqlite"; value: SqliteMigrationProgress }
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
export type CommandChild = {
pid: number | undefined
kill: () => void
}
const root = dirname(fileURLToPath(import.meta.url))
export function getSidecarPath() {
const suffix = process.platform === "win32" ? ".exe" : ""
const path = app.isPackaged
? join(process.resourcesPath, `opencode-cli${suffix}`)
: join(root, "../../resources", `opencode-cli${suffix}`)
console.log(`[cli] Sidecar path resolved: ${path} (isPackaged: ${app.isPackaged})`)
return path
}
export async function getConfig(): Promise<Config | null> {
const { events } = spawnCommand("debug config", {})
let output = ""
await new Promise<void>((resolve) => {
events.on("stdout", (line: string) => {
output += line
})
events.on("stderr", (line: string) => {
output += line
})
events.on("terminated", () => resolve())
events.on("error", () => resolve())
})
try {
return JSON.parse(output) as Config
} catch {
return null
}
}
export async function installCli(): Promise<string> {
if (process.platform === "win32") {
throw new Error("CLI installation is only supported on macOS & Linux")
}
const sidecar = getSidecarPath()
const scriptPath = join(app.getAppPath(), "install")
const script = readFileSync(scriptPath, "utf8")
const tempScript = join(tmpdir(), "opencode-install.sh")
writeFileSync(tempScript, script, "utf8")
chmodSync(tempScript, 0o755)
const cmd = spawn(tempScript, ["--binary", sidecar], { stdio: "pipe" })
return await new Promise<string>((resolve, reject) => {
cmd.on("exit", (code: number | null) => {
try {
unlinkSync(tempScript)
} catch {}
if (code === 0) {
const installPath = getCliInstallPath()
if (installPath) return resolve(installPath)
return reject(new Error("Could not determine install path"))
}
reject(new Error("Install script failed"))
})
})
}
export function syncCli() {
if (!app.isPackaged) return
const installPath = getCliInstallPath()
if (!installPath) return
let version = ""
try {
version = execFileSync(installPath, ["--version"], { windowsHide: true }).toString().trim()
} catch {
return
}
const cli = parseVersion(version)
const appVersion = parseVersion(app.getVersion())
if (!cli || !appVersion) return
if (compareVersions(cli, appVersion) >= 0) return
void installCli().catch(() => undefined)
}
export function serve(hostname: string, port: number, password: string) {
const args = `--print-logs --log-level WARN serve --hostname ${hostname} --port ${port}`
const env = {
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
}
return spawnCommand(args, env)
}
export function spawnCommand(args: string, extraEnv: Record<string, string>) {
console.log(`[cli] Spawning command with args: ${args}`)
const base = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
)
const env = {
...base,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_CLIENT: "desktop",
XDG_STATE_HOME: app.getPath("userData"),
...extraEnv,
}
const shell = process.platform === "win32" ? null : getUserShell()
const envs = shell ? mergeShellEnv(loadShellEnv(shell), env) : env
const { cmd, cmdArgs } = buildCommand(args, envs, shell)
console.log(`[cli] Executing: ${cmd} ${cmdArgs.join(" ")}`)
const child = spawn(cmd, cmdArgs, {
env: envs,
detached: process.platform !== "win32",
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
})
console.log(`[cli] Spawned process with PID: ${child.pid}`)
const events = new EventEmitter()
const exit = new Promise<TerminatedPayload>((resolve) => {
child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
console.log(`[cli] Process exited with code: ${code}, signal: ${signal}`)
resolve({ code: code ?? null, signal: null })
})
child.on("error", (error: Error) => {
console.error(`[cli] Process error: ${error.message}`)
events.emit("error", error.message)
})
})
const stdout = child.stdout
const stderr = child.stderr
if (stdout) {
readline.createInterface({ input: stdout }).on("line", (line: string) => {
if (handleSqliteProgress(events, line)) return
events.emit("stdout", `${line}\n`)
})
}
if (stderr) {
readline.createInterface({ input: stderr }).on("line", (line: string) => {
if (handleSqliteProgress(events, line)) return
events.emit("stderr", `${line}\n`)
})
}
exit.then((payload) => {
events.emit("terminated", payload)
})
const kill = () => {
if (!child.pid) return
treeKill(child.pid)
}
return { events, child: { pid: child.pid, kill }, exit }
}
function handleSqliteProgress(events: EventEmitter, line: string) {
const stripped = line.startsWith("sqlite-migration:") ? line.slice("sqlite-migration:".length).trim() : null
if (!stripped) return false
if (stripped === "done") {
events.emit("sqlite", { type: "Done" })
return true
}
const value = Number.parseInt(stripped, 10)
if (!Number.isNaN(value)) {
events.emit("sqlite", { type: "InProgress", value })
return true
}
return false
}
function buildCommand(args: string, env: Record<string, string>, shell: string | null) {
if (process.platform === "win32" && isWslEnabled()) {
console.log(`[cli] Using WSL mode`)
const version = app.getVersion()
const script = [
"set -e",
'BIN="$HOME/.opencode/bin/opencode"',
'if [ ! -x "$BIN" ]; then',
` curl -fsSL https://opencode.ai/install | bash -s -- --version ${shellEscape(version)} --no-modify-path`,
"fi",
`${envPrefix(env)} exec "$BIN" ${args}`,
].join("\n")
return { cmd: "wsl", cmdArgs: ["-e", "bash", "-lc", script] }
}
if (process.platform === "win32") {
const sidecar = getSidecarPath()
console.log(`[cli] Windows direct mode, sidecar: ${sidecar}`)
return { cmd: sidecar, cmdArgs: args.split(" ") }
}
const sidecar = getSidecarPath()
const user = shell || getUserShell()
const line = user.endsWith("/nu") ? `^\"${sidecar}\" ${args}` : `\"${sidecar}\" ${args}`
console.log(`[cli] Unix mode, shell: ${user}, command: ${line}`)
return { cmd: user, cmdArgs: ["-l", "-c", line] }
}
function envPrefix(env: Record<string, string>) {
const entries = Object.entries(env).map(([key, value]) => `${key}=${shellEscape(value)}`)
return entries.join(" ")
}
function shellEscape(input: string) {
if (!input) return "''"
return `'${input.replace(/'/g, `'"'"'`)}'`
}
function getCliInstallPath() {
const home = process.env.HOME
if (!home) return null
return join(home, CLI_INSTALL_DIR, CLI_BINARY_NAME)
}
function isWslEnabled() {
return store.get(WSL_ENABLED_KEY) === true
}
function parseVersion(value: string) {
const parts = value
.replace(/^v/, "")
.split(".")
.map((part) => Number.parseInt(part, 10))
if (parts.some((part) => Number.isNaN(part))) return null
return parts
}
function compareVersions(a: number[], b: number[]) {
const len = Math.max(a.length, b.length)
for (let i = 0; i < len; i += 1) {
const left = a[i] ?? 0
const right = b[i] ?? 0
if (left > right) return 1
if (left < right) return -1
}
return 0
}
+22
View File
@@ -5,3 +5,25 @@ interface ImportMetaEnv {
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }
declare module "virtual:opencode-server" {
export namespace Server {
export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen
export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener
}
export namespace Config {
export const get: typeof import("../../../opencode/dist/types/src/node").Config.get
export type Info = import("../../../opencode/dist/types/src/node").Config.Info
}
export namespace Log {
export const init: typeof import("../../../opencode/dist/types/src/node").Log.init
}
export namespace Database {
export const Path: typeof import("../../../opencode/dist/types/src/node").Database.Path
export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client
}
export namespace JsonMigration {
export type Progress = import("../../../opencode/dist/types/src/node").JsonMigration.Progress
export const run: typeof import("../../../opencode/dist/types/src/node").JsonMigration.run
}
export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap
}
+10 -22
View File
@@ -11,6 +11,8 @@ import pkg from "electron-updater"
import contextMenu from "electron-context-menu" import contextMenu from "electron-context-menu"
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false }) contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = "true"
const APP_NAMES: Record<string, string> = { const APP_NAMES: Record<string, string> = {
dev: "OpenCode Dev", dev: "OpenCode Dev",
beta: "OpenCode Beta", beta: "OpenCode Beta",
@@ -27,8 +29,6 @@ const { autoUpdater } = pkg
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types" import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
import { checkAppExists, resolveAppPath, wslPath } from "./apps" import { checkAppExists, resolveAppPath, wslPath } from "./apps"
import type { CommandChild } from "./cli"
import { installCli, syncCli } from "./cli"
import { CHANNEL, UPDATER_ENABLED } from "./constants" import { CHANNEL, UPDATER_ENABLED } from "./constants"
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc" import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
import { initLogging } from "./logging" import { initLogging } from "./logging"
@@ -36,12 +36,13 @@ import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu" import { createMenu } from "./menu"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows" import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
import type { Server } from "virtual:opencode-server"
const initEmitter = new EventEmitter() const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" } let initStep: InitStep = { phase: "server_waiting" }
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
let sidecar: CommandChild | null = null let server: Server.Listener | null = null
const loadingComplete = defer<void>() const loadingComplete = defer<void>()
const pendingDeepLinks: string[] = [] const pendingDeepLinks: string[] = []
@@ -96,11 +97,9 @@ function setupApp() {
} }
void app.whenReady().then(async () => { void app.whenReady().then(async () => {
// migrate()
app.setAsDefaultProtocolClient("opencode") app.setAsDefaultProtocolClient("opencode")
setDockIcon() setDockIcon()
setupAutoUpdater() setupAutoUpdater()
syncCli()
await initialize() await initialize()
}) })
} }
@@ -134,8 +133,8 @@ async function initialize() {
const password = randomUUID() const password = randomUUID()
logger.log("spawning sidecar", { url }) logger.log("spawning sidecar", { url })
const { child, health, events } = spawnLocalServer(hostname, port, password) const { listener, health } = await spawnLocalServer(hostname, port, password)
sidecar = child server = listener
serverReady.resolve({ serverReady.resolve({
url, url,
username: "opencode", username: "opencode",
@@ -145,7 +144,7 @@ async function initialize() {
const loadingTask = (async () => { const loadingTask = (async () => {
logger.log("sidecar connection started", { url }) logger.log("sidecar connection started", { url })
events.on("sqlite", (progress: SqliteMigrationProgress) => { initEmitter.on("sqlite", (progress: SqliteMigrationProgress) => {
setInitStep({ phase: "sqlite_waiting" }) setInitStep({ phase: "sqlite_waiting" })
if (overlay) sendSqliteMigrationProgress(overlay, progress) if (overlay) sendSqliteMigrationProgress(overlay, progress)
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress) if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
@@ -198,9 +197,6 @@ function wireMenu() {
if (!mainWindow) return if (!mainWindow) return
createMenu({ createMenu({
trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id), trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
installCli: () => {
void installCli()
},
checkForUpdates: () => { checkForUpdates: () => {
void checkForUpdates(true) void checkForUpdates(true)
}, },
@@ -215,7 +211,6 @@ function wireMenu() {
registerIpcHandlers({ registerIpcHandlers({
killSidecar: () => killSidecar(), killSidecar: () => killSidecar(),
installCli: async () => installCli(),
awaitInitialization: async (sendStep) => { awaitInitialization: async (sendStep) => {
sendStep(initStep) sendStep(initStep)
const listener = (step: InitStep) => sendStep(step) const listener = (step: InitStep) => sendStep(step)
@@ -247,16 +242,9 @@ registerIpcHandlers({
}) })
function killSidecar() { function killSidecar() {
if (!sidecar) return if (!server) return
const pid = sidecar.pid server.stop()
sidecar.kill() server = null
sidecar = null
// tree-kill is async; also send process group signal as immediate fallback
if (pid && process.platform !== "win32") {
try {
process.kill(-pid, "SIGTERM")
} catch {}
}
} }
function ensureLoopbackNoProxy() { function ensureLoopbackNoProxy() {
@@ -13,7 +13,6 @@ const pickerFilters = (ext?: string[]) => {
type Deps = { type Deps = {
killSidecar: () => void killSidecar: () => void
installCli: () => Promise<string>
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData> awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
getDefaultServerUrl: () => Promise<string | null> | string | null getDefaultServerUrl: () => Promise<string | null> | string | null
setDefaultServerUrl: (url: string | null) => Promise<void> | void setDefaultServerUrl: (url: string | null) => Promise<void> | void
@@ -34,7 +33,6 @@ type Deps = {
export function registerIpcHandlers(deps: Deps) { export function registerIpcHandlers(deps: Deps) {
ipcMain.handle("kill-sidecar", () => deps.killSidecar()) ipcMain.handle("kill-sidecar", () => deps.killSidecar())
ipcMain.handle("install-cli", () => deps.installCli())
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => { ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
const send = (step: InitStep) => event.sender.send("init-step", step) const send = (step: InitStep) => event.sender.send("init-step", step)
return deps.awaitInitialization(send) return deps.awaitInitialization(send)
@@ -5,7 +5,6 @@ import { createMainWindow } from "./windows"
type Deps = { type Deps = {
trigger: (id: string) => void trigger: (id: string) => void
installCli: () => void
checkForUpdates: () => void checkForUpdates: () => void
reload: () => void reload: () => void
relaunch: () => void relaunch: () => void
@@ -24,10 +23,6 @@ export function createMenu(deps: Deps) {
enabled: UPDATER_ENABLED, enabled: UPDATER_ENABLED,
click: () => deps.checkForUpdates(), click: () => deps.checkForUpdates(),
}, },
{
label: "Install CLI...",
click: () => deps.installCli(),
},
{ {
label: "Reload Webview", label: "Reload Webview",
click: () => deps.reload(), click: () => deps.reload(),
+30 -16
View File
@@ -1,5 +1,6 @@
import { serve, type CommandChild } from "./cli" import { app } from "electron"
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
import { getUserShell, loadShellEnv } from "./shell-env"
import { store } from "./store" import { store } from "./store"
export type WslConfig = { enabled: boolean } export type WslConfig = { enabled: boolean }
@@ -29,8 +30,16 @@ export function setWslConfig(config: WslConfig) {
store.set(WSL_ENABLED_KEY, config.enabled) store.set(WSL_ENABLED_KEY, config.enabled)
} }
export function spawnLocalServer(hostname: string, port: number, password: string) { export async function spawnLocalServer(hostname: string, port: number, password: string) {
const { child, exit, events } = serve(hostname, port, password) prepareServerEnv(password)
const { Log, Server } = await import("virtual:opencode-server")
await Log.init({ level: "WARN" })
const listener = await Server.listen({
port,
hostname,
username: "opencode",
password,
})
const wait = (async () => { const wait = (async () => {
const url = `http://${hostname}:${port}` const url = `http://${hostname}:${port}`
@@ -42,19 +51,26 @@ export function spawnLocalServer(hostname: string, port: number, password: strin
} }
} }
const terminated = async () => { await ready()
const payload = await exit
throw new Error(
`Sidecar terminated before becoming healthy (code=${payload.code ?? "unknown"} signal=${
payload.signal ?? "unknown"
})`,
)
}
await Promise.race([ready(), terminated()])
})() })()
return { child, health: { wait }, events } return { listener, health: { wait } }
}
function prepareServerEnv(password: string) {
const shell = process.platform === "win32" ? null : getUserShell()
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
const env = {
...process.env,
...shellEnv,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_CLIENT: "desktop",
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
XDG_STATE_HOME: app.getPath("userData"),
}
Object.assign(process.env, env)
} }
export async function checkHealth(url: string, password?: string | null): Promise<boolean> { export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
@@ -82,5 +98,3 @@ export async function checkHealth(url: string, password?: string | null): Promis
return false return false
} }
} }
export type { CommandChild }
+13 -13
View File
@@ -1,7 +1,7 @@
import { spawnSync } from "node:child_process" import { spawnSync } from "node:child_process"
import { basename } from "node:path" import { basename } from "node:path"
const SHELL_ENV_TIMEOUT = 5_000 const TIMEOUT = 5_000
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" } type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
@@ -20,28 +20,28 @@ export function parseShellEnv(out: Buffer) {
return env return env
} }
function probeShellEnv(shell: string, mode: "-il" | "-l"): Probe { function probe(shell: string, mode: "-il" | "-l"): Probe {
const out = spawnSync(shell, [mode, "-c", "env -0"], { const out = spawnSync(shell, [mode, "-c", "env -0"], {
stdio: ["ignore", "pipe", "ignore"], stdio: ["ignore", "pipe", "ignore"],
timeout: SHELL_ENV_TIMEOUT, timeout: TIMEOUT,
windowsHide: true, windowsHide: true,
}) })
const err = out.error as NodeJS.ErrnoException | undefined const err = out.error as NodeJS.ErrnoException | undefined
if (err) { if (err) {
if (err.code === "ETIMEDOUT") return { type: "Timeout" } if (err.code === "ETIMEDOUT") return { type: "Timeout" }
console.log(`[cli] Shell env probe failed for ${shell} ${mode}: ${err.message}`) console.log(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
return { type: "Unavailable" } return { type: "Unavailable" }
} }
if (out.status !== 0) { if (out.status !== 0) {
console.log(`[cli] Shell env probe exited with non-zero status for ${shell} ${mode}`) console.log(`[server] Shell env probe exited with non-zero status for ${shell} ${mode}`)
return { type: "Unavailable" } return { type: "Unavailable" }
} }
const env = parseShellEnv(out.stdout) const env = parseShellEnv(out.stdout)
if (Object.keys(env).length === 0) { if (Object.keys(env).length === 0) {
console.log(`[cli] Shell env probe returned empty env for ${shell} ${mode}`) console.log(`[server] Shell env probe returned empty env for ${shell} ${mode}`)
return { type: "Unavailable" } return { type: "Unavailable" }
} }
@@ -56,27 +56,27 @@ export function isNushell(shell: string) {
export function loadShellEnv(shell: string) { export function loadShellEnv(shell: string) {
if (isNushell(shell)) { if (isNushell(shell)) {
console.log(`[cli] Skipping shell env probe for nushell: ${shell}`) console.log(`[server] Skipping shell env probe for nushell: ${shell}`)
return null return null
} }
const interactive = probeShellEnv(shell, "-il") const interactive = probe(shell, "-il")
if (interactive.type === "Loaded") { if (interactive.type === "Loaded") {
console.log(`[cli] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`) console.log(`[server] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`)
return interactive.value return interactive.value
} }
if (interactive.type === "Timeout") { if (interactive.type === "Timeout") {
console.warn(`[cli] Interactive shell env probe timed out: ${shell}`) console.warn(`[server] Interactive shell env probe timed out: ${shell}`)
return null return null
} }
const login = probeShellEnv(shell, "-l") const login = probe(shell, "-l")
if (login.type === "Loaded") { if (login.type === "Loaded") {
console.log(`[cli] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`) console.log(`[server] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`)
return login.value return login.value
} }
console.warn(`[cli] Falling back to app environment: ${shell}`) console.warn(`[server] Falling back to app environment: ${shell}`)
return null return null
} }
@@ -66,7 +66,7 @@ export function createMainWindow(globals: Globals) {
y: state.y, y: state.y,
width: state.width, width: state.width,
height: state.height, height: state.height,
show: true, show: false,
title: "OpenCode", title: "OpenCode",
icon: iconPath(), icon: iconPath(),
backgroundColor, backgroundColor,
@@ -94,6 +94,10 @@ export function createMainWindow(globals: Globals) {
wireZoom(win) wireZoom(win)
injectGlobals(win, globals) injectGlobals(win, globals)
win.once("ready-to-show", () => {
win.show()
})
return win return win
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.4.0", "version": "1.4.3",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@@ -21,7 +21,7 @@ const releaseId = process.env.OPENCODE_RELEASE
if (!releaseId) throw new Error("OPENCODE_RELEASE is required") if (!releaseId) throw new Error("OPENCODE_RELEASE is required")
const version = process.env.OPENCODE_VERSION const version = process.env.OPENCODE_VERSION
if (!releaseId) throw new Error("OPENCODE_VERSION is required") if (!version) throw new Error("OPENCODE_VERSION is required")
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required") if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
@@ -54,7 +54,10 @@ const assets = release.assets ?? []
const assetByName = new Map(assets.map((asset) => [asset.name, asset])) const assetByName = new Map(assets.map((asset) => [asset.name, asset]))
const latestAsset = assetByName.get("latest.json") const latestAsset = assetByName.get("latest.json")
if (!latestAsset) throw new Error("latest.json asset not found") if (!latestAsset) {
console.log("latest.json not found, skipping tauri finalization")
process.exit(0)
}
const latestRes = await fetch(latestAsset.url, { const latestRes = await fetch(latestAsset.url, {
headers: { headers: {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.4.0", "version": "1.4.3",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.4.0" version = "1.4.3"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode" repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-darwin-arm64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-darwin-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-linux-arm64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-linux-x64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-windows-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.4.0", "version": "1.4.3",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+2 -2
View File
@@ -13,7 +13,7 @@
Use these rules when writing or migrating Effect code. Use these rules when writing or migrating Effect code.
See `specs/effect-migration.md` for the compact pattern reference and examples. See `specs/effect/migration.md` for the compact pattern reference and examples.
## Core ## Core
@@ -51,7 +51,7 @@ See `specs/effect-migration.md` for the compact pattern reference and examples.
## Effect.cached for deduplication ## Effect.cached for deduplication
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect-migration.md` for the full pattern. Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect/migration.md` for the full pattern.
## Instance.bind — ALS for native callbacks ## Instance.bind — ALS for native callbacks
+13 -15
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.4.0", "version": "1.4.3",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -14,18 +14,11 @@
"fix-node-pty": "bun run script/fix-node-pty.ts", "fix-node-pty": "bun run script/fix-node-pty.ts",
"upgrade-opentui": "bun run script/upgrade-opentui.ts", "upgrade-opentui": "bun run script/upgrade-opentui.ts",
"dev": "bun run --conditions=browser ./src/index.ts", "dev": "bun run --conditions=browser ./src/index.ts",
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
"lint": "echo 'Running lint checks...' && bun test --coverage",
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'",
"db": "bun drizzle-kit" "db": "bun drizzle-kit"
}, },
"bin": { "bin": {
"opencode": "./bin/opencode" "opencode": "./bin/opencode"
}, },
"randomField": "this-is-a-random-value-12345",
"exports": { "exports": {
"./*": "./src/*.ts" "./*": "./src/*.ts"
}, },
@@ -39,11 +32,16 @@
"bun": "./src/pty/pty.bun.ts", "bun": "./src/pty/pty.bun.ts",
"node": "./src/pty/pty.node.ts", "node": "./src/pty/pty.node.ts",
"default": "./src/pty/pty.bun.ts" "default": "./src/pty/pty.bun.ts"
},
"#hono": {
"bun": "./src/server/adapter.bun.ts",
"node": "./src/server/adapter.node.ts",
"default": "./src/server/adapter.bun.ts"
} }
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.28.4", "@babel/core": "7.28.4",
"@effect/language-service": "0.79.0", "@effect/language-service": "0.84.2",
"@octokit/webhooks-types": "7.6.1", "@octokit/webhooks-types": "7.6.1",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -78,7 +76,8 @@
"@actions/core": "1.11.1", "@actions/core": "1.11.1",
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.16.1", "@agentclientprotocol/sdk": "0.16.1",
"@ai-sdk/amazon-bedrock": "4.0.83", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.93",
"@ai-sdk/anthropic": "3.0.67", "@ai-sdk/anthropic": "3.0.67",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
@@ -90,7 +89,7 @@
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
"@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/openai-compatible": "2.0.41",
"@ai-sdk/perplexity": "3.0.26", "@ai-sdk/perplexity": "3.0.26",
"@ai-sdk/provider": "3.0.8", "@ai-sdk/provider": "3.0.8",
"@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/provider-utils": "4.0.23",
@@ -100,13 +99,12 @@
"@aws-sdk/credential-providers": "3.993.0", "@aws-sdk/credential-providers": "3.993.0",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3", "@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11", "@hono/node-server": "1.19.11",
"@hono/node-ws": "1.3.0", "@hono/node-ws": "1.3.0",
"@hono/standard-validator": "0.1.5", "@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:", "@hono/zod-validator": "catalog:",
"@lydell/node-pty": "1.2.0-beta.10", "@lydell/node-pty": "catalog:",
"@modelcontextprotocol/sdk": "1.27.1", "@modelcontextprotocol/sdk": "1.27.1",
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@octokit/graphql": "9.0.2", "@octokit/graphql": "9.0.2",
@@ -116,7 +114,7 @@
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "2.4.2", "@openrouter/ai-sdk-provider": "2.5.1",
"@opentui/core": "0.1.97", "@opentui/core": "0.1.97",
"@opentui/solid": "0.1.97", "@opentui/solid": "0.1.97",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
@@ -137,7 +135,7 @@
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"fuzzysort": "3.1.0", "fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.0.0", "gitlab-ai-provider": "6.4.2",
"glob": "13.0.5", "glob": "13.0.5",
"google-auth-library": "10.5.0", "google-auth-library": "10.5.0",
"gray-matter": "4.0.3", "gray-matter": "4.0.3",
+7 -16
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { $ } from "bun"
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
@@ -9,18 +8,11 @@ import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..") const dir = path.resolve(__dirname, "..")
const root = path.resolve(dir, "../..")
function linker(): "hoisted" | "isolated" {
// jsonc-parser is only declared in packages/opencode, so its install location
// tells us whether Bun used a hoisted or isolated workspace layout.
if (fs.existsSync(path.join(dir, "node_modules", "jsonc-parser"))) return "isolated"
if (fs.existsSync(path.join(root, "node_modules", "jsonc-parser"))) return "hoisted"
throw new Error("Could not detect Bun linker from jsonc-parser")
}
process.chdir(dir) process.chdir(dir)
await import("./generate.ts")
// Load migrations from migration directories // Load migrations from migration directories
const migrationDirs = ( const migrationDirs = (
await fs.promises.readdir(path.join(dir, "migration"), { await fs.promises.readdir(path.join(dir, "migration"), {
@@ -51,21 +43,20 @@ const migrations = await Promise.all(
) )
console.log(`Loaded ${migrations.length} migrations`) console.log(`Loaded ${migrations.length} migrations`)
const link = linker()
await $`bun install --linker=${link} --os="*" --cpu="*" @lydell/node-pty@1.2.0-beta.10`
await Bun.build({ await Bun.build({
target: "node", target: "node",
entrypoints: ["./src/node.ts"], entrypoints: ["./src/node.ts"],
outdir: "./dist", outdir: "./dist/node",
format: "esm", format: "esm",
sourcemap: "linked", sourcemap: "linked",
external: ["jsonc-parser"], external: ["jsonc-parser", "@lydell/node-pty"],
define: { define: {
OPENCODE_MIGRATIONS: JSON.stringify(migrations), OPENCODE_MIGRATIONS: JSON.stringify(migrations),
OPENCODE_CHANNEL: `'${Script.channel}'`, OPENCODE_CHANNEL: `'${Script.channel}'`,
}, },
files: {
"opencode-web-ui.gen.ts": "",
},
}) })
console.log("Build complete") console.log("Build complete")
+2 -15
View File
@@ -12,24 +12,11 @@ const dir = path.resolve(__dirname, "..")
process.chdir(dir) process.chdir(dir)
await import("./generate.ts")
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import pkg from "../package.json" import pkg from "../package.json"
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
// Fetch and generate models.dev snapshot
const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
await Bun.write(
path.join(dir, "src/provider/models-snapshot.js"),
`// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
)
await Bun.write(
path.join(dir, "src/provider/models-snapshot.d.ts"),
`// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record<string, unknown>\n`,
)
console.log("Generated models-snapshot.js")
// Load migrations from migration directories // Load migrations from migration directories
const migrationDirs = ( const migrationDirs = (
await fs.promises.readdir(path.join(dir, "migration"), { await fs.promises.readdir(path.join(dir, "migration"), {
+23
View File
@@ -0,0 +1,23 @@
import path from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
process.chdir(dir)
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
// Fetch and generate models.dev snapshot
const modelsData = process.env.MODELS_DEV_API_JSON
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
await Bun.write(
path.join(dir, "src/provider/models-snapshot.js"),
`// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
)
await Bun.write(
path.join(dir, "src/provider/models-snapshot.d.ts"),
`// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record<string, unknown>\n`,
)
console.log("Generated models-snapshot.js")
+11 -2
View File
@@ -1,3 +1,5 @@
import { AppRuntime } from "@/effect/app-runtime"
const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd() const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session" const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e" const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
@@ -16,14 +18,20 @@ const seed = async () => {
const { Project } = await import("../src/project/project") const { Project } = await import("../src/project/project")
const { ModelID, ProviderID } = await import("../src/provider/schema") const { ModelID, ProviderID } = await import("../src/provider/schema")
const { ToolRegistry } = await import("../src/tool/registry") const { ToolRegistry } = await import("../src/tool/registry")
const { Effect } = await import("effect")
try { try {
await Instance.provide({ await Instance.provide({
directory: dir, directory: dir,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: async () => { fn: async () => {
await Config.waitForDependencies() await Config.waitForDependencies()
await ToolRegistry.ids() await AppRuntime.runPromise(
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
yield* registry.ids()
}),
)
const session = await Session.create({ title }) const session = await Session.create({ title })
const messageID = MessageID.ascending() const messageID = MessageID.ascending()
@@ -54,6 +62,7 @@ const seed = async () => {
}) })
} finally { } finally {
await Instance.disposeAll().catch(() => {}) await Instance.disposeAll().catch(() => {})
await AppRuntime.dispose().catch(() => {})
} }
} }
+137
View File
@@ -0,0 +1,137 @@
# HttpApi migration
Practical notes for an eventual migration of `packages/opencode` server routes from the current Hono handlers to Effect `HttpApi`, either as a full replacement or as a parallel surface.
## Goal
Use Effect `HttpApi` where it gives us a better typed contract for:
- route definition
- request decoding and validation
- typed success and error responses
- OpenAPI generation
- handler composition inside Effect
This should be treated as a later-stage HTTP boundary migration, not a prerequisite for ongoing service, route-handler, or schema work.
## Core model
`HttpApi` is definition-first.
- `HttpApi` is the root API
- `HttpApiGroup` groups related endpoints
- `HttpApiEndpoint` defines a single route and its request / response schemas
- handlers are implemented separately from the contract
This is a better fit once route inputs and outputs are already moving toward Effect Schema-first models.
## Why it is relevant here
The current route-effectification work is already pushing handlers toward:
- one `AppRuntime.runPromise(Effect.gen(...))` body
- yielding services from context
- using typed Effect errors instead of Promise wrappers
That work is a good prerequisite for `HttpApi`. Once the handler body is already a composed Effect, the remaining migration is mostly about replacing the Hono route declaration and validator layer.
## What HttpApi gives us
### Contracts
Request params, query, payload, success payloads, and typed error payloads are declared in one place using Effect Schema.
### Validation and decoding
Incoming data is decoded through Effect Schema instead of hand-maintained Zod validators per route.
### OpenAPI
`HttpApi` can derive OpenAPI from the API definition, which overlaps with the current `describeRoute(...)` and `resolver(...)` pattern.
### Typed errors
`Schema.TaggedErrorClass` maps naturally to endpoint error contracts.
## Likely fit for opencode
Best fit first:
- JSON request / response endpoints
- route groups that already mostly delegate into services
- endpoints whose request and response models can be defined with Effect Schema
Harder / later fit:
- SSE endpoints
- websocket endpoints
- streaming handlers
- routes with heavy Hono-specific middleware assumptions
## Current blockers and gaps
### Schema split
Many route boundaries still use Zod-first validators. That does not block all experimentation, but full `HttpApi` adoption is easier after the domain and boundary types are more consistently Schema-first with `.zod` compatibility only where needed.
### Mixed handler styles
Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first.
### Non-JSON routes
The server currently includes SSE, websocket, and streaming-style endpoints. Those should not be the first `HttpApi` targets.
### Existing Hono integration
The current server composition, middleware, and docs flow are Hono-centered today. That suggests a parallel or incremental adoption plan is safer than a flag day rewrite.
## Recommended strategy
### 1. Finish the prerequisites first
- continue route-handler effectification in `server/instance/*.ts`
- continue schema migration toward Effect Schema-first DTOs and errors
- keep removing service facades
### 2. Start with one parallel group
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
- `server/instance/question.ts`
- `server/instance/provider.ts`
- `server/instance/permission.ts`
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
### 3. Reuse existing services
Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers.
### 4. Run in parallel before replacing
Prefer mounting an experimental `HttpApi` surface alongside the existing Hono routes first. That lowers migration risk and lets us compare:
- handler ergonomics
- OpenAPI output
- auth and middleware integration
- test ergonomics
### 5. Migrate JSON route groups gradually
If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
## Proposed first steps
- [ ] add one small spike that defines an `HttpApi` group for a simple JSON route set
- [ ] use Effect Schema request / response types for that slice
- [ ] keep the underlying service calls identical to the current handlers
- [ ] compare generated OpenAPI against the current Hono/OpenAPI setup
- [ ] document how auth, instance lookup, and error mapping would compose in the new stack
- [ ] decide after the spike whether `HttpApi` should stay parallel, replace only some groups, or become the long-term default
## Rule of thumb
Do not start with the hardest route file.
If `HttpApi` is adopted here, it should arrive after the handler body is already Effect-native and after the relevant request / response models have moved to Effect Schema.
@@ -23,7 +23,7 @@ export namespace Foo {
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError> readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
} }
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Foo") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -178,7 +178,9 @@ That is fine for leaf files like `schema.ts`. Keep the service surface in the ow
## Migration checklist ## Migration checklist
Fully migrated (single namespace, InstanceState where needed, flattened facade): Service-shape migrated (single namespace, traced methods, `InstanceState` where needed).
This checklist is only about the service shape migration. Many of these services still keep `makeRuntime(...)` plus async facade exports; that facade-removal phase is tracked separately in [Destroying the facades](#destroying-the-facades).
- [x] `Account``account/index.ts` - [x] `Account``account/index.ts`
- [x] `Agent``agent/agent.ts` - [x] `Agent``agent/agent.ts`
@@ -217,62 +219,20 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
- [x] `SessionSummary``session/summary.ts` - [x] `SessionSummary``session/summary.ts`
- [x] `SessionRevert``session/revert.ts` - [x] `SessionRevert``session/revert.ts`
- [x] `Instruction``session/instruction.ts` - [x] `Instruction``session/instruction.ts`
- [x] `SystemPrompt``session/system.ts`
- [x] `Provider``provider/provider.ts` - [x] `Provider``provider/provider.ts`
- [x] `Storage``storage/storage.ts` - [x] `Storage``storage/storage.ts`
- [x] `ShareNext``share/share-next.ts`
- [x] `SessionTodo``session/todo.ts`
Still open: Still open at the service-shape level:
- [ ] `SessionTodo``session/todo.ts` - [ ] `SyncEvent``sync/index.ts` (deferred pending sync with James)
- [ ] `ShareNext``share/share-next.ts` - [ ] `Workspace``control-plane/workspace.ts` (deferred pending sync with James)
- [ ] `SyncEvent``sync/index.ts`
- [ ] `Workspace``control-plane/workspace.ts`
## Tool interface → Effect ## Tool migration
Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `init` and `execute` return `Effect` instead of `Promise`. This lets tool implementations compose natively with the Effect pipeline rather than being wrapped in `Effect.promise()` at the call site. Requires: Tool-specific migration guidance and checklist live in `tools.md`.
1. Migrate each tool to return Effects
2. Update `Tool.define()` factory to work with Effects
3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
### Tool migration details
Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
- Keep the bridge at the Promise boundary only. Prefer a single `Effect.runPromise(...)` in the temporary `async execute(...)` implementation, and move the inner logic into `Effect.fn(...)` helpers instead of scattering `runPromise` islands through the tool body.
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* Effect.promise(() => info.init())`.
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info``Effect` cleanup mostly mechanical later.
Individual tools, ordered by value:
- [ ] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
- [ ] `bash.ts` — HIGH: shell orchestration, quoting, timeout handling, output capture
- [x] `read.ts` — HIGH: streaming I/O, readline, binary detection → FileSystem + Stream
- [ ] `edit.ts` — HIGH: multi-step diff/format/publish pipeline, FileWatcher lock
- [ ] `grep.ts` — MEDIUM: spawns ripgrep → ChildProcessSpawner, timeout handling
- [ ] `write.ts` — MEDIUM: permission checks, diagnostics polling, Bus events
- [ ] `codesearch.ts` — MEDIUM: HTTP + SSE + manual timeout → HttpClient + Effect.timeout
- [ ] `webfetch.ts` — MEDIUM: fetch with UA retry, size limits → HttpClient
- [ ] `websearch.ts` — MEDIUM: MCP over HTTP → HttpClient
- [ ] `batch.ts` — MEDIUM: parallel execution, per-call error recovery → Effect.all
- [ ] `task.ts` — MEDIUM: task state management
- [ ] `ls.ts` — MEDIUM: bounded directory listing over ripgrep-backed traversal
- [ ] `multiedit.ts` — MEDIUM: sequential edit orchestration over `edit.ts`
- [ ] `glob.ts` — LOW: simple async generator
- [ ] `lsp.ts` — LOW: dispatch switch over LSP operations
- [ ] `question.ts` — LOW: prompt wrapper
- [ ] `skill.ts` — LOW: skill tool adapter
- [ ] `todo.ts` — LOW: todo persistence wrapper
- [ ] `invalid.ts` — LOW: invalid-tool fallback
- [ ] `plan.ts` — LOW: plan file operations
## Effect service adoption in already-migrated code ## Effect service adoption in already-migrated code
@@ -280,27 +240,19 @@ Some already-effectified areas still use raw `Filesystem.*` or `Process.spawn` i
### `Filesystem.*``AppFileSystem.Service` (yield in layer) ### `Filesystem.*``AppFileSystem.Service` (yield in layer)
- [ ] `file/index.ts` — 1 remaining `Filesystem.readText()` call in untracked diff handling - [x] `config/config.ts``installDependencies()` now uses `AppFileSystem`
- [ ] `config/config.ts` 5 remaining `Filesystem.*` calls in `installDependencies()` - [x] `provider/provider.ts` — recent model state now reads via `AppFileSystem.Service`
- [ ] `provider/provider.ts` — 1 remaining `Filesystem.readJson()` call for recent model state
### `Process.spawn``ChildProcessSpawner` (yield in layer) ### `Process.spawn``ChildProcessSpawner` (yield in layer)
- [ ] `format/formatter.ts`2 remaining `Process.spawn()` checks (`air`, `uv`) - [x] `format/formatter.ts`direct `Process.spawn()` checks removed (`air`, `uv`)
- [ ] `lsp/server.ts` — multiple `Process.spawn()` installs/download helpers - [ ] `lsp/server.ts` — multiple `Process.spawn()` installs/download helpers
## Filesystem consolidation ## Filesystem consolidation
`util/filesystem.ts` (raw fs wrapper) is currently imported by **34 files**. The effectified `AppFileSystem` service (`filesystem/index.ts`) is currently imported by **15 files**. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` — this happens naturally during each migration, not as a separate effort. `util/filesystem.ts` is still used widely across `src/`, and raw `fs` / `fs/promises` imports still exist in multiple tooling and infrastructure files. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` where possible — this should happen naturally during each migration, not as a separate sweep.
Similarly, **21 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched. Tool-specific filesystem cleanup notes live in `tools.md`.
Current raw fs users that will convert during tool migration:
- `tool/read.ts` — fs.createReadStream, readline
- `tool/apply_patch.ts` — fs/promises
- `file/ripgrep.ts` — fs/promises
- `patch/index.ts` — fs, fs/promises
## Primitives & utilities ## Primitives & utilities
@@ -308,3 +260,48 @@ Current raw fs users that will convert during tool migration:
- [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer - [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer
- [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise - [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise
- [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code - [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code
## Destroying the facades
This phase is still broadly open. As of 2026-04-11 there are still 31 `makeRuntime(...)` call sites under `src/`, and many service namespaces still export async facade helpers like `export async function read(...) { return runPromise(...) }`.
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
### Process
For each service, the migration is roughly:
1. **Find callers.** `grep -n "Namespace\.(methodA|methodB|...)"` across `src/` and `test/`. Skip the service file itself.
2. **Migrate production callers.** For each effectful caller that does `Effect.tryPromise(() => Namespace.method(...))`:
- Add the service to the caller's layer R type (`Layer.Layer<Self, never, ... | Namespace.Service>`)
- Yield it at the top of the layer: `const ns = yield* Namespace.Service`
- Replace `Effect.tryPromise(() => Namespace.method(...))` with `yield* ns.method(...)` (or `ns.method(...).pipe(Effect.orElseSucceed(...))` for the common fallback case)
- Add `Layer.provide(Namespace.defaultLayer)` to the caller's own `defaultLayer` chain
3. **Fix tests that used the caller's raw `.layer`.** Any test that composes `Caller.layer` (not `defaultLayer`) needs to also provide the newly-required service tag. The fastest fix is usually switching to `Caller.defaultLayer` since it now pulls in the new dependency.
4. **Migrate test callers of the facade.** Tests calling `Namespace.method(...)` directly get converted to full effectful style using `testEffect(Namespace.defaultLayer)` + `it.live` / `it.effect` + `yield* svc.method(...)`. Don't wrap the test body in `Effect.promise(async () => {...})` — do the whole thing in `Effect.gen` and use `AppFileSystem.Service` / `tmpdirScoped` / `Effect.addFinalizer` for what used to be raw `fs` / `Bun.write` / `try/finally`.
5. **Delete the facades.** Once `grep` shows zero callers, remove the `export async function` block AND the `makeRuntime(...)` line from the service namespace. Also remove the now-unused `import { makeRuntime }`.
### Pitfalls
- **Layer caching inside tests.** `testEffect(layer)` constructs the Storage (or whatever) service once and memoizes it. If a test then tries `inner.pipe(Effect.provide(customStorage))` to swap in a differently-configured Storage, the outer cached one wins and the inner provision is a no-op. Fix: wrap the overriding layer in `Layer.fresh(...)`, which forces a new instance to be built instead of hitting the memoMap cache. This lets a single `testEffect(...)` serve both simple and per-test-customized cases.
- **`Effect.tryPromise``yield*` drops the Promise layer.** The old code was `Effect.tryPromise(() => Storage.read(...))` — a `tryPromise` wrapper because the facade returned a Promise. The new code is `yield* storage.read(...)` directly — the service method already returns an Effect, so no wrapper is needed. Don't reach for `Effect.promise` or `Effect.tryPromise` during migration; if you're using them on a service method call, you're doing it wrong.
- **Raw `.layer` test callers break silently in the type checker.** When you add a new R requirement to a service's `.layer`, any test that composes it raw (not `defaultLayer`) becomes under-specified. `tsgo` will flag this — the error looks like `Type 'Storage.Service' is not assignable to type '... | Service | TestConsole'`. Usually the fix is to switch that composition to `defaultLayer`, or add `Layer.provide(NewDep.defaultLayer)` to the custom composition.
- **Tests that do async setup with `fs`, `Bun.write`, `tmpdir`.** Convert these to `AppFileSystem.Service` calls inside `Effect.gen`, and use `tmpdirScoped()` instead of `tmpdir()` so cleanup happens via the scope finalizer. For file operations on the actual filesystem (not via a service), a small helper like `const writeJson = Effect.fnUntraced(function* (file, value) { const fs = yield* AppFileSystem.Service; yield* fs.makeDirectory(path.dirname(file), { recursive: true }); yield* fs.writeFileString(file, JSON.stringify(value, null, 2)) })` keeps the migration tests clean.
### Migration log
- `SessionStatus` — migrated 2026-04-11. Replaced the last route and retry-policy callers with `AppRuntime.runPromise(SessionStatus.Service.use(...))` and removed the `makeRuntime(...)` facade.
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed.
- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed.
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
- `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed.
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
## Route handler effectification
Route-handler migration guidance and checklist live in `routes.md`.
+66
View File
@@ -0,0 +1,66 @@
# Route handler effectification
Practical reference for converting server route handlers in `packages/opencode` to a single `AppRuntime.runPromise(Effect.gen(...))` body.
## Goal
Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one.
This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
```ts
// Before - one facade call per service
;async (c) => {
await SessionRunState.assertNotBusy(id)
await Session.removeMessage({ sessionID: id, messageID })
return c.json(true)
}
// After - one Effect.gen, yield services from context
;async (c) => {
await AppRuntime.runPromise(
Effect.gen(function* () {
const state = yield* SessionRunState.Service
const session = yield* Session.Service
yield* state.assertNotBusy(id)
yield* session.removeMessage({ sessionID: id, messageID })
}),
)
return c.json(true)
}
```
## Rules
- Wrap the whole handler body in one `AppRuntime.runPromise(Effect.gen(...))` call when the handler is service-heavy.
- Yield services from context instead of calling async facades repeatedly.
- When independent service calls can run in parallel, use `Effect.all(..., { concurrency: "unbounded" })`.
- Prefer one composed Effect body over multiple separate `runPromise(...)` calls in the same handler.
## Current route files
Current instance route files live under `src/server/instance`, not `server/routes`.
The main migration targets are:
- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus
- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions
- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider
- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style
- [ ] `server/instance/pty.ts` — still calls Pty facades directly
- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
Additional route files that still participate in the migration:
- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format
- [ ] `server/instance/file.ts` — Ripgrep, File, LSP
- [ ] `server/instance/mcp.ts` — MCP facade-heavy
- [ ] `server/instance/permission.ts` — Permission
- [ ] `server/instance/workspace.ts` — Workspace
- [ ] `server/instance/tui.ts` — Bus and Session
- [ ] `server/instance/middleware.ts` — Session and Workspace lookups
## Notes
- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style.
- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly.
+99
View File
@@ -0,0 +1,99 @@
# Schema migration
Practical reference for migrating data types in `packages/opencode` from Zod-first definitions to Effect Schema with Zod compatibility shims.
## Goal
Use Effect Schema as the source of truth for domain models, IDs, inputs, outputs, and typed errors.
Keep Zod available at existing HTTP, tool, and compatibility boundaries by exposing a `.zod` field derived from the Effect schema.
## Preferred shapes
### Data objects
Use `Schema.Class` for structured data.
```ts
export class Info extends Schema.Class<Info>("Foo.Info")({
id: FooID,
name: Schema.String,
enabled: Schema.Boolean,
}) {
static readonly zod = zod(Info)
}
```
If the class cannot reference itself cleanly during initialization, use the existing two-step pattern:
```ts
const _Info = Schema.Struct({
id: FooID,
name: Schema.String,
})
export const Info = Object.assign(_Info, {
zod: zod(_Info),
})
```
### Errors
Use `Schema.TaggedErrorClass` for domain errors.
```ts
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("FooNotFoundError", {
id: FooID,
}) {}
```
### IDs and branded leaf types
Keep branded/schema-backed IDs as Effect schemas and expose `static readonly zod` for compatibility when callers still expect Zod.
## Compatibility rule
During migration, route validators, tool parameters, and any existing Zod-based boundary should consume the derived `.zod` schema instead of maintaining a second hand-written Zod schema.
The default should be:
- Effect Schema owns the type
- `.zod` exists only as a compatibility surface
- new domain models should not start Zod-first unless there is a concrete boundary-specific need
## When Zod can stay
It is fine to keep a Zod-native schema temporarily when:
- the type is only used at an HTTP or tool boundary
- the validator depends on Zod-only transforms or behavior not yet covered by `zod()`
- the migration would force unrelated churn across a large call graph
When this happens, prefer leaving a short note or TODO rather than silently creating a parallel schema source of truth.
## Ordering
Migrate in this order:
1. Shared leaf models and `schema.ts` files
2. Exported `Info`, `Input`, `Output`, and DTO types
3. Tagged domain errors
4. Service-local internal models
5. Route and tool boundary validators that can switch to `.zod`
This keeps shared types canonical first and makes boundary updates mostly mechanical.
## Checklist
- [ ] Shared `schema.ts` leaf models are Effect Schema-first
- [ ] Exported `Info` / `Input` / `Output` types use `Schema.Class` where appropriate
- [ ] Domain errors use `Schema.TaggedErrorClass`
- [ ] Migrated types expose `.zod` for back compatibility
- [ ] Route and tool validators consume derived `.zod` instead of duplicate Zod definitions
- [ ] New domain models default to Effect Schema first
## Notes
- Use `@/util/effect-zod` for all Schema -> Zod conversion.
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and Effect definitions for the same domain type.
- Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change.
+96
View File
@@ -0,0 +1,96 @@
# Tool migration
Practical reference for the current tool-migration state in `packages/opencode`.
## Status
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch, and the built-in tool surface is now largely on the target shape.
The current exported tools in `src/tool` all use `Tool.define(...)` with Effect-based initialization, and nearly all of them already build their tool body with `Effect.gen(...)` and `Effect.fn(...)`.
So the remaining work is no longer "convert tools to Effect at all". The remaining work is mostly:
1. remove Promise and raw platform bridges inside individual tool bodies
2. swap tool internals to Effect-native services like `AppFileSystem`, `HttpClient`, and `ChildProcessSpawner`
3. keep tests and callers aligned with `yield* info.init()` and real service graphs
## Current shape
`Tool.define(...)` is already the Effect-native helper here.
- `init` is an `Effect`
- `info.init()` returns an `Effect`
- `execute(...)` returns an `Effect`
That means a tool does not need a separate `Tool.defineEffect(...)` helper to count as migrated. A tool is effectively migrated when its init and execute path stay Effect-native, even if some internals still bridge to Promise-based or raw APIs.
## Tests
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
This keeps tool tests aligned with the production service graph and makes follow-up cleanup mostly mechanical.
## Exported tools
These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path:
- [x] `apply_patch.ts`
- [x] `bash.ts`
- [x] `codesearch.ts`
- [x] `edit.ts`
- [x] `glob.ts`
- [x] `grep.ts`
- [x] `invalid.ts`
- [x] `ls.ts`
- [x] `lsp.ts`
- [x] `multiedit.ts`
- [x] `plan.ts`
- [x] `question.ts`
- [x] `read.ts`
- [x] `skill.ts`
- [x] `task.ts`
- [x] `todo.ts`
- [x] `webfetch.ts`
- [x] `websearch.ts`
- [x] `write.ts`
Notes:
- `batch.ts` is no longer a current tool file and should not be tracked here.
- `truncate.ts` is an Effect service used by tools, not a tool definition itself.
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
## Follow-up cleanup
Most exported tools are already on the intended Effect-native shape. The remaining cleanup is narrower than the old checklist implied.
Current spot cleanups worth tracking:
- [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts`
- [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
- `apply_patch.ts`
- `grep.ts`
- `write.ts`
- `codesearch.ts`
- `websearch.ts`
- `ls.ts`
- `multiedit.ts`
- `edit.ts`
## Filesystem notes
Current raw fs users that still appear relevant here:
- `tool/read.ts``fs.createReadStream`, `readline`
- `file/ripgrep.ts``fs/promises`
- `patch/index.ts``fs`, `fs/promises`
+1 -4
View File
@@ -202,7 +202,7 @@ Top-level API groups exposed to `tui(api, options, meta)`:
- `api.kv.get`, `set`, `ready` - `api.kv.get`, `set`, `ready`
- `api.state` - `api.state`
- `api.theme.current`, `selected`, `has`, `set`, `install`, `mode`, `ready` - `api.theme.current`, `selected`, `has`, `set`, `install`, `mode`, `ready`
- `api.client`, `api.scopedClient(workspaceID?)`, `api.workspace.current()`, `api.workspace.set(workspaceID?)` - `api.client`
- `api.event.on(type, handler)` - `api.event.on(type, handler)`
- `api.renderer` - `api.renderer`
- `api.slots.register(plugin)` - `api.slots.register(plugin)`
@@ -270,7 +270,6 @@ Command behavior:
- `provider` - `provider`
- `path.{state,config,worktree,directory}` - `path.{state,config,worktree,directory}`
- `vcs?.branch` - `vcs?.branch`
- `workspace.list()` / `workspace.get(workspaceID)`
- `session.count()` - `session.count()`
- `session.diff(sessionID)` - `session.diff(sessionID)`
- `session.todo(sessionID)` - `session.todo(sessionID)`
@@ -282,8 +281,6 @@ Command behavior:
- `lsp()` - `lsp()`
- `mcp()` - `mcp()`
- `api.client` always reflects the current runtime client. - `api.client` always reflects the current runtime client.
- `api.scopedClient(workspaceID?)` creates or reuses a client bound to a workspace.
- `api.workspace.set(...)` rebinds the active workspace; `api.client` follows that rebind.
- `api.event.on(type, handler)` subscribes to the TUI event stream and returns an unsubscribe function. - `api.event.on(type, handler)` subscribes to the TUI event stream and returns an unsubscribe function.
- `api.renderer` exposes the raw `CliRenderer`. - `api.renderer` exposes the raw `CliRenderer`.
+2 -34
View File
@@ -1,4 +1,4 @@
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect" import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
import { import {
FetchHttpClient, FetchHttpClient,
HttpClient, HttpClient,
@@ -7,7 +7,6 @@ import {
HttpClientResponse, HttpClientResponse,
} from "effect/unstable/http" } from "effect/unstable/http"
import { makeRuntime } from "@/effect/run-service"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { AccountRepo, type AccountRow } from "./repo" import { AccountRepo, type AccountRow } from "./repo"
import { normalizeServerUrl } from "./url" import { normalizeServerUrl } from "./url"
@@ -181,7 +180,7 @@ export namespace Account {
readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError> readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
} }
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Account") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect( export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
Service, Service,
@@ -454,35 +453,4 @@ export namespace Account {
) )
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer)) export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
export const { runPromise } = makeRuntime(Service, defaultLayer)
export async function active(): Promise<Info | undefined> {
return Option.getOrUndefined(await runPromise((service) => service.active()))
}
export async function list(): Promise<Info[]> {
return runPromise((service) => service.list())
}
export async function activeOrg(): Promise<ActiveOrg | undefined> {
return Option.getOrUndefined(await runPromise((service) => service.activeOrg()))
}
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
return runPromise((service) => service.orgsByAccount())
}
export async function orgs(accountID: AccountID): Promise<readonly Org[]> {
return runPromise((service) => service.orgs(accountID))
}
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
return runPromise((service) => service.use(accountID, Option.some(orgID)))
}
export async function token(accountID: AccountID): Promise<AccessToken | undefined> {
const t = await runPromise((service) => service.token(accountID))
return Option.getOrUndefined(t)
}
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Effect, Layer, Option, Schema, ServiceMap } from "effect" import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
import { AccountStateTable, AccountTable } from "./account.sql" import { AccountStateTable, AccountTable } from "./account.sql"
@@ -38,7 +38,7 @@ export namespace AccountRepo {
} }
} }
export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") { export class AccountRepo extends Context.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
static readonly layer: Layer.Layer<AccountRepo> = Layer.effect( static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
AccountRepo, AccountRepo,
Effect.gen(function* () { Effect.gen(function* () {
+6 -26
View File
@@ -1,42 +1,22 @@
import { Schema } from "effect" import { Schema } from "effect"
import type * as HttpClientError from "effect/unstable/http/HttpClientError" import type * as HttpClientError from "effect/unstable/http/HttpClientError"
import { withStatics } from "@/util/schema" export const AccountID = Schema.String.pipe(Schema.brand("AccountID"))
export const AccountID = Schema.String.pipe(
Schema.brand("AccountID"),
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
)
export type AccountID = Schema.Schema.Type<typeof AccountID> export type AccountID = Schema.Schema.Type<typeof AccountID>
export const OrgID = Schema.String.pipe( export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
Schema.brand("OrgID"),
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
)
export type OrgID = Schema.Schema.Type<typeof OrgID> export type OrgID = Schema.Schema.Type<typeof OrgID>
export const AccessToken = Schema.String.pipe( export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
Schema.brand("AccessToken"),
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
)
export type AccessToken = Schema.Schema.Type<typeof AccessToken> export type AccessToken = Schema.Schema.Type<typeof AccessToken>
export const RefreshToken = Schema.String.pipe( export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
Schema.brand("RefreshToken"),
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
)
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken> export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
export const DeviceCode = Schema.String.pipe( export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
Schema.brand("DeviceCode"),
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
)
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode> export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
export const UserCode = Schema.String.pipe( export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
Schema.brand("UserCode"),
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
)
export type UserCode = Schema.Schema.Type<typeof UserCode> export type UserCode = Schema.Schema.Type<typeof UserCode>
export class Info extends Schema.Class<Info>("Account")({ export class Info extends Schema.Class<Info>("Account")({
+16 -11
View File
@@ -19,7 +19,7 @@ import { Global } from "@/global"
import path from "path" import path from "path"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Effect, ServiceMap, Layer } from "effect" import { Effect, Context, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
@@ -67,7 +67,7 @@ export namespace Agent {
type State = Omit<Interface, "generate"> type State = Omit<Interface, "generate">
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -341,6 +341,10 @@ export namespace Agent {
) )
const existing = yield* InstanceState.useEffect(state, (s) => s.list()) const existing = yield* InstanceState.useEffect(state, (s) => s.list())
// TODO: clean this up so provider specific logic doesnt bleed over
const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth"
const params = { const params = {
experimental_telemetry: { experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry, isEnabled: cfg.experimental?.openTelemetry,
@@ -350,12 +354,14 @@ export namespace Agent {
}, },
temperature: 0.3, temperature: 0.3,
messages: [ messages: [
...system.map( ...(isOpenaiOauth
(item): ModelMessage => ({ ? []
role: "system", : system.map(
content: item, (item): ModelMessage => ({
}), role: "system",
), content: item,
}),
)),
{ {
role: "user", role: "user",
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
@@ -369,13 +375,12 @@ export namespace Agent {
}), }),
} satisfies Parameters<typeof generateObject>[0] } satisfies Parameters<typeof generateObject>[0]
// TODO: clean this up so provider specific logic doesnt bleed over if (isOpenaiOauth) {
const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
if (model.providerID === "openai" && authInfo?.type === "oauth") {
return yield* Effect.promise(async () => { return yield* Effect.promise(async () => {
const result = streamObject({ const result = streamObject({
...params, ...params,
providerOptions: ProviderTransform.providerOptions(resolved, { providerOptions: ProviderTransform.providerOptions(resolved, {
instructions: system.join("\n"),
store: false, store: false,
}), }),
onError: () => {}, onError: () => {},
+2 -21
View File
@@ -1,6 +1,5 @@
import path from "path" import path from "path"
import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect" import { Effect, Layer, Record, Result, Schema, Context } from "effect"
import { makeRuntime } from "@/effect/run-service"
import { zod } from "@/util/effect-zod" import { zod } from "@/util/effect-zod"
import { Global } from "../global" import { Global } from "../global"
import { AppFileSystem } from "../filesystem" import { AppFileSystem } from "../filesystem"
@@ -49,7 +48,7 @@ export namespace Auth {
readonly remove: (key: string) => Effect.Effect<void, AuthError> readonly remove: (key: string) => Effect.Effect<void, AuthError>
} }
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Auth") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -89,22 +88,4 @@ export namespace Auth {
) )
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function get(providerID: string) {
return runPromise((service) => service.get(providerID))
}
export async function all(): Promise<Record<string, Info>> {
return runPromise((service) => service.all())
}
export async function set(key: string, info: Info) {
return runPromise((service) => service.set(key, info))
}
export async function remove(key: string) {
return runPromise((service) => service.remove(key))
}
} }
+2
View File
@@ -4,6 +4,8 @@ export const GlobalBus = new EventEmitter<{
event: [ event: [
{ {
directory?: string directory?: string
project?: string
workspace?: string
payload: any payload: any
}, },
] ]
+12 -4
View File
@@ -1,9 +1,10 @@
import z from "zod" import z from "zod"
import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect" import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
import { EffectLogger } from "@/effect/logger"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Instance } from "../project/instance"
import { BusEvent } from "./bus-event" import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global" import { GlobalBus } from "./global"
import { WorkspaceContext } from "@/control-plane/workspace-context"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
@@ -41,7 +42,7 @@ export namespace Bus {
readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void> readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void>
} }
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Bus") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -91,8 +92,13 @@ export namespace Bus {
yield* PubSub.publish(s.wildcard, payload) yield* PubSub.publish(s.wildcard, payload)
const dir = yield* InstanceState.directory const dir = yield* InstanceState.directory
const context = yield* InstanceState.context
const workspace = yield* InstanceState.workspaceID
GlobalBus.emit("event", { GlobalBus.emit("event", {
directory: dir, directory: dir,
project: context.project.id,
workspace,
payload, payload,
}) })
}) })
@@ -141,7 +147,7 @@ export namespace Bus {
return () => { return () => {
log.info("unsubscribing", { type }) log.info("unsubscribing", { type })
Effect.runFork(Scope.close(scope, Exit.void)) Effect.runFork(Scope.close(scope, Exit.void).pipe(Effect.provide(EffectLogger.layer)))
} }
}) })
} }
@@ -164,6 +170,8 @@ export namespace Bus {
}), }),
) )
export const defaultLayer = layer
const { runPromise, runSync } = makeRuntime(Service, layer) const { runPromise, runSync } = makeRuntime(Service, layer)
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe, // runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
+2 -1
View File
@@ -1,10 +1,11 @@
import { AppRuntime } from "@/effect/app-runtime"
import { InstanceBootstrap } from "../project/bootstrap" import { InstanceBootstrap } from "../project/bootstrap"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) { export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
return Instance.provide({ return Instance.provide({
directory, directory,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: async () => { fn: async () => {
try { try {
const result = await cb() const result = await cb()
+6 -5
View File
@@ -3,6 +3,7 @@ import { Duration, Effect, Match, Option } from "effect"
import { UI } from "../ui" import { UI } from "../ui"
import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account" import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
import { type AccountError } from "@/account/schema" import { type AccountError } from "@/account/schema"
import { AppRuntime } from "@/effect/app-runtime"
import * as Prompt from "../effect/prompt" import * as Prompt from "../effect/prompt"
import open from "open" import open from "open"
@@ -182,7 +183,7 @@ export const LoginCommand = cmd({
}), }),
async handler(args) { async handler(args) {
UI.empty() UI.empty()
await Account.runPromise((_svc) => loginEffect(args.url)) await AppRuntime.runPromise(loginEffect(args.url))
}, },
}) })
@@ -196,7 +197,7 @@ export const LogoutCommand = cmd({
}), }),
async handler(args) { async handler(args) {
UI.empty() UI.empty()
await Account.runPromise((_svc) => logoutEffect(args.email)) await AppRuntime.runPromise(logoutEffect(args.email))
}, },
}) })
@@ -205,7 +206,7 @@ export const SwitchCommand = cmd({
describe: false, describe: false,
async handler() { async handler() {
UI.empty() UI.empty()
await Account.runPromise((_svc) => switchEffect()) await AppRuntime.runPromise(switchEffect())
}, },
}) })
@@ -214,7 +215,7 @@ export const OrgsCommand = cmd({
describe: false, describe: false,
async handler() { async handler() {
UI.empty() UI.empty()
await Account.runPromise((_svc) => orgsEffect()) await AppRuntime.runPromise(orgsEffect())
}, },
}) })
@@ -223,7 +224,7 @@ export const OpenCommand = cmd({
describe: false, describe: false,
async handler() { async handler() {
UI.empty() UI.empty()
await Account.runPromise((_svc) => openEffect()) await AppRuntime.runPromise(openEffect())
}, },
}) })
+2 -1
View File
@@ -1,6 +1,7 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { spawn } from "child_process" import { spawn } from "child_process"
import { Database } from "../../storage/db" import { Database } from "../../storage/db"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { Database as BunDatabase } from "bun:sqlite" import { Database as BunDatabase } from "bun:sqlite"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
@@ -74,7 +75,7 @@ const MigrateCommand = cmd({
let last = -1 let last = -1
if (tty) process.stderr.write("\x1b[?25l") if (tty) process.stderr.write("\x1b[?25l")
try { try {
const stats = await JsonMigration.run(sqlite, { const stats = await JsonMigration.run(drizzle({ client: sqlite }), {
progress: (event) => { progress: (event) => {
const percent = Math.floor((event.current / event.total) * 100) const percent = Math.floor((event.current / event.total) * 100)
if (percent === last) return if (percent === last) return
+30 -13
View File
@@ -1,5 +1,6 @@
import { EOL } from "os" import { EOL } from "os"
import { basename } from "path" import { basename } from "path"
import { Effect } from "effect"
import { Agent } from "../../../agent/agent" import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider/provider" import { Provider } from "../../../provider/provider"
import { Session } from "../../../session" import { Session } from "../../../session"
@@ -11,6 +12,7 @@ import { Permission } from "../../../permission"
import { iife } from "../../../util/iife" import { iife } from "../../../util/iife"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { AppRuntime } from "@/effect/app-runtime"
export const AgentCommand = cmd({ export const AgentCommand = cmd({
command: "agent <name>", command: "agent <name>",
@@ -70,11 +72,17 @@ export const AgentCommand = cmd({
}) })
async function getAvailableTools(agent: Agent.Info) { async function getAvailableTools(agent: Agent.Info) {
const model = agent.model ?? (await Provider.defaultModel()) return AppRuntime.runPromise(
return ToolRegistry.tools({ Effect.gen(function* () {
...model, const provider = yield* Provider.Service
agent, const registry = yield* ToolRegistry.Service
}) const model = agent.model ?? (yield* provider.defaultModel())
return yield* registry.tools({
...model,
agent,
})
}),
)
} }
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) { async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
@@ -117,7 +125,14 @@ function parseToolParams(input?: string) {
async function createToolContext(agent: Agent.Info) { async function createToolContext(agent: Agent.Info) {
const session = await Session.create({ title: `Debug tool run (${agent.name})` }) const session = await Session.create({ title: `Debug tool run (${agent.name})` })
const messageID = MessageID.ascending() const messageID = MessageID.ascending()
const model = agent.model ?? (await Provider.defaultModel()) const model =
agent.model ??
(await AppRuntime.runPromise(
Effect.gen(function* () {
const provider = yield* Provider.Service
return yield* provider.defaultModel()
}),
))
const now = Date.now() const now = Date.now()
const message: MessageV2.Assistant = { const message: MessageV2.Assistant = {
id: messageID, id: messageID,
@@ -157,14 +172,16 @@ async function createToolContext(agent: Agent.Info) {
agent: agent.name, agent: agent.name,
abort: new AbortController().signal, abort: new AbortController().signal,
messages: [], messages: [],
metadata: () => {}, metadata: () => Effect.void,
async ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) { ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
for (const pattern of req.patterns) { return Effect.sync(() => {
const rule = Permission.evaluate(req.permission, pattern, ruleset) for (const pattern of req.patterns) {
if (rule.action === "deny") { const rule = Permission.evaluate(req.permission, pattern, ruleset)
throw new Permission.DeniedError({ ruleset }) if (rule.action === "deny") {
throw new Permission.DeniedError({ ruleset })
}
} }
} })
}, },
} }
} }
+6 -4
View File
@@ -1,4 +1,6 @@
import { EOL } from "os" import { EOL } from "os"
import { Effect } from "effect"
import { AppRuntime } from "@/effect/app-runtime"
import { File } from "../../../file" import { File } from "../../../file"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
@@ -15,7 +17,7 @@ const FileSearchCommand = cmd({
}), }),
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const results = await File.search({ query: args.query }) const results = await AppRuntime.runPromise(File.Service.use((svc) => svc.search({ query: args.query })))
process.stdout.write(results.join(EOL) + EOL) process.stdout.write(results.join(EOL) + EOL)
}) })
}, },
@@ -32,7 +34,7 @@ const FileReadCommand = cmd({
}), }),
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const content = await File.read(args.path) const content = await AppRuntime.runPromise(File.Service.use((svc) => svc.read(args.path)))
process.stdout.write(JSON.stringify(content, null, 2) + EOL) process.stdout.write(JSON.stringify(content, null, 2) + EOL)
}) })
}, },
@@ -44,7 +46,7 @@ const FileStatusCommand = cmd({
builder: (yargs) => yargs, builder: (yargs) => yargs,
async handler() { async handler() {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const status = await File.status() const status = await AppRuntime.runPromise(File.Service.use((svc) => svc.status()))
process.stdout.write(JSON.stringify(status, null, 2) + EOL) process.stdout.write(JSON.stringify(status, null, 2) + EOL)
}) })
}, },
@@ -61,7 +63,7 @@ const FileListCommand = cmd({
}), }),
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const files = await File.list(args.path) const files = await AppRuntime.runPromise(File.Service.use((svc) => svc.list(args.path)))
process.stdout.write(JSON.stringify(files, null, 2) + EOL) process.stdout.write(JSON.stringify(files, null, 2) + EOL)
}) })
}, },
+14 -5
View File
@@ -1,4 +1,6 @@
import { LSP } from "../../../lsp" import { LSP } from "../../../lsp"
import { AppRuntime } from "../../../effect/app-runtime"
import { Effect } from "effect"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { Log } from "../../../util/log" import { Log } from "../../../util/log"
@@ -19,9 +21,16 @@ const DiagnosticsCommand = cmd({
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }), builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
await LSP.touchFile(args.file, true) const out = await AppRuntime.runPromise(
await sleep(1000) LSP.Service.use((lsp) =>
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL) Effect.gen(function* () {
yield* lsp.touchFile(args.file, true)
yield* Effect.sleep(1000)
return yield* lsp.diagnostics()
}),
),
)
process.stdout.write(JSON.stringify(out, null, 2) + EOL)
}) })
}, },
}) })
@@ -33,7 +42,7 @@ export const SymbolsCommand = cmd({
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
using _ = Log.Default.time("symbols") using _ = Log.Default.time("symbols")
const results = await LSP.workspaceSymbol(args.query) const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
process.stdout.write(JSON.stringify(results, null, 2) + EOL) process.stdout.write(JSON.stringify(results, null, 2) + EOL)
}) })
}, },
@@ -46,7 +55,7 @@ export const DocumentSymbolsCommand = cmd({
async handler(args) { async handler(args) {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
using _ = Log.Default.time("document-symbols") using _ = Log.Default.time("document-symbols")
const results = await LSP.documentSymbol(args.uri) const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
process.stdout.write(JSON.stringify(results, null, 2) + EOL) process.stdout.write(JSON.stringify(results, null, 2) + EOL)
}) })
}, },
+13 -6
View File
@@ -1,4 +1,5 @@
import { EOL } from "os" import { EOL } from "os"
import { AppRuntime } from "../../../effect/app-runtime"
import { Ripgrep } from "../../../file/ripgrep" import { Ripgrep } from "../../../file/ripgrep"
import { Instance } from "../../../project/instance" import { Instance } from "../../../project/instance"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
@@ -76,12 +77,18 @@ const SearchCommand = cmd({
description: "Limit number of results", description: "Limit number of results",
}), }),
async handler(args) { async handler(args) {
const results = await Ripgrep.search({ await bootstrap(process.cwd(), async () => {
cwd: process.cwd(), const results = await AppRuntime.runPromise(
pattern: args.pattern, Ripgrep.Service.use((svc) =>
glob: args.glob as string[] | undefined, svc.search({
limit: args.limit, cwd: Instance.directory,
pattern: args.pattern,
glob: args.glob as string[] | undefined,
limit: args.limit,
}),
),
)
process.stdout.write(JSON.stringify(results.items, null, 2) + EOL)
}) })
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
}, },
}) })
+8 -1
View File
@@ -1,4 +1,6 @@
import { EOL } from "os" import { EOL } from "os"
import { Effect } from "effect"
import { AppRuntime } from "@/effect/app-runtime"
import { Skill } from "../../../skill" import { Skill } from "../../../skill"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
@@ -9,7 +11,12 @@ export const SkillCommand = cmd({
builder: (yargs) => yargs, builder: (yargs) => yargs,
async handler() { async handler() {
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const skills = await Skill.all() const skills = await AppRuntime.runPromise(
Effect.gen(function* () {
const skill = yield* Skill.Service
return yield* skill.all()
}),
)
process.stdout.write(JSON.stringify(skills, null, 2) + EOL) process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
}) })
}, },
+10 -5
View File
@@ -21,6 +21,7 @@ import { cmd } from "./cmd"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider/models"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share/session"
import { Session } from "../../session" import { Session } from "../../session"
import type { SessionID } from "../../session/schema" import type { SessionID } from "../../session/schema"
import { MessageID, PartID } from "../../session/schema" import { MessageID, PartID } from "../../session/schema"
@@ -28,6 +29,7 @@ import { Provider } from "../../provider/provider"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt" import { SessionPrompt } from "@/session/prompt"
import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { setTimeout as sleep } from "node:timers/promises" import { setTimeout as sleep } from "node:timers/promises"
import { Process } from "@/util/process" import { Process } from "@/util/process"
@@ -257,7 +259,9 @@ export const GithubInstallCommand = cmd({
} }
// Get repo info // Get repo info
const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim() const info = await AppRuntime.runPromise(
Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
).then((x) => x.text().trim())
const parsed = parseGitHubRemote(info) const parsed = parseGitHubRemote(info)
if (!parsed) { if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`) prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
@@ -496,20 +500,21 @@ export const GithubRunCommand = cmd({
: "issue" : "issue"
: undefined : undefined
const gitText = async (args: string[]) => { const gitText = async (args: string[]) => {
const result = await Git.run(args, { cwd: Instance.worktree }) const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr) throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
} }
return result.text().trim() return result.text().trim()
} }
const gitRun = async (args: string[]) => { const gitRun = async (args: string[]) => {
const result = await Git.run(args, { cwd: Instance.worktree }) const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr) throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
} }
return result return result
} }
const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree }) const gitStatus = (args: string[]) =>
AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
const commitChanges = async (summary: string, actor?: string) => { const commitChanges = async (summary: string, actor?: string) => {
const args = ["commit", "-m", summary] const args = ["commit", "-m", summary]
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`) if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
@@ -559,7 +564,7 @@ export const GithubRunCommand = cmd({
shareId = await (async () => { shareId = await (async () => {
if (share === false) return if (share === false) return
if (!share && repoData.data.private) return if (!share && repoData.data.private) return
await Session.share(session.id) await SessionShare.share(session.id)
return session.id.slice(-8) return session.id.slice(-8)
})() })()
console.log("opencode session", session.id) console.log("opencode session", session.id)
+3 -2
View File
@@ -10,6 +10,7 @@ import { Instance } from "../../project/instance"
import { ShareNext } from "../../share/share-next" import { ShareNext } from "../../share/share-next"
import { EOL } from "os" import { EOL } from "os"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util/filesystem"
import { AppRuntime } from "@/effect/app-runtime"
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */ /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
export type ShareData = export type ShareData =
@@ -100,7 +101,7 @@ export const ImportCommand = cmd({
if (isUrl) { if (isUrl) {
const slug = parseShareUrl(args.file) const slug = parseShareUrl(args.file)
if (!slug) { if (!slug) {
const baseUrl = await ShareNext.url() const baseUrl = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.url()))
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`) process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
process.stdout.write(EOL) process.stdout.write(EOL)
return return
@@ -108,7 +109,7 @@ export const ImportCommand = cmd({
const parsed = new URL(args.file) const parsed = new URL(args.file)
const baseUrl = parsed.origin const baseUrl = parsed.origin
const req = await ShareNext.request() const req = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.request()))
const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {} const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {}
const dataPath = req.api.data(slug) const dataPath = req.api.data(slug)
+1
View File
@@ -688,6 +688,7 @@ export const McpDebugCommand = cmd({
clientId: oauthConfig?.clientId, clientId: oauthConfig?.clientId,
clientSecret: oauthConfig?.clientSecret, clientSecret: oauthConfig?.clientSecret,
scope: oauthConfig?.scope, scope: oauthConfig?.scope,
redirectUri: oauthConfig?.redirectUri,
}, },
{ {
onRedirect: async () => {}, onRedirect: async () => {},
+41 -31
View File
@@ -6,6 +6,8 @@ import { ModelsDev } from "../../provider/models"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { UI } from "../ui" import { UI } from "../ui"
import { EOL } from "os" import { EOL } from "os"
import { AppRuntime } from "@/effect/app-runtime"
import { Effect } from "effect"
export const ModelsCommand = cmd({ export const ModelsCommand = cmd({
command: "models [provider]", command: "models [provider]",
@@ -35,43 +37,51 @@ export const ModelsCommand = cmd({
await Instance.provide({ await Instance.provide({
directory: process.cwd(), directory: process.cwd(),
async fn() { async fn() {
const providers = await Provider.list() await AppRuntime.runPromise(
Effect.gen(function* () {
const svc = yield* Provider.Service
const providers = yield* svc.list()
function printModels(providerID: ProviderID, verbose?: boolean) { const print = (providerID: ProviderID, verbose?: boolean) => {
const provider = providers[providerID] const provider = providers[providerID]
const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b)) const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
for (const [modelID, model] of sortedModels) { for (const [modelID, model] of sorted) {
process.stdout.write(`${providerID}/${modelID}`) process.stdout.write(`${providerID}/${modelID}`)
process.stdout.write(EOL) process.stdout.write(EOL)
if (verbose) { if (verbose) {
process.stdout.write(JSON.stringify(model, null, 2)) process.stdout.write(JSON.stringify(model, null, 2))
process.stdout.write(EOL) process.stdout.write(EOL)
}
}
} }
}
}
if (args.provider) { if (args.provider) {
const provider = providers[ProviderID.make(args.provider)] const providerID = ProviderID.make(args.provider)
if (!provider) { const provider = providers[providerID]
UI.error(`Provider not found: ${args.provider}`) if (!provider) {
return yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
} return
}
printModels(ProviderID.make(args.provider), args.verbose) yield* Effect.sync(() => print(providerID, args.verbose))
return return
} }
const providerIDs = Object.keys(providers).sort((a, b) => { const ids = Object.keys(providers).sort((a, b) => {
const aIsOpencode = a.startsWith("opencode") const aIsOpencode = a.startsWith("opencode")
const bIsOpencode = b.startsWith("opencode") const bIsOpencode = b.startsWith("opencode")
if (aIsOpencode && !bIsOpencode) return -1 if (aIsOpencode && !bIsOpencode) return -1
if (!aIsOpencode && bIsOpencode) return 1 if (!aIsOpencode && bIsOpencode) return 1
return a.localeCompare(b) return a.localeCompare(b)
}) })
for (const providerID of providerIDs) { yield* Effect.sync(() => {
printModels(ProviderID.make(providerID), args.verbose) for (const providerID of ids) {
} print(ProviderID.make(providerID), args.verbose)
}
})
}),
)
}, },
}) })
}, },
+18 -7
View File
@@ -1,5 +1,6 @@
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { AppRuntime } from "@/effect/app-runtime"
import { Git } from "@/git" import { Git } from "@/git"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { Process } from "@/util/process" import { Process } from "@/util/process"
@@ -67,19 +68,29 @@ export const PrCommand = cmd({
const remoteName = forkOwner const remoteName = forkOwner
// Check if remote already exists // Check if remote already exists
const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim() const remotes = await AppRuntime.runPromise(
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
).then((x) => x.text().trim())
if (!remotes.split("\n").includes(remoteName)) { if (!remotes.split("\n").includes(remoteName)) {
await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], { await AppRuntime.runPromise(
cwd: Instance.worktree, Git.Service.use((git) =>
}) git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
cwd: Instance.worktree,
}),
),
)
UI.println(`Added fork remote: ${remoteName}`) UI.println(`Added fork remote: ${remoteName}`)
} }
// Set upstream to the fork so pushes go there // Set upstream to the fork so pushes go there
const headRefName = prInfo.headRefName const headRefName = prInfo.headRefName
await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], { await AppRuntime.runPromise(
cwd: Instance.worktree, Git.Service.use((git) =>
}) git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
cwd: Instance.worktree,
}),
),
)
} }
// Check for opencode session link in PR body // Check for opencode session link in PR body
+45 -13
View File
@@ -1,4 +1,5 @@
import { Auth } from "../../auth" import { Auth } from "../../auth"
import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { UI } from "../ui" import { UI } from "../ui"
@@ -13,9 +14,18 @@ import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin" import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util/process" import { Process } from "../../util/process"
import { text } from "node:stream/consumers" import { text } from "node:stream/consumers"
import { Effect } from "effect"
type PluginAuth = NonNullable<Hooks["auth"]> type PluginAuth = NonNullable<Hooks["auth"]>
const put = (key: string, info: Auth.Info) =>
AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.set(key, info)
}),
)
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> { async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
let index = 0 let index = 0
if (methodName) { if (methodName) {
@@ -93,7 +103,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
const saveProvider = result.provider ?? provider const saveProvider = result.provider ?? provider
if ("refresh" in result) { if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
await Auth.set(saveProvider, { await put(saveProvider, {
type: "oauth", type: "oauth",
refresh, refresh,
access, access,
@@ -102,7 +112,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
}) })
} }
if ("key" in result) { if ("key" in result) {
await Auth.set(saveProvider, { await put(saveProvider, {
type: "api", type: "api",
key: result.key, key: result.key,
}) })
@@ -125,7 +135,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
const saveProvider = result.provider ?? provider const saveProvider = result.provider ?? provider
if ("refresh" in result) { if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
await Auth.set(saveProvider, { await put(saveProvider, {
type: "oauth", type: "oauth",
refresh, refresh,
access, access,
@@ -134,7 +144,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
}) })
} }
if ("key" in result) { if ("key" in result) {
await Auth.set(saveProvider, { await put(saveProvider, {
type: "api", type: "api",
key: result.key, key: result.key,
}) })
@@ -148,6 +158,12 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
} }
if (method.type === "api") { if (method.type === "api") {
const key = await prompts.password({
message: "Enter your API key",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(key)) throw new UI.CancelledError()
if (method.authorize) { if (method.authorize) {
const result = await method.authorize(inputs) const result = await method.authorize(inputs)
if (result.type === "failed") { if (result.type === "failed") {
@@ -155,9 +171,9 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
} }
if (result.type === "success") { if (result.type === "success") {
const saveProvider = result.provider ?? provider const saveProvider = result.provider ?? provider
await Auth.set(saveProvider, { await put(saveProvider, {
type: "api", type: "api",
key: result.key, key: result.key ?? key,
}) })
prompts.log.success("Login successful") prompts.log.success("Login successful")
} }
@@ -215,7 +231,12 @@ export const ProvidersListCommand = cmd({
const homedir = os.homedir() const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`) prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
const results = Object.entries(await Auth.all()) const results = await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
return Object.entries(yield* auth.all())
}),
)
const database = await ModelsDev.get() const database = await ModelsDev.get()
for (const [providerID, result] of results) { for (const [providerID, result] of results) {
@@ -294,7 +315,7 @@ export const ProvidersLoginCommand = cmd({
prompts.outro("Done") prompts.outro("Done")
return return
} }
await Auth.set(url, { await put(url, {
type: "wellknown", type: "wellknown",
key: wellknown.auth.env, key: wellknown.auth.env,
token: token.trim(), token: token.trim(),
@@ -441,7 +462,7 @@ export const ProvidersLoginCommand = cmd({
validate: (x) => (x && x.length > 0 ? undefined : "Required"), validate: (x) => (x && x.length > 0 ? undefined : "Required"),
}) })
if (prompts.isCancel(key)) throw new UI.CancelledError() if (prompts.isCancel(key)) throw new UI.CancelledError()
await Auth.set(provider, { await put(provider, {
type: "api", type: "api",
key, key,
}) })
@@ -457,22 +478,33 @@ export const ProvidersLogoutCommand = cmd({
describe: "log out from a configured provider", describe: "log out from a configured provider",
async handler(_args) { async handler(_args) {
UI.empty() UI.empty()
const credentials = await Auth.all().then((x) => Object.entries(x)) const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
return Object.entries(yield* auth.all())
}),
)
prompts.intro("Remove credential") prompts.intro("Remove credential")
if (credentials.length === 0) { if (credentials.length === 0) {
prompts.log.error("No credentials found") prompts.log.error("No credentials found")
return return
} }
const database = await ModelsDev.get() const database = await ModelsDev.get()
const providerID = await prompts.select({ const selected = await prompts.select({
message: "Select provider", message: "Select provider",
options: credentials.map(([key, value]) => ({ options: credentials.map(([key, value]) => ({
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")", label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
value: key, value: key,
})), })),
}) })
if (prompts.isCancel(providerID)) throw new UI.CancelledError() if (prompts.isCancel(selected)) throw new UI.CancelledError()
await Auth.remove(providerID) const providerID = selected as string
await AppRuntime.runPromise(
Effect.gen(function* () {
const auth = yield* Auth.Service
yield* auth.remove(providerID)
}),
)
prompts.outro("Logout successful") prompts.outro("Logout successful")
}, },
}) })
+2 -2
View File
@@ -7,7 +7,7 @@ import { Flag } from "../../flag/flag"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { EOL } from "os" import { EOL } from "os"
import { Filesystem } from "../../util/filesystem" import { Filesystem } from "../../util/filesystem"
import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2" import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server" import { Server } from "../../server/server"
import { Provider } from "../../provider/provider" import { Provider } from "../../provider/provider"
import { Agent } from "../../agent/agent" import { Agent } from "../../agent/agent"
@@ -680,7 +680,7 @@ export const RunCommand = cmd({
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init) const request = new Request(input, init)
return Server.Default().fetch(request) return Server.Default().app.fetch(request)
}) as typeof globalThis.fetch }) as typeof globalThis.fetch
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn }) const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
await execute(sdk) await execute(sdk)
+35 -110
View File
@@ -1,6 +1,7 @@
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard" import { Clipboard } from "@tui/util/clipboard"
import { Selection } from "@tui/util/selection" import { Selection } from "@tui/util/selection"
import { Terminal } from "@tui/util/terminal"
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core" import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route" import { RouteProvider, useRoute } from "@tui/context/route"
import { import {
@@ -14,7 +15,6 @@ import {
batch, batch,
Show, Show,
on, on,
onCleanup,
} from "solid-js" } from "solid-js"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
@@ -23,6 +23,8 @@ import { DialogProvider, useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
import { ErrorComponent } from "@tui/component/error-component" import { ErrorComponent } from "@tui/component/error-component"
import { PluginRouteMissing } from "@tui/component/plugin-route-missing" import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
import { ProjectProvider, useProject } from "@tui/context/project"
import { useEvent } from "@tui/context/event"
import { SDKProvider, useSDK } from "@tui/context/sdk" import { SDKProvider, useSDK } from "@tui/context/sdk"
import { StartupLoading } from "@tui/component/startup-loading" import { StartupLoading } from "@tui/component/startup-loading"
import { SyncProvider, useSync } from "@tui/context/sync" import { SyncProvider, useSync } from "@tui/context/sync"
@@ -35,7 +37,6 @@ import { DialogHelp } from "./ui/dialog-help"
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
import { DialogAgent } from "@tui/component/dialog-agent" import { DialogAgent } from "@tui/component/dialog-agent"
import { DialogSessionList } from "@tui/component/dialog-session-list" import { DialogSessionList } from "@tui/component/dialog-session-list"
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
import { DialogConsoleOrg } from "@tui/component/dialog-console-org" import { DialogConsoleOrg } from "@tui/component/dialog-console-org"
import { KeybindProvider, useKeybind } from "@tui/context/keybind" import { KeybindProvider, useKeybind } from "@tui/context/keybind"
import { ThemeProvider, useTheme } from "@tui/context/theme" import { ThemeProvider, useTheme } from "@tui/context/theme"
@@ -54,73 +55,12 @@ import { KVProvider, useKV } from "./context/kv"
import { Provider } from "@/provider/provider" import { Provider } from "@/provider/provider"
import { ArgsProvider, useArgs, type Args } from "./context/args" import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open" import open from "open"
import { writeHeapSnapshot } from "v8"
import { PromptRefProvider, usePromptRef } from "./context/prompt" import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config/tui"
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin" import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
import { FormatError, FormatUnknownError } from "@/cli/error" import { FormatError, FormatUnknownError } from "@/cli/error"
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
// can't set raw mode if not a TTY
if (!process.stdin.isTTY) return "dark"
return new Promise((resolve) => {
let timeout: NodeJS.Timeout
const cleanup = () => {
process.stdin.setRawMode(false)
process.stdin.removeListener("data", handler)
clearTimeout(timeout)
}
const handler = (data: Buffer) => {
const str = data.toString()
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
if (match) {
cleanup()
const color = match[1]
// Parse RGB values from color string
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
let r = 0,
g = 0,
b = 0
if (color.startsWith("rgb:")) {
const parts = color.substring(4).split("/")
r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
} else if (color.startsWith("#")) {
r = parseInt(color.substring(1, 3), 16)
g = parseInt(color.substring(3, 5), 16)
b = parseInt(color.substring(5, 7), 16)
} else if (color.startsWith("rgb(")) {
const parts = color.substring(4, color.length - 1).split(",")
r = parseInt(parts[0])
g = parseInt(parts[1])
b = parseInt(parts[2])
}
// Calculate luminance using relative luminance formula
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
// Determine if dark or light based on luminance threshold
resolve(luminance > 0.5 ? "light" : "dark")
}
}
process.stdin.setRawMode(true)
process.stdin.on("data", handler)
process.stdout.write("\x1b]11;?\x07")
timeout = setTimeout(() => {
cleanup()
resolve("dark")
}, 1000)
})
}
import type { EventSource } from "./context/sdk" import type { EventSource } from "./context/sdk"
import { DialogVariant } from "./component/dialog-variant" import { DialogVariant } from "./component/dialog-variant"
@@ -179,7 +119,7 @@ export function tui(input: {
const unguard = win32InstallCtrlCGuard() const unguard = win32InstallCtrlCGuard()
win32DisableProcessedInput() win32DisableProcessedInput()
const mode = await getTerminalBackgroundColor() const mode = await Terminal.getTerminalBackgroundColor()
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores // Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
// the original console mode which re-enables ENABLE_PROCESSED_INPUT. // the original console mode which re-enables ENABLE_PROCESSED_INPUT.
@@ -216,27 +156,29 @@ export function tui(input: {
headers={input.headers} headers={input.headers}
events={input.events} events={input.events}
> >
<SyncProvider> <ProjectProvider>
<ThemeProvider mode={mode}> <SyncProvider>
<LocalProvider> <ThemeProvider mode={mode}>
<KeybindProvider> <LocalProvider>
<PromptStashProvider> <KeybindProvider>
<DialogProvider> <PromptStashProvider>
<CommandProvider> <DialogProvider>
<FrecencyProvider> <CommandProvider>
<PromptHistoryProvider> <FrecencyProvider>
<PromptRefProvider> <PromptHistoryProvider>
<App onSnapshot={input.onSnapshot} /> <PromptRefProvider>
</PromptRefProvider> <App onSnapshot={input.onSnapshot} />
</PromptHistoryProvider> </PromptRefProvider>
</FrecencyProvider> </PromptHistoryProvider>
</CommandProvider> </FrecencyProvider>
</DialogProvider> </CommandProvider>
</PromptStashProvider> </DialogProvider>
</KeybindProvider> </PromptStashProvider>
</LocalProvider> </KeybindProvider>
</ThemeProvider> </LocalProvider>
</SyncProvider> </ThemeProvider>
</SyncProvider>
</ProjectProvider>
</SDKProvider> </SDKProvider>
</TuiConfigProvider> </TuiConfigProvider>
</RouteProvider> </RouteProvider>
@@ -260,6 +202,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
const kv = useKV() const kv = useKV()
const command = useCommandDialog() const command = useCommandDialog()
const keybind = useKeybind() const keybind = useKeybind()
const event = useEvent()
const sdk = useSDK() const sdk = useSDK()
const toast = useToast() const toast = useToast()
const themeState = useTheme() const themeState = useTheme()
@@ -283,6 +226,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
route, route,
routes, routes,
bump: () => setRouteRev((x) => x + 1), bump: () => setRouteRev((x) => x + 1),
event,
sdk, sdk,
sync, sync,
theme: themeState, theme: themeState,
@@ -461,22 +405,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
dialog.replace(() => <DialogSessionList />) dialog.replace(() => <DialogSessionList />)
}, },
}, },
...(Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
? [
{
title: "Manage workspaces",
value: "workspace.list",
category: "Workspace",
suggested: true,
slash: {
name: "workspaces",
},
onSelect: () => {
dialog.replace(() => <DialogWorkspaceList />)
},
},
]
: []),
{ {
title: "New session", title: "New session",
suggested: route.data.type === "session", suggested: route.data.type === "session",
@@ -491,12 +419,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
const current = promptRef.current const current = promptRef.current
// Don't require focus - if there's any text, preserve it // Don't require focus - if there's any text, preserve it
const currentPrompt = current?.current?.input ? current.current : undefined const currentPrompt = current?.current?.input ? current.current : undefined
const workspaceID =
route.data.type === "session" ? sync.session.get(route.data.sessionID)?.workspaceID : undefined
route.navigate({ route.navigate({
type: "home", type: "home",
initialPrompt: currentPrompt, initialPrompt: currentPrompt,
workspaceID,
}) })
dialog.clear() dialog.clear()
}, },
@@ -806,11 +731,11 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}, },
]) ])
sdk.event.on(TuiEvent.CommandExecute.type, (evt) => { event.on(TuiEvent.CommandExecute.type, (evt) => {
command.trigger(evt.properties.command) command.trigger(evt.properties.command)
}) })
sdk.event.on(TuiEvent.ToastShow.type, (evt) => { event.on(TuiEvent.ToastShow.type, (evt) => {
toast.show({ toast.show({
title: evt.properties.title, title: evt.properties.title,
message: evt.properties.message, message: evt.properties.message,
@@ -819,14 +744,14 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}) })
}) })
sdk.event.on(TuiEvent.SessionSelect.type, (evt) => { event.on(TuiEvent.SessionSelect.type, (evt) => {
route.navigate({ route.navigate({
type: "session", type: "session",
sessionID: evt.properties.sessionID, sessionID: evt.properties.sessionID,
}) })
}) })
sdk.event.on("session.deleted", (evt) => { event.on("session.deleted", (evt) => {
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) { if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
route.navigate({ type: "home" }) route.navigate({ type: "home" })
toast.show({ toast.show({
@@ -836,7 +761,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
} }
}) })
sdk.event.on("session.error", (evt) => { event.on("session.error", (evt) => {
const error = evt.properties.error const error = evt.properties.error
if (error && typeof error === "object" && error.name === "MessageAbortedError") return if (error && typeof error === "object" && error.name === "MessageAbortedError") return
const message = errorMessage(error) const message = errorMessage(error)
@@ -848,7 +773,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}) })
}) })
sdk.event.on("installation.update-available", async (evt) => { event.on("installation.update-available", async (evt) => {
const version = evt.properties.version const version = evt.properties.version
const skipped = kv.get("skipped_version") const skipped = kv.get("skipped_version")
@@ -0,0 +1,99 @@
import { RGBA, TextAttributes } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import open from "open"
import { createSignal } from "solid-js"
import { selectedForeground, useTheme } from "@tui/context/theme"
import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { Link } from "@tui/ui/link"
const GO_URL = "https://opencode.ai/go"
export type DialogGoUpsellProps = {
onClose?: (dontShowAgain?: boolean) => void
}
function subscribe(props: DialogGoUpsellProps, dialog: ReturnType<typeof useDialog>) {
open(GO_URL).catch(() => {})
props.onClose?.()
dialog.clear()
}
function dismiss(props: DialogGoUpsellProps, dialog: ReturnType<typeof useDialog>) {
props.onClose?.(true)
dialog.clear()
}
export function DialogGoUpsell(props: DialogGoUpsellProps) {
const dialog = useDialog()
const { theme } = useTheme()
const fg = selectedForeground(theme)
const [selected, setSelected] = createSignal(0)
useKeyboard((evt) => {
if (evt.name === "left" || evt.name === "right" || evt.name === "tab") {
setSelected((s) => (s === 0 ? 1 : 0))
return
}
if (evt.name !== "return") return
if (selected() === 0) subscribe(props, dialog)
else dismiss(props, dialog)
})
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Free limit reached
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<box gap={1} paddingBottom={1}>
<text fg={theme.textMuted}>
Subscribe to OpenCode Go to keep going with reliable access to the best open-source models, starting at
$5/month.
</text>
<box flexDirection="row" gap={1}>
<Link href={GO_URL} fg={theme.primary} />
</box>
</box>
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
<box
paddingLeft={3}
paddingRight={3}
backgroundColor={selected() === 0 ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
onMouseOver={() => setSelected(0)}
onMouseUp={() => subscribe(props, dialog)}
>
<text fg={selected() === 0 ? fg : theme.text} attributes={selected() === 0 ? TextAttributes.BOLD : undefined}>
subscribe
</text>
</box>
<box
paddingLeft={3}
paddingRight={3}
backgroundColor={selected() === 1 ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
onMouseOver={() => setSelected(1)}
onMouseUp={() => dismiss(props, dialog)}
>
<text
fg={selected() === 1 ? fg : theme.textMuted}
attributes={selected() === 1 ? TextAttributes.BOLD : undefined}
>
don't show again
</text>
</box>
</box>
</box>
)
}
DialogGoUpsell.show = (dialog: DialogContext) => {
return new Promise<boolean>((resolve) => {
dialog.replace(
() => <DialogGoUpsell onClose={(dontShow) => resolve(dontShow ?? false)} />,
() => resolve(false),
)
})
}
@@ -2,25 +2,31 @@ import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select" import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js" import { createMemo, createResource, createSignal, onMount } from "solid-js"
import { Locale } from "@/util/locale" import { Locale } from "@/util/locale"
import { useProject } from "@tui/context/project"
import { useKeybind } from "../context/keybind" import { useKeybind } from "../context/keybind"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk" import { useSDK } from "../context/sdk"
import { Flag } from "@/flag/flag"
import { DialogSessionRename } from "./dialog-session-rename" import { DialogSessionRename } from "./dialog-session-rename"
import { useKV } from "../context/kv" import { Keybind } from "@/util/keybind"
import { createDebouncedSignal } from "../util/signal" import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast"
import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create"
import { Spinner } from "./spinner" import { Spinner } from "./spinner"
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
export function DialogSessionList() { export function DialogSessionList() {
const dialog = useDialog() const dialog = useDialog()
const route = useRoute() const route = useRoute()
const sync = useSync() const sync = useSync()
const project = useProject()
const keybind = useKeybind() const keybind = useKeybind()
const { theme } = useTheme() const { theme } = useTheme()
const sdk = useSDK() const sdk = useSDK()
const kv = useKV() const toast = useToast()
const [toDelete, setToDelete] = createSignal<string>() const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150) const [search, setSearch] = createDebouncedSignal("", 150)
@@ -31,15 +37,68 @@ export function DialogSessionList() {
}) })
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const sessions = createMemo(() => searchResults() ?? sync.data.session) const sessions = createMemo(() => searchResults() ?? sync.data.session)
function createWorkspace() {
dialog.replace(() => (
<DialogWorkspaceCreate
onSelect={(workspaceID) =>
openWorkspaceSession({
dialog,
route,
sdk,
sync,
toast,
workspaceID,
})
}
/>
))
}
const options = createMemo(() => { const options = createMemo(() => {
const today = new Date().toDateString() const today = new Date().toDateString()
return sessions() return sessions()
.filter((x) => x.parentID === undefined) .filter((x) => x.parentID === undefined)
.toSorted((a, b) => b.time.updated - a.time.updated) .toSorted((a, b) => b.time.updated - a.time.updated)
.map((x) => { .map((x) => {
const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
let workspaceStatus: WorkspaceStatus | null = null
if (x.workspaceID) {
workspaceStatus = project.workspace.status(x.workspaceID) || "error"
}
let footer = ""
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
if (x.workspaceID) {
let desc = "unknown"
if (workspace) {
desc = `${workspace.type}: ${workspace.name}`
}
footer = (
<>
{desc}{" "}
<span
style={{
fg:
workspaceStatus === "error"
? theme.error
: workspaceStatus === "disconnected"
? theme.textMuted
: theme.success,
}}
>
</span>
</>
)
}
} else {
footer = Locale.time(x.time.updated)
}
const date = new Date(x.time.updated) const date = new Date(x.time.updated)
let category = date.toDateString() let category = date.toDateString()
if (category === today) { if (category === today) {
@@ -53,7 +112,7 @@ export function DialogSessionList() {
bg: isDeleting ? theme.error : undefined, bg: isDeleting ? theme.error : undefined,
value: x.id, value: x.id,
category, category,
footer: Locale.time(x.time.updated), footer,
gutter: isWorking ? <Spinner /> : undefined, gutter: isWorking ? <Spinner /> : undefined,
} }
}) })
@@ -102,6 +161,15 @@ export function DialogSessionList() {
dialog.replace(() => <DialogSessionRename session={option.value} />) dialog.replace(() => <DialogSessionRename session={option.value} />)
}, },
}, },
{
keybind: Keybind.parse("ctrl+w")[0],
title: "new workspace",
side: "right",
disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
onTrigger: () => {
createWorkspace()
},
},
]} ]}
/> />
) )
@@ -0,0 +1,121 @@
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { useProject } from "@tui/context/project"
import { createMemo, createSignal, onMount } from "solid-js"
import { setTimeout as sleep } from "node:timers/promises"
import { useSDK } from "../context/sdk"
import { useToast } from "../ui/toast"
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
return createOpencodeClient({
baseUrl: sdk.url,
fetch: sdk.fetch,
directory: sync.path.directory || sdk.directory,
experimental_workspaceID: workspaceID,
})
}
export async function openWorkspaceSession(input: {
dialog: ReturnType<typeof useDialog>
route: ReturnType<typeof useRoute>
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
toast: ReturnType<typeof useToast>
workspaceID: string
}) {
const client = scoped(input.sdk, input.sync, input.workspaceID)
while (true) {
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
if (!result) {
input.toast.show({
message: "Failed to create workspace session",
variant: "error",
})
return
}
if (result.response.status >= 500 && result.response.status < 600) {
await sleep(1000)
continue
}
if (!result.data) {
input.toast.show({
message: "Failed to create workspace session",
variant: "error",
})
return
}
input.route.navigate({
type: "session",
sessionID: result.data.id,
})
input.dialog.clear()
return
}
}
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
const dialog = useDialog()
const sync = useSync()
const project = useProject()
const sdk = useSDK()
const toast = useToast()
const [creating, setCreating] = createSignal<string>()
onMount(() => {
dialog.setSize("medium")
})
const options = createMemo(() => {
const type = creating()
if (type) {
return [
{
title: `Creating ${type} workspace...`,
value: "creating" as const,
description: "This can take a while for remote environments",
},
]
}
return [
{
title: "Worktree",
value: "worktree" as const,
description: "Create a local git worktree",
},
]
})
const create = async (type: string) => {
if (creating()) return
setCreating(type)
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => undefined)
const workspace = result?.data
if (!workspace) {
setCreating(undefined)
toast.show({
message: "Failed to create workspace",
variant: "error",
})
return
}
await project.workspace.sync()
await props.onSelect(workspace.id)
setCreating(undefined)
}
return (
<DialogSelect
title={creating() ? "Creating Workspace" : "New Workspace"}
skipFilter={true}
options={options()}
onSelect={(option) => {
if (option.value === "creating") return
void create(option.value)
}}
/>
)
}
@@ -1,320 +0,0 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
import { createOpencodeClient, type Session } from "@opencode-ai/sdk/v2"
import { useSDK } from "../context/sdk"
import { useToast } from "../ui/toast"
import { useKeybind } from "../context/keybind"
import { DialogSessionList } from "./workspace/dialog-session-list"
import { setTimeout as sleep } from "node:timers/promises"
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID?: string) {
return createOpencodeClient({
baseUrl: sdk.url,
fetch: sdk.fetch,
directory: sync.data.path.directory || sdk.directory,
experimental_workspaceID: workspaceID,
})
}
async function openWorkspace(input: {
dialog: ReturnType<typeof useDialog>
route: ReturnType<typeof useRoute>
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
toast: ReturnType<typeof useToast>
workspaceID: string
forceCreate?: boolean
}) {
const cacheSession = (session: Session) => {
input.sync.set(
"session",
[...input.sync.data.session.filter((item) => item.id !== session.id), session].toSorted((a, b) =>
a.id.localeCompare(b.id),
),
)
}
const client = scoped(input.sdk, input.sync, input.workspaceID)
const listed = input.forceCreate ? undefined : await client.session.list({ roots: true, limit: 1 })
const session = listed?.data?.[0]
if (session?.id) {
cacheSession(session)
input.route.navigate({
type: "session",
sessionID: session.id,
})
input.dialog.clear()
return
}
let created: Session | undefined
while (!created) {
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
if (!result) {
input.toast.show({
message: "Failed to open workspace",
variant: "error",
})
return
}
if (result.response.status >= 500 && result.response.status < 600) {
await sleep(1000)
continue
}
if (!result.data) {
input.toast.show({
message: "Failed to open workspace",
variant: "error",
})
return
}
created = result.data
}
cacheSession(created)
input.route.navigate({
type: "session",
sessionID: created.id,
})
input.dialog.clear()
}
function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> }) {
const dialog = useDialog()
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const [creating, setCreating] = createSignal<string>()
onMount(() => {
dialog.setSize("medium")
})
const options = createMemo(() => {
const type = creating()
if (type) {
return [
{
title: `Creating ${type} workspace...`,
value: "creating" as const,
description: "This can take a while for remote environments",
},
]
}
return [
{
title: "Worktree",
value: "worktree" as const,
description: "Create a local git worktree",
},
]
})
const createWorkspace = async (type: string) => {
if (creating()) return
setCreating(type)
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => {
console.log(err)
return undefined
})
console.log(JSON.stringify(result, null, 2))
const workspace = result?.data
if (!workspace) {
setCreating(undefined)
toast.show({
message: "Failed to create workspace",
variant: "error",
})
return
}
await sync.workspace.sync()
await props.onSelect(workspace.id)
setCreating(undefined)
}
return (
<DialogSelect
title={creating() ? "Creating Workspace" : "New Workspace"}
skipFilter={true}
options={options()}
onSelect={(option) => {
if (option.value === "creating") return
void createWorkspace(option.value)
}}
/>
)
}
export function DialogWorkspaceList() {
const dialog = useDialog()
const route = useRoute()
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const keybind = useKeybind()
const [toDelete, setToDelete] = createSignal<string>()
const [counts, setCounts] = createSignal<Record<string, number | null | undefined>>({})
const open = (workspaceID: string, forceCreate?: boolean) =>
openWorkspace({
dialog,
route,
sdk,
sync,
toast,
workspaceID,
forceCreate,
})
async function selectWorkspace(workspaceID: string) {
if (workspaceID === "__local__") {
if (localCount() > 0) {
dialog.replace(() => <DialogSessionList localOnly={true} />)
return
}
route.navigate({
type: "home",
})
dialog.clear()
return
}
const count = counts()[workspaceID]
if (count && count > 0) {
dialog.replace(() => <DialogSessionList workspaceID={workspaceID} />)
return
}
if (count === 0) {
await open(workspaceID)
return
}
const client = scoped(sdk, sync, workspaceID)
const listed = await client.session.list({ roots: true, limit: 1 }).catch(() => undefined)
if (listed?.data?.length) {
dialog.replace(() => <DialogSessionList workspaceID={workspaceID} />)
return
}
await open(workspaceID)
}
const currentWorkspaceID = createMemo(() => {
if (route.data.type === "session") {
return sync.session.get(route.data.sessionID)?.workspaceID ?? "__local__"
}
return "__local__"
})
const localCount = createMemo(
() => sync.data.session.filter((session) => !session.workspaceID && !session.parentID).length,
)
let run = 0
createEffect(() => {
const workspaces = sync.data.workspaceList
const next = ++run
if (!workspaces.length) {
setCounts({})
return
}
setCounts(Object.fromEntries(workspaces.map((workspace) => [workspace.id, undefined])))
void Promise.all(
workspaces.map(async (workspace) => {
const client = scoped(sdk, sync, workspace.id)
const result = await client.session.list({ roots: true }).catch(() => undefined)
return [workspace.id, result ? (result.data?.length ?? 0) : null] as const
}),
).then((entries) => {
if (run !== next) return
setCounts(Object.fromEntries(entries))
})
})
const options = createMemo(() => [
{
title: "Local",
value: "__local__",
category: "Workspace",
description: "Use the local machine",
footer: `${localCount()} session${localCount() === 1 ? "" : "s"}`,
},
...sync.data.workspaceList.map((workspace) => {
const count = counts()[workspace.id]
return {
title:
toDelete() === workspace.id
? `Delete ${workspace.id}? Press ${keybind.print("session_delete")} again`
: workspace.id,
value: workspace.id,
category: workspace.type,
description: workspace.branch ? `Branch ${workspace.branch}` : undefined,
footer:
count === undefined
? "Loading sessions..."
: count === null
? "Sessions unavailable"
: `${count} session${count === 1 ? "" : "s"}`,
}
}),
{
title: "+ New workspace",
value: "__create__",
category: "Actions",
description: "Create a new workspace",
},
])
onMount(() => {
dialog.setSize("large")
void sync.workspace.sync()
})
return (
<DialogSelect
title="Workspaces"
skipFilter={true}
options={options()}
current={currentWorkspaceID()}
onMove={() => {
setToDelete(undefined)
}}
onSelect={(option) => {
setToDelete(undefined)
if (option.value === "__create__") {
dialog.replace(() => <DialogWorkspaceCreate onSelect={(workspaceID) => open(workspaceID, true)} />)
return
}
void selectWorkspace(option.value)
}}
keybind={[
{
keybind: keybind.all.session_delete?.[0],
title: "delete",
onTrigger: async (option) => {
if (option.value === "__create__" || option.value === "__local__") return
if (toDelete() !== option.value) {
setToDelete(option.value)
return
}
const result = await sdk.client.experimental.workspace.remove({ id: option.value }).catch(() => undefined)
setToDelete(undefined)
if (result?.error) {
toast.show({
message: "Failed to delete workspace",
variant: "error",
})
return
}
if (currentWorkspaceID() === option.value) {
route.navigate({
type: "home",
})
}
await sync.workspace.sync()
},
},
]}
/>
)
}
@@ -250,7 +250,7 @@ export function Autocomplete(props: {
const width = props.anchor().width - 4 const width = props.anchor().width - 4
options.push( options.push(
...sortedFiles.map((item): AutocompleteOption => { ...sortedFiles.map((item): AutocompleteOption => {
const baseDir = (sync.data.path.directory || process.cwd()).replace(/\/+$/, "") const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
const fullPath = `${baseDir}/${item}` const fullPath = `${baseDir}/${item}`
const urlObj = pathToFileURL(fullPath) const urlObj = pathToFileURL(fullPath)
let filename = item let filename = item
@@ -10,6 +10,7 @@ import { EmptyBorder, SplitBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { useEvent } from "@tui/context/event"
import { MessageID, PartID } from "@/session/schema" import { MessageID, PartID } from "@/session/schema"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind" import { useKeybind } from "@tui/context/keybind"
@@ -115,8 +116,9 @@ export function Prompt(props: PromptProps) {
const agentStyleId = syntax().getStyleId("extmark.agent")! const agentStyleId = syntax().getStyleId("extmark.agent")!
const pasteStyleId = syntax().getStyleId("extmark.paste")! const pasteStyleId = syntax().getStyleId("extmark.paste")!
let promptPartTypeId = 0 let promptPartTypeId = 0
const event = useEvent()
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => { event.on(TuiEvent.PromptAppend.type, (evt) => {
if (!input || input.isDestroyed) return if (!input || input.isDestroyed) return
input.insertText(evt.properties.text) input.insertText(evt.properties.text)
setTimeout(() => { setTimeout(() => {
@@ -587,6 +589,13 @@ export function Prompt(props: PromptProps) {
]) ])
async function submit() { async function submit() {
// IME: double-defer may fire before onContentChange flushes the last
// composed character (e.g. Korean hangul) to the store, so read
// plainText directly and sync before any downstream reads.
if (input && !input.isDestroyed && input.plainText !== store.prompt.input) {
setStore("prompt", "input", input.plainText)
syncExtmarksWithPromptParts()
}
if (props.disabled) return if (props.disabled) return
if (autocomplete?.visible) return if (autocomplete?.visible) return
if (!store.prompt.input) return if (!store.prompt.input) return
@@ -992,7 +1001,11 @@ export function Prompt(props: PromptProps) {
input.cursorOffset = input.plainText.length input.cursorOffset = input.plainText.length
} }
}} }}
onSubmit={submit} onSubmit={() => {
// IME: double-defer so the last composed character (e.g. Korean
// hangul) is flushed to plainText before we read it for submission.
setTimeout(() => setTimeout(() => submit(), 0), 0)
}}
onPaste={async (event: PasteEvent) => { onPaste={async (event: PasteEvent) => {
if (props.disabled) { if (props.disabled) {
event.preventDefault() event.preventDefault()
@@ -1,151 +0,0 @@
import { useDialog } from "@tui/ui/dialog"
import { DialogSelect } from "@tui/ui/dialog-select"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
import { Locale } from "@/util/locale"
import { useKeybind } from "../../context/keybind"
import { useTheme } from "../../context/theme"
import { useSDK } from "../../context/sdk"
import { DialogSessionRename } from "../dialog-session-rename"
import { useKV } from "../../context/kv"
import { createDebouncedSignal } from "../../util/signal"
import { Spinner } from "../spinner"
import { useToast } from "../../ui/toast"
export function DialogSessionList(props: { workspaceID?: string; localOnly?: boolean } = {}) {
const dialog = useDialog()
const route = useRoute()
const sync = useSync()
const keybind = useKeybind()
const { theme } = useTheme()
const sdk = useSDK()
const kv = useKV()
const toast = useToast()
const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150)
const [listed, listedActions] = createResource(
() => props.workspaceID,
async (workspaceID) => {
if (!workspaceID) return undefined
const result = await sdk.client.session.list({ roots: true })
return result.data ?? []
},
)
const [searchResults] = createResource(search, async (query) => {
if (!query || props.localOnly) return undefined
const result = await sdk.client.session.list({
search: query,
limit: 30,
...(props.workspaceID ? { roots: true } : {}),
})
return result.data ?? []
})
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const sessions = createMemo(() => {
if (searchResults()) return searchResults()!
if (props.workspaceID) return listed() ?? []
if (props.localOnly) return sync.data.session.filter((session) => !session.workspaceID)
return sync.data.session
})
const options = createMemo(() => {
const today = new Date().toDateString()
return sessions()
.filter((x) => {
if (x.parentID !== undefined) return false
if (props.workspaceID && listed()) return true
if (props.workspaceID) return x.workspaceID === props.workspaceID
if (props.localOnly) return !x.workspaceID
return true
})
.toSorted((a, b) => b.time.updated - a.time.updated)
.map((x) => {
const date = new Date(x.time.updated)
let category = date.toDateString()
if (category === today) {
category = "Today"
}
const isDeleting = toDelete() === x.id
const status = sync.data.session_status?.[x.id]
const isWorking = status?.type === "busy"
return {
title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
footer: Locale.time(x.time.updated),
gutter: isWorking ? <Spinner /> : undefined,
}
})
})
onMount(() => {
dialog.setSize("large")
})
return (
<DialogSelect
title={props.workspaceID ? `Workspace Sessions` : props.localOnly ? "Local Sessions" : "Sessions"}
options={options()}
skipFilter={!props.localOnly}
current={currentSessionID()}
onFilter={setSearch}
onMove={() => {
setToDelete(undefined)
}}
onSelect={(option) => {
route.navigate({
type: "session",
sessionID: option.value,
})
dialog.clear()
}}
keybind={[
{
keybind: keybind.all.session_delete?.[0],
title: "delete",
onTrigger: async (option) => {
if (toDelete() === option.value) {
const deleted = await sdk.client.session
.delete({
sessionID: option.value,
})
.then(() => true)
.catch(() => false)
setToDelete(undefined)
if (!deleted) {
toast.show({
message: "Failed to delete session",
variant: "error",
})
return
}
if (props.workspaceID) {
listedActions.mutate((sessions) => sessions?.filter((session) => session.id !== option.value))
return
}
sync.set(
"session",
sync.data.session.filter((session) => session.id !== option.value),
)
return
}
setToDelete(option.value)
},
},
{
keybind: keybind.all.session_rename?.[0],
title: "rename",
onTrigger: async (option) => {
dialog.replace(() => <DialogSessionRename session={option.value} />)
},
},
]}
/>
)
}
@@ -1,11 +1,13 @@
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
import { useProject } from "./project"
import { useSync } from "./sync" import { useSync } from "./sync"
import { Global } from "@/global" import { Global } from "@/global"
export function useDirectory() { export function useDirectory() {
const project = useProject()
const sync = useSync() const sync = useSync()
return createMemo(() => { return createMemo(() => {
const directory = sync.data.path.directory || process.cwd() const directory = project.instance.path().directory || process.cwd()
const result = directory.replace(Global.Path.home, "~") const result = directory.replace(Global.Path.home, "~")
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
return result return result
@@ -0,0 +1,41 @@
import type { Event } from "@opencode-ai/sdk/v2"
import { useProject } from "./project"
import { useSDK } from "./sdk"
export function useEvent() {
const project = useProject()
const sdk = useSDK()
function subscribe(handler: (event: Event) => void) {
return sdk.event.on("event", (event) => {
// Special hack for truly global events
if (event.directory === "global") {
handler(event.payload)
}
if (project.workspace.current()) {
if (event.workspace === project.workspace.current()) {
handler(event.payload)
}
return
}
if (event.directory === project.instance.directory()) {
handler(event.payload)
}
})
}
function on<T extends Event["type"]>(type: T, handler: (event: Extract<Event, { type: T }>) => void) {
return subscribe((event) => {
if (event.type !== type) return
handler(event as Extract<Event, { type: T }>)
})
}
return {
subscribe,
on,
}
}
@@ -0,0 +1,106 @@
import { batch } from "solid-js"
import type { Path, Workspace } from "@opencode-ai/sdk/v2"
import { createStore, reconcile } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
name: "Project",
init: () => {
const sdk = useSDK()
const [store, setStore] = createStore({
project: {
id: undefined as string | undefined,
},
instance: {
path: {
home: "",
state: "",
config: "",
worktree: "",
directory: sdk.directory ?? "",
} satisfies Path,
},
workspace: {
current: undefined as string | undefined,
list: [] as Workspace[],
status: {} as Record<string, WorkspaceStatus>,
},
})
async function sync() {
const workspace = store.workspace.current
const [path, project] = await Promise.all([
sdk.client.path.get({ workspace }),
sdk.client.project.current({ workspace }),
])
batch(() => {
setStore("instance", "path", reconcile(path.data!))
setStore("project", "id", project.data?.id)
})
}
async function syncWorkspace() {
const listed = await sdk.client.experimental.workspace.list().catch(() => undefined)
if (!listed?.data) return
const status = await sdk.client.experimental.workspace.status().catch(() => undefined)
const next = Object.fromEntries((status?.data ?? []).map((item) => [item.workspaceID, item.status]))
batch(() => {
setStore("workspace", "list", reconcile(listed.data))
setStore("workspace", "status", reconcile(next))
if (!listed.data.some((item) => item.id === store.workspace.current)) {
setStore("workspace", "current", undefined)
}
})
}
sdk.event.on("event", (event) => {
if (event.payload.type === "workspace.status") {
setStore("workspace", "status", event.payload.properties.workspaceID, event.payload.properties.status)
}
})
return {
data: store,
project() {
return store.project.id
},
instance: {
path() {
return store.instance.path
},
directory() {
return store.instance.path.directory
},
},
workspace: {
current() {
return store.workspace.current
},
set(next?: string | null) {
const workspace = next ?? undefined
if (store.workspace.current === workspace) return
setStore("workspace", "current", workspace)
},
list() {
return store.workspace.list
},
get(workspaceID: string) {
return store.workspace.list.find((item) => item.id === workspaceID)
},
status(workspaceID: string) {
return store.workspace.status[workspaceID]
},
statuses() {
return store.workspace.status
},
sync: syncWorkspace,
},
sync,
}
},
})
@@ -5,7 +5,6 @@ import type { PromptInfo } from "../component/prompt/history"
export type HomeRoute = { export type HomeRoute = {
type: "home" type: "home"
initialPrompt?: PromptInfo initialPrompt?: PromptInfo
workspaceID?: string
} }
export type SessionRoute = { export type SessionRoute = {
@@ -1,10 +1,11 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2" import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import type { GlobalEvent, Event } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup, onMount } from "solid-js" import { batch, onCleanup, onMount } from "solid-js"
export type EventSource = { export type EventSource = {
subscribe: (directory: string | undefined, handler: (event: Event) => void) => Promise<() => void> subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void>
} }
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
@@ -32,10 +33,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
let sdk = createSDK() let sdk = createSDK()
const emitter = createGlobalEmitter<{ const emitter = createGlobalEmitter<{
[key in Event["type"]]: Extract<Event, { type: key }> event: GlobalEvent
}>() }>()
let queue: Event[] = [] let queue: GlobalEvent[] = []
let timer: Timer | undefined let timer: Timer | undefined
let last = 0 let last = 0
@@ -48,12 +49,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
// Batch all event emissions so all store updates result in a single render // Batch all event emissions so all store updates result in a single render
batch(() => { batch(() => {
for (const event of events) { for (const event of events) {
emitter.emit(event.type, event) emitter.emit("event", event)
} }
}) })
} }
const handleEvent = (event: Event) => { const handleEvent = (event: GlobalEvent) => {
queue.push(event) queue.push(event)
const elapsed = Date.now() - last const elapsed = Date.now() - last
@@ -74,7 +75,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
;(async () => { ;(async () => {
while (true) { while (true) {
if (abort.signal.aborted || ctrl.signal.aborted) break if (abort.signal.aborted || ctrl.signal.aborted) break
const events = await sdk.event.subscribe({}, { signal: ctrl.signal }) const events = await sdk.global.event({ signal: ctrl.signal })
for await (const event of events.stream) { for await (const event of events.stream) {
if (ctrl.signal.aborted) break if (ctrl.signal.aborted) break
@@ -89,7 +90,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
onMount(async () => { onMount(async () => {
if (props.events) { if (props.events) {
const unsub = await props.events.subscribe(props.directory, handleEvent) const unsub = await props.events.subscribe(handleEvent)
onCleanup(unsub) onCleanup(unsub)
} else { } else {
startSSE() startSSE()
@@ -19,16 +19,16 @@ import type {
VcsInfo, VcsInfo,
} from "@opencode-ai/sdk/v2" } from "@opencode-ai/sdk/v2"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { useProject } from "@tui/context/project"
import { useEvent } from "@tui/context/event"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { Binary } from "@opencode-ai/util/binary" import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
import type { Snapshot } from "@/snapshot" import type { Snapshot } from "@/snapshot"
import { useExit } from "./exit" import { useExit } from "./exit"
import { useArgs } from "./args" import { useArgs } from "./args"
import { batch, onMount } from "solid-js" import { batch, createEffect, on } from "solid-js"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import type { Path } from "@opencode-ai/sdk"
import type { Workspace } from "@opencode-ai/sdk/v2"
import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state" import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
@@ -75,8 +75,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
} }
formatter: FormatterStatus[] formatter: FormatterStatus[]
vcs: VcsInfo | undefined vcs: VcsInfo | undefined
path: Path
workspaceList: Workspace[]
}>({ }>({
provider_next: { provider_next: {
all: [], all: [],
@@ -104,20 +102,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
mcp_resource: {}, mcp_resource: {},
formatter: [], formatter: [],
vcs: undefined, vcs: undefined,
path: { state: "", config: "", worktree: "", directory: "" },
workspaceList: [],
}) })
const event = useEvent()
const project = useProject()
const sdk = useSDK() const sdk = useSDK()
async function syncWorkspaces() { event.subscribe((event) => {
const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
if (!result?.data) return
setStore("workspaceList", reconcile(result.data))
}
sdk.event.listen((e) => {
const event = e.details
switch (event.type) { switch (event.type) {
case "server.instance.disposed": case "server.instance.disposed":
bootstrap() bootstrap()
@@ -344,7 +335,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
} }
case "lsp.updated": { case "lsp.updated": {
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)) const workspace = project.workspace.current()
sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data!))
break break
} }
@@ -360,25 +352,28 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
async function bootstrap() { async function bootstrap() {
console.log("bootstrapping") console.log("bootstrapping")
const workspace = project.workspace.current()
const start = Date.now() - 30 * 24 * 60 * 60 * 1000 const start = Date.now() - 30 * 24 * 60 * 60 * 1000
const sessionListPromise = sdk.client.session const sessionListPromise = sdk.client.session
.list({ start: start }) .list({ start: start })
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
// blocking - include session.list when continuing a session // blocking - include session.list when continuing a session
const providersPromise = sdk.client.config.providers({}, { throwOnError: true }) const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true })
const providerListPromise = sdk.client.provider.list({}, { throwOnError: true }) const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true })
const consoleStatePromise = sdk.client.experimental.console const consoleStatePromise = sdk.client.experimental.console
.get({}, { throwOnError: true }) .get({ workspace }, { throwOnError: true })
.then((x) => ConsoleState.parse(x.data)) .then((x) => ConsoleState.parse(x.data))
.catch(() => emptyConsoleState) .catch(() => emptyConsoleState)
const agentsPromise = sdk.client.app.agents({}, { throwOnError: true }) const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
const configPromise = sdk.client.config.get({}, { throwOnError: true }) const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
const projectPromise = project.sync()
const blockingRequests: Promise<unknown>[] = [ const blockingRequests: Promise<unknown>[] = [
providersPromise, providersPromise,
providerListPromise, providerListPromise,
agentsPromise, agentsPromise,
configPromise, configPromise,
projectPromise,
...(args.continue ? [sessionListPromise] : []), ...(args.continue ? [sessionListPromise] : []),
] ]
@@ -423,18 +418,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
Promise.all([ Promise.all([
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]), ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))), consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))), sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))),
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))), sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data!))),
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))), sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data!))),
sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))), sdk.client.experimental.resource
sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))), .list({ workspace })
sdk.client.session.status().then((x) => { .then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data!))),
sdk.client.session.status({ workspace }).then((x) => {
setStore("session_status", reconcile(x.data!)) setStore("session_status", reconcile(x.data!))
}), }),
sdk.client.provider.auth().then((x) => setStore("provider_auth", reconcile(x.data ?? {}))), sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))), sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))), project.workspace.sync(),
syncWorkspaces(),
]).then(() => { ]).then(() => {
setStore("status", "complete") setStore("status", "complete")
}) })
@@ -449,11 +445,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}) })
} }
onMount(() => {
bootstrap()
})
const fullSyncedSessions = new Set<string>() const fullSyncedSessions = new Set<string>()
createEffect(
on(
() => project.workspace.current(),
() => {
fullSyncedSessions.clear()
void bootstrap()
},
),
)
const result = { const result = {
data: store, data: store,
set: setStore, set: setStore,
@@ -463,6 +465,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
get ready() { get ready() {
return store.status !== "loading" return store.status !== "loading"
}, },
get path() {
return project.instance.path()
},
session: { session: {
get(sessionID: string) { get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id) const match = Binary.search(store.session, sessionID, (s) => s.id)
@@ -481,11 +486,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}, },
async sync(sessionID: string) { async sync(sessionID: string) {
if (fullSyncedSessions.has(sessionID)) return if (fullSyncedSessions.has(sessionID)) return
const workspace = project.workspace.current()
const [session, messages, todo, diff] = await Promise.all([ const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ sessionID }, { throwOnError: true }), sdk.client.session.get({ sessionID, workspace }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }), sdk.client.session.messages({ sessionID, limit: 100, workspace }),
sdk.client.session.todo({ sessionID }), sdk.client.session.todo({ sessionID, workspace }),
sdk.client.session.diff({ sessionID }), sdk.client.session.diff({ sessionID, workspace }),
]) ])
setStore( setStore(
produce((draft) => { produce((draft) => {
@@ -503,12 +509,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
fullSyncedSessions.add(sessionID) fullSyncedSessions.add(sessionID)
}, },
}, },
workspace: {
get(workspaceID: string) {
return store.workspaceList.find((workspace) => workspace.id === workspaceID)
},
sync: syncWorkspaces,
},
bootstrap, bootstrap,
} }
return result return result
@@ -1,6 +1,7 @@
import type { ParsedKey } from "@opentui/core" import type { ParsedKey } from "@opentui/core"
import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui" import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui"
import type { useCommandDialog } from "@tui/component/dialog-command" import type { useCommandDialog } from "@tui/component/dialog-command"
import type { useEvent } from "@tui/context/event"
import type { useKeybind } from "@tui/context/keybind" import type { useKeybind } from "@tui/context/keybind"
import type { useRoute } from "@tui/context/route" import type { useRoute } from "@tui/context/route"
import type { useSDK } from "@tui/context/sdk" import type { useSDK } from "@tui/context/sdk"
@@ -36,6 +37,7 @@ type Input = {
route: ReturnType<typeof useRoute> route: ReturnType<typeof useRoute>
routes: RouteMap routes: RouteMap
bump: () => void bump: () => void
event: ReturnType<typeof useEvent>
sdk: ReturnType<typeof useSDK> sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync> sync: ReturnType<typeof useSync>
theme: ReturnType<typeof useTheme> theme: ReturnType<typeof useTheme>
@@ -136,7 +138,7 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
return sync.data.provider return sync.data.provider
}, },
get path() { get path() {
return sync.data.path return sync.path
}, },
get vcs() { get vcs() {
if (!sync.data.vcs) return if (!sync.data.vcs) return
@@ -144,14 +146,6 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
branch: sync.data.vcs.branch, branch: sync.data.vcs.branch,
} }
}, },
workspace: {
list() {
return sync.data.workspaceList
},
get(workspaceID) {
return sync.workspace.get(workspaceID)
},
},
session: { session: {
count() { count() {
return sync.data.session.length return sync.data.session.length
@@ -342,7 +336,7 @@ export function createTuiApi(input: Input): TuiPluginApi {
get client() { get client() {
return input.sdk.client return input.sdk.client
}, },
event: input.sdk.event, event: input.event,
renderer: input.renderer, renderer: input.renderer,
slots: { slots: {
register() { register() {
@@ -1,6 +1,7 @@
import { Prompt, type PromptRef } from "@tui/component/prompt" import { Prompt, type PromptRef } from "@tui/component/prompt"
import { createEffect, createSignal } from "solid-js" import { createEffect, createSignal } from "solid-js"
import { Logo } from "../component/logo" import { Logo } from "../component/logo"
import { useProject } from "../context/project"
import { useSync } from "../context/sync" import { useSync } from "../context/sync"
import { Toast } from "../ui/toast" import { Toast } from "../ui/toast"
import { useArgs } from "../context/args" import { useArgs } from "../context/args"
@@ -18,6 +19,7 @@ const placeholder = {
export function Home() { export function Home() {
const sync = useSync() const sync = useSync()
const project = useProject()
const route = useRouteData("home") const route = useRouteData("home")
const promptRef = usePromptRef() const promptRef = usePromptRef()
const [ref, setRef] = createSignal<PromptRef | undefined>() const [ref, setRef] = createSignal<PromptRef | undefined>()
@@ -63,11 +65,16 @@ export function Home() {
</box> </box>
<box height={1} minHeight={0} flexShrink={1} /> <box height={1} minHeight={0} flexShrink={1} />
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}> <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
<TuiPluginRuntime.Slot name="home_prompt" mode="replace" workspace_id={route.workspaceID} ref={bind}> <TuiPluginRuntime.Slot
name="home_prompt"
mode="replace"
workspace_id={project.workspace.current()}
ref={bind}
>
<Prompt <Prompt
ref={bind} ref={bind}
workspaceID={route.workspaceID} workspaceID={project.workspace.current()}
right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={route.workspaceID} />} right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={project.workspace.current()} />}
placeholders={placeholder} placeholders={placeholder}
/> />
</TuiPluginRuntime.Slot> </TuiPluginRuntime.Slot>
@@ -15,7 +15,9 @@ import {
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import path from "path" import path from "path"
import { useRoute, useRouteData } from "@tui/context/route" import { useRoute, useRouteData } from "@tui/context/route"
import { useProject } from "@tui/context/project"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { useEvent } from "@tui/context/event"
import { SplitBorder } from "@tui/component/border" import { SplitBorder } from "@tui/component/border"
import { Spinner } from "@tui/component/spinner" import { Spinner } from "@tui/component/spinner"
import { selectedForeground, useTheme } from "@tui/context/theme" import { selectedForeground, useTheme } from "@tui/context/theme"
@@ -83,9 +85,15 @@ import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config" import { useTuiConfig } from "../../context/tui-config"
import { getScrollAcceleration } from "../../util/scroll" import { getScrollAcceleration } from "../../util/scroll"
import { TuiPluginRuntime } from "../../plugin" import { TuiPluginRuntime } from "../../plugin"
import { DialogGoUpsell } from "../../component/dialog-go-upsell"
import { SessionRetry } from "@/session/retry"
addDefaultParsers(parsers.parsers) addDefaultParsers(parsers.parsers)
const GO_UPSELL_LAST_SEEN_AT = "go_upsell_last_seen_at"
const GO_UPSELL_DONT_SHOW = "go_upsell_dont_show"
const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
const context = createContext<{ const context = createContext<{
width: number width: number
sessionID: string sessionID: string
@@ -110,6 +118,8 @@ export function Session() {
const route = useRouteData("session") const route = useRouteData("session")
const { navigate } = useRoute() const { navigate } = useRoute()
const sync = useSync() const sync = useSync()
const event = useEvent()
const project = useProject()
const tuiConfig = useTuiConfig() const tuiConfig = useTuiConfig()
const kv = useKV() const kv = useKV()
const { theme } = useTheme() const { theme } = useTheme()
@@ -149,7 +159,7 @@ export function Session() {
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide") const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true) const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true) const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", true) const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word") const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true) const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false) const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
@@ -166,10 +176,16 @@ export function Session() {
const providers = createMemo(() => Model.index(sync.data.provider)) const providers = createMemo(() => Model.index(sync.data.provider))
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const toast = useToast()
const sdk = useSDK()
createEffect(async () => { createEffect(async () => {
await sync.session await sdk.client.session
.sync(route.sessionID) .get({ sessionID: route.sessionID }, { throwOnError: true })
.then((x) => {
project.workspace.set(x.data?.workspaceID)
})
.then(() => sync.session.sync(route.sessionID))
.then(() => { .then(() => {
if (scroll) scroll.scrollBy(100_000) if (scroll) scroll.scrollBy(100_000)
}) })
@@ -183,13 +199,10 @@ export function Session() {
}) })
}) })
const toast = useToast()
const sdk = useSDK()
// Handle initial prompt from fork // Handle initial prompt from fork
let seeded = false let seeded = false
let lastSwitch: string | undefined = undefined let lastSwitch: string | undefined = undefined
sdk.event.on("message.part.updated", (evt) => { event.on("message.part.updated", (evt) => {
const part = evt.properties.part const part = evt.properties.part
if (part.type !== "tool") return if (part.type !== "tool") return
if (part.sessionID !== route.sessionID) return if (part.sessionID !== route.sessionID) return
@@ -218,6 +231,23 @@ export function Session() {
const dialog = useDialog() const dialog = useDialog()
const renderer = useRenderer() const renderer = useRenderer()
event.on("session.status", (evt) => {
if (evt.properties.sessionID !== route.sessionID) return
if (evt.properties.status.type !== "retry") return
if (evt.properties.status.message !== SessionRetry.GO_UPSELL_MESSAGE) return
if (dialog.stack.length > 0) return
const seen = kv.get(GO_UPSELL_LAST_SEEN_AT)
if (typeof seen === "number" && Date.now() - seen < GO_UPSELL_WINDOW) return
if (kv.get(GO_UPSELL_DONT_SHOW)) return
DialogGoUpsell.show(dialog).then((dontShowAgain) => {
if (dontShowAgain) kv.set(GO_UPSELL_DONT_SHOW, true)
kv.set(GO_UPSELL_LAST_SEEN_AT, Date.now())
})
})
// Allow exit when in child session (prompt is hidden) // Allow exit when in child session (prompt is hidden)
const exit = useExit() const exit = useExit()
@@ -1768,7 +1798,7 @@ function Bash(props: ToolProps<typeof BashTool>) {
const workdir = props.input.workdir const workdir = props.input.workdir
if (!workdir || workdir === ".") return undefined if (!workdir || workdir === ".") return undefined
const base = sync.data.path.directory const base = sync.path.directory
if (!base) return undefined if (!base) return undefined
const absolute = path.resolve(base, workdir) const absolute = path.resolve(base, workdir)
+12 -14
View File
@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
import { withTimeout } from "@/util/timeout" import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import type { Event } from "@opencode-ai/sdk/v2" import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk" import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { TuiConfig } from "@/config/tui" import { TuiConfig } from "@/config/tui"
@@ -43,18 +43,10 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
function createEventSource(client: RpcClient): EventSource { function createEventSource(client: RpcClient): EventSource {
return { return {
subscribe: async (directory, handler) => { subscribe: async (handler) => {
const id = await client.call("subscribe", { directory }) return client.on<GlobalEvent>("global.event", (e) => {
const unsub = client.on<{ id: string; event: Event }>("event", (e) => { handler(e)
if (e.id === id) {
handler(e.event)
}
}) })
return () => {
unsub()
client.call("unsubscribe", { id })
}
}, },
} }
} }
@@ -145,12 +137,18 @@ export const TuiThreadCommand = cmd({
), ),
}) })
worker.onerror = (e) => { worker.onerror = (e) => {
Log.Default.error(e) Log.Default.error("thread error", {
message: e.message,
filename: e.filename,
lineno: e.lineno,
colno: e.colno,
error: e.error,
})
} }
const client = Rpc.client<typeof rpc>(worker) const client = Rpc.client<typeof rpc>(worker)
const error = (e: unknown) => { const error = (e: unknown) => {
Log.Default.error(e) Log.Default.error("process error", { error: errorMessage(e) })
} }
const reload = () => { const reload = () => {
client.call("reload", undefined).catch((err) => { client.call("reload", undefined).catch((err) => {
@@ -26,6 +26,7 @@ export interface DialogSelectProps<T> {
keybind?: { keybind?: {
keybind?: Keybind.Info keybind?: Keybind.Info
title: string title: string
side?: "left" | "right"
disabled?: boolean disabled?: boolean
onTrigger: (option: DialogSelectOption<T>) => void onTrigger: (option: DialogSelectOption<T>) => void
}[] }[]
@@ -42,6 +43,7 @@ export interface DialogSelectOption<T = any> {
disabled?: boolean disabled?: boolean
bg?: RGBA bg?: RGBA
gutter?: JSX.Element gutter?: JSX.Element
margin?: JSX.Element
onSelect?: (ctx: DialogContext) => void onSelect?: (ctx: DialogContext) => void
} }
@@ -234,6 +236,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
props.ref?.(ref) props.ref?.(ref)
const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled && x.keybind) ?? []) const keybinds = createMemo(() => props.keybind?.filter((x) => !x.disabled && x.keybind) ?? [])
const left = createMemo(() => keybinds().filter((item) => item.side !== "right"))
const right = createMemo(() => keybinds().filter((item) => item.side === "right"))
return ( return (
<box gap={1} paddingBottom={1}> <box gap={1} paddingBottom={1}>
@@ -312,6 +316,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
<box <box
id={JSON.stringify(option.value)} id={JSON.stringify(option.value)}
flexDirection="row" flexDirection="row"
position="relative"
onMouseMove={() => { onMouseMove={() => {
setStore("input", "mouse") setStore("input", "mouse")
}} }}
@@ -335,6 +340,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
paddingRight={3} paddingRight={3}
gap={1} gap={1}
> >
<Show when={!current() && option.margin}>
<box position="absolute" left={1} flexShrink={0}>
{option.margin}
</box>
</Show>
<Option <Option
title={option.title} title={option.title}
footer={flatten() ? (option.category ?? option.footer) : option.footer} footer={flatten() ? (option.category ?? option.footer) : option.footer}
@@ -353,17 +363,38 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
</scrollbox> </scrollbox>
</Show> </Show>
<Show when={keybinds().length} fallback={<box flexShrink={0} />}> <Show when={keybinds().length} fallback={<box flexShrink={0} />}>
<box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}> <box
<For each={keybinds()}> paddingRight={2}
{(item) => ( paddingLeft={4}
<text> flexDirection="row"
<span style={{ fg: theme.text }}> justifyContent="space-between"
<b>{item.title}</b>{" "} flexShrink={0}
</span> paddingTop={1}
<span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span> >
</text> <box flexDirection="row" gap={2}>
)} <For each={left()}>
</For> {(item) => (
<text>
<span style={{ fg: theme.text }}>
<b>{item.title}</b>{" "}
</span>
<span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span>
</text>
)}
</For>
</box>
<box flexDirection="row" gap={2}>
<For each={right()}>
{(item) => (
<text>
<span style={{ fg: theme.text }}>
<b>{item.title}</b>{" "}
</span>
<span style={{ fg: theme.textMuted }}>{Keybind.toString(item.keybind)}</span>
</text>
)}
</For>
</box>
</box> </box>
</Show> </Show>
</box> </box>
@@ -2,6 +2,28 @@ import { RGBA } from "@opentui/core"
export namespace Terminal { export namespace Terminal {
export type Colors = Awaited<ReturnType<typeof colors>> export type Colors = Awaited<ReturnType<typeof colors>>
function parse(color: string): RGBA | null {
if (color.startsWith("rgb:")) {
const parts = color.substring(4).split("/")
return RGBA.fromInts(parseInt(parts[0], 16) >> 8, parseInt(parts[1], 16) >> 8, parseInt(parts[2], 16) >> 8, 255)
}
if (color.startsWith("#")) {
return RGBA.fromHex(color)
}
if (color.startsWith("rgb(")) {
const parts = color.substring(4, color.length - 1).split(",")
return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
}
return null
}
function mode(bg: RGBA | null): "dark" | "light" {
if (!bg) return "dark"
const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255
return luminance > 0.5 ? "light" : "dark"
}
/** /**
* Query terminal colors including background, foreground, and palette (0-15). * Query terminal colors including background, foreground, and palette (0-15).
* Uses OSC escape sequences to retrieve actual terminal color values. * Uses OSC escape sequences to retrieve actual terminal color values.
@@ -31,46 +53,26 @@ export namespace Terminal {
clearTimeout(timeout) clearTimeout(timeout)
} }
const parseColor = (colorStr: string): RGBA | null => {
if (colorStr.startsWith("rgb:")) {
const parts = colorStr.substring(4).split("/")
return RGBA.fromInts(
parseInt(parts[0], 16) >> 8, // Convert 16-bit to 8-bit
parseInt(parts[1], 16) >> 8,
parseInt(parts[2], 16) >> 8,
255,
)
}
if (colorStr.startsWith("#")) {
return RGBA.fromHex(colorStr)
}
if (colorStr.startsWith("rgb(")) {
const parts = colorStr.substring(4, colorStr.length - 1).split(",")
return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
}
return null
}
const handler = (data: Buffer) => { const handler = (data: Buffer) => {
const str = data.toString() const str = data.toString()
// Match OSC 11 (background color) // Match OSC 11 (background color)
const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/) const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
if (bgMatch) { if (bgMatch) {
background = parseColor(bgMatch[1]) background = parse(bgMatch[1])
} }
// Match OSC 10 (foreground color) // Match OSC 10 (foreground color)
const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/) const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
if (fgMatch) { if (fgMatch) {
foreground = parseColor(fgMatch[1]) foreground = parse(fgMatch[1])
} }
// Match OSC 4 (palette colors) // Match OSC 4 (palette colors)
const paletteMatches = str.matchAll(/\x1b]4;(\d+);([^\x07\x1b]+)/g) const paletteMatches = str.matchAll(/\x1b]4;(\d+);([^\x07\x1b]+)/g)
for (const match of paletteMatches) { for (const match of paletteMatches) {
const index = parseInt(match[1]) const index = parseInt(match[1])
const color = parseColor(match[2]) const color = parse(match[2])
if (color) paletteColors[index] = color if (color) paletteColors[index] = color
} }
@@ -100,15 +102,36 @@ export namespace Terminal {
}) })
} }
// Keep startup mode detection separate from `colors()`: the TUI boot path only
// needs OSC 11 and should resolve on the first background response instead of
// waiting on the full palette query used by system theme generation.
export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
const result = await colors() if (!process.stdin.isTTY) return "dark"
if (!result.background) return "dark"
const { r, g, b } = result.background return new Promise((resolve) => {
// Calculate luminance using relative luminance formula let timeout: NodeJS.Timeout
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
// Determine if dark or light based on luminance threshold const cleanup = () => {
return luminance > 0.5 ? "light" : "dark" process.stdin.setRawMode(false)
process.stdin.removeListener("data", handler)
clearTimeout(timeout)
}
const handler = (data: Buffer) => {
const match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/)
if (!match) return
cleanup()
resolve(mode(parse(match[1])))
}
process.stdin.setRawMode(true)
process.stdin.on("data", handler)
process.stdout.write("\x1b]11;?\x07")
timeout = setTimeout(() => {
cleanup()
resolve("dark")
}, 1000)
})
} }
} }
+3 -97
View File
@@ -6,14 +6,11 @@ import { InstanceBootstrap } from "@/project/bootstrap"
import { Rpc } from "@/util/rpc" import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade" import { upgrade } from "@/cli/upgrade"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import type { Event } from "@opencode-ai/sdk/v2"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { setTimeout as sleep } from "node:timers/promises"
import { writeHeapSnapshot } from "node:v8" import { writeHeapSnapshot } from "node:v8"
import { WorkspaceID } from "@/control-plane/schema"
import { Heap } from "@/cli/heap" import { Heap } from "@/cli/heap"
import { AppRuntime } from "@/effect/app-runtime"
await Log.init({ await Log.init({
print: process.argv.includes("--print-logs"), print: process.argv.includes("--print-logs"),
@@ -45,87 +42,6 @@ GlobalBus.on("event", (event) => {
let server: Awaited<ReturnType<typeof Server.listen>> | undefined let server: Awaited<ReturnType<typeof Server.listen>> | undefined
const eventStreams = new Map<string, AbortController>()
function startEventStream(directory: string) {
const id = crypto.randomUUID()
const abort = new AbortController()
const signal = abort.signal
eventStreams.set(id, abort)
async function run() {
while (!signal.aborted) {
const shouldReconnect = await Instance.provide({
directory,
init: InstanceBootstrap,
fn: () =>
new Promise<boolean>((resolve) => {
Rpc.emit("event", {
type: "server.connected",
properties: {},
} satisfies Event)
let settled = false
const settle = (value: boolean) => {
if (settled) return
settled = true
signal.removeEventListener("abort", onAbort)
unsub()
resolve(value)
}
const unsub = Bus.subscribeAll((event) => {
Rpc.emit("event", {
id,
event: event as Event,
})
if (event.type === Bus.InstanceDisposed.type) {
settle(true)
}
})
const onAbort = () => {
settle(false)
}
signal.addEventListener("abort", onAbort, { once: true })
}),
}).catch((error) => {
Log.Default.error("event stream subscribe error", {
error: error instanceof Error ? error.message : error,
})
return false
})
if (!shouldReconnect || signal.aborted) {
break
}
if (!signal.aborted) {
await sleep(250)
}
}
}
run().catch((error) => {
Log.Default.error("event stream error", {
error: error instanceof Error ? error.message : error,
})
})
return id
}
function stopEventStream(id: string) {
const abortController = eventStreams.get(id)
if (!abortController) return
abortController.abort()
eventStreams.delete(id)
}
export const rpc = { export const rpc = {
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) { async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
const headers = { ...input.headers } const headers = { ...input.headers }
@@ -138,7 +54,7 @@ export const rpc = {
headers, headers,
body: input.body, body: input.body,
}) })
const response = await Server.Default().fetch(request) const response = await Server.Default().app.fetch(request)
const body = await response.text() const body = await response.text()
return { return {
status: response.status, status: response.status,
@@ -158,7 +74,7 @@ export const rpc = {
async checkUpgrade(input: { directory: string }) { async checkUpgrade(input: { directory: string }) {
await Instance.provide({ await Instance.provide({
directory: input.directory, directory: input.directory,
init: InstanceBootstrap, init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: async () => { fn: async () => {
await upgrade().catch(() => {}) await upgrade().catch(() => {})
}, },
@@ -167,19 +83,9 @@ export const rpc = {
async reload() { async reload() {
await Config.invalidate(true) await Config.invalidate(true)
}, },
async subscribe(input: { directory: string | undefined }) {
return startEventStream(input.directory || process.cwd())
},
async unsubscribe(input: { id: string }) {
stopEventStream(input.id)
},
async shutdown() { async shutdown() {
Log.Default.info("worker shutting down") Log.Default.info("worker shutting down")
for (const id of [...eventStreams.keys()]) {
stopEventStream(id)
}
await Instance.disposeAll() await Instance.disposeAll()
if (server) await server.stop(true) if (server) await server.stop(true)
}, },
+2 -1
View File
@@ -1,6 +1,7 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { UI } from "../ui" import { UI } from "../ui"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "../../installation" import { Installation } from "../../installation"
import { Global } from "../../global" import { Global } from "../../global"
import fs from "fs/promises" import fs from "fs/promises"
@@ -57,7 +58,7 @@ export const UninstallCommand = {
UI.empty() UI.empty()
prompts.intro("Uninstall OpenCode") prompts.intro("Uninstall OpenCode")
const method = await Installation.method() const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
prompts.log.info(`Installation method: ${method}`) prompts.log.info(`Installation method: ${method}`)
const targets = await collectRemovalTargets(args, method) const targets = await collectRemovalTargets(args, method)
+8 -3
View File
@@ -1,6 +1,7 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { UI } from "../ui" import { UI } from "../ui"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { AppRuntime } from "@/effect/app-runtime"
import { Installation } from "../../installation" import { Installation } from "../../installation"
export const UpgradeCommand = { export const UpgradeCommand = {
@@ -24,7 +25,7 @@ export const UpgradeCommand = {
UI.println(UI.logo(" ")) UI.println(UI.logo(" "))
UI.empty() UI.empty()
prompts.intro("Upgrade") prompts.intro("Upgrade")
const detectedMethod = await Installation.method() const detectedMethod = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
const method = (args.method as Installation.Method) ?? detectedMethod const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") { if (method === "unknown") {
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`) prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
@@ -42,7 +43,9 @@ export const UpgradeCommand = {
} }
} }
prompts.log.info("Using method: " + method) prompts.log.info("Using method: " + method)
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest() const target = args.target
? args.target.replace(/^v/, "")
: await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest()))
if (Installation.VERSION === target) { if (Installation.VERSION === target) {
prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`) prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
@@ -53,7 +56,9 @@ export const UpgradeCommand = {
prompts.log.info(`From ${Installation.VERSION}${target}`) prompts.log.info(`From ${Installation.VERSION}${target}`)
const spinner = prompts.spinner() const spinner = prompts.spinner()
spinner.start("Upgrading...") spinner.start("Upgrading...")
const err = await Installation.upgrade(method, target).catch((err) => err) const err = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, target))).catch(
(err) => err,
)
if (err) { if (err) {
spinner.stop("Upgrade failed", 1) spinner.stop("Upgrade failed", 1)
if (err instanceof Installation.UpgradeFailedError) { if (err instanceof Installation.UpgradeFailedError) {
+4 -3
View File
@@ -1,12 +1,13 @@
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { AppRuntime } from "@/effect/app-runtime"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { Installation } from "@/installation" import { Installation } from "@/installation"
export async function upgrade() { export async function upgrade() {
const config = await Config.getGlobal() const config = await Config.getGlobal()
const method = await Installation.method() const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
const latest = await Installation.latest(method).catch(() => {}) const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {})
if (!latest) return if (!latest) return
if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) { if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) {
@@ -25,7 +26,7 @@ export async function upgrade() {
} }
if (method === "unknown") return if (method === "unknown") return
await Installation.upgrade(method, latest) await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, latest)))
.then(() => Bus.publish(Installation.Event.Updated, { version: latest })) .then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
.catch(() => {}) .catch(() => {})
} }
+6 -10
View File
@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import type { InstanceContext } from "@/project/instance"
import { SessionID, MessageID } from "@/session/schema" import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, ServiceMap } from "effect" import { Effect, Layer, Context } from "effect"
import { EffectLogger } from "@/effect/logger"
import z from "zod" import z from "zod"
import { Config } from "../config/config" import { Config } from "../config/config"
import { MCP } from "../mcp" import { MCP } from "../mcp"
@@ -70,7 +71,7 @@ export namespace Command {
readonly list: () => Effect.Effect<Info[]> readonly list: () => Effect.Effect<Info[]>
} }
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Command") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
@@ -79,7 +80,7 @@ export namespace Command {
const mcp = yield* MCP.Service const mcp = yield* MCP.Service
const skill = yield* Skill.Service const skill = yield* Skill.Service
const init = Effect.fn("Command.state")(function* (ctx) { const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) {
const cfg = yield* config.get() const cfg = yield* config.get()
const commands: Record<string, Info> = {} const commands: Record<string, Info> = {}
@@ -140,6 +141,7 @@ export namespace Command {
.map((message) => (message.content.type === "text" ? message.content.text : "")) .map((message) => (message.content.type === "text" ? message.content.text : ""))
.join("\n") || "", .join("\n") || "",
), ),
Effect.provide(EffectLogger.layer),
), ),
) )
}, },
@@ -186,10 +188,4 @@ export namespace Command {
Layer.provide(MCP.defaultLayer), Layer.provide(MCP.defaultLayer),
Layer.provide(Skill.defaultLayer), Layer.provide(Skill.defaultLayer),
) )
const { runPromise } = makeRuntime(Service, defaultLayer)
export async function list() {
return runPromise((svc) => svc.list())
}
} }

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