Compare commits

..

242 Commits

Author SHA1 Message Date
Kit Langton b60294c696 test(provider): avoid plugin dependency install timeout 2026-04-25 21:55:59 -04:00
Kit Langton 5a4763f560 test(httpapi): skip worktree mutation bridge on windows 2026-04-25 21:46:50 -04:00
Kit Langton 2a600797fb Merge remote-tracking branch 'origin/dev' into kit/httpapi-experimental-tools 2026-04-25 19:32:05 -04:00
Kit Langton 9175c68a77 feat(httpapi): bridge experimental tool routes 2026-04-25 19:32:05 -04:00
opencode-agent[bot] f77277a69e chore: generate 2026-04-25 23:28:25 +00:00
Kit Langton 450128f9be feat(httpapi): bridge mcp oauth endpoints (#24405) 2026-04-25 19:27:11 -04:00
opencode-agent[bot] 3e35c974a4 chore: generate 2026-04-25 23:17:18 +00:00
Kit Langton a14c22d4e9 feat(httpapi): bridge mcp control endpoints (#24403) 2026-04-25 19:16:19 -04:00
Kit Langton 58c65874ba feat(httpapi): bridge project update endpoint (#24398) 2026-04-25 18:55:49 -04:00
opencode-agent[bot] 27b0877714 chore: generate 2026-04-25 22:43:13 +00:00
Kit Langton 5904f599a9 feat(httpapi): bridge project git init endpoint (#24394) 2026-04-25 18:42:02 -04:00
Kit Langton df9e1d9854 feat(httpapi): bridge config update endpoint (#24387) 2026-04-25 17:52:34 -04:00
opencode-agent[bot] 75a22f82bd chore: generate 2026-04-25 19:36:15 +00:00
Kit Langton a369130226 feat(httpapi): bridge worktree mutations (#24371) 2026-04-25 15:35:15 -04:00
opencode-agent[bot] 474024f9e6 chore: generate 2026-04-25 19:25:41 +00:00
Kit Langton b4f4134e81 feat(httpapi): bridge instance dispose endpoint (#24368) 2026-04-25 15:24:07 -04:00
Kit Langton cd64b67038 feat(tui): show /connect tip when user has no models configured (#24014) 2026-04-25 15:01:41 -04:00
opencode-agent[bot] 9af46df535 chore: generate 2026-04-25 18:56:31 +00:00
Kit Langton b749866f0b feat(httpapi): bridge worktree read endpoint (#24366) 2026-04-25 14:55:29 -04:00
opencode-agent[bot] 60fa708f0b chore: update nix node_modules hashes 2026-04-25 18:49:27 +00:00
opencode-agent[bot] 3b74077437 chore: generate 2026-04-25 18:47:06 +00:00
Kit Langton 95d4bb2130 feat(httpapi): bridge experimental read endpoints (#24365) 2026-04-25 14:46:06 -04:00
Dax Raad f5dce6d960 core: move npm service to core package for shared dependency management 2026-04-25 14:36:15 -04:00
Dax Raad 1e98167b0e core: move cross-spawn-spawner to root and remove unused types
The cross-spawn-spawner module has been moved from src/effect/ to src/
to simplify the core package structure. The src/types.d.ts file which
contained unused type declarations has also been removed. All imports
throughout the codebase have been updated to reflect the new location.

This change reduces the package's internal complexity by flattening the
module hierarchy and removing dead code, making future maintenance easier.
2026-04-25 14:30:16 -04:00
Dax Raad 3eee2f6afa core: move cross-spawn-spawner from opencode to core package
Moved the cross-spawn-spawner module from packages/opencode to packages/core
to enable code sharing across the monorepo. This consolidates the process
spawning infrastructure into the core package so other packages can use
cross-platform child process spawning without duplicating the implementation.

Updated all import statements across the codebase to reference the new
location (@opencode-ai/core/effect/cross-spawn-spawner). Removed the
local copy from the opencode package along with its tests.
2026-04-25 14:23:17 -04:00
opencode-agent[bot] ff4b60e1f3 chore: generate 2026-04-25 18:14:26 +00:00
Aiden Cline f91b73b938 ci: fix model name 2026-04-25 14:13:19 -04:00
Kit Langton 05661c60ff feat(httpapi): bridge file search endpoints (#24356) 2026-04-25 14:12:54 -04:00
Kit Langton 625aca49de feat(tui): read Zed editor context from state db (#24352) 2026-04-25 18:10:58 +00:00
opencode-agent[bot] 3bc0c36ace chore: generate 2026-04-25 18:01:35 +00:00
Kit Langton eb0219988b feat(httpapi): bridge catalog read endpoints (#24353) 2026-04-25 14:00:30 -04:00
Dax Raad 705f792e87 core: move Global module to @opencode-ai/core for centralized path management
Move the Global module from packages/opencode/src/global to packages/core/src/global
to provide a unified location for managing XDG directories and application paths.
This eliminates duplicate path definitions across packages and ensures consistent
access to data, config, cache, state, log, and bin directories throughout the codebase.
2026-04-25 13:52:32 -04:00
Aiden Cline 716cf74190 ci: adjust review flow (#24355) 2026-04-25 13:52:19 -04:00
opencode-agent[bot] fc8dae2422 chore: update nix node_modules hashes 2026-04-25 17:50:26 +00:00
opencode-agent[bot] 27353df0cc chore: generate 2026-04-25 17:31:57 +00:00
Dax Raad 1a734adb4d core: consolidate shared infrastructure into core package
Moves effect logging, observability, runtime utilities, flags, installation
version info, and process utilities from opencode to core package. This
enables better code sharing across packages and establishes core as the
single source of truth for foundational utilities.

All internal imports updated to use @opencode-ai/core paths for consistency.
2026-04-25 13:30:37 -04:00
Kit Langton a9740b9133 fix(config): preserve permission order with Effect decode (#24308) 2026-04-25 13:30:12 -04:00
opencode-agent[bot] 62651c7114 chore: update nix node_modules hashes 2026-04-25 15:16:42 +00:00
Dax 1d728fc627 feat: add startup debug command (#24310) 2026-04-25 15:08:19 +00:00
Dax 62ef2a2207 refactor: rename shared package to core (#24309) 2026-04-25 10:59:17 -04:00
Dax 37aa8442dc refactor: remove lazy cross-spawn runtime (#24305) 2026-04-25 14:46:16 +00:00
opencode-agent[bot] 5b0e828c10 chore: generate 2026-04-25 14:43:27 +00:00
Kit Langton d5bfaef53d feat(httpapi): bridge instance read endpoints (#24258) 2026-04-25 10:42:31 -04:00
opencode bad732c26a sync release versions for v1.14.25 2026-04-25 14:37:01 +00:00
Dax Raad 1b92c95425 core: permission config schema now provides full IntelliSense for all tool permission keys
The permission configuration previously used a generic record type that didn't offer editor completions. Updated the schema to explicitly list all tool permission keys (read, edit, glob, grep, list, bash, task, external_directory, lsp, skill, todowrite, question, webfetch, websearch, codesearch, doom_loop) with proper types, enabling autocomplete when editing permission files.
2026-04-25 09:48:09 -04:00
Dax Raad d748c71845 ci: centralize opentui dependencies in workspace catalog
Use catalog references for @opentui/core, @opentui/solid, and opentui-spinner
across packages to ensure consistent versions and simplify updates.
2026-04-25 09:41:30 -04:00
opencode-agent[bot] fc88ed1262 chore: generate 2026-04-25 13:19:42 +00:00
Dax 66f93035b0 fix permission config order (#24222) 2026-04-25 13:18:42 +00:00
Simon Klee 9ff999cc2b tool/lsp: include request details in permission metadata (#24139) 2026-04-25 14:21:35 +02:00
Kit Langton 4877eccc0d Fix shell cwd after login startup (#24215) 2026-04-25 01:14:52 -04:00
Aiden Cline f7d527cd28 ci: adjust auto close issue script to use not planned instead of completed (#24253) 2026-04-25 00:47:36 -04:00
opencode-agent[bot] e29058c346 chore: update nix node_modules hashes 2026-04-25 03:04:44 +00:00
Maddison Hellstrom 49894330d9 fix(build): add prettier to devDependencies (#23255) 2026-04-24 22:47:05 -04:00
Luke Parker cdc7d5f2ea chore: group beta PR logs (#24236) 2026-04-25 10:42:33 +10:00
opencode-agent[bot] ec201623fb chore: generate 2026-04-25 00:34:02 +00:00
Luke Parker 386091b79a fix: validate beta before pushing (#24230) 2026-04-25 10:32:41 +10:00
Luke Parker 1e4b7b5451 Add Roslyn support for Razor and C# scripts (#24228) 2026-04-25 10:25:57 +10:00
opencode-agent[bot] 5cd178ba70 chore: generate 2026-04-24 22:05:23 +00:00
Kit Langton 97eb9fdee8 test(httpapi): cover hono bridge middleware (#24216) 2026-04-24 18:03:51 -04:00
Kit Langton 5a04de231e refactor(ripgrep): migrate result schemas to effect (#24213) 2026-04-24 17:42:52 -04:00
Kyle Altendorf bb3509b5ff fix(opencode): clarify git amend condition to require verifying commit landed (#19937)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-04-24 17:42:10 -04:00
Aiden Cline 4a67905266 fix: ensure gpt-5.5 compacts at correct context size when using openai oauth (#24212) 2026-04-24 17:39:06 -04:00
opencode-agent[bot] c4e33d3168 chore: generate 2026-04-24 21:38:31 +00:00
Aiden Cline 872cdff2ab ignore: denounce ai spammer 2026-04-24 17:37:28 -04:00
Kit Langton 361d7001d0 Clarify HttpApi migration plan (#24211) 2026-04-24 17:36:49 -04:00
opencode-agent[bot] 7a02eee0f4 chore: generate 2026-04-24 21:13:00 +00:00
Kit Langton cf45a8d807 refactor(schema): decode effect schemas directly (#24169) 2026-04-24 17:11:52 -04:00
Kit Langton 435becbea0 Refactor HttpApi auth middleware wiring (#24168) 2026-04-24 17:11:07 -04:00
Frank 0405bc74e9 zen: gpt-5.5 2026-04-24 14:57:23 -04:00
Frank 4dab2a8555 zen: gpt-5.5 2026-04-24 14:43:04 -04:00
Frank 3776d85e15 zen: gpt-5.5 2026-04-24 14:39:24 -04:00
Frank d01ad4c499 zen: gpt-5.5 2026-04-24 14:05:29 -04:00
opencode 28025a0f36 sync release versions for v1.14.24 2026-04-24 15:53:03 +00:00
opencode-agent[bot] 1220f784fe chore: generate 2026-04-24 15:48:08 +00:00
Frank 28f7d31e72 zen: deepseek v4 pro 2026-04-24 11:46:01 -04:00
opencode-agent[bot] 66936b0fff chore: update nix node_modules hashes 2026-04-24 15:45:10 +00:00
Sebastian 3a5507de95 Use OpenTUI theme detection for initial TUI mode, again (#23846) 2026-04-24 17:28:07 +02:00
Aiden Cline 86715fecc4 fix: ensure assistant messages always have reasoning on them for deepseek (#24180) 2026-04-24 11:10:47 -04:00
07akioni 3062d3e070 fix: use existingModel as fallback for interleaved field (#24172) 2026-04-24 10:41:41 -04:00
opencode-agent[bot] d89bfc32ac chore: generate 2026-04-24 13:20:23 +00:00
Kit Langton 011c23761b feat(httpapi): bridge mcp status endpoint (#24100) 2026-04-24 09:18:57 -04:00
opencode a8c8d2dd79 sync release versions for v1.14.23 2026-04-24 13:17:13 +00:00
Kit Langton 9f7ecd65e5 feat(httpapi): bridge file read endpoints (#24098) 2026-04-24 09:12:05 -04:00
Aiden Cline f8e939d96f fix: support max for deepseek (#24163) 2026-04-24 08:48:52 -04:00
黑墨水鱼 923af96d26 fix: preserve empty reasoning_content for DeepSeek V4 thinking mode (#24146)
Co-authored-by: Simon Klee <hello@simonklee.dk>
2026-04-24 08:42:57 -04:00
Aiden Cline a882e958b3 fix: deepseek variants (#24157) 2026-04-24 08:34:48 -04:00
Simon Klee 2cda629c8d test(prompt): align shell placeholder expectation (#24147) 2026-04-24 13:00:11 +02:00
Brendan Allan f033d2d8fb fix(app): conditionally show model variant selector (#24115) 2026-04-24 15:20:53 +08:00
Frank a4bd88ab97 zen: deepseek v4 pro 2026-04-24 02:09:14 -04:00
Frank f4616c8268 sync 2026-04-24 02:05:22 -04:00
Brendan Allan 4712f0f3c1 feat(prompt): add shell mode UI with cancel button, custom icon, and example placeholder (#24105) 2026-04-24 14:04:55 +08:00
opencode-agent[bot] 6c1268f3b1 chore: generate 2026-04-24 05:28:43 +00:00
Brendan Allan 2e156b8990 fix(desktop): avoid relaunching without installing updates (#23806) 2026-04-24 13:27:36 +08:00
Brendan Allan 3bfe6a1ef6 ci: add platform-specific bun install flags (#23822) 2026-04-24 04:50:34 +00:00
opencode-agent[bot] 5c5069b622 chore: generate 2026-04-23 21:44:44 +00:00
rahul f8c6ddd4cb feat(truncate): allow configuring tool output truncation limits (#23770)
Co-authored-by: rgs_ramp <rgs@ramp.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-23 17:43:33 -04:00
Kit Langton e50a688ca3 feat(httpapi): bridge workspace read endpoints (#24062) 2026-04-23 17:32:02 -04:00
Aiden Cline 334ab4707c fix: account for additional openai retry case (#24063) 2026-04-23 17:31:43 -04:00
Aiden Cline 87321942fe chore: update copilot readme to symlink to an agents md to prevent dumbass agents from touching these files (#24057) 2026-04-23 17:08:46 -04:00
opencode-agent[bot] a771859362 chore: generate 2026-04-23 20:50:19 +00:00
Kit Langton 31d01d404a refactor(control-plane): migrate workspace DTO schemas (#24056) 2026-04-23 16:48:56 -04:00
Kit Langton 814e83ffec docs: update effect schema migration tracker (#24054) 2026-04-23 16:23:45 -04:00
opencode-agent[bot] 4c3e65c877 chore: generate 2026-04-23 20:20:26 +00:00
James Long 98ea5b6e7e feat(tui): support builtin protocol for handling context from editors (#24034) 2026-04-23 16:19:19 -04:00
opencode-agent[bot] 3f8c659056 chore: generate 2026-04-23 20:10:56 +00:00
Kit Langton 3910a6e527 refactor(tool): migrate tool framework + all 18 built-in tools to Effect Schema (#23244) 2026-04-23 16:09:34 -04:00
opencode-agent[bot] 24892559ae chore: generate 2026-04-23 19:38:54 +00:00
Kit Langton cd93533b1f refactor(bus): migrate BusEvent to Effect Schema (#24040) 2026-04-23 15:37:44 -04:00
Kit Langton 0590452456 refactor(schema): use Schema.Int and consolidate PositiveInt/NonNegativeInt (#24029) 2026-04-23 13:22:48 -04:00
Kit Langton 93940a1859 refactor(provider): migrate provider domain to Effect Schema (#24027) 2026-04-23 13:17:33 -04:00
Frank 1e439b8226 sync 2026-04-23 13:13:29 -04:00
Kit Langton 8b2f8355b2 docs(schema): mark sync/index.ts migrated with compat-bridge note (#24024) 2026-04-23 12:54:46 -04:00
opencode-agent[bot] aed03078f8 chore: generate 2026-04-23 16:44:32 +00:00
Kit Langton c50d65b4d6 refactor(sync): make session events schema-first (#24019) 2026-04-23 12:43:08 -04:00
opencode-agent[bot] 353532b1c1 chore: generate 2026-04-23 16:00:01 +00:00
Shoubhit Dash 9df7c78ebe fix(npm): respect npmrc for version lookups (#24016) 2026-04-23 21:28:54 +05:30
opencode eb7555d3c6 sync release versions for v1.14.22 2026-04-23 15:56:49 +00:00
opencode-agent[bot] 2cd89d64e9 chore: generate 2026-04-23 15:31:16 +00:00
Kit Langton 0517ab4695 refactor(session): migrate session domain to Effect Schema (#24005) 2026-04-23 11:30:02 -04:00
James Long bbf67d0fff fix(tui): render all non-synthetic text parts of a user message (#24009) 2026-04-23 11:24:40 -04:00
Shoubhit Dash 38deb0f3ee fix(npm): respect npmrc config (#24001) 2026-04-23 19:54:01 +05:30
Simon Klee 9b6db08d21 chore: add to TEAM_MEMBERS (#23975) 2026-04-23 13:02:40 +02:00
opencode-agent[bot] 3ae74cb047 chore: generate 2026-04-23 10:58:14 +00:00
Brendan Allan 6002500bc0 feat(project): add icon_url_override field to projects (#23955) 2026-04-23 18:57:04 +08:00
Brendan Allan 785f3589ab fix: add keyed prop to Show components for proper reactivity (#23935) 2026-04-23 17:32:01 +08:00
Frank a419f1c50f zen: hy3 preview 2026-04-23 02:56:41 -04:00
opencode 871789ce64 sync release versions for v1.14.21 2026-04-23 05:45:21 +00:00
Brendan Allan df27baa00d refactor: remove redundant pending check from working memo (#23929) 2026-04-23 13:15:02 +08:00
Aiden Cline 9730008543 tweak: codex model logic (#23925) 2026-04-23 00:29:56 -04:00
Luke Parker ac26394fcb fix(beta): PR resolvers/smoke check should typecheck all pacakges (#23913) 2026-04-23 00:25:41 -04:00
Luke Parker 6387b35a2d log session sdk errors (#23652) 2026-04-23 00:25:41 -04:00
opencode-agent[bot] 1cd4c92242 chore: generate 2026-04-23 00:25:40 -04:00
Luke Parker e383df4b17 feat: support pull diagnostics in the LSP client (C#, Kotlin, etc) (#23771) 2026-04-23 00:25:40 -04:00
Caleb Norton 58db41b4b9 chore: update nix bun version (#23881) 2026-04-23 00:25:40 -04:00
opencode-agent[bot] 5d133f2785 chore: generate 2026-04-23 00:25:39 -04:00
Jack e9b1d3b940 docs: add MiMo V2.5 to Go pages (#23876) 2026-04-23 00:25:39 -04:00
Steven T. Cramer 3a082a0efd fix(project): use git common dir for bare repo project cache (#19054) 2026-04-23 00:25:39 -04:00
opencode-agent[bot] 504fd1b373 chore: generate 2026-04-23 00:25:39 -04:00
Shoubhit Dash 574b2c2170 fix(session): improve session compaction (#23870) 2026-04-23 00:25:38 -04:00
opencode-agent[bot] fa8b7bc4d2 chore: generate 2026-04-23 00:25:38 -04:00
Shoubhit Dash 6196b81e0a fix(tui): fail fast on invalid session startup (#23837) 2026-04-23 00:25:38 -04:00
Brendan Allan d884ab73d5 fix: consolidate project avatar source logic (#23819) 2026-04-23 00:25:37 -04:00
opencode-agent[bot] 71d196d3cd chore: generate 2026-04-23 00:25:37 -04:00
Luke Parker 20756e0ee4 test: fix cross-spawn stderr race on Windows CI (#23808) 2026-04-23 00:25:37 -04:00
opencode-agent[bot] 894e638914 chore: generate 2026-04-23 00:25:37 -04:00
Luke Parker 8113a4360e fix: preserve BOM in text tool round-trips (#23797) 2026-04-23 00:25:36 -04:00
opencode-agent[bot] c819804638 chore: update nix node_modules hashes 2026-04-23 00:25:36 -04:00
Brendan Allan 06066dbb7b fix(app): improve icon override handling in project edit dialog (#23768) 2026-04-23 00:25:36 -04:00
Luke Parker 69b8ea0d66 chore: bump Bun to 1.3.13 (#23791) 2026-04-23 00:25:36 -04:00
opencode-agent[bot] b0455583aa chore: generate 2026-04-22 03:41:42 +00:00
Kit Langton ed802fd121 refactor(core): migrate MessageV2 errors to Schema-backed named errors (#23764) 2026-04-21 23:40:32 -04:00
Kit Langton 1593c3ed16 refactor(core): migrate MessageV2 internal Cursor to Effect Schema (#23763) 2026-04-21 23:28:33 -04:00
Kit Langton e89543811c refactor(core): migrate MessageV2 message DTOs (User/Assistant/Part/Info/WithParts) to Effect Schema (#23757) 2026-04-21 23:26:12 -04:00
opencode-agent[bot] 1a76799fd8 chore: generate 2026-04-22 03:18:25 +00:00
Kit Langton fa623964a2 refactor(core): migrate MessageV2 part leaves + ToolPart to Effect Schema (#23756) 2026-04-21 23:17:23 -04:00
Frank 628102ad04 zen: handle alibaba format 2026-04-21 23:13:44 -04:00
Kit Langton ad7ae7353f refactor(core): derive all schema.ts leaves' .zod via effect-zod walker (#23754) 2026-04-21 22:51:18 -04:00
NN708 8043cfa68d fix(desktop): update desktop file and MetaInfo file (#14933) 2026-04-22 07:19:04 +08:00
opencode-agent[bot] d2181e9273 chore: generate 2026-04-21 22:04:16 +00:00
Mathews Bryan 5e9fb3cc90 feat: replace csharp-ls with roslyn-language-server (#14463)
Co-authored-by: Mathews <Mathews.Bryan@cincpro.com>
2026-04-21 22:03:09 +00:00
Kit Langton 2da6d860e0 refactor(core): derive provider schema .zod via effect-zod walker (#23753) 2026-04-21 17:49:24 -04:00
Kit Langton df0c1f649c refactor(core): migrate MessageV2 tool state schemas to Effect Schema (#23752) 2026-04-21 17:47:50 -04:00
Kit Langton d6dea3f3e0 chore(core): clean up after ConfigPermission Effect Schema migration (#23749) 2026-04-21 17:40:54 -04:00
Kit Langton 0bcf734a67 migrate Snapshot schemas to Effect Schema (#23747) 2026-04-21 17:37:27 -04:00
opencode-agent[bot] b1c3095edd chore: generate 2026-04-21 21:34:17 +00:00
Kit Langton b0f565b74a refactor(core): migrate ConfigPermission.Info to Effect Schema canonical (#23740) 2026-04-21 17:33:13 -04:00
Kit Langton 2ae64f426b refactor(core): migrate MessageV2.Format to Effect Schema (#23744) 2026-04-21 17:30:08 -04:00
Kit Langton 7933657135 migrate LSP data schemas to Effect Schema (#23745) 2026-04-21 17:26:50 -04:00
Frank caaddf0964 zen: ling 2.6 free 2026-04-21 17:06:31 -04:00
Ruben De Smet 1a20703469 feat: add Mistral Small reasoning variant support (issue #19479) (#23735) 2026-04-21 16:45:06 -04:00
github-actions[bot] 8751f48a75 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/23735#issuecomment-4291498854
2026-04-21 20:17:13 +00:00
Aiden Cline 58232d896e fix: dont show variants for kimi models that dont support them (#23696) 2026-04-21 15:33:35 -04:00
Rahul Iyer cd6415f332 fix(tui): don't check for version upgrades if it's disabled by the user (#20089)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-21 15:30:15 -04:00
opencode c9fb8d0ce7 sync release versions for v1.14.20 2026-04-21 19:07:36 +00:00
opencode-agent[bot] 1e1a500603 chore: generate 2026-04-21 18:08:04 +00:00
Kit Langton ecc06a3d8f refactor(core): make Config.Info canonical Effect Schema (#23716) 2026-04-21 14:06:47 -04:00
opencode-agent[bot] 3205f122eb chore: update nix node_modules hashes 2026-04-21 16:57:27 +00:00
Aiden Cline e95474df05 fix: revert parts of a824064c4 which caused system theme regression (#23714) 2026-04-21 12:08:12 -04:00
Kit Langton 96a534d8c6 feat(core): bridge GET /config through experimental HttpApi (#23712) 2026-04-21 16:05:23 +00:00
Kit Langton 9579429276 test(opencode): consolidate session prompt tests into Effect style (#23710) 2026-04-21 15:54:40 +00:00
Aiden Cline 2486621ca1 chore: kill unused tool (#23701) 2026-04-21 11:31:20 -04:00
James Long b5acc2203c fix(core): fix permissions routing when using remote workspace (#23593) 2026-04-21 14:42:54 +00:00
Brendan Allan 8cc2c81d57 fix(app): prevent prompt input animations from rerunning on every render (#23676) 2026-04-21 19:12:32 +08:00
opencode-agent[bot] 8d2d12d9c6 chore: generate 2026-04-21 11:11:54 +00:00
Brendan Allan 811a7e9a8b feat(app): allow disabling progress bar in settings (#23674) 2026-04-21 19:10:50 +08:00
Brendan Allan febadc5589 fix(ui): correct diff render condition logic (#23670) 2026-04-21 18:49:04 +08:00
Luke Parker 92c005866b fix(core): use file:// URLs for local dynamic import() on Windows+Node (#23639) 2026-04-21 07:54:53 +00:00
OpeOginni 224548d87d fix(desktop): adjust layout properties in DialogSelectServer component (#23589) 2026-04-21 14:38:56 +08:00
Frank 8a7bb7c6a9 zen: tpm routing 2026-04-21 02:36:40 -04:00
Brendan Allan 22d33c57af fix(app): properly wrap produce calls in setProjects (#23638) 2026-04-21 14:11:23 +08:00
Jack 1e0137f624 go: promote kimi k2.6 usage limits (#23634)
Co-authored-by: Frank <frank@anoma.ly>
2026-04-21 02:01:52 -04:00
Brendan Allan 38e2f4cdda fix(desktop-electron): add CORS headers to main window webRequest (#23633) 2026-04-21 13:32:31 +08:00
Frank bd54b68c12 zen: m2.7 & k2.6 2026-04-21 01:15:07 -04:00
opencode-agent[bot] a08aa21cb3 chore: generate 2026-04-21 04:40:02 +00:00
Brendan Allan eb9906420f refactor(desktop-electron): enable contextIsolation and sandbox (#23523) 2026-04-21 12:38:59 +08:00
opencode-agent[bot] 4964ce480c chore: generate 2026-04-21 04:37:57 +00:00
Brendan Allan e5687d646c electron: use custom oc:// protocol for renderer windows (#23516) 2026-04-21 12:36:56 +08:00
opencode-agent[bot] a38d53fe2f chore: generate 2026-04-21 02:42:45 +00:00
Frank 6278ce51ce zen: tpm routing 2026-04-20 22:41:36 -04:00
opencode-agent[bot] 53b0084ce2 chore: generate 2026-04-21 02:22:12 +00:00
Frank f74a255ca9 zen: tpm routing 2026-04-20 22:21:06 -04:00
Frank 3e8abac625 sync 2026-04-20 17:01:27 -04:00
opencode-agent[bot] 65e99fcc2e chore: generate 2026-04-20 20:06:22 +00:00
Frank bad025eba9 sync 2026-04-20 16:05:04 -04:00
opencode-agent[bot] 06dde3afd3 chore: generate 2026-04-20 19:04:24 +00:00
Frank ad65af28e7 zen: tpm routing 2026-04-20 15:02:54 -04:00
opencode-agent[bot] bd1bdc4f04 chore: generate 2026-04-20 18:29:48 +00:00
James Long debcff2b6b feat(core): add debug workspace server (#23590) 2026-04-20 18:27:58 +00:00
opencode-agent[bot] 8b3323708d chore: generate 2026-04-20 17:43:32 +00:00
James Murdza 3406f18746 fix(plugin): add env parameter to WorkspaceAdaptor.create type (#23235) 2026-04-20 13:41:38 -04:00
opencode-agent[bot] 7e576eea41 chore: generate 2026-04-20 15:59:40 +00:00
Jack d68ebee555 docs(go): add Kimi K2.6 to Go and Zen content (#23558) 2026-04-20 23:58:32 +08:00
Frank ae7a3518f7 zen: tpm based routing 2026-04-20 09:56:26 -04:00
Chris Yang 16caaa2229 fix(app): fall back to icon.url in sidebar avatar (#18747) 2026-04-20 19:32:54 +08:00
黑墨水鱼 91468fe455 fix(ui): use parentID matching instead of positional scan for assistant messages (#23093) 2026-04-20 07:35:06 +00:00
opencode 7c6948cf6f sync release versions for v1.14.19 2026-04-20 07:21:46 +00:00
Luke Parker 7a568a457f fix: defer MessageV2.Assistant.shape access to break circular dep in compiled binary (#23495) 2026-04-20 06:39:13 +00:00
opencode-agent[bot] 3ddc69ec2b chore: update nix node_modules hashes 2026-04-20 06:36:01 +00:00
opencode-agent[bot] f3d5a71620 chore: generate 2026-04-20 06:07:28 +00:00
Aiden Cline c6c56ac2cf tweak: rename tail_tokens -> preserve_recent_tokens (#23491) 2026-04-20 01:06:29 -05:00
Aiden Cline e539efe2b9 fix: patch arborist to get around bun bug (#23460) 2026-04-20 00:49:46 -05:00
Brendan Allan 687b758882 app: better loading (#23489) 2026-04-20 05:17:37 +00:00
opencode-agent[bot] 84e322b0fd chore: generate 2026-04-20 05:15:29 +00:00
Aiden Cline 8bc4f91fd9 fix: parallel edits sometimes would override each other (#23483) 2026-04-20 00:14:21 -05:00
Brendan Allan 93e633fb7d refactor(app): move QueryProvider to AppInterface (#23484) 2026-04-20 12:51:34 +08:00
opencode-agent[bot] cbe702c09d chore: generate 2026-04-20 04:40:12 +00:00
Luke Parker a7a85c94b8 fix(core): fix Windows managed install and bump ripgrep to 15.1.0 for ARM64 support (#23477) 2026-04-20 14:39:15 +10:00
Annie Surla 6e0178655b feat(provider): add NVIDIA to popular providers, docs, and attribution headers (#22927)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-04-19 19:57:49 -05:00
Dax Raad e4be557928 ci: skip beta smoke fixes for now 2026-04-19 20:01:30 -04:00
Dax Raad b9640fc7e4 core: fix session compaction test to properly enable prune config option 2026-04-19 19:53:43 -04:00
opencode-agent[bot] 29f05cb1ee chore: generate 2026-04-19 23:48:44 +00:00
Dax Raad 48acab48ad ci: skip Docker builds during preview releases to save time 2026-04-19 19:47:29 -04:00
Dax Raad 5ae74aa881 Merge branch 'nxl/improve-compaction-strategy' into dev 2026-04-19 19:38:46 -04:00
Dax Raad 6eddf08244 flip toolcall prune defaults 2026-04-19 19:34:44 -04:00
opencode-agent[bot] 9c7e52b8a1 chore: update nix node_modules hashes 2026-04-19 22:52:42 +00:00
Sebastian a824064c4c stabilize TUI theme persistence and KV writes (#23188) 2026-04-20 00:10:31 +02:00
Brendan Allan a4882290aa Merge branch 'dev' into nxl/improve-compaction-strategy 2026-04-17 11:53:17 +08:00
Shoubhit Dash 42771c1db3 fix(compaction): budget retained tail with media 2026-04-16 17:30:29 +05:30
Shoubhit Dash 2e18a603f0 merge dev 2026-04-16 17:30:14 +05:30
Aiden Cline 9819eb0461 tweak: disable 2026-04-09 23:11:09 -05:00
Shoubhit Dash aa86fb75ad refactor compaction tail selection 2026-04-10 09:36:39 +05:30
Shoubhit Dash 6f5a3d30fd keep recent turns during session compaction 2026-04-10 09:34:01 +05:30
711 changed files with 30621 additions and 22673 deletions
+1
View File
@@ -13,3 +13,4 @@ R44VC0RP
rekram1-node
RhysSullivan
thdxr
simonklee
+2
View File
@@ -27,8 +27,10 @@ r44vc0rp
rekram1-node
-ricardo-m-l
-robinmordasiewicz
rubdos
shantur
simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr
-toastythebot
-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person
+7 -2
View File
@@ -1,5 +1,10 @@
name: "Setup Bun"
description: "Setup Bun with caching and install dependencies"
inputs:
install-flags:
description: "Additional flags to pass to 'bun install'"
required: false
default: ""
runs:
using: "composite"
steps:
@@ -46,8 +51,8 @@ runs:
# e.g. ./patches/ for standard-openapi
# https://github.com/oven-sh/bun/issues/28147
if [ "$RUNNER_OS" = "Windows" ]; then
bun install --linker hoisted
bun install --linker hoisted ${{ inputs.install-flags }}
else
bun install
bun install ${{ inputs.install-flags }}
fi
shell: bash
@@ -1,27 +0,0 @@
"on":
push:
branches:
- opencode-remote-voice
name: Deploy to apn-relay
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: porter apply
env:
PORTER_APP_NAME: apn-relay
PORTER_CLUSTER: "5534"
PORTER_DEPLOYMENT_TARGET_ID: d60e67f5-b0a6-4275-8ed6-3cebaf092147
PORTER_HOST: https://dashboard.porter.run
PORTER_PROJECT: "18525"
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_APP_18525_975734319 }}
+6 -2
View File
@@ -402,12 +402,14 @@ jobs:
fail-fast: false
matrix:
settings:
- host: macos-latest
- host: macos-26-intel
target: x86_64-apple-darwin
platform_flag: --mac --x64
- host: macos-latest
bun_install_flags: --os=darwin --cpu=x64
- host: macos-26
target: aarch64-apple-darwin
platform_flag: --mac --arm64
bun_install_flags: --os=darwin --cpu=arm64
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: "windows-2025"
target: aarch64-pc-windows-msvc
@@ -437,6 +439,8 @@ jobs:
run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
- uses: ./.github/actions/setup-bun
with:
install-flags: ${{ matrix.settings.bun_install_flags }}
- name: Azure login
if: runner.os == 'Windows'
+2 -2
View File
@@ -45,13 +45,13 @@ jobs:
- name: Check PR guidelines compliance
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
PR_TITLE: ${{ steps.pr-details.outputs.title }}
run: |
PR_BODY=$(jq -r .body pr_data.json)
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}'
<pr-number>
${{ steps.pr-number.outputs.number }}
+19 -10
View File
@@ -1,21 +1,30 @@
---
name: effect
description: Answer questions about the Effect framework
description: Work with Effect v4 / effect-smol TypeScript code in this repo
---
# Effect
This codebase uses Effect, a framework for writing typescript.
This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows.
## How to Answer Effect Questions
## Source Of Truth
1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
`.opencode/references/effect-smol` in this project NOT the skill folder.
2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
3. Provide responses based on the actual Effect source code and documentation
Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder.
2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
3. Also inspect existing repo code for local house style before introducing new patterns.
4. Prefer answers and implementations backed by specific source files or nearby repo examples.
## Guidelines
- Always use the explore agent with the cloned repository when answering Effect-related questions
- Reference specific files and patterns found in the Effect codebase
- Do not answer from memory - always verify against the source
- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses.
- Use `Effect.gen(function* () { ... })` for multi-step workflows.
- Use `Effect.fn("Name")` or `Effect.fnUntraced(...)` for named effects when adding reusable service methods or important workflows.
- Prefer Effect `Schema` for API and domain data shapes. Use branded schemas for IDs and `Schema.TaggedErrorClass` for typed domain errors when modeling new error surfaces.
- Keep HTTP handlers thin: decode input, read request context, call services, and map transport errors. Put business rules in services.
- In Effect service code, prefer Effect-aware platform abstractions and dependencies over ad hoc promises where the surrounding code already does so.
- Keep layer composition explicit. Avoid broad hidden provisioning that makes missing dependencies hard to see.
- In tests, prefer the repo's existing Effect test helpers and live tests for filesystem, git, child process, locks, or timing behavior.
- Do not introduce `any`, non-null assertions, unchecked casts, or older Effect APIs just to satisfy types.
- Do not answer from memory. Verify against `.opencode/references/effect-smol` or nearby code first.
+42 -38
View File
@@ -1,8 +1,12 @@
# OpenCode Monorepo Agent Guide
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`.
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
This file is for coding agents working in `/Users/ryanvogel/dev/opencode`.
## Style Guide
## Scope And Precedence
### General Principles
- Keep things in one function unless composable or reusable
- Avoid `try`/`catch` where possible
@@ -52,48 +56,48 @@ else foo = 2
### Control Flow
- Prefer early returns over nested `else` blocks.
- Keep functions focused; split only when it improves reuse or readability.
Avoid `else` statements. Prefer early returns.
### Error Handling
```ts
// Good
function foo() {
if (condition) return 1
return 2
}
- Fail with actionable messages.
- Avoid swallowing errors silently.
- Log enough context to debug production issues (IDs, env, status), but never secrets.
- In UI code, degrade gracefully for missing capabilities.
// Bad
function foo() {
if (condition) return 1
else return 2
}
```
### Data / DB
### Schema Definitions (Drizzle)
- For Drizzle schema, use snake_case fields and columns.
- Keep migration and schema changes minimal and explicit.
- Follow package-specific DB guidance in `packages/opencode/AGENTS.md`.
Use snake_case for field names so column names don't need to be redefined as strings.
### Testing Philosophy
```ts
// Good
const table = sqliteTable("session", {
id: text().primaryKey(),
project_id: text().notNull(),
created_at: integer().notNull(),
})
- Prefer testing real behavior over mocks.
- Add regression tests for bug fixes where practical.
- Keep fixtures small and focused.
// Bad
const table = sqliteTable("session", {
id: text("id").primaryKey(),
projectID: text("project_id").notNull(),
createdAt: integer("created_at").notNull(),
})
```
## Agent Workflow Tips
## Testing
- Read existing code paths before introducing new abstractions.
- Match local patterns first; do not impose a new style per file.
- If a package has its own `AGENTS.md`, review it before editing.
- For OpenCode Effect services, follow `packages/opencode/AGENTS.md` strictly.
- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
## Known Operational Notes
## Type Checking
- `packages/app/AGENTS.md` says: never restart app/server processes during that package's debugging workflow.
- `packages/app/AGENTS.md` also documents local backend+web split for UI work.
- `packages/opencode/AGENTS.md` contains mandatory Effect and database conventions.
## Regeneration / Special Scripts
- Regenerate JS SDK with: `./packages/sdk/js/script/build.ts`
## Quick Checklist Before Finishing
- Ran relevant package checks.
- Updated docs/config when behavior changed.
- Avoided committing unrelated files.
- Kept edits minimal and aligned with local conventions.
- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
+107 -1596
View File
File diff suppressed because it is too large Load Diff
View File
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1773909469,
"narHash": "sha256-vglVrLfHjFIzIdV9A27Ugul6rh3I1qHbbitGW7dk420=",
"lastModified": 1776683584,
"narHash": "sha256-NuTLMrr10Tng72hurYG8jYQ4XKK8wnpJmOGcPiis96g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7149c06513f335be57f26fcbbbe34afda923882b",
"rev": "9dd5558b06dbdacbf635a3dd36dce1b1a7ee3a89",
"type": "github"
},
"original": {
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-i9TxYwWkJAR+kW6pbvhgQbRW9UYPtdrPQAGic4zPoa4=",
"aarch64-linux": "sha256-RYc/OYlETXUwkWBRDas+/P4cBW6zde4FqxxnMARu5vs=",
"aarch64-darwin": "sha256-jIhUOIRIQEa2WT62TVIedmRIhl/edhK8sbiAFvU3yCM=",
"x86_64-darwin": "sha256-xLGzaX7OofFlZzVgpORJR5QXD2u+54hp+t3cCfUtO84="
"x86_64-linux": "sha256-LpzWEZzURUEj7fcHGvh33gM7D9GNPE+XIvU0/hmdcQM=",
"aarch64-linux": "sha256-0zdO3zuj6g9cMZFEOsvQJcKKcPjGVZJ2DkJdDcb2VCM=",
"aarch64-darwin": "sha256-dmT8R9Pmzh5tjO8NCCCtENiQpJQeifQpVdhaty1MXOs=",
"x86_64-darwin": "sha256-Q6rAQRoC6WaMAQl++YHAZmbNuO303cWgGaYzXaRlzy4="
}
}
+6 -4
View File
@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.11",
"packageManager": "bun@1.3.13",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev:desktop": "bun --cwd packages/desktop-electron dev",
@@ -30,12 +30,12 @@
"@effect/opentelemetry": "4.0.0-beta.48",
"@effect/platform-node": "4.0.0-beta.48",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.11",
"@types/bun": "1.3.12",
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.1.99",
"@opentui/solid": "0.1.99",
"@opentui/core": "0.1.103",
"@opentui/solid": "0.1.103",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
@@ -46,6 +46,7 @@
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.1.0-beta.18",
"opentui-spinner": "0.0.6",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
@@ -127,6 +128,7 @@
"@types/node": "catalog:"
},
"patchedDependencies": {
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
}
-11
View File
@@ -1,11 +0,0 @@
PORT=8787
DATABASE_HOST=
DATABASE_USERNAME=
DATABASE_PASSWORD=
DATABASE_NAME=main
APNS_TEAM_ID=
APNS_KEY_ID=
APNS_PRIVATE_KEY=
APNS_DEFAULT_BUNDLE_ID=com.anomalyco.mobilevoice
-106
View File
@@ -1,106 +0,0 @@
# apn-relay Agent Guide
This file defines package-specific guidance for agents working in `packages/apn-relay`.
## Scope And Precedence
- Follow root `AGENTS.md` first.
- This file provides stricter package-level conventions for relay service work.
- If future local guides are added, closest guide wins.
## Project Overview
- Minimal APNs relay service (Hono + Bun + PlanetScale via Drizzle).
- Core routes:
- `GET /health`
- `GET /`
- `POST /v1/device/register`
- `POST /v1/device/unregister`
- `POST /v1/event`
## Commands
Run all commands from `packages/apn-relay`.
- Install deps: `bun install`
- Start relay locally: `bun run dev`
- Typecheck: `bun run typecheck`
- DB connectivity check: `bun run db:check`
## Build / Test Expectations
- There is no dedicated package test script currently.
- Required validation for behavior changes:
- `bun run typecheck`
- `bun run db:check` when DB/env changes are involved
- manual endpoint verification against `/health`, `/v1/device/register`, `/v1/event`
## Single-Test Guidance
- No single-test command exists for this package today.
- For focused checks, run endpoint-level manual tests against a local dev server.
## Code Style Guidelines
### Formatting / Structure
- Keep handlers compact and explicit.
- Prefer small local helpers for repeated route logic.
- Avoid broad refactors when a targeted fix is enough.
### Types / Validation
- Validate request bodies with `zod` at route boundaries.
- Keep payload and DB row shapes explicit and close to usage.
- Avoid `any`; narrow unknown input immediately after parsing.
### Naming
- Follow existing concise naming in this package (`reg`, `unreg`, `evt`, `row`, `key`).
- For DB columns, keep snake_case alignment with schema.
### Error Handling
- Return clear JSON errors for invalid input.
- Keep handler failures observable via `app.onError` and structured logs.
- Do not leak secrets in responses or logs.
### Logging
- Log delivery lifecycle at key checkpoints:
- registration/unregistration attempts
- event fanout start/end
- APNs send failures and retries
- Mask sensitive values; prefer token suffixes and metadata.
### APNs Environment Rules
- Keep APNs env explicit per registration (`sandbox` / `production`).
- For `BadEnvironmentKeyInToken`, retry once with flipped env and persist correction.
- Avoid infinite retry loops; one retry max per delivery attempt.
## Database Conventions
- Schema is in `src/schema.sql.ts`.
- Keep table/column names snake_case.
- Maintain index naming consistency with existing schema.
- For upserts, update only fields required by current behavior.
## API Behavior Expectations
- `register`/`unregister` must be idempotent.
- `event` should return success envelope even when no devices are registered.
- Delivery logs should capture per-attempt result and error payload.
## Operational Notes
- Ensure `APNS_PRIVATE_KEY` supports escaped newline format (`\n`) and raw multiline.
- Validate that `APNS_DEFAULT_BUNDLE_ID` matches mobile app bundle identifier.
- Avoid coupling route behavior to deployment platform specifics.
## Before Finishing
- Run `bun run typecheck`.
- If DB/env behavior changed, run `bun run db:check`.
- Manually exercise affected endpoints.
- Confirm logs are useful and secret-safe.
-14
View File
@@ -1,14 +0,0 @@
FROM oven/bun:1.3.11-alpine
WORKDIR /app
COPY package.json ./
COPY tsconfig.json ./
COPY drizzle.config.ts ./
RUN bun install --production
COPY src ./src
EXPOSE 8787
CMD ["bun", "run", "src/index.ts"]
-46
View File
@@ -1,46 +0,0 @@
# APN Relay
Minimal APNs relay for OpenCode mobile background notifications.
## What it does
- Registers iOS device tokens for a shared secret.
- Receives OpenCode event posts (`complete`, `permission`, `error`).
- Sends APNs notifications to mapped devices.
- Stores delivery rows in PlanetScale.
## Routes
- `GET /health`
- `GET /` (simple dashboard)
- `POST /v1/device/register`
- `POST /v1/device/unregister`
- `POST /v1/event`
## Environment
Use `.env.example` as a starting point.
- `DATABASE_HOST`
- `DATABASE_USERNAME`
- `DATABASE_PASSWORD`
- `APNS_TEAM_ID`
- `APNS_KEY_ID`
- `APNS_PRIVATE_KEY`
- `APNS_DEFAULT_BUNDLE_ID`
## Run locally
```bash
bun install
bun run src/index.ts
```
## Docker
Build from this directory:
```bash
docker build -t apn-relay .
docker run --rm -p 8787:8787 --env-file .env apn-relay
```
-17
View File
@@ -1,17 +0,0 @@
import { defineConfig } from "drizzle-kit"
export default defineConfig({
out: "./migration",
strict: true,
schema: ["./src/**/*.sql.ts"],
dialect: "mysql",
dbCredentials: {
host: process.env.DATABASE_HOST ?? "",
user: process.env.DATABASE_USERNAME ?? "",
password: process.env.DATABASE_PASSWORD ?? "",
database: process.env.DATABASE_NAME ?? "main",
ssl: {
rejectUnauthorized: false,
},
},
})
-27
View File
@@ -1,27 +0,0 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/apn-relay",
"version": "0.0.0",
"private": true,
"type": "module",
"license": "MIT",
"scripts": {
"dev": "bun run src/index.ts",
"db:check": "bun run --env-file .env src/check.ts",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@planetscale/database": "1.19.0",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"hono": "4.10.7",
"jose": "6.0.11",
"zod": "4.1.8"
},
"devDependencies": {
"@tsconfig/bun": "1.0.9",
"@types/bun": "1.3.11",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"typescript": "5.8.2"
}
}
-185
View File
@@ -1,185 +0,0 @@
import { connect } from "node:http2"
import { SignJWT, importPKCS8 } from "jose"
import { env } from "./env"
export type PushEnv = "sandbox" | "production"
type PushInput = {
token: string
bundle: string
env: PushEnv
title: string
body: string
data: Record<string, unknown>
}
type PushResult = {
ok: boolean
code: number
error?: string
}
function tokenSuffix(input: string) {
return input.length > 8 ? input.slice(-8) : input
}
let jwt = ""
let exp = 0
let pk: Awaited<ReturnType<typeof importPKCS8>> | undefined
function host(input: PushEnv) {
if (input === "sandbox") return "api.sandbox.push.apple.com"
return "api.push.apple.com"
}
function key() {
if (env.APNS_PRIVATE_KEY.includes("\\n")) return env.APNS_PRIVATE_KEY.replace(/\\n/g, "\n")
return env.APNS_PRIVATE_KEY
}
async function sign() {
if (!pk) pk = await importPKCS8(key(), "ES256")
const now = Math.floor(Date.now() / 1000)
if (jwt && now < exp) return jwt
jwt = await new SignJWT({})
.setProtectedHeader({ alg: "ES256", kid: env.APNS_KEY_ID })
.setIssuer(env.APNS_TEAM_ID)
.setIssuedAt(now)
.sign(pk)
exp = now + 50 * 60
return jwt
}
function post(input: {
host: string
token: string
auth: string
bundle: string
payload: string
}): Promise<{ code: number; body: string }> {
return new Promise((resolve, reject) => {
const cli = connect(`https://${input.host}`)
let done = false
let code = 0
let body = ""
const stop = (fn: () => void) => {
if (done) return
done = true
fn()
}
cli.on("error", (err) => {
stop(() => reject(err))
cli.close()
})
const req = cli.request({
":method": "POST",
":path": `/3/device/${input.token}`,
authorization: `bearer ${input.auth}`,
"apns-topic": input.bundle,
"apns-push-type": "alert",
"apns-priority": "10",
"content-type": "application/json",
})
req.setEncoding("utf8")
req.on("response", (headers) => {
code = Number(headers[":status"] ?? 0)
})
req.on("data", (chunk) => {
body += chunk
})
req.on("end", () => {
stop(() => resolve({ code, body }))
cli.close()
})
req.on("error", (err) => {
stop(() => reject(err))
cli.close()
})
req.end(input.payload)
})
}
export async function send(input: PushInput): Promise<PushResult> {
const apnsHost = host(input.env)
const suffix = tokenSuffix(input.token)
console.log("[ APN RELAY ] push:start", {
env: input.env,
host: apnsHost,
bundle: input.bundle,
tokenSuffix: suffix,
})
const auth = await sign().catch((err) => {
return `error:${String(err)}`
})
if (auth.startsWith("error:")) {
console.log("[ APN RELAY ] push:auth-failed", {
env: input.env,
host: apnsHost,
bundle: input.bundle,
tokenSuffix: suffix,
error: auth,
})
return {
ok: false,
code: 0,
error: auth,
}
}
const payload = JSON.stringify({
aps: {
alert: {
title: input.title,
body: input.body,
},
sound: "alert.wav",
},
...input.data,
})
const out = await post({
host: apnsHost,
token: input.token,
auth,
bundle: input.bundle,
payload,
}).catch((err) => ({
code: 0,
body: String(err),
}))
if (out.code === 200) {
console.log("[ APN RELAY ] push:sent", {
env: input.env,
host: apnsHost,
bundle: input.bundle,
tokenSuffix: suffix,
code: out.code,
})
return {
ok: true,
code: 200,
}
}
console.log("[ APN RELAY ] push:failed", {
env: input.env,
host: apnsHost,
bundle: input.bundle,
tokenSuffix: suffix,
code: out.code,
error: out.body,
})
return {
ok: false,
code: out.code,
error: out.body,
}
}
-28
View File
@@ -1,28 +0,0 @@
import { sql } from "drizzle-orm"
import { db } from "./db"
import { env } from "./env"
import { delivery_log, device_registration } from "./schema.sql"
import { setup } from "./setup"
async function run() {
console.log(`[apn-relay] DB host: ${env.DATABASE_HOST}`)
await db.execute(sql`SELECT 1`)
console.log("[apn-relay] DB connection OK")
await setup()
console.log("[apn-relay] Setup migration OK")
const [a] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
const [b] = await db.select({ value: sql<number>`count(*)` }).from(delivery_log)
console.log(`[apn-relay] device_registration rows: ${Number(a?.value ?? 0)}`)
console.log(`[apn-relay] delivery_log rows: ${Number(b?.value ?? 0)}`)
console.log("[apn-relay] DB check passed")
}
run().catch((err) => {
console.error("[apn-relay] DB check failed")
console.error(err)
process.exit(1)
})
-11
View File
@@ -1,11 +0,0 @@
import { Client } from "@planetscale/database"
import { drizzle } from "drizzle-orm/planetscale-serverless"
import { env } from "./env"
const client = new Client({
host: env.DATABASE_HOST,
username: env.DATABASE_USERNAME,
password: env.DATABASE_PASSWORD,
})
export const db = drizzle({ client })
-47
View File
@@ -1,47 +0,0 @@
import { z } from "zod"
const bad = new Set(["undefined", "null"])
const txt = z
.string()
.transform((input) => input.trim())
.refine((input) => input.length > 0 && !bad.has(input.toLowerCase()))
const schema = z.object({
PORT: z.coerce.number().int().positive().default(8787),
DATABASE_HOST: txt,
DATABASE_USERNAME: txt,
DATABASE_PASSWORD: txt,
APNS_TEAM_ID: txt,
APNS_KEY_ID: txt,
APNS_PRIVATE_KEY: txt,
APNS_DEFAULT_BUNDLE_ID: txt,
})
const req = [
"DATABASE_HOST",
"DATABASE_USERNAME",
"DATABASE_PASSWORD",
"APNS_TEAM_ID",
"APNS_KEY_ID",
"APNS_PRIVATE_KEY",
"APNS_DEFAULT_BUNDLE_ID",
] as const
const out = schema.safeParse(process.env)
if (!out.success) {
const miss = req.filter((key) => !process.env[key]?.trim())
const bad = out.error.issues
.map((item) => item.path[0])
.filter((key): key is string => typeof key === "string")
.filter((key) => !miss.includes(key as (typeof req)[number]))
console.error("[apn-relay] Invalid startup configuration")
if (miss.length) console.error(`[apn-relay] Missing required env vars: ${miss.join(", ")}`)
if (bad.length) console.error(`[apn-relay] Invalid env vars: ${Array.from(new Set(bad)).join(", ")}`)
console.error("[apn-relay] Check .env.example and restart")
throw new Error("Startup configuration invalid")
}
export const env = out.data
-5
View File
@@ -1,5 +0,0 @@
import { createHash } from "node:crypto"
export function hash(input: string) {
return createHash("sha256").update(input).digest("hex")
}
-448
View File
@@ -1,448 +0,0 @@
import { randomUUID } from "node:crypto"
import { and, desc, eq, sql } from "drizzle-orm"
import { Hono } from "hono"
import { z } from "zod"
import { send } from "./apns"
import { db } from "./db"
import { env } from "./env"
import { hash } from "./hash"
import { delivery_log, device_registration } from "./schema.sql"
import { setup } from "./setup"
function bad(input?: string) {
if (!input) return false
return input.includes("BadEnvironmentKeyInToken")
}
function flip(input: "sandbox" | "production") {
if (input === "sandbox") return "production"
return "sandbox"
}
function tail(input: string) {
return input.slice(-8)
}
function esc(input: unknown) {
return String(input ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
}
function fmt(input: number) {
return new Date(input).toISOString()
}
const reg = z.object({
secret: z.string().min(1),
deviceToken: z.string().min(1),
bundleId: z.string().min(1).optional(),
apnsEnv: z.enum(["sandbox", "production"]).default("production"),
})
const unreg = z.object({
secret: z.string().min(1),
deviceToken: z.string().min(1),
})
const evt = z.object({
secret: z.string().min(1),
serverID: z.string().min(1).optional(),
eventType: z.enum(["complete", "permission", "error"]),
sessionID: z.string().min(1),
title: z.string().min(1).optional(),
body: z.string().min(1).optional(),
})
function title(input: z.infer<typeof evt>["eventType"]) {
if (input === "complete") return "Session complete"
if (input === "permission") return "Action needed"
return "Session error"
}
function body(input: z.infer<typeof evt>["eventType"]) {
if (input === "complete") return "OpenCode finished your session."
if (input === "permission") return "OpenCode needs your permission decision."
return "OpenCode reported an error for your session."
}
const app = new Hono()
app.onError((err, c) => {
return c.json(
{
ok: false,
error: err.message,
},
500,
)
})
app.notFound((c) => {
return c.json(
{
ok: false,
error: "Not found",
},
404,
)
})
app.get("/health", async (c) => {
const [a] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
const [b] = await db.select({ value: sql<number>`count(*)` }).from(delivery_log)
return c.json({
ok: true,
devices: Number(a?.value ?? 0),
deliveries: Number(b?.value ?? 0),
})
})
app.get("/", async (c) => {
const [a] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
const [b] = await db.select({ value: sql<number>`count(*)` }).from(delivery_log)
const devices = await db.select().from(device_registration).orderBy(desc(device_registration.updated_at)).limit(100)
const byBundle = await db
.select({
bundle: device_registration.bundle_id,
env: device_registration.apns_env,
value: sql<number>`count(*)`,
})
.from(device_registration)
.groupBy(device_registration.bundle_id, device_registration.apns_env)
.orderBy(desc(sql<number>`count(*)`))
const rows = await db.select().from(delivery_log).orderBy(desc(delivery_log.created_at)).limit(20)
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>APN Relay</title>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 24px; color: #111827; }
h1 { margin: 0 0 12px 0; }
h2 { margin: 22px 0 10px 0; font-size: 16px; }
.stats { display: flex; gap: 16px; margin: 0 0 18px 0; }
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 10px 12px; min-width: 160px; }
.muted { color: #6b7280; font-size: 12px; }
.small { font-size: 11px; color: #6b7280; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #e5e7eb; text-align: left; padding: 8px; font-size: 12px; }
th { background: #f9fafb; }
</style>
</head>
<body>
<h1>APN Relay</h1>
<p class="muted">MVP dashboard</p>
<div class="stats">
<div class="card">
<div class="muted">Registered devices</div>
<div>${Number(a?.value ?? 0)}</div>
</div>
<div class="card">
<div class="muted">Delivery log rows</div>
<div>${Number(b?.value ?? 0)}</div>
</div>
</div>
<h2>Registered devices</h2>
<p class="small">Most recent 100 registrations. Token values are masked to suffix only.</p>
<table>
<thead>
<tr>
<th>updated</th>
<th>created</th>
<th>token suffix</th>
<th>env</th>
<th>bundle</th>
<th>secret hash</th>
</tr>
</thead>
<tbody>
${
devices.length
? devices
.map(
(row) => `<tr>
<td>${esc(fmt(row.updated_at))}</td>
<td>${esc(fmt(row.created_at))}</td>
<td>${esc(tail(row.device_token))}</td>
<td>${esc(row.apns_env)}</td>
<td>${esc(row.bundle_id)}</td>
<td>${esc(`${row.secret_hash.slice(0, 12)}`)}</td>
</tr>`,
)
.join("")
: `<tr><td colspan="6" class="muted">No devices registered.</td></tr>`
}
</tbody>
</table>
<h2>Bundle breakdown</h2>
<table>
<thead>
<tr>
<th>bundle</th>
<th>env</th>
<th>count</th>
</tr>
</thead>
<tbody>
${
byBundle.length
? byBundle
.map(
(row) => `<tr>
<td>${esc(row.bundle)}</td>
<td>${esc(row.env)}</td>
<td>${esc(Number(row.value ?? 0))}</td>
</tr>`,
)
.join("")
: `<tr><td colspan="3" class="muted">No device data.</td></tr>`
}
</tbody>
</table>
<h2>Recent deliveries</h2>
<table>
<thead>
<tr>
<th>time</th>
<th>event</th>
<th>session</th>
<th>status</th>
<th>error</th>
</tr>
</thead>
<tbody>
${rows
.map(
(row) => `<tr>
<td>${esc(fmt(row.created_at))}</td>
<td>${esc(row.event_type)}</td>
<td>${esc(row.session_id)}</td>
<td>${esc(row.status)}</td>
<td>${esc(row.error ?? "")}</td>
</tr>`,
)
.join("")}
</tbody>
</table>
</body>
</html>`
return c.html(html)
})
app.post("/v1/device/register", async (c) => {
const raw = await c.req.json().catch(() => undefined)
const check = reg.safeParse(raw)
if (!check.success) {
return c.json(
{
ok: false,
error: "Invalid request body",
},
400,
)
}
const now = Date.now()
const key = hash(check.data.secret)
const row = {
id: randomUUID(),
secret_hash: key,
device_token: check.data.deviceToken,
bundle_id: check.data.bundleId ?? env.APNS_DEFAULT_BUNDLE_ID,
apns_env: check.data.apnsEnv,
created_at: now,
updated_at: now,
}
console.log("[relay] register", {
token: tail(row.device_token),
env: row.apns_env,
bundle: row.bundle_id,
secretHash: `${key.slice(0, 12)}...`,
})
await db
.insert(device_registration)
.values(row)
.onDuplicateKeyUpdate({
set: {
bundle_id: row.bundle_id,
apns_env: row.apns_env,
updated_at: now,
},
})
return c.json({ ok: true })
})
app.post("/v1/device/unregister", async (c) => {
const raw = await c.req.json().catch(() => undefined)
const check = unreg.safeParse(raw)
if (!check.success) {
return c.json(
{
ok: false,
error: "Invalid request body",
},
400,
)
}
const key = hash(check.data.secret)
console.log("[relay] unregister", {
token: tail(check.data.deviceToken),
secretHash: `${key.slice(0, 12)}...`,
})
await db
.delete(device_registration)
.where(and(eq(device_registration.secret_hash, key), eq(device_registration.device_token, check.data.deviceToken)))
return c.json({ ok: true })
})
app.post("/v1/event", async (c) => {
const raw = await c.req.json().catch(() => undefined)
const check = evt.safeParse(raw)
if (!check.success) {
return c.json(
{
ok: false,
error: "Invalid request body",
},
400,
)
}
const key = hash(check.data.secret)
const list = await db.select().from(device_registration).where(eq(device_registration.secret_hash, key))
console.log("[relay] event", {
type: check.data.eventType,
serverID: check.data.serverID,
session: check.data.sessionID,
secretHash: `${key.slice(0, 12)}...`,
devices: list.length,
})
if (!list.length) {
const [total] = await db.select({ value: sql<number>`count(*)` }).from(device_registration)
console.log("[relay] event:no-matching-devices", {
type: check.data.eventType,
serverID: check.data.serverID,
session: check.data.sessionID,
secretHash: `${key.slice(0, 12)}...`,
totalDevices: Number(total?.value ?? 0),
})
return c.json({
ok: true,
sent: 0,
failed: 0,
})
}
const out = await Promise.all(
list.map(async (row) => {
const env = row.apns_env === "sandbox" ? "sandbox" : "production"
const payload = {
token: row.device_token,
bundle: row.bundle_id,
title: check.data.title ?? title(check.data.eventType),
body: check.data.body ?? body(check.data.eventType),
data: {
serverID: check.data.serverID,
eventType: check.data.eventType,
sessionID: check.data.sessionID,
},
}
const first = await send({ ...payload, env })
if (first.ok || !bad(first.error)) {
if (!first.ok) {
console.log("[relay] send:error", {
token: tail(row.device_token),
env,
error: first.error,
})
}
return first
}
const alt = flip(env)
console.log("[relay] send:retry-env", {
token: tail(row.device_token),
from: env,
to: alt,
})
const second = await send({ ...payload, env: alt })
if (!second.ok) {
console.log("[relay] send:error", {
token: tail(row.device_token),
env: alt,
error: second.error,
})
return second
}
await db
.update(device_registration)
.set({ apns_env: alt, updated_at: Date.now() })
.where(
and(
eq(device_registration.secret_hash, row.secret_hash),
eq(device_registration.device_token, row.device_token),
),
)
console.log("[relay] send:env-updated", {
token: tail(row.device_token),
env: alt,
})
return second
}),
)
const now = Date.now()
await db.insert(delivery_log).values(
out.map((item) => ({
id: randomUUID(),
secret_hash: key,
event_type: check.data.eventType,
session_id: check.data.sessionID,
status: item.ok ? "sent" : "failed",
error: item.error,
created_at: now,
})),
)
const sent = out.filter((item) => item.ok).length
console.log("[relay] event:done", {
type: check.data.eventType,
session: check.data.sessionID,
sent,
failed: out.length - sent,
})
return c.json({
ok: true,
sent,
failed: out.length - sent,
})
})
await setup()
if (import.meta.main) {
Bun.serve({
port: env.PORT,
fetch: app.fetch,
})
console.log(`apn-relay listening on http://0.0.0.0:${env.PORT}`)
}
export { app }
-35
View File
@@ -1,35 +0,0 @@
import { bigint, index, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
export const device_registration = mysqlTable(
"device_registration",
{
id: varchar("id", { length: 36 }).primaryKey(),
secret_hash: varchar("secret_hash", { length: 64 }).notNull(),
device_token: varchar("device_token", { length: 255 }).notNull(),
bundle_id: varchar("bundle_id", { length: 255 }).notNull(),
apns_env: varchar("apns_env", { length: 16 }).notNull().default("production"),
created_at: bigint("created_at", { mode: "number" }).notNull(),
updated_at: bigint("updated_at", { mode: "number" }).notNull(),
},
(table) => [
uniqueIndex("device_registration_secret_token_idx").on(table.secret_hash, table.device_token),
index("device_registration_secret_hash_idx").on(table.secret_hash),
],
)
export const delivery_log = mysqlTable(
"delivery_log",
{
id: varchar("id", { length: 36 }).primaryKey(),
secret_hash: varchar("secret_hash", { length: 64 }).notNull(),
event_type: varchar("event_type", { length: 32 }).notNull(),
session_id: varchar("session_id", { length: 255 }).notNull(),
status: varchar("status", { length: 16 }).notNull(),
error: varchar("error", { length: 1024 }),
created_at: bigint("created_at", { mode: "number" }).notNull(),
},
(table) => [
index("delivery_log_secret_hash_idx").on(table.secret_hash),
index("delivery_log_created_at_idx").on(table.created_at),
],
)
-34
View File
@@ -1,34 +0,0 @@
import { sql } from "drizzle-orm"
import { db } from "./db"
export async function setup() {
await db.execute(sql`
CREATE TABLE IF NOT EXISTS device_registration (
id varchar(36) NOT NULL,
secret_hash varchar(64) NOT NULL,
device_token varchar(255) NOT NULL,
bundle_id varchar(255) NOT NULL,
apns_env varchar(16) NOT NULL DEFAULT 'production',
created_at bigint NOT NULL,
updated_at bigint NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY device_registration_secret_token_idx (secret_hash, device_token),
KEY device_registration_secret_hash_idx (secret_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS delivery_log (
id varchar(36) NOT NULL,
secret_hash varchar(64) NOT NULL,
event_type varchar(32) NOT NULL,
session_id varchar(255) NOT NULL,
status varchar(16) NOT NULL,
error varchar(1024) NULL,
created_at bigint NOT NULL,
PRIMARY KEY (id),
KEY delivery_log_secret_hash_idx (secret_hash),
KEY delivery_log_created_at_idx (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`)
}
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.18",
"version": "1.14.25",
"description": "",
"type": "module",
"exports": {
@@ -42,7 +42,7 @@
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/shared": "workspace:*",
"@opencode-ai/core": "workspace:*",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2",
+21 -21
View File
@@ -141,13 +141,11 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
<LanguageProvider locale={props.locale}>
<UiI18nBridge>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<QueryProvider>
<DialogProvider>
<MarkedProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
</MarkedProvider>
</DialogProvider>
</QueryProvider>
<DialogProvider>
<MarkedProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
</UiI18nBridge>
</LanguageProvider>
@@ -293,20 +291,22 @@ export function AppInterface(props: {
>
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Dynamic
component={props.router ?? Router}
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
>
<Route path="/" component={HomeRoute} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={SessionIndexRoute} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
</Dynamic>
</GlobalSyncProvider>
</GlobalSDKProvider>
<QueryProvider>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Dynamic
component={props.router ?? Router}
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
>
<Route path="/" component={HomeRoute} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={SessionIndexRoute} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
</Dynamic>
</GlobalSyncProvider>
</GlobalSDKProvider>
</QueryProvider>
</ServerKey>
</ConnectionGate>
</ServerProvider>
@@ -9,9 +9,10 @@ import { createStore } from "solid-js/store"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { type LocalProject, getAvatarColors } from "@/context/layout"
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { Avatar } from "@opencode-ai/ui/avatar"
import { useLanguage } from "@/context/language"
import { getProjectAvatarSource } from "@/pages/layout/sidebar-items"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
@@ -26,8 +27,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
const [store, setStore] = createStore({
name: defaultName(),
color: props.project.icon?.color || "pink",
iconUrl: props.project.icon?.override || "",
color: props.project.icon?.color,
iconOverride: props.project.icon?.override,
startup: props.project.commands?.start ?? "",
dragOver: false,
iconHover: false,
@@ -39,7 +40,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
if (!file.type.startsWith("image/")) return
const reader = new FileReader()
reader.onload = (e) => {
setStore("iconUrl", e.target?.result as string)
setStore("iconOverride", e.target?.result as string)
setStore("iconHover", false)
}
reader.readAsDataURL(file)
@@ -68,7 +69,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
}
function clearIcon() {
setStore("iconUrl", "")
setStore("iconOverride", "")
}
const saveMutation = useMutation(() => ({
@@ -81,17 +82,17 @@ export function DialogEditProject(props: { project: LocalProject }) {
projectID: props.project.id,
directory: props.project.worktree,
name,
icon: { color: store.color, override: store.iconUrl },
icon: { color: store.color || "", override: store.iconOverride || "" },
commands: { start },
})
globalSync.project.icon(props.project.worktree, store.iconUrl || undefined)
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
dialog.close()
return
}
globalSync.project.meta(props.project.worktree, {
name,
icon: { color: store.color, override: store.iconUrl || undefined },
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
commands: { start: start || undefined },
})
dialog.close()
@@ -130,13 +131,13 @@ export function DialogEditProject(props: { project: LocalProject }) {
classList={{
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
"border-border-base hover:border-border-strong": !store.dragOver,
"overflow-hidden": !!store.iconUrl,
"overflow-hidden": !!store.iconOverride,
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => {
if (store.iconUrl && store.iconHover) {
if (store.iconOverride && store.iconHover) {
clearIcon()
} else {
iconInput?.click()
@@ -144,7 +145,11 @@ export function DialogEditProject(props: { project: LocalProject }) {
}}
>
<Show
when={store.iconUrl}
when={getProjectAvatarSource(props.project.id, {
color: store.color,
url: props.project.icon?.url,
override: store.iconOverride,
})}
fallback={
<div class="size-full flex items-center justify-center">
<Avatar
@@ -155,18 +160,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
}
>
<img
src={store.iconUrl}
alt={language.t("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
{(src) => (
<img
src={src()}
alt={language.t("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
)}
</Show>
</div>
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !store.iconUrl,
"opacity-0": !(store.iconHover && !store.iconUrl),
"opacity-100": store.iconHover && !store.iconOverride,
"opacity-0": !(store.iconHover && !store.iconOverride),
}}
>
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
@@ -174,8 +181,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !!store.iconUrl,
"opacity-0": !(store.iconHover && !!store.iconUrl),
"opacity-100": store.iconHover && !!store.iconOverride,
"opacity-0": !(store.iconHover && !!store.iconOverride),
}}
>
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
@@ -198,7 +205,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
</div>
<Show when={!store.iconUrl}>
<Show when={!store.iconOverride}>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
<div class="flex gap-1.5">
@@ -215,7 +222,10 @@ export function DialogEditProject(props: { project: LocalProject }) {
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
store.color !== color,
}}
onClick={() => setStore("color", color)}
onClick={() => {
if (store.color === color && !props.project.icon?.url) return
setStore("color", store.color === color ? undefined : color)
}}
>
<Avatar
fallback={store.name || defaultName()}
+1 -1
View File
@@ -9,7 +9,7 @@ import { List } from "@opencode-ai/ui/list"
import { showToast } from "@opencode-ai/ui/toast"
import { extractPromptFromParts } from "@/utils/prompt"
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { useLanguage } from "@/context/language"
interface ForkableMessage {
@@ -3,7 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { List } from "@opencode-ai/ui/list"
import type { ListRef } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import fuzzysort from "fuzzysort"
import { createMemo, createResource, createSignal } from "solid-js"
import { useGlobalSDK } from "@/context/global-sdk"
@@ -4,8 +4,8 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { List } from "@opencode-ai/ui/list"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { useNavigate } from "@solidjs/router"
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
@@ -504,7 +504,7 @@ export function DialogSelectServer() {
return (
<Dialog title={formTitle()}>
<div class="flex flex-col gap-2">
<div class="flex flex-1 min-h-0 flex-col gap-2">
<Show
when={!isFormMode()}
fallback={
@@ -539,7 +539,7 @@ export function DialogSelectServer() {
if (x) void select(x)
}}
divider={true}
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
>
{(i) => {
const key = ServerConnection.key(i)
@@ -619,7 +619,7 @@ export function DialogSelectServer() {
</List>
</Show>
<div class="px-5 pb-5">
<div class="shrink-0 px-5 pb-5">
<Show
when={isFormMode()}
fallback={
@@ -8,20 +8,14 @@ import { SettingsGeneral } from "./settings-general"
import { SettingsKeybinds } from "./settings-keybinds"
import { SettingsProviders } from "./settings-providers"
import { SettingsModels } from "./settings-models"
import { SettingsPair } from "./settings-pair"
export const DialogSettings: Component<{ defaultTab?: string }> = (props) => {
export const DialogSettings: Component = () => {
const language = useLanguage()
const platform = usePlatform()
return (
<Dialog size="x-large" transition>
<Tabs
orientation="vertical"
variant="settings"
defaultValue={props.defaultTab ?? "general"}
class="h-full settings-dialog"
>
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
<Tabs.List>
<div class="flex flex-col justify-between h-full w-full">
<div class="flex flex-col gap-3 w-full pt-3">
@@ -51,10 +45,6 @@ export const DialogSettings: Component<{ defaultTab?: string }> = (props) => {
<Icon name="models" />
{language.t("settings.models.title")}
</Tabs.Trigger>
<Tabs.Trigger value="pair">
<Icon name="link" />
{language.t("settings.pair.title")}
</Tabs.Trigger>
</div>
</div>
</div>
@@ -77,9 +67,6 @@ export const DialogSettings: Component<{ defaultTab?: string }> = (props) => {
<Tabs.Content value="models" class="no-scrollbar">
<SettingsModels />
</Tabs.Content>
<Tabs.Content value="pair" class="no-scrollbar">
<SettingsPair />
</Tabs.Content>
</Tabs>
</Dialog>
)
+75 -50
View File
@@ -1,6 +1,6 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocal } from "@/context/local"
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
@@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
import { promptPlaceholder } from "./prompt-input/placeholder"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useQuery } from "@tanstack/solid-query"
import { useQueries } from "@tanstack/solid-query"
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
interface PromptInputProps {
@@ -270,7 +270,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
const motion = (value: number) => ({
opacity: value,
transform: `scale(${0.95 + value * 0.05})`,
transform: `scale(${0.98 + value * 0.02})`,
filter: `blur(${(1 - value) * 2}px)`,
"pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
})
@@ -345,7 +345,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
promptPlaceholder({
mode: store.mode,
commentCount: commentCount(),
example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "",
example: suggest() ? (store.mode === "shell" ? "git status" : language.t(EXAMPLES[store.placeholder])) : "",
suggest: suggest(),
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
}),
@@ -1252,16 +1252,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
}))
const agentsLoading = () => agentsQuery.isLoading
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading())
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading())
const [promptReady] = createResource(
() => prompt.ready().promise,
(p) => p,
)
return (
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
{(promptReady(), null)}
<PromptPopover
popover={store.popover}
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
@@ -1358,15 +1365,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}}
style={{ "padding-bottom": space }}
/>
<Show when={!prompt.dirty()}>
<div
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
classList={{ "font-mono!": store.mode === "shell" }}
style={{ "padding-bottom": space }}
>
{placeholder()}
</div>
</Show>
<div
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
classList={{ "font-mono!": store.mode === "shell" }}
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
>
{placeholder()}
</div>
</div>
<div
@@ -1398,12 +1403,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<IconButton
data-action="prompt-submit"
type="submit"
disabled={store.mode !== "normal" || (!working() && blank())}
disabled={!working() && blank()}
tabIndex={store.mode === "normal" ? undefined : -1}
icon={stopping() ? "stop" : "arrow-up"}
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
variant="primary"
class="size-8"
style={buttons()}
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>
@@ -1446,18 +1450,31 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div class="px-1.75 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
<div
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
class="h-7 flex items-center gap-1.5 min-w-0 absolute inset-0"
style={{
padding: "0 4px 0 8px",
padding: "0 0px 0 8px",
...shell(),
}}
>
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
<div class="size-4 shrink-0" />
<Icon name="console" />
<span class="truncate text-13-medium text-text-base">{language.t("prompt.mode.shell")}</span>
<div class="flex-1" />
<Button
variant="ghost"
class="text-text-base"
onClick={() => {
setStore("mode", "normal")
}}
>
{language.t("common.cancel")}
</Button>
</div>
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
<Show when={!agentsLoading()}>
<div data-component="prompt-agent-control">
<div
data-component="prompt-agent-control"
style={agentsShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
>
<TooltipKeybind
placement="top"
gutter={4}
@@ -1483,7 +1500,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Show>
<Show when={!providersLoading()}>
<Show when={store.mode !== "shell"}>
<div data-component="prompt-model-control">
<div
data-component="prompt-model-control"
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
>
<Show
when={providers.paid().length > 0}
fallback={
@@ -1554,30 +1574,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</TooltipKeybind>
</Show>
</div>
<div data-component="prompt-variant-control">
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.variant.cycle")}
<Show when={variants().length > 2}>
<div
data-component="prompt-variant-control"
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}
>
<Select
size="normal"
options={variants()}
current={local.model.variant.current() ?? "default"}
label={(x) => (x === "default" ? language.t("common.default") : x)}
onSelect={(value) => {
local.model.variant.set(value === "default" ? undefined : value)
restoreFocus()
}}
class="capitalize max-w-[160px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-model-variant" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.variant.cycle")}
>
<Select
size="normal"
options={variants()}
current={local.model.variant.current() ?? "default"}
label={(x) => (x === "default" ? language.t("common.default") : x)}
onSelect={(value) => {
local.model.variant.set(value === "default" ? undefined : value)
restoreFocus()
}}
class="capitalize max-w-[160px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-model-variant" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
</Show>
</Show>
</Show>
</div>
@@ -1,4 +1,4 @@
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
import type { FileSelection } from "@/context/file"
import { encodeFilePath } from "@/context/file/path"
@@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path"
import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path"
import type { ContextItem } from "@/context/prompt"
type PromptContextItem = ContextItem & { key: string }
@@ -12,7 +12,7 @@ describe("promptPlaceholder", () => {
suggest: true,
t,
})
expect(value).toBe("prompt.placeholder.shell")
expect(value).toBe("prompt.placeholder.shell:example")
})
test("returns summarize placeholders for comment context", () => {
@@ -7,7 +7,7 @@ type PromptPlaceholderInput = {
}
export function promptPlaceholder(input: PromptPlaceholderInput) {
if (input.mode === "shell") return input.t("prompt.placeholder.shell")
if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example })
if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments")
if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment")
if (!input.suggest) return input.t("prompt.placeholder.simple")
@@ -1,7 +1,7 @@
import { Component, For, Match, Show, Switch } from "solid-js"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
export type AtOption =
| { type: "agent"; name: string; display: string }
@@ -74,7 +74,7 @@ beforeAll(async () => {
showToast: () => 0,
}))
mock.module("@opencode-ai/shared/util/encode", () => ({
mock.module("@opencode-ai/core/util/encode", () => ({
base64Encode: (value: string) => value,
}))
@@ -1,7 +1,7 @@
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { Binary } from "@opencode-ai/shared/util/binary"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { Binary } from "@opencode-ai/core/util/binary"
import { useNavigate, useParams } from "@solidjs/router"
import { batch, type Accessor } from "solid-js"
import type { FileSelection } from "@/context/file"
@@ -1,8 +1,8 @@
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { checksum } from "@opencode-ai/shared/util/encode"
import { findLast } from "@opencode-ai/shared/util/array"
import { checksum } from "@opencode-ai/core/util/encode"
import { findLast } from "@opencode-ai/core/util/array"
import { same } from "@/utils/same"
import { Icon } from "@opencode-ai/ui/icon"
import { Accordion } from "@opencode-ai/ui/accordion"
@@ -7,7 +7,7 @@ import { Keybind } from "@opencode-ai/ui/keybind"
import { Spinner } from "@opencode-ai/ui/spinner"
import { showToast } from "@opencode-ai/ui/toast"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
@@ -5,7 +5,7 @@ import { useSDK } from "@/context/sdk"
import { useLanguage } from "@/context/language"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
const MAIN_WORKTREE = "main"
const CREATE_WORKTREE = "create"
@@ -5,7 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Tabs } from "@opencode-ai/ui/tabs"
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useCommand } from "@/context/command"
@@ -128,27 +128,25 @@ export const SettingsGeneral: Component = () => {
return
}
const actions =
platform.update && platform.restart
? [
{
label: language.t("toast.update.action.installRestart"),
onClick: async () => {
await platform.update!()
await platform.restart!()
},
const actions = platform.updateAndRestart
? [
{
label: language.t("toast.update.action.installRestart"),
onClick: async () => {
await platform.updateAndRestart!()
},
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]
: [
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]
},
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]
: [
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]
showToast({
persistent: true,
@@ -280,6 +278,18 @@ export const SettingsGeneral: Component = () => {
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showSessionProgressBar.title")}
description={language.t("settings.general.row.showSessionProgressBar.description")}
>
<div data-action="settings-show-session-progress-bar">
<Switch
checked={settings.general.showSessionProgressBar()}
onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
@@ -1,166 +0,0 @@
import { type Component, createResource, Show } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language"
import { useGlobalSDK } from "@/context/global-sdk"
import { useServer } from "@/context/server"
import { usePlatform } from "@/context/platform"
import { SettingsList } from "./settings-list"
type PairResult =
| { enabled: false }
| {
enabled: true
hosts: string[]
relayURL?: string
serverID?: string
relaySecretHash?: string
link: string
qr: string
}
export const SettingsPair: Component = () => {
const language = useLanguage()
const globalSDK = useGlobalSDK()
const server = useServer()
const platform = usePlatform()
const [data] = createResource(async () => {
const url = `${globalSDK.url}/experimental/push/pair`
console.debug("[settings-pair] fetching pair data", {
serverUrl: globalSDK.url,
serverName: server.name,
serverKey: server.key,
})
const f = platform.fetch ?? fetch
const res = await f(url)
if (!res.ok) {
console.debug("[settings-pair] pair endpoint returned non-ok", {
status: res.status,
serverUrl: globalSDK.url,
})
return { enabled: false as const }
}
const result = (await res.json()) as PairResult
console.debug("[settings-pair] pair data received", {
enabled: result.enabled,
serverUrl: globalSDK.url,
serverName: server.name,
...(result.enabled
? {
relayURL: result.relayURL,
serverID: result.serverID,
relaySecretHash: result.relaySecretHash,
hostCount: result.hosts.length,
hosts: result.hosts,
}
: {}),
})
return result
})
return (
<div class="flex flex-col gap-6 py-4 px-5">
<div class="flex flex-col gap-1">
<h2 class="text-16-semibold text-text-strong">{language.t("settings.pair.title")}</h2>
<p class="text-13-regular text-text-weak">{language.t("settings.pair.description")}</p>
</div>
<Show when={data.loading}>
<SettingsList>
<div class="flex items-center justify-center py-12">
<span class="text-14-regular text-text-weak">{language.t("settings.pair.loading")}</span>
</div>
</SettingsList>
</Show>
<Show when={data.error}>
<SettingsList>
<div class="flex flex-col items-center justify-center py-12 gap-3 text-center">
<Icon name="warning" size="large" />
<div class="flex flex-col gap-1">
<span class="text-14-medium text-text-strong">{language.t("settings.pair.error.title")}</span>
<span class="text-13-regular text-text-weak max-w-md">
{language.t("settings.pair.error.description")}
</span>
</div>
</div>
</SettingsList>
</Show>
<Show when={!data.loading && !data.error && data()}>
{(result) => (
<Show
when={result().enabled && result()}
fallback={
<SettingsList>
<div class="flex flex-col items-center justify-center py-12 gap-3 text-center">
<Icon name="link" size="large" />
<div class="flex flex-col gap-1">
<span class="text-14-medium text-text-strong">{language.t("settings.pair.disabled.title")}</span>
<span class="text-13-regular text-text-weak max-w-md">
{language.t("settings.pair.disabled.description")}
</span>
</div>
<code class="text-12-regular text-text-weak bg-surface-inset px-3 py-1.5 rounded mt-1">
opencode serve --relay-url &lt;url&gt; --relay-secret &lt;secret&gt;
</code>
</div>
</SettingsList>
}
>
{(pair) => {
const p = pair() as PairResult & { enabled: true }
return (
<SettingsList>
<div class="flex flex-col items-center py-8 gap-4">
<Show when={server.list.length > 1 || p.relayURL}>
<div class="flex flex-col gap-1.5 w-full max-w-sm text-left">
<div class="flex items-center gap-2">
<span class="text-12-medium text-text-weak shrink-0">
{language.t("settings.pair.server.label")}
</span>
<code class="text-12-regular text-text-default bg-surface-inset px-2 py-0.5 rounded truncate">
{server.name}
</code>
</div>
<Show when={p.relayURL}>
<div class="flex items-center gap-2">
<span class="text-12-medium text-text-weak shrink-0">
{language.t("settings.pair.relay.label")}
</span>
<code class="text-12-regular text-text-default bg-surface-inset px-2 py-0.5 rounded truncate">
{p.relayURL}
</code>
</div>
</Show>
<Show when={p.relaySecretHash}>
<div class="flex items-center gap-2">
<span class="text-12-medium text-text-weak shrink-0">
{language.t("settings.pair.secret.label")}
</span>
<code class="text-12-regular text-text-default bg-surface-inset px-2 py-0.5 rounded truncate">
{p.relaySecretHash}
</code>
</div>
</Show>
</div>
</Show>
<img src={p.qr} alt="Pairing QR code" class="w-64 h-64" />
<div class="flex flex-col gap-1 text-center max-w-sm">
<span class="text-14-medium text-text-strong">
{language.t("settings.pair.instructions.title")}
</span>
<span class="text-13-regular text-text-weak">
{language.t("settings.pair.instructions.description")}
</span>
</div>
</div>
</SettingsList>
)
}}
</Show>
)}
</Show>
</div>
)
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { showToast } from "@opencode-ai/ui/toast"
import { useParams } from "@solidjs/router"
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { useLanguage } from "@/context/language"
+30 -51
View File
@@ -8,11 +8,10 @@ import type {
Todo,
} from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/shared/util/path"
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
import { getFilename } from "@opencode-ai/core/util/path"
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { Persist, persisted } from "@/utils/persist"
import type { InitError } from "../pages/error"
import { useGlobalSDK } from "./global-sdk"
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
@@ -24,7 +23,6 @@ import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global
import { trimSessions } from "./global-sync/session-trim"
import type { ProjectMeta } from "./global-sync/types"
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
import { sanitizeProject } from "./global-sync/utils"
import { formatServerError } from "@/utils/server-errors"
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
@@ -56,15 +54,10 @@ function createGlobalSync() {
const sessionLoads = new Map<string, Promise<void>>()
const sessionMeta = new Map<string, { limit: number }>()
const [projectCache, setProjectCache, projectInit] = persisted(
Persist.global("globalSync.project", ["globalSync.project.v1"]),
createStore({ value: [] as Project[] }),
)
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: projectCache.value,
project: [],
session_todo: {},
provider: { all: [], connected: [], default: {} },
provider_auth: {},
@@ -73,37 +66,18 @@ function createGlobalSync() {
})
const queryClient = useQueryClient()
let active = true
let projectWritten = false
let bootedAt = 0
let bootingRoot = false
let eventFrame: number | undefined
let eventTimer: ReturnType<typeof setTimeout> | undefined
onCleanup(() => {
active = false
})
onCleanup(() => {
if (eventFrame !== undefined) cancelAnimationFrame(eventFrame)
if (eventTimer !== undefined) clearTimeout(eventTimer)
})
const cacheProjects = () => {
setProjectCache(
"value",
untrack(() => globalStore.project.map(sanitizeProject)),
)
}
const setProjects = (next: Project[] | ((draft: Project[]) => void)) => {
projectWritten = true
if (typeof next === "function") {
setGlobalStore("project", produce(next))
cacheProjects()
return
}
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
setGlobalStore("project", next)
cacheProjects()
}
const setBootStore = ((...input: unknown[]) => {
@@ -116,22 +90,12 @@ function createGlobalSync() {
const set = ((...input: unknown[]) => {
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
setProjects(input[1] as Project[] | ((draft: Project[]) => void))
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
return input[1]
}
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
}) as typeof setGlobalStore
if (projectInit instanceof Promise) {
void projectInit.then(() => {
if (!active) return
if (projectWritten) return
const cached = projectCache.value
if (cached.length === 0) return
setGlobalStore("project", cached)
})
}
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
if (!sessionID) return
if (!todos) {
@@ -223,16 +187,18 @@ function createGlobalSync() {
limit,
permission: store.permission,
})
setStore(
"sessionTotal",
estimateRootSessionTotal({
count: nonArchived.length,
limit: x.limit,
limited: x.limited,
}),
)
setStore("session", reconcile(sessions, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
batch(() => {
setStore(
"sessionTotal",
estimateRootSessionTotal({
count: nonArchived.length,
limit: x.limit,
limited: x.limited,
}),
)
setStore("session", reconcile(sessions, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
})
sessionMeta.set(directory, { limit })
})
.catch((err) => {
@@ -298,6 +264,19 @@ function createGlobalSync() {
const event = e.details
const recent = bootingRoot || Date.now() - bootedAt < 1500
if (event.type === "session.error") {
const error = event.properties.error
if (error?.name !== "MessageAbortedError") {
console.error("[global-sync] session error", {
scope: directory === "global" ? "global" : "workspace",
directory: directory === "global" ? undefined : directory,
project: directory === "global" ? undefined : getFilename(directory),
sessionID: event.properties.sessionID,
error,
})
}
}
if (directory === "global") {
applyGlobalEvent({
event,
@@ -11,15 +11,14 @@ import type {
Todo,
} from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/shared/util/path"
import { retry } from "@opencode-ai/shared/util/retry"
import { getFilename } from "@opencode-ai/core/util/path"
import { retry } from "@opencode-ai/core/util/retry"
import { batch } from "solid-js"
import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { State, VcsCache } from "./types"
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
import { loadSessionsQuery } from "../global-sync"
type GlobalStore = {
ready: boolean
@@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: {
input.setGlobalStore("config", x.data!)
}),
),
]
const slow = [
() =>
input.queryClient.fetchQuery({
...loadProvidersQuery(null),
@@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: {
}),
),
}),
]
const slow = [
() =>
retry(() =>
input.globalSDK.path.get().then((x) => {
@@ -183,8 +182,43 @@ function warmSessions(input: {
export const loadProvidersQuery = (directory: string | null) =>
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
export const loadAgentsQuery = (directory: string | null) =>
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
export const loadAgentsQuery = (
directory: string | null,
sdk?: OpencodeClient,
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
) =>
queryOptions<null>({
queryKey: [directory, "agents"],
queryFn:
sdk && transform
? () =>
retry(() =>
sdk.app
.agents()
.then(transform)
.then(() => null),
)
: skipToken,
})
export const loadPathQuery = (
directory: string | null,
sdk?: OpencodeClient,
transform?: (x: Awaited<ReturnType<OpencodeClient["path"]["get"]>>) => void,
) =>
queryOptions<Path>({
queryKey: [directory, "path"],
queryFn:
sdk && transform
? () =>
retry(() =>
sdk.path.get().then(async (x) => {
transform(x)
return x.data!
}),
)
: skipToken,
})
export async function bootstrapDirectory(input: {
directory: string
@@ -222,45 +256,27 @@ export async function bootstrapDirectory(input: {
input.setStore("lsp", [])
if (loading) input.setStore("status", "partial")
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
const errs = errors(await runAll(fast))
if (errs.length > 0) {
console.error("Failed to bootstrap instance", errs[0])
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(errs[0], input.translate),
})
}
const rev = (providerRev.get(input.directory) ?? 0) + 1
providerRev.set(input.directory, rev)
;(async () => {
const slow = [
() => Promise.resolve(input.loadSessions(input.directory)),
() =>
input.queryClient.ensureQueryData({
...loadAgentsQuery(input.directory),
queryFn: () =>
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
() => null,
),
}),
input.queryClient.ensureQueryData(
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
),
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
() =>
seededProject
? Promise.resolve()
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
() =>
seededPath
? Promise.resolve()
: retry(() =>
input.sdk.path.get().then((x) => {
input.setStore("path", x.data!)
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
if (next) input.setStore("project", next)
}),
),
!seededProject &&
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
!seededPath &&
(() =>
input.queryClient.ensureQueryData(
loadPathQuery(input.directory, input.sdk, (x) => {
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
if (next) input.setStore("project", next)
}),
)),
() =>
retry(() =>
input.sdk.vcs.get().then((x) => {
@@ -330,7 +346,28 @@ export async function bootstrapDirectory(input: {
input.setStore("mcp_ready", true)
}),
),
]
() =>
input.queryClient.ensureQueryData({
...loadProvidersQuery(input.directory),
queryFn: () =>
retry(() => input.sdk.provider.list())
.then((x) => {
if (providerRev.get(input.directory) !== rev) return
input.setStore("provider", normalizeProviderList(x.data!))
input.setStore("provider_ready", true)
})
.catch((err) => {
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
})
.then(() => null),
}),
].filter(Boolean) as (() => Promise<any>)[]
await waitForPaint()
const slowErrs = errors(await runAll(slow))
@@ -344,29 +381,6 @@ export async function bootstrapDirectory(input: {
})
}
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
const rev = (providerRev.get(input.directory) ?? 0) + 1
providerRev.set(input.directory, rev)
void input.queryClient.ensureQueryData({
...loadSessionsQuery(input.directory),
queryFn: () =>
retry(() => input.sdk.provider.list())
.then((x) => {
if (providerRev.get(input.directory) !== rev) return
input.setStore("provider", normalizeProviderList(x.data!))
input.setStore("provider_ready", true)
})
.catch((err) => {
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
})
.then(() => null),
})
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
})()
}
@@ -14,6 +14,8 @@ import {
type VcsCache,
} from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { useQuery } from "@tanstack/solid-query"
import { loadPathQuery } from "./bootstrap"
export function createChildStoreManager(input: {
owner: Owner
@@ -154,16 +156,21 @@ export function createChildStoreManager(input: {
const init = () =>
createRoot((dispose) => {
const initialMeta = meta[0].value
const initialIcon = icon[0].value
const pathQuery = useQuery(() => loadPathQuery(directory))
const child = createStore<State>({
project: "",
projectMeta: initialMeta,
projectMeta: undefined,
icon: initialIcon,
provider_ready: false,
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
get path() {
if (pathQuery.isLoading || !pathQuery.data)
return { state: "", config: "", worktree: "", directory: "", home: "" }
return pathQuery.data
},
status: "loading" as const,
agent: [],
command: [],
@@ -200,11 +207,6 @@ export function createChildStoreManager(input: {
child[1]("vcs", (value) => value ?? cached)
})
onPersistedInit(meta[2], () => {
if (child[0].projectMeta !== initialMeta) return
child[1]("projectMeta", meta[0].value)
})
onPersistedInit(icon[2], () => {
if (child[0].icon !== initialIcon) return
child[1]("icon", icon[0].value)
@@ -1,4 +1,4 @@
import { Binary } from "@opencode-ai/shared/util/binary"
import { Binary } from "@opencode-ai/core/util/binary"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type {
Message,
@@ -21,7 +21,7 @@ const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
export function applyGlobalEvent(input: {
event: { type: string; properties?: unknown }
project: Project[]
setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
setGlobalProject: (next: Project[] | ((draft: Project[]) => Project[])) => void
refresh: () => void
}) {
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
@@ -33,14 +33,18 @@ export function applyGlobalEvent(input: {
const properties = input.event.properties as Project
const result = Binary.search(input.project, properties.id, (s) => s.id)
if (result.found) {
input.setGlobalProject((draft) => {
draft[result.index] = { ...draft[result.index], ...properties }
})
input.setGlobalProject(
produce((draft) => {
draft[result.index] = { ...draft[result.index], ...properties }
}),
)
return
}
input.setGlobalProject((draft) => {
draft.splice(result.index, 0, properties)
})
input.setGlobalProject(
produce((draft) => {
draft.splice(result.index, 0, properties)
}),
)
}
function cleanupSessionCaches(
+2 -32
View File
@@ -391,37 +391,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
? globalSync.data.project.find((x) => x.id === projectID)
: globalSync.data.project.find((x) => x.worktree === project.worktree)
const local = childStore.projectMeta
const localOverride =
local?.name !== undefined ||
local?.commands?.start !== undefined ||
local?.icon?.override !== undefined ||
local?.icon?.color !== undefined
const base = {
...metadata,
...project,
icon: {
url: metadata?.icon?.url,
override: metadata?.icon?.override ?? childStore.icon,
color: metadata?.icon?.color,
},
}
const isGlobal = projectID === "global" || (metadata?.id === undefined && localOverride)
if (!isGlobal) return base
return {
...base,
id: base.id ?? "global",
name: local?.name,
commands: local?.commands,
icon: {
url: base.icon?.url,
override: local?.icon?.override,
color: local?.icon?.color,
},
}
return { ...metadata, ...project }
}
const roots = createMemo(() => {
@@ -516,7 +486,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
for (const project of projects) {
if (project.icon?.color) continue
if (project.icon?.color || project.icon?.override || project.icon?.url) continue
const worktree = project.worktree
const existing = colors[worktree]
const color = existing ?? pickAvailableColor(used)
+1 -1
View File
@@ -1,5 +1,5 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { useParams } from "@solidjs/router"
import { batch, createEffect, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
+2 -2
View File
@@ -7,8 +7,8 @@ import { useGlobalSync } from "./global-sync"
import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { Binary } from "@opencode-ai/shared/util/binary"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { Binary } from "@opencode-ai/core/util/binary"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { decode64 } from "@/utils/base64"
import { EventSessionError } from "@opencode-ai/sdk/v2"
import { Persist, persisted } from "@/utils/persist"
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond"
const session = (input: { id: string; parentID?: string }) =>
@@ -1,4 +1,4 @@
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
export function acceptKey(sessionID: string, directory?: string) {
if (!directory) return sessionID
+3 -3
View File
@@ -49,11 +49,11 @@ export type Platform = {
/** Storage mechanism, defaults to localStorage */
storage?: (name?: string) => SyncStorage | AsyncStorage
/** Check for updates (Tauri only) */
/** Check for a downloadable desktop update */
checkUpdate?(): Promise<UpdateInfo>
/** Install updates (Tauri only) */
update?(): Promise<void>
/** Install the downloaded update using the platform restart flow */
updateAndRestart?(): Promise<void>
/** Fetch override */
fetch?: typeof fetch
+4 -4
View File
@@ -1,5 +1,5 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { checksum } from "@opencode-ai/shared/util/encode"
import { checksum } from "@opencode-ai/core/util/encode"
import { useParams } from "@solidjs/router"
import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js"
import { createStore, type SetStoreFunction } from "solid-js/store"
@@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {
return {
ready,
current: createMemo(() => store.prompt),
current: () => store.prompt,
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
context: {
items: createMemo(() => store.context.items),
add(item: ContextItem) {
@@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())
return {
ready: () => session().ready(),
ready: () => session().ready,
current: () => session().current(),
cursor: () => session().cursor(),
dirty: () => session().dirty(),
+9
View File
@@ -31,6 +31,7 @@ export interface Settings {
showReasoningSummaries: boolean
shellToolPartsExpanded: boolean
editToolPartsExpanded: boolean
showSessionProgressBar: boolean
}
updates: {
startup: boolean
@@ -115,6 +116,7 @@ const defaultSettings: Settings = {
showReasoningSummaries: false,
shellToolPartsExpanded: false,
editToolPartsExpanded: false,
showSessionProgressBar: true,
},
updates: {
startup: true,
@@ -227,6 +229,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setEditToolPartsExpanded(value: boolean) {
setStore("general", "editToolPartsExpanded", value)
},
showSessionProgressBar: withFallback(
() => store.general?.showSessionProgressBar,
defaultSettings.general.showSessionProgressBar,
),
setShowSessionProgressBar(value: boolean) {
setStore("general", "showSessionProgressBar", value)
},
},
updates: {
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
+2 -2
View File
@@ -1,7 +1,7 @@
import { batch, createMemo } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/shared/util/binary"
import { retry } from "@opencode-ai/shared/util/retry"
import { Binary } from "@opencode-ai/core/util/binary"
import { retry } from "@opencode-ai/core/util/retry"
import { createSimpleContext } from "@opencode-ai/ui/context"
import {
clearSessionPrefetch,
+3 -1
View File
@@ -210,7 +210,7 @@ export const dict = {
"common.saving": "جارٍ الحفظ...",
"common.default": "افتراضي",
"common.attachment": "مرفق",
"prompt.placeholder.shell": "أدخل أمر shell...",
"prompt.placeholder.shell": "أدخل أمر shell... {{example}}",
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
"prompt.placeholder.simple": "اسأل أي شيء...",
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
@@ -582,6 +582,8 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
"settings.general.row.editToolPartsExpanded.description":
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
"settings.general.row.showSessionProgressBar.title": "إظهار شريط تقدم الجلسة",
"settings.general.row.showSessionProgressBar.description": "عرض شريط التقدم المتحرك أعلى الجلسة أثناء عمل الوكيل",
"settings.general.row.wayland.title": "استخدام Wayland الأصلي",
"settings.general.row.wayland.description": "تعطيل التراجع إلى X11 على Wayland. يتطلب إعادة التشغيل.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -210,7 +210,7 @@ export const dict = {
"common.saving": "Salvando...",
"common.default": "Padrão",
"common.attachment": "anexo",
"prompt.placeholder.shell": "Digite comando do shell...",
"prompt.placeholder.shell": "Digite comando do shell... {{example}}",
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
"prompt.placeholder.simple": "Pergunte qualquer coisa...",
"prompt.placeholder.summarizeComments": "Resumir comentários…",
@@ -590,6 +590,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
"settings.general.row.editToolPartsExpanded.description":
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progresso da sessão",
"settings.general.row.showSessionProgressBar.description":
"Exibir a barra de progresso animada no topo da sessão quando o agente estiver trabalhando",
"settings.general.row.wayland.title": "Usar Wayland nativo",
"settings.general.row.wayland.description": "Desabilitar fallback X11 no Wayland. Requer reinicialização.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -228,7 +228,7 @@ export const dict = {
"common.default": "Podrazumijevano",
"common.attachment": "prilog",
"prompt.placeholder.shell": "Unesi shell naredbu...",
"prompt.placeholder.shell": "Unesi shell naredbu... {{example}}",
"prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"',
"prompt.placeholder.simple": "Pitaj bilo šta...",
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
@@ -655,6 +655,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
"settings.general.row.editToolPartsExpanded.description":
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
"settings.general.row.showSessionProgressBar.title": "Prikaži traku napretka sesije",
"settings.general.row.showSessionProgressBar.description":
"Prikaži animiranu traku napretka na vrhu sesije kada agent radi",
"settings.general.row.wayland.title": "Koristi nativni Wayland",
"settings.general.row.wayland.description": "Onemogući X11 fallback na Waylandu. Zahtijeva restart.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -226,7 +226,7 @@ export const dict = {
"common.default": "Standard",
"common.attachment": "vedhæftning",
"prompt.placeholder.shell": "Indtast shell-kommando...",
"prompt.placeholder.shell": "Indtast shell-kommando... {{example}}",
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
"prompt.placeholder.simple": "Spørg om hvad som helst...",
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
@@ -649,6 +649,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
"settings.general.row.editToolPartsExpanded.description":
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
"settings.general.row.showSessionProgressBar.title": "Vis sessionens fremdriftslinje",
"settings.general.row.showSessionProgressBar.description":
"Vis den animerede fremdriftslinje øverst i sessionen, når agenten arbejder",
"settings.general.row.wayland.title": "Brug native Wayland",
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Kræver genstart.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -215,7 +215,7 @@ export const dict = {
"common.saving": "Speichert...",
"common.default": "Standard",
"common.attachment": "Anhang",
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
"prompt.placeholder.shell": "Shell-Befehl eingeben... {{example}}",
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
"prompt.placeholder.simple": "Fragen Sie alles...",
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
@@ -601,6 +601,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
"settings.general.row.editToolPartsExpanded.description":
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
"settings.general.row.showSessionProgressBar.title": "Sitzungsfortschrittsleiste anzeigen",
"settings.general.row.showSessionProgressBar.description":
"Die animierte Fortschrittsleiste oben in der Sitzung anzeigen, wenn der Agent arbeitet",
"settings.general.row.wayland.title": "Natives Wayland verwenden",
"settings.general.row.wayland.description": "X11-Fallback unter Wayland deaktivieren. Erfordert Neustart.",
"settings.general.row.wayland.tooltip":
+4 -16
View File
@@ -28,7 +28,6 @@ export const dict = {
"command.provider.connect": "Connect provider",
"command.server.switch": "Switch server",
"command.settings.open": "Open settings",
"command.pair.show": "Pair mobile device",
"command.session.previous": "Previous session",
"command.session.next": "Next session",
"command.session.previous.unseen": "Previous unread session",
@@ -231,7 +230,7 @@ export const dict = {
"common.default": "Default",
"common.attachment": "attachment",
"prompt.placeholder.shell": "Enter shell command...",
"prompt.placeholder.shell": "Enter shell command... {{example}}",
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
"prompt.placeholder.simple": "Ask anything...",
"prompt.placeholder.summarizeComments": "Summarize comments…",
@@ -763,6 +762,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
"settings.general.row.editToolPartsExpanded.description":
"Show edit, write, and patch tool parts expanded by default in the timeline",
"settings.general.row.showSessionProgressBar.title": "Show session progress bar",
"settings.general.row.showSessionProgressBar.description":
"Display the animated progress bar at the top of the session when the agent is working",
"settings.general.row.wayland.title": "Use native Wayland",
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
@@ -871,20 +873,6 @@ export const dict = {
"settings.providers.tag.config": "Config",
"settings.providers.tag.custom": "Custom",
"settings.providers.tag.other": "Other",
"settings.pair.title": "Pair",
"settings.pair.description": "Pair a mobile device for push notifications.",
"settings.pair.loading": "Loading pairing info...",
"settings.pair.error.title": "Could not load pairing info",
"settings.pair.error.description": "Check that the server is reachable and try again.",
"settings.pair.disabled.title": "Push relay is not enabled",
"settings.pair.disabled.description": "Start the server with push relay options to enable mobile pairing.",
"settings.pair.server.label": "Server",
"settings.pair.relay.label": "Relay",
"settings.pair.secret.label": "Secret",
"settings.pair.instructions.title": "Scan with the OpenCode Control app",
"settings.pair.instructions.description":
"Open the OpenCode Control app and scan this QR code to pair your device for push notifications.",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.agents.title": "Agents",
+4 -1
View File
@@ -227,7 +227,7 @@ export const dict = {
"common.default": "Predeterminado",
"common.attachment": "adjunto",
"prompt.placeholder.shell": "Introduce comando de shell...",
"prompt.placeholder.shell": "Introduce comando de shell... {{example}}",
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
"prompt.placeholder.simple": "Pregunta cualquier cosa...",
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
@@ -659,6 +659,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
"settings.general.row.editToolPartsExpanded.description":
"Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
"settings.general.row.showSessionProgressBar.title": "Mostrar barra de progreso de la sesión",
"settings.general.row.showSessionProgressBar.description":
"Mostrar la barra de progreso animada en la parte superior de la sesión cuando el agente esté trabajando",
"settings.general.row.wayland.title": "Usar Wayland nativo",
"settings.general.row.wayland.description": "Deshabilitar fallback a X11 en Wayland. Requiere reinicio.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -210,7 +210,7 @@ export const dict = {
"common.saving": "Enregistrement...",
"common.default": "Défaut",
"common.attachment": "pièce jointe",
"prompt.placeholder.shell": "Entrez une commande shell...",
"prompt.placeholder.shell": "Entrez une commande shell... {{example}}",
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
"prompt.placeholder.simple": "Demandez n'importe quoi...",
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
@@ -598,6 +598,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
"settings.general.row.editToolPartsExpanded.description":
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
"settings.general.row.showSessionProgressBar.title": "Afficher la barre de progression de la session",
"settings.general.row.showSessionProgressBar.description":
"Afficher la barre de progression animée en haut de la session lorsque l'agent travaille",
"settings.general.row.wayland.title": "Utiliser Wayland natif",
"settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -209,7 +209,7 @@ export const dict = {
"common.saving": "保存中...",
"common.default": "デフォルト",
"common.attachment": "添付ファイル",
"prompt.placeholder.shell": "シェルコマンドを入力...",
"prompt.placeholder.shell": "シェルコマンドを入力... {{example}}",
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
"prompt.placeholder.simple": "何でも聞いてください...",
"prompt.placeholder.summarizeComments": "コメントを要約…",
@@ -587,6 +587,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
"settings.general.row.editToolPartsExpanded.description":
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
"settings.general.row.showSessionProgressBar.title": "セッション進行状況バーを表示",
"settings.general.row.showSessionProgressBar.description":
"エージェントの作業中に、セッション上部にアニメーション付きの進行状況バーを表示します",
"settings.general.row.wayland.title": "ネイティブWaylandを使用",
"settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -209,7 +209,7 @@ export const dict = {
"common.saving": "저장 중...",
"common.default": "기본값",
"common.attachment": "첨부 파일",
"prompt.placeholder.shell": "셸 명령어 입력...",
"prompt.placeholder.shell": "셸 명령어 입력... {{example}}",
"prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"',
"prompt.placeholder.simple": "무엇이든 물어보세요...",
"prompt.placeholder.summarizeComments": "댓글 요약…",
@@ -583,6 +583,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "edit 도구 파트 펼치기",
"settings.general.row.editToolPartsExpanded.description":
"타임라인에서 기본적으로 edit, write, patch 도구 파트를 펼친 상태로 표시합니다",
"settings.general.row.showSessionProgressBar.title": "세션 진행 표시줄 표시",
"settings.general.row.showSessionProgressBar.description":
"에이전트가 작업 중일 때 세션 상단에 애니메이션 진행 표시줄을 표시합니다",
"settings.general.row.wayland.title": "네이티브 Wayland 사용",
"settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -230,7 +230,7 @@ export const dict = {
"common.default": "Standard",
"common.attachment": "vedlegg",
"prompt.placeholder.shell": "Skriv inn shell-kommando...",
"prompt.placeholder.shell": "Skriv inn shell-kommando... {{example}}",
"prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"',
"prompt.placeholder.simple": "Spør om hva som helst...",
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
@@ -656,6 +656,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
"settings.general.row.editToolPartsExpanded.description":
"Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen",
"settings.general.row.showSessionProgressBar.title": "Vis fremdriftslinje for sesjonen",
"settings.general.row.showSessionProgressBar.description":
"Vis den animerte fremdriftslinjen øverst i sesjonen når agenten jobber",
"settings.general.row.wayland.title": "Bruk innebygd Wayland",
"settings.general.row.wayland.description": "Deaktiver X11-fallback på Wayland. Krever omstart.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -211,7 +211,7 @@ export const dict = {
"common.saving": "Zapisywanie...",
"common.default": "Domyślny",
"common.attachment": "załącznik",
"prompt.placeholder.shell": "Wpisz polecenie terminala...",
"prompt.placeholder.shell": "Wpisz polecenie terminala... {{example}}",
"prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"',
"prompt.placeholder.simple": "Zapytaj o cokolwiek...",
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
@@ -588,6 +588,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
"settings.general.row.editToolPartsExpanded.description":
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
"settings.general.row.showSessionProgressBar.title": "Pokazuj pasek postępu sesji",
"settings.general.row.showSessionProgressBar.description":
"Wyświetlaj animowany pasek postępu u góry sesji, gdy agent pracuje",
"settings.general.row.wayland.title": "Użyj natywnego Wayland",
"settings.general.row.wayland.description": "Wyłącz fallback X11 na Wayland. Wymaga restartu.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -227,7 +227,7 @@ export const dict = {
"common.default": "По умолчанию",
"common.attachment": "вложение",
"prompt.placeholder.shell": "Введите команду оболочки...",
"prompt.placeholder.shell": "Введите команду оболочки... {{example}}",
"prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"',
"prompt.placeholder.simple": "Спросите что угодно...",
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
@@ -656,6 +656,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
"settings.general.row.editToolPartsExpanded.description":
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
"settings.general.row.showSessionProgressBar.title": "Показывать индикатор прогресса сессии",
"settings.general.row.showSessionProgressBar.description":
"Показывать анимированный индикатор прогресса вверху сессии, когда агент работает",
"settings.general.row.wayland.title": "Использовать нативный Wayland",
"settings.general.row.wayland.description": "Отключить X11 fallback на Wayland. Требуется перезапуск.",
"settings.general.row.wayland.tooltip":
+4 -1
View File
@@ -227,7 +227,7 @@ export const dict = {
"common.default": "ค่าเริ่มต้น",
"common.attachment": "ไฟล์แนบ",
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์...",
"prompt.placeholder.shell": "ป้อนคำสั่งเชลล์... {{example}}",
"prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"',
"prompt.placeholder.simple": "ถามอะไรก็ได้...",
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
@@ -647,6 +647,9 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
"settings.general.row.editToolPartsExpanded.description":
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
"settings.general.row.showSessionProgressBar.title": "แสดงแถบความคืบหน้าของเซสชัน",
"settings.general.row.showSessionProgressBar.description":
"แสดงแถบความคืบหน้าแบบเคลื่อนไหวที่ด้านบนของเซสชันเมื่อเอเจนต์กำลังทำงาน",
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
"settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท",
"settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า",
+5 -1
View File
@@ -232,7 +232,7 @@ export const dict = {
"common.default": "Varsayılan",
"common.attachment": "ek",
"prompt.placeholder.shell": "Kabuk komutu girin...",
"prompt.placeholder.shell": "Kabuk komutu girin... {{example}}",
"prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"',
"prompt.placeholder.simple": "Bir şeyler sorun...",
"prompt.placeholder.summarizeComments": "Yorumları özetle…",
@@ -663,6 +663,10 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.description":
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
"settings.general.row.showSessionProgressBar.title": "Oturum ilerleme çubuğunu göster",
"settings.general.row.showSessionProgressBar.description":
"Ajan çalışırken oturumun üst kısmında animasyonlu ilerleme çubuğunu göster",
"settings.general.row.wayland.title": "Yerel Wayland kullan",
"settings.general.row.wayland.description":
"Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.",
+3 -1
View File
@@ -249,7 +249,7 @@ export const dict = {
"common.default": "默认",
"common.attachment": "附件",
"prompt.placeholder.shell": "输入 shell 命令...",
"prompt.placeholder.shell": "输入 shell 命令... {{example}}",
"prompt.placeholder.normal": '随便问点什么... "{{example}}"',
"prompt.placeholder.simple": "随便问点什么...",
"prompt.placeholder.summarizeComments": "总结评论…",
@@ -646,6 +646,8 @@ export const dict = {
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
"settings.general.row.showSessionProgressBar.title": "显示会话进度条",
"settings.general.row.showSessionProgressBar.description": "当智能体正在工作时,在会话顶部显示动画进度条",
"settings.general.row.wayland.title": "使用原生 Wayland",
"settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。",
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
+3 -1
View File
@@ -227,7 +227,7 @@ export const dict = {
"common.default": "預設",
"common.attachment": "附件",
"prompt.placeholder.shell": "輸入 shell 命令...",
"prompt.placeholder.shell": "輸入 shell 命令... {{example}}",
"prompt.placeholder.normal": '隨便問點什麼... "{{example}}"',
"prompt.placeholder.simple": "隨便問點什麼...",
"prompt.placeholder.summarizeComments": "摘要評論…",
@@ -642,6 +642,8 @@ export const dict = {
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
"settings.general.row.showSessionProgressBar.title": "顯示工作階段進度列",
"settings.general.row.showSessionProgressBar.description": "當代理程式正在運作時,在工作階段頂部顯示動畫進度列",
"settings.general.row.wayland.title": "使用原生 Wayland",
"settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。",
"settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。",
+9
View File
@@ -73,4 +73,13 @@
width: auto;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
+6 -7
View File
@@ -1,8 +1,8 @@
import { DataProvider } from "@opencode-ai/ui/context"
import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { createEffect, createMemo, type ParentProps, Show } from "solid-js"
import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { LocalProvider } from "@/context/local"
import { SDKProvider } from "@/context/sdk"
@@ -23,11 +23,10 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
})
createEffect(() => {
const id = params.id
if (!id) return
void sync.session.sync(id)
})
createResource(
() => params.id,
(id) => sync.session.sync(id),
)
return (
<DataProvider
+2 -3
View File
@@ -244,10 +244,9 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
}
async function installUpdate() {
if (!platform.update || !platform.restart) return
if (!platform.updateAndRestart) return
await platform
.update()
.then(() => platform.restart!())
.updateAndRestart()
.then(() => setStore("actionError", undefined))
.catch((err) => {
setStore("actionError", formatError(err, language.t))
+1 -1
View File
@@ -3,7 +3,7 @@ import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo"
import { useLayout } from "@/context/layout"
import { useNavigate } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
import { usePlatform } from "@/context/platform"
import { DateTime } from "luxon"
+8 -16
View File
@@ -17,7 +17,7 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { Persist, persisted } from "@/utils/persist"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { decode64 } from "@/utils/base64"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
@@ -25,7 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog"
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { Session, type Message } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
@@ -48,8 +48,8 @@ import {
} from "@/context/global-sync/session-prefetch"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/shared/util/binary"
import { retry } from "@opencode-ai/shared/util/retry"
import { Binary } from "@opencode-ai/core/util/binary"
import { retry } from "@opencode-ai/core/util/retry"
import { playSoundById } from "@/utils/sound"
import { createAim } from "@/utils/aim"
import { setNavigate } from "@/utils/notification-click"
@@ -366,7 +366,7 @@ export default function Layout(props: ParentProps) {
const useUpdatePolling = () =>
onMount(() => {
if (!platform.checkUpdate || !platform.update || !platform.restart) return
if (!platform.checkUpdate || !platform.updateAndRestart) return
let toastId: number | undefined
let interval: ReturnType<typeof setInterval> | undefined
@@ -384,8 +384,7 @@ export default function Layout(props: ParentProps) {
{
label: language.t("toast.update.action.installRestart"),
onClick: async () => {
await platform.update!()
await platform.restart!()
await platform.updateAndRestart!()
},
},
{
@@ -1061,13 +1060,6 @@ export default function Layout(props: ParentProps) {
keybind: "mod+comma",
onSelect: () => openSettings(),
},
{
id: "pair.show",
title: language.t("command.pair.show"),
category: language.t("command.category.settings"),
slash: "pair",
onSelect: () => openSettings("pair"),
},
{
id: "session.previous",
title: language.t("command.session.previous"),
@@ -1220,11 +1212,11 @@ export default function Layout(props: ParentProps) {
})
}
function openSettings(defaultTab?: string) {
function openSettings() {
const run = ++dialogRun
void import("@/components/dialog-settings").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSettings defaultTab={defaultTab} />)
dialog.show(() => <x.DialogSettings />)
})
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { type Session } from "@opencode-ai/sdk/v2/client"
type SessionStore = {
@@ -31,7 +31,7 @@ function sortSessions(now: number) {
const isRootVisibleSession = (session: Session, directory: string) =>
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
const roots = (store: SessionStore) =>
export const roots = (store: SessionStore) =>
(store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
@@ -4,7 +4,7 @@ import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/shared/util/path"
import { getFilename } from "@opencode-ai/core/util/path"
import { A, useParams } from "@solidjs/router"
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
import { useGlobalSync } from "@/context/global-sync"
@@ -19,6 +19,12 @@ import { childSessionOnPath, hasProjectPermissions } from "./helpers"
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) {
return id === OPENCODE_PROJECT_ID
? "https://opencode.ai/favicon.svg"
: (icon?.override ?? (icon?.color ? undefined : icon?.url))
}
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
const globalSync = useGlobalSync()
const notification = useNotification()
@@ -42,9 +48,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
src={
props.project.id === OPENCODE_PROJECT_ID ? "https://opencode.ai/favicon.svg" : props.project.icon?.override
}
src={getProjectAvatarSource(props.project.id, props.project.icon)}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
classList={{ "badge-mask": notify() }}
@@ -263,10 +267,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
</Show>
</div>
</div>
<Show when={currentChild()}>
<Show when={currentChild()} keyed>
{(child) => (
<div class="w-full">
<SessionItem {...props} session={child()} level={(props.level ?? 0) + 1} />
<SessionItem {...props} session={child} level={(props.level ?? 0) + 1} />
</div>
)}
</Show>
@@ -1,6 +1,6 @@
import { createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { Button } from "@opencode-ai/ui/button"
import { ContextMenu } from "@opencode-ai/ui/context-menu"
import { HoverCard } from "@opencode-ai/ui/hover-card"
@@ -3,8 +3,8 @@ import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "so
import { createStore } from "solid-js/store"
import { createSortable } from "@thisbeyond/solid-dnd"
import { createMediaQuery } from "@solid-primitives/media"
import { base64Encode } from "@opencode-ai/shared/util/encode"
import { getFilename } from "@opencode-ai/shared/util/path"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { getFilename } from "@opencode-ai/core/util/path"
import { Button } from "@opencode-ai/ui/button"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
@@ -321,7 +321,7 @@ export const SortableWorkspace = (props: {
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => query.isLoading
const loading = () => query.isLoading && count() === 0
const touch = createMediaQuery("(hover: none)")
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
const loadMore = async () => {
+1 -1
View File
@@ -28,7 +28,7 @@ import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { Button } from "@opencode-ai/ui/button"
import { showToast } from "@opencode-ai/ui/toast"
import { checksum } from "@opencode-ai/shared/util/encode"
import { checksum } from "@opencode-ai/core/util/encode"
import { useSearchParams } from "@solidjs/router"
import { NewSessionView, SessionHeader } from "@/components/session"
import { useComments } from "@/context/comments"
+1 -1
View File
@@ -6,7 +6,7 @@ import type { FileSearchHandle } from "@opencode-ai/ui/file"
import { useFileComponent } from "@opencode-ai/ui/context/file"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
import { createLineCommentController } from "@opencode-ai/ui/line-comment-annotations"
import { sampledChecksum } from "@opencode-ai/shared/util/encode"
import { sampledChecksum } from "@opencode-ai/core/util/encode"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tabs } from "@opencode-ai/ui/tabs"
@@ -15,8 +15,8 @@ import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { TextField } from "@opencode-ai/ui/text-field"
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
import { showToast } from "@opencode-ai/ui/toast"
import { Binary } from "@opencode-ai/shared/util/binary"
import { getFilename } from "@opencode-ai/shared/util/path"
import { Binary } from "@opencode-ai/core/util/binary"
import { getFilename } from "@opencode-ai/core/util/path"
import { Popover as KobaltePopover } from "@kobalte/core/popover"
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
import { SessionContextUsage } from "@/components/session-context-usage"
@@ -259,7 +259,7 @@ export function MessageTimeline(props: {
if (!id) return idle
return sync.data.session_status[id] ?? idle
})
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
const working = createMemo(() => sessionStatus().type !== "idle")
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
const [timeoutDone, setTimeoutDone] = createSignal(true)
@@ -721,7 +721,7 @@ export function MessageTimeline(props: {
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
}}
>
<Show when={workingStatus() !== "hidden"}>
<Show when={workingStatus() !== "hidden" && settings.general.showSessionProgressBar()}>
<div
data-component="session-progress"
data-state={workingStatus()}
@@ -812,7 +812,7 @@ export function MessageTimeline(props: {
</Show>
</div>
</div>
<Show when={sessionID()}>
<Show when={sessionID()} keyed>
{(id) => (
<div class="shrink-0 flex items-center gap-3">
<SessionContextUsage placement="bottom" />
@@ -878,12 +878,12 @@ export function MessageTimeline(props: {
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
>
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
@@ -14,7 +14,7 @@ import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal"
import { showToast } from "@opencode-ai/ui/toast"
import { findLast } from "@opencode-ai/shared/util/array"
import { findLast } from "@opencode-ai/core/util/array"
import { createSessionTabs } from "@/pages/session/helpers"
import { extractPromptFromParts } from "@/utils/prompt"
import { UserMessage } from "@opencode-ai/sdk/v2"
+1 -1
View File
@@ -1,4 +1,4 @@
import { base64Decode } from "@opencode-ai/shared/util/encode"
import { base64Decode } from "@opencode-ai/core/util/encode"
export function decode64(value: string | undefined) {
if (value === undefined) return
+1 -1
View File
@@ -1,6 +1,6 @@
import { Platform, usePlatform } from "@/context/platform"
import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage"
import { checksum } from "@opencode-ai/shared/util/encode"
import { checksum } from "@opencode-ai/core/util/encode"
import { createResource, type Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.18",
"version": "1.14.25",
"type": "module",
"license": "MIT",
"scripts": {
+6 -4
View File
@@ -249,7 +249,7 @@ export const dict = {
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.meta.description":
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و Qwen3.5 Plus و Qwen3.6 Plus و MiniMax M2.5 وMiniMax M2.7.",
"يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body":
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
@@ -261,6 +261,8 @@ export const dict = {
"go.cta.promo": "$5 للشهر الأول",
"go.pricing.body":
"استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"go.banner.badge": "3x",
"go.banner.text": "Kimi K2.6: حد الاستخدام 3 أضعاف حتى 27 أبريل",
"go.graph.free": "مجاني",
"go.graph.freePill": "Big Pickle ونماذج مجانية",
"go.graph.go": "Go",
@@ -298,7 +300,7 @@ export const dict = {
"go.problem.item2": "حدود سخية ووصول موثوق",
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
"go.problem.item4":
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7",
"يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash",
"go.how.title": "كيف يعمل Go",
"go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.",
"go.how.step1.title": "أنشئ حسابًا",
@@ -322,7 +324,7 @@ export const dict = {
"go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.",
"go.faq.q3": "هل Go هو نفسه Zen؟",
"go.faq.a3":
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 و Kimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni و Qwen3.5 Plus و Qwen3.6 Plus و MiniMax M2.5 وMiniMax M2.7.",
"لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.",
"go.faq.q4": "كم تكلفة Go؟",
"go.faq.a4.p1.beforePricing": "تكلفة Go",
"go.faq.a4.p1.pricingLink": "$5 للشهر الأول",
@@ -345,7 +347,7 @@ export const dict = {
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
"go.faq.a9":
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وMiMo-V2-Pro وMiMo-V2-Omni وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
+6 -4
View File
@@ -253,7 +253,7 @@ export const dict = {
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
"go.meta.description":
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
"go.hero.title": "Modelos de codificação de baixo custo para todos",
"go.hero.body":
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
@@ -265,6 +265,8 @@ export const dict = {
"go.cta.promo": "$5 no primeiro mês",
"go.pricing.body":
"Use com qualquer agente. $5 no primeiro mês, depois $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.",
"go.banner.badge": "3x",
"go.banner.text": "Kimi K2.6: limite de uso 3x maior até 27 de abril",
"go.graph.free": "Grátis",
"go.graph.freePill": "Big Pickle e modelos gratuitos",
"go.graph.go": "Go",
@@ -303,7 +305,7 @@ export const dict = {
"go.problem.item2": "Limites generosos e acesso confiável",
"go.problem.item3": "Feito para o maior número possível de programadores",
"go.problem.item4":
"Inclui GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7",
"Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash",
"go.how.title": "Como o Go funciona",
"go.how.body":
"O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.",
@@ -329,7 +331,7 @@ export const dict = {
"go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.",
"go.faq.q3": "O Go é o mesmo que o Zen?",
"go.faq.a3":
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.",
"Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.",
"go.faq.q4": "Quanto custa o Go?",
"go.faq.a4.p1.beforePricing": "O Go custa",
"go.faq.a4.p1.pricingLink": "$5 no primeiro mês",
@@ -353,7 +355,7 @@ export const dict = {
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
"go.faq.a9":
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
+6 -4
View File
@@ -251,7 +251,7 @@ export const dict = {
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
"go.meta.description":
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
"go.hero.title": "Kodningsmodeller til lav pris for alle",
"go.hero.body":
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
@@ -263,6 +263,8 @@ export const dict = {
"go.cta.promo": "$5 første måned",
"go.pricing.body":
"Brug med enhver agent. $5 første måned, derefter $10/måned. Tank op med kredit efter behov. Afmeld når som helst.",
"go.banner.badge": "3x",
"go.banner.text": "Kimi K2.6: brugsgrænsen tredoblet til 27. april",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller",
"go.graph.go": "Go",
@@ -300,7 +302,7 @@ export const dict = {
"go.problem.item2": "Generøse grænser og pålidelig adgang",
"go.problem.item3": "Bygget til så mange programmører som muligt",
"go.problem.item4":
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7",
"Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash",
"go.how.title": "Hvordan Go virker",
"go.how.body":
"Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.",
@@ -326,7 +328,7 @@ export const dict = {
"go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.",
"go.faq.q3": "Er Go det samme som Zen?",
"go.faq.a3":
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.",
"Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.",
"go.faq.q4": "Hvad koster Go?",
"go.faq.a4.p1.beforePricing": "Go koster",
"go.faq.a4.p1.pricingLink": "$5 første måned",
@@ -349,7 +351,7 @@ export const dict = {
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
"go.faq.a9":
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, MiMo-V2-Pro, MiMo-V2-Omni, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",

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