Compare commits

...

658 Commits

Author SHA1 Message Date
Kit Langton 7c09c2f17f refactor(project): apply simplify review
- Fix prunable parser to match 'prunable <reason>' lines, not just bare 'prunable'.
- Parallelize Effect.forEach in Worktree.list with concurrency: 'unbounded' to match the rest of the file.
- Simplify the experimental worktree GET handler to use Effect.fn(fn, Effect.map(...)).
- Drop unused isProjectList/isProjectUpdate type guards and the unknown-cast set/setBootStore wrappers in global-sync; expose setGlobalStore directly.
- Centralize project worktree mutations as globalSync.project.addWorktree / removeWorktree, replacing three near-duplicate produce blocks in pages/layout.tsx and prompt-input/submit.ts. Use pathKey for dedup in all sites.
- Cast project.updated event properties as Project (not ProjectInfo) and seed worktrees: [] on insert.
- Extract projectDirectories(project) helper in pages/layout/helpers.ts; use at six call sites that built [project.worktree, ...(project.worktrees ?? [])] inline.
2026-05-11 11:48:52 -04:00
Kit Langton c4132543c3 refactor(project): drop persisted Project.sandboxes column
The Project row stored a sandboxes string array that duplicated what git
already tracks. The /experimental/worktree GET handler now reads from
`Worktree.list()` (git worktree list --porcelain) instead, the write-side
addSandbox/removeSandbox bookkeeping is gone, and the column is dropped
from the schema with a Drizzle migration. The app maps the live list onto
project.worktrees client-side instead of reading project.sandboxes.
2026-05-11 11:48:52 -04:00
Kit Langton 52f7ba7d4d fix(llm): drop removed dispatch option from recorded cache tests (#26900) 2026-05-11 11:48:30 -04:00
opencode-agent[bot] 19fce2bc6f chore: generate 2026-05-11 15:21:20 +00:00
Shoubhit Dash 4bae84c8b0 feat(scout): autocomplete configured mentions (#26843) 2026-05-11 20:50:03 +05:30
Kit Langton f240bba8e7 chore(http-recorder): remove content-matching dispatch mode (#26792) 2026-05-11 11:10:18 -04:00
Kit Langton bcee247988 Define project update input with Effect Schema (#26803) 2026-05-11 15:05:31 +00:00
Kit Langton 64c504212a Parse command config with Effect Schema (#26801) 2026-05-11 10:22:45 -04:00
opencode-agent[bot] 6592d80460 chore: generate 2026-05-11 14:13:39 +00:00
Sebastian d821650b48 add default diff parser for markdown fenced code blocks (#26887) 2026-05-11 14:12:32 +00:00
Victor Navarro 6c9f12bb2d chore: exclude status 429 from free model alerts (#26879) 2026-05-11 16:04:54 +02:00
opencode-agent[bot] fca89e5e4d chore: generate 2026-05-11 13:44:39 +00:00
Frank 1eff01b6a5 sync 2026-05-11 09:43:12 -04:00
opencode-agent[bot] e93b1f3a7f chore: generate 2026-05-11 13:38:44 +00:00
Frank 8874d4a42f zen: deekseek v4 flash free 2026-05-11 09:37:14 -04:00
Brendan Allan c933504d9c fix(ui): better handle patch file support when rendering patch/edit tools (#26828) 2026-05-11 17:21:59 +08:00
Shoubhit Dash 2d0d3d596e feat(compaction): serialize compaction tail (#26830) 2026-05-11 13:40:36 +05:30
opencode-agent[bot] 3bd98ea055 chore: generate 2026-05-11 07:28:32 +00:00
Shoubhit Dash 7e997cfba4 refactor(scout): resolve configured reference mentions (#26701) 2026-05-11 12:57:26 +05:30
Brendan Allan 5d6f2a1524 fix(ui): use part_text_accum_delta to prevent markdown cutoff during streaming (#26822) 2026-05-11 15:15:03 +08:00
opencode-agent[bot] b1cb71856e chore: generate 2026-05-11 05:27:40 +00:00
Aiden Cline 518264fcd9 fix(opencode): fix full session fork (#26811) 2026-05-11 00:26:36 -05:00
Dax 7235c9c9b8 Trace data migrations (#26809) 2026-05-11 03:23:01 +00:00
Kit Langton 274033cd52 Validate prompt messages with Effect Schema (#26796) 2026-05-11 02:59:20 +00:00
Kit Langton 9b369ee815 chore(llm): make cache: 'auto' the default (#26798) 2026-05-10 22:46:01 -04:00
Sebastian 721ff5121e fix prompt history behaviour and session line up/down commands (#26797) 2026-05-11 02:27:46 +00:00
opencode-agent[bot] cfbf5d1c6f chore: update nix node_modules hashes 2026-05-11 02:20:35 +00:00
opencode-agent[bot] 02cb7e7b71 chore: generate 2026-05-11 02:11:07 +00:00
Kit Langton 942630eb4a feat(llm): cache-policy auto-placement (#26786) 2026-05-10 22:09:55 -04:00
opencode ce66b191d1 sync release versions for v1.14.48 2026-05-11 02:07:48 +00:00
Kit Langton 6f1f5944ce Delete unused opencode Zod helpers (#26793) 2026-05-10 21:57:18 -04:00
opencode-agent[bot] 1c49b2ed67 chore: generate 2026-05-11 01:55:22 +00:00
opencode-agent[bot] 3c6be604ec chore: update nix node_modules hashes 2026-05-11 01:54:13 +00:00
Kit Langton ddc02c2893 Drop synchronous SyncEvent facades (#26789) 2026-05-11 01:53:50 +00:00
Kit Langton 2703eff2e2 refactor(llm): normalize Usage as inclusive total + non-overlapping breakdown (#26735) 2026-05-10 21:52:50 -04:00
opencode-agent[bot] 38e4540119 chore: generate 2026-05-11 01:41:26 +00:00
Dax Raad 4100fcbd17 disable image resizing 2026-05-10 21:40:12 -04:00
Kit Langton 83cb0f60ec Drop EventV2 run facade (#26783) 2026-05-10 21:26:28 -04:00
Kit Langton effd96755e Use SyncEvent service at event call sites (#26782) 2026-05-10 21:20:13 -04:00
opencode-agent[bot] 5801cce1b5 chore: generate 2026-05-11 01:18:39 +00:00
Kit Langton 77e6c0d329 feat(llm): cache hint TTL, breakpoint cap, and tool placement (#26779) 2026-05-10 21:17:38 -04:00
Kit Langton fed716ada5 Clarify compaction test harness (#26777) 2026-05-10 20:41:21 -04:00
opencode 426d92e352 sync release versions for v1.14.47 2026-05-11 00:23:17 +00:00
opencode-agent[bot] d0412a213b chore: generate 2026-05-11 00:18:03 +00:00
Kit Langton 16aa67086b Effectify remaining compaction process tests (#26776) 2026-05-10 20:17:01 -04:00
Dax 7cea32ee08 Add background code migration service (#26652) 2026-05-11 00:16:42 +00:00
Kit Langton 128d10d9e9 Simplify compaction test helpers (#26742) 2026-05-10 20:03:11 -04:00
Kit Langton 5ef72e1101 Drop unused ID Zod statics (#26740) 2026-05-10 19:58:21 -04:00
Kit Langton d8060dc9ad Drop compaction create facade (#26739) 2026-05-10 19:54:16 -04:00
Kit Langton f71fb18d3d Replace compaction create test fixtures (#26738) 2026-05-10 19:53:22 -04:00
Sebastian 5654dd2aad restore managed textarea keymap handling (#26771) 2026-05-11 01:45:59 +02:00
Kit Langton 64dde0cb15 Migrate compaction tool-call test (#26737) 2026-05-10 19:44:42 -04:00
Kit Langton a658e43eeb Migrate compaction plugin test (#26736) 2026-05-10 19:42:44 -04:00
Kit Langton ca8a42c973 Migrate LLM compaction tail tests (#26734) 2026-05-10 19:41:28 -04:00
Kit Langton 8f5f75db12 Migrate compaction LLM test (#26733) 2026-05-10 19:20:44 -04:00
Kit Langton 4ff0d07b1d Migrate configurable compaction tests (#26732) 2026-05-10 19:18:02 -04:00
Kit Langton 2a571b3cee Migrate compaction overflow tests (#26731) 2026-05-10 19:12:49 -04:00
Dax 0fffcdfe46 Persist session model switches outside event flag (#26765) 2026-05-10 18:53:14 -04:00
Dax 2ba2ee52e8 docs: document bun dev tmux capture (#26764) 2026-05-10 17:41:20 -04:00
Frank 56d818fc34 zen: fix reasoning token for openai compatible endpoint 2026-05-10 13:36:02 -04:00
Kit Langton 9c8da69196 Use Effect timeout in compaction test (#26728) 2026-05-10 12:45:54 -04:00
opencode-agent[bot] a78018697c chore: generate 2026-05-10 16:44:40 +00:00
Kit Langton e45b6ef1de refactor(http-recorder): use Schema.TaggedErrorClass for cassette errors (#26729) 2026-05-10 16:43:33 +00:00
opencode-agent[bot] b616543ac2 chore: generate 2026-05-10 16:30:55 +00:00
Kit Langton 2bd3d9a696 refactor(http-recorder): hide cassette format behind Cassette seam (#26725) 2026-05-10 12:29:55 -04:00
Kit Langton fa15dbc5ec Migrate compaction process tests (#26723) 2026-05-10 12:25:44 -04:00
opencode-agent[bot] 312e5c7a7c chore: generate 2026-05-10 16:22:29 +00:00
Kit Langton 049502fac6 fix(server): return diagnosable body for schema rejections (#26631) 2026-05-10 16:21:32 +00:00
opencode-agent[bot] cc2915be16 chore: generate 2026-05-10 16:20:16 +00:00
Kit Langton ce061bf661 Add explicit LLM stream lifecycle events (#26722) 2026-05-10 12:19:13 -04:00
Frank 3b8790e034 zen: fix usage css on mobile 2026-05-10 12:14:11 -04:00
Kit Langton a4f3cedcdf Start effect-style compaction tests 2026-05-10 16:12:00 +00:00
opencode-agent[bot] 1c9a2eb239 chore: generate 2026-05-10 16:06:18 +00:00
Kit Langton 4fb417d3b5 feat(http-recorder): default mode to "auto" (#26719) 2026-05-10 16:05:11 +00:00
Kit Langton 11030c627b Scope boolean query overrides 2026-05-10 11:57:52 -04:00
opencode-agent[bot] c104098a66 chore: generate 2026-05-10 15:55:49 +00:00
Kit Langton 49ee3ba85a Source diff message query pattern (#26638) 2026-05-10 11:54:54 -04:00
opencode-agent[bot] 4fc538378d chore: generate 2026-05-10 14:50:21 +00:00
Kit Langton d28b5ad2f4 refactor(http-recorder): Redactor + Recorder seams, README (#26636) 2026-05-10 10:49:22 -04:00
opencode-agent[bot] 6589a66822 chore: generate 2026-05-10 12:28:11 +00:00
Shoubhit Dash 5cf9abe743 feat(scout): materialize configured reference repos (#26692) 2026-05-10 17:57:11 +05:30
Frank 903d81819d Zen: add Ring 2.6 1T 2026-05-10 03:51:34 -04:00
opencode-agent[bot] 472f9e64a6 chore: update nix node_modules hashes 2026-05-10 07:06:30 +00:00
Frank c04fa9e253 sync: revert
This reverts commit 3a7f617098.
2026-05-10 02:58:46 -04:00
opencode-agent[bot] 3a78fb1f42 chore: generate 2026-05-10 06:49:21 +00:00
Aiden Cline 85ce6a5f95 feat: better image handling (auto resize & max size constraints) (#26401) 2026-05-10 01:48:19 -05:00
opencode-agent[bot] 5217e6c1af chore: generate 2026-05-10 06:39:09 +00:00
Frank 3a7f617098 go: add tencent icon 2026-05-10 02:37:50 -04:00
opencode-agent[bot] d9150413cb chore: generate 2026-05-10 06:24:35 +00:00
Jack bcbc1dba22 Go add hy3 preview (#26533) 2026-05-10 02:23:34 -04:00
Frank ce3235e115 sync 2026-05-10 02:17:32 -04:00
opencode-agent[bot] a9a2a597d5 chore: generate 2026-05-10 04:30:04 +00:00
Dax 3753601f87 Format TUI paths relative to session directory (#26648) 2026-05-10 04:29:02 +00:00
Kit Langton fb4bab8a66 Remove redundant ID Zod overrides (#26633) 2026-05-09 23:12:21 -04:00
opencode-agent[bot] b3526f6ce9 chore: generate 2026-05-10 03:03:37 +00:00
Kit Langton f220f02a2f Source workspace path pattern (#26632) 2026-05-09 23:02:31 -04:00
opencode-agent[bot] 235a86fb60 chore: generate 2026-05-10 02:59:46 +00:00
Kit Langton 67b9c9c027 Source HTTP API ID path patterns (#26623) 2026-05-09 22:58:47 -04:00
opencode 2f11c9f7ed sync release versions for v1.14.46 2026-05-10 02:34:36 +00:00
opencode-agent[bot] e1c1193f3e chore: generate 2026-05-10 02:11:45 +00:00
Kit Langton 29250a0efb fix(session): loosen remaining stored numeric schemas to tolerate legacy data (#26622) 2026-05-09 22:10:48 -04:00
Kit Langton c6e6bdf59f fix(session): tolerate negative token counts in stored parts (#26620) 2026-05-09 22:10:44 -04:00
opencode-agent[bot] d80e1199ca chore: generate 2026-05-10 02:06:39 +00:00
Kit Langton 10ea59066f feat(skill): built-in opencode-meta skill (#26617) 2026-05-09 22:05:37 -04:00
Kit Langton 79d6b10d7c fix(mcp): tolerate output schema ref failures (#26614) 2026-05-09 22:03:59 -04:00
Kit Langton 6e78f36a0f Narrow HTTP API numeric query overrides (#26618) 2026-05-09 22:02:51 -04:00
Kit Langton 16866e1180 Share HTTP API boolean query schema (#26615) 2026-05-09 21:41:15 -04:00
opencode-agent[bot] 6d130e5deb chore: generate 2026-05-10 01:13:41 +00:00
Kit Langton e30d8173c1 Fix OpenAPI workspace query drift (#26609) 2026-05-09 21:12:34 -04:00
opencode 7a79f3a5ea sync release versions for v1.14.45 2026-05-10 00:07:24 +00:00
Kit Langton b8ca71d309 fix(task): subagent inherits parent agent's deny rules (Plan Mode security bypass) (#26597)
Co-authored-by: Developer <temp@example.com>
2026-05-09 23:51:55 +00:00
Kit Langton 6849f96825 refactor(provider): share model status schema (#26595)
Co-authored-by: Developer <temp@example.com>
2026-05-09 19:20:31 -04:00
Kit Langton 00c3248295 fix(config): allow active provider model status (#26592)
Co-authored-by: Developer <temp@example.com>
2026-05-09 18:59:05 -04:00
opencode-agent[bot] 818b56dbd0 chore: generate 2026-05-09 22:47:54 +00:00
Kit Langton 29b5b24787 fix(tui): aggregate bootstrap request failures
TUI bootstrap now reports all parallel fetch failures together instead of losing sibling failures after the first rejection.
2026-05-09 18:46:52 -04:00
Kit Langton 11363170ca fix(sdk): wrap thrown error bodies in Error
SDK throwOnError paths now convert structured response bodies into real Error instances while preserving the original body and status in cause.
2026-05-09 18:46:43 -04:00
Kit Langton ba9e4b67ed fix(tool/read): match permission patterns against worktree-relative path
Read permission checks now use the same worktree-relative path basis as edit/write/apply_patch, so configured patterns apply consistently.
2026-05-09 18:46:29 -04:00
Kit Langton bd1029b19f test(server): cover HttpApi context inheritance
Adds regression coverage for request context inheritance in promptAsync and explicit context provisioning in stream bodies.
2026-05-09 18:46:10 -04:00
Kit Langton 43b51f09d0 fix(httpapi): align runtime query schemas with workspace routing params (#26581)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:50:30 -04:00
opencode-agent[bot] c61ab51886 chore: generate 2026-05-09 20:45:29 +00:00
Kit Langton d373c562f2 fix(session): accept legacy summary diffs (#26579)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:44:24 -04:00
opencode-agent[bot] 5fa5d876fc chore: generate 2026-05-09 20:31:32 +00:00
Kit Langton 805af011c9 test(session): regression test for #26574 + mirror loosening on Vcs.FileDiff (#26578)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:30:31 -04:00
opencode-agent[bot] 480aa8b23b chore: generate 2026-05-09 20:24:28 +00:00
OpeOginni d62442bb5d fix(sessions): allow optional patch field in diff for migrated sessions (#26574) 2026-05-09 16:23:28 -04:00
Kit Langton 8602937a37 test(session): cover workspace-routed messages (#26576) 2026-05-09 16:19:06 -04:00
Kit Langton 77da433e0a fix(session): accept routing params in message list (#26569)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:02:35 -04:00
opencode-agent[bot] ad79f3e0cf chore: generate 2026-05-09 19:59:00 +00:00
Kit Langton 6c2dfd2f52 fix(tui): guard messages.data in session.sync against undefined (#26566)
Co-authored-by: Developer <temp@example.com>
2026-05-09 19:58:03 +00:00
Sebastian 9a8b54fe62 Plugin command API shim (#26564) 2026-05-09 21:56:49 +02:00
Dax dcdbdb218f Move schema utilities into core (#26565) 2026-05-09 19:51:09 +00:00
Kit Langton 5e49029e70 fix(provider): isolate plugin model mutations (#26561)
Co-authored-by: Developer <temp@example.com>
2026-05-09 15:47:07 -04:00
opencode 19abadaf27 sync release versions for v1.14.44 2026-05-09 19:33:26 +00:00
Kit Langton 6fea0178eb fix(server): defer validation error body change 2026-05-09 14:50:45 -04:00
Kit Langton ebe9dcf27e fix(server): return validation error bodies 2026-05-09 14:39:25 -04:00
Kit Langton 57efec4429 fix(storage): default workspace time migration (#26556) 2026-05-09 14:18:55 -04:00
opencode e22144806f sync release versions for v1.14.43 2026-05-09 18:06:16 +00:00
Kit Langton 27fa297a42 fix(server): keep provider lists JSON-safe (#26550) 2026-05-09 13:40:46 -04:00
Aiden Cline b1cd25de3d ignore: fix typerrs on dev (#26544) 2026-05-09 12:08:00 -05:00
opencode 780bbb0f3b sync release versions for v1.14.42 2026-05-09 16:54:43 +00:00
Steffen Deusch 817cb076a8 fix(acp): include tool image attachments in updates (#25128) 2026-05-09 11:45:44 -05:00
opencode-agent[bot] 347526e9fd chore: generate 2026-05-09 16:32:52 +00:00
Polo123456789 092bc674a5 fix(sidebar): fix logic and missleading message #26469 (#26470) 2026-05-09 11:31:46 -05:00
Kit Langton b24a4e897e chore(server): clean up post-Hono-deletion scar tissue (#26542) 2026-05-09 12:30:18 -04:00
opencode-agent[bot] 3afa622eab chore: update nix node_modules hashes 2026-05-09 13:22:38 +00:00
opencode-agent[bot] d01cb7f013 chore: generate 2026-05-09 13:11:45 +00:00
Kit Langton 28b03595bf research: delete Hono backend (do not merge) (#25667) 2026-05-09 13:10:42 +00:00
Kit Langton 32684e70e6 test(server): expect null body from HTTP API authorize() with no redirect (#26515) 2026-05-09 08:50:00 -04:00
opencode-agent[bot] b2baddcd37 chore: generate 2026-05-09 04:31:07 +00:00
Kit Langton dbd48d423d fix(server): match Hono wire format for authorize undefined and share errors (#26474) 2026-05-09 00:30:00 -04:00
Kit Langton e7cc8259b5 test(server): drop flaky account error-mapping test (#26475) 2026-05-09 00:29:31 -04:00
opencode-agent[bot] a9ccb0804f chore: generate 2026-05-09 04:22:25 +00:00
Kit Langton ebe6087e8f fix(server): return structured validation errors (#26457) 2026-05-09 00:21:19 -04:00
Kit Langton dc978cb889 fix(server): validate permission and question ids (#26456) 2026-05-09 00:20:28 -04:00
Kit Langton 8cbc43fbb0 fix(server): include auth challenge on typed 401 (#26455) 2026-05-09 00:15:20 -04:00
opencode-agent[bot] 82359c4b1b chore: generate 2026-05-09 04:06:08 +00:00
Kit Langton 7f3e51453b test(server): use Layer.mock for partial Account service stub (#26472) 2026-05-09 00:05:05 -04:00
Kit Langton cbdb2d9825 test(server): expand workspace routing fixed-id coverage (#26458) 2026-05-09 00:00:18 -04:00
Kit Langton 96bde05f6e docs(server): explain why HTTP API PTY handler has no early-frame buffer (#26464) 2026-05-08 23:58:33 -04:00
Kit Langton dcb8ed8eb0 test(server): cover workspace sync fence protocol (#26441) 2026-05-08 23:55:47 -04:00
Kit Langton aab82cc1a7 test(project): rescue non-Hono InstanceBootstrap boundary tests (#26453) 2026-05-08 23:53:42 -04:00
Kit Langton cd1d1e81a6 test(server): run httpapi exercise effect mode in test:httpapi (#26452) 2026-05-08 23:48:29 -04:00
opencode-agent[bot] cff441909a chore: generate 2026-05-09 03:45:35 +00:00
Kit Langton 3615d5aab1 fix(server): map Account failures to typed 500 instead of defect (#26448) 2026-05-08 23:44:28 -04:00
opencode-agent[bot] 11d9e82eaf chore: generate 2026-05-09 03:42:23 +00:00
Kit Langton eadda11ec9 refactor(server): use JSON response for OpenAPI doc route (#26447) 2026-05-08 23:41:20 -04:00
Kit Langton f73a56c223 fix(server): log instance disposal failures from HTTP API lifecycle (#26446) 2026-05-08 23:40:06 -04:00
opencode-agent[bot] c0acf5c43f chore: generate 2026-05-09 03:18:57 +00:00
Kit Langton 4d585464f3 fix(server): include Origin in CORS preflight Vary header (#26445) 2026-05-08 23:17:47 -04:00
Jose Vargas f0cb17a812 fix(tui): sort session picker by full updated timestamp (#24725)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-05-08 22:17:13 -05:00
Kit Langton 357a74714a fix(test): set OPENCODE_EXPERIMENTAL_WORKSPACES in fence header test (#26466) 2026-05-08 23:12:08 -04:00
Kit Langton ffea6c7974 feat(server): add HTTP API response compression (#26440) 2026-05-08 23:06:00 -04:00
Kit Langton 8e9550d90d fix(server): emit fixed workspace fence headers (#26443) 2026-05-08 22:45:54 -04:00
James Long 9b7b6cb30f feat(core): be smarter about generating a worktree name (#26368) 2026-05-08 22:45:44 -04:00
Kit Langton cc68afb2de test(server): lock fixed workspace routing context (#26454) 2026-05-08 22:35:32 -04:00
Kit Langton 11c33d52a5 test(server): cover REST API project skills (#26451) 2026-05-09 02:23:21 +00:00
opencode-agent[bot] 2a487ea990 chore: generate 2026-05-09 02:21:54 +00:00
Kit Langton 9c05d4e2fd fix(server): serve HttpApi OpenAPI document (#26438) 2026-05-08 22:20:50 -04:00
Kit Langton 0745162eab test(server): harden HttpApi exercise coverage (#26425) 2026-05-08 20:50:01 -04:00
Kit Langton 21d055be19 fix(workspace): claim detached sessions to source project (#26413) 2026-05-08 20:25:53 -04:00
Luke Parker d19f7bc77c fix(web): normalize shell output carriage returns (#26426) 2026-05-09 10:24:57 +10:00
opencode-agent[bot] 8694da9309 chore: update nix node_modules hashes 2026-05-08 23:46:19 +00:00
Sebastian a0fc27e424 flatten to keybind compatible config (#26421) 2026-05-09 01:29:13 +02:00
opencode-agent[bot] 35deef6175 chore: generate 2026-05-08 21:19:51 +00:00
vimtor 36f8b7e47d chore: reduce alerts false positives 2026-05-08 23:18:04 +02:00
opencode-agent[bot] f4337dff3c chore: update nix node_modules hashes 2026-05-08 21:11:02 +00:00
opencode-agent[bot] ba8c920639 chore: generate 2026-05-08 20:57:36 +00:00
Kit Langton 5bb7b23440 Add native LLM core foundation (#24712) 2026-05-08 16:56:20 -04:00
opencode-agent[bot] dc7d665e94 chore: generate 2026-05-08 20:22:22 +00:00
Shoubhit Dash 40d5ea1cf1 feat(core): add scout agent for repo research (#24149)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-05-08 20:20:08 +00:00
Chris Huber 6e47ae769e fix(cli): forward signals from npm shim (#26259)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-08 14:00:51 -05:00
opencode-agent[bot] 9ca4be62bd chore: generate 2026-05-08 18:51:43 +00:00
Dax fed221e0b0 fix(skill): allow missing descriptions (#26391) 2026-05-08 18:50:06 +00:00
Kit Langton 75308ea47d test(server): add HttpApi auth exercise mode (#26386) 2026-05-08 14:05:46 -04:00
Kit Langton daa3116f4b refactor(server): split HttpApi exercise harness (#26385) 2026-05-08 17:58:14 +00:00
opencode-agent[bot] 9e7f7bf8e4 chore: generate 2026-05-08 17:46:57 +00:00
Simon Klee 4d43d584fe cli/run: switch to global event stream (#26383) 2026-05-08 19:44:31 +02:00
Kit Langton 3052a79b32 refactor(server): clarify HttpApi route auth layers (#26372) 2026-05-08 13:06:00 -04:00
Rajvardhan Patil 13b3117ca9 fix(server): require auth for effect root routes (#26361)
Co-authored-by: Rajvardhan Patil <243567420+RajvardhanPatil07@users.noreply.github.com>
2026-05-08 16:39:11 +00:00
Aiden Cline 83bb216486 fix: ensure tools are always in same order (#26370) 2026-05-08 10:59:41 -05:00
opencode-agent[bot] fc46cef5fd chore: generate 2026-05-08 15:49:37 +00:00
Aiden Cline 799996db76 fix: adjust tui retry dialog logic to be more provider specific and error case specific (#26366) 2026-05-08 10:48:19 -05:00
opencode-agent[bot] df75bfe07c chore: generate 2026-05-08 15:10:32 +00:00
James Long c818c9dcb6 feat(core): allow external workspace creation (#26212) 2026-05-08 11:09:12 -04:00
Kit Langton c36ab3f935 fix(provider): align Gemini thinking controls (#26279) 2026-05-08 10:22:45 -04:00
vimtor e3c983c21f chore: reduce provider alerts query frequency 2026-05-08 14:00:24 +02:00
opencode-agent[bot] a196569f51 chore: generate 2026-05-08 11:56:55 +00:00
Sebastian 19da27e1cb internal which-key plugin, inactive by default (#26337) 2026-05-08 13:55:49 +02:00
Frank 4a737493ac Revert "zen: update tpm rate limit algo"
This reverts commit 6869186fc6.
2026-05-08 06:31:12 -04:00
opencode-agent[bot] ec301f6a2c chore: update nix node_modules hashes 2026-05-08 10:31:03 +00:00
opencode-agent[bot] ac7a885ae9 chore: generate 2026-05-08 10:18:22 +00:00
Simon Klee 7f2b5ee8c2 feat(opencode): add interactive split-footer mode to run (#23557) 2026-05-08 12:17:14 +02:00
vimtor 15784aa036 chore: reduce alerts threshold 2026-05-08 12:06:39 +02:00
opencode-agent[bot] edbc02855d chore: generate 2026-05-08 08:50:42 +00:00
Shoubhit Dash a43d3e0e1e feat(websearch): add parallel provider rollout (#26227) 2026-05-08 14:19:36 +05:30
Shoubhit Dash ae25278eda test(session): update go retry fixture (#26312) 2026-05-08 14:10:18 +05:30
Frank 6869186fc6 zen: update tpm rate limit algo 2026-05-08 03:52:59 -04:00
Frank f8c6742e54 zen: lift default rate limit 2026-05-08 03:52:59 -04:00
opencode-agent[bot] 014dbd34c4 chore: generate 2026-05-08 06:20:21 +00:00
Brendan Allan 21ae91b4f2 refactor(desktop): convert main process to Effect-TS (#26148) 2026-05-08 14:19:09 +08:00
Frank bb3f14119b tui: update go upsell copy 2026-05-08 01:51:33 -04:00
Luke Parker 6f165e23de perf(ui): defer tool status width measurement (#26282) 2026-05-08 15:36:28 +10:00
opencode-agent[bot] cef0c8ac84 chore: generate 2026-05-08 05:36:06 +00:00
Brendan Allan dd8bb44d1d refactor(desktop): use electron-log in shell-env and simplify env merging (#26284) 2026-05-08 13:34:53 +08:00
Frank 30868f52ea go: update rate limit error copy 2026-05-08 00:27:28 -04:00
opencode-agent[bot] 9c88235121 chore: generate 2026-05-08 04:18:54 +00:00
Aiden Cline 4e14f79511 fix: tweaks to transform logic for anthropic and bedrock (#26276) 2026-05-07 23:17:43 -05:00
Kit Langton e0396b809a fix(provider): align Anthropic Opus 4.5 efforts (#26275) 2026-05-08 00:06:07 -04:00
Kit Langton 319498e2fd fix(provider): constrain OpenAI deep research efforts (#26273) 2026-05-07 23:43:42 -04:00
Erik Demaine 2ba9aa2196 feat(desktop): working indicator on project sidebar (#26223) 2026-05-08 11:42:39 +08:00
opencode-agent[bot] 114eeb21dc chore: generate 2026-05-08 03:33:55 +00:00
Kit Langton 1cf8123bc6 fix(provider): align GPT-5 reasoning variants (#26268) 2026-05-07 23:32:47 -04:00
Frank db6a038829 sync 2026-05-07 22:55:50 -04:00
opencode-agent[bot] 6ff833a22b chore: generate 2026-05-08 02:40:47 +00:00
Sebastian 5c401673b2 improve go sub animation perf (#26251) 2026-05-08 04:39:42 +02:00
Kit Langton e8ce5df414 fix(tui): retain cleared prompt drafts (#26258) 2026-05-08 02:08:29 +00:00
Luke Parker b8799be3c8 feat(desktop): allow silent install and only user-wide scope (#26253) 2026-05-07 23:58:16 +00:00
opencode-agent[bot] 7ded0ec9e9 chore: generate 2026-05-07 21:54:31 +00:00
Aiden Cline f5d0371efe tui: go plan payg msg (#26248) 2026-05-07 16:53:24 -05:00
Frank 22e64cac67 sync: cleanup 2026-05-07 15:27:36 -04:00
vimtor 2a1305f231 chore: increase alerting threshold 2026-05-07 21:07:26 +02:00
opencode-agent[bot] e691e8f274 chore: update nix node_modules hashes 2026-05-07 18:55:24 +00:00
vimtor cc6dd5321c chore: improve variant parsing for zen 2026-05-07 20:38:14 +02:00
opencode-agent[bot] 2c17e3a4db chore: generate 2026-05-07 18:36:42 +00:00
Sebastian 98f5e6e713 introduce opentui keymap as sole key/cmd engine (#26053) 2026-05-07 20:35:31 +02:00
Frank 474e311f6f sync 2026-05-07 14:23:23 -04:00
Victor Navarro 626a488fb8 chore: track model variant in honeycomb (#26188) 2026-05-07 19:38:04 +02:00
Dax Raad a300a6cc7a rebase migrations properly 2026-05-07 12:38:29 -04:00
opencode fe594693a4 sync release versions for v1.14.41 2026-05-07 14:52:09 +00:00
opencode-agent[bot] 98e091796b chore: generate 2026-05-07 14:25:30 +00:00
James Long 3c4b4d5faf feat(core): copy file changes when warping (#26190) 2026-05-07 10:24:17 -04:00
vimtor b6ff1b18c7 chore: activate free tier requests query 2026-05-07 14:10:47 +02:00
vimtor 9c9bc09f52 chore: fix free tier query 2026-05-07 13:58:41 +02:00
vimtor d6e06c8950 chore: fix free tier query 2026-05-07 13:41:14 +02:00
opencode-agent[bot] 844fb71938 chore: generate 2026-05-07 11:37:34 +00:00
vimtor fbb7b5b1bf chore: add free tier usage alert 2026-05-07 13:36:18 +02:00
Shoubhit Dash 95280ebec9 fix(tui): restore custom provider in /connect (#26168) 2026-05-07 17:05:35 +05:30
YGoetschel fea9a0bd4c fix: guard undefined contents in diff renderer to fix share viewer SSR crash (#21763) 2026-05-07 18:55:40 +08:00
vimtor 30c4fcb1a5 chore: fix honeycomb query frequency 2026-05-07 11:10:36 +02:00
vimtor 193c169ca5 chore: improve provider down query 2026-05-07 11:00:43 +02:00
vimtor cee04f2924 chore: make provider down queries live 2026-05-07 10:56:37 +02:00
Brendan Allan 1219691c11 docs(desktop): update README from Tauri to Electron (#26146) 2026-05-07 16:31:37 +08:00
vimtor b2cc40f09c chore: first provider alert version 2026-05-07 10:30:29 +02:00
vimtor 0b2e65f16d chore: reactivate alerts 2026-05-07 10:15:52 +02:00
opencode-agent[bot] 1ea01fdad0 chore: update nix node_modules hashes 2026-05-07 08:08:37 +00:00
Victor Navarro f8aa4a3be0 chore: simplify honeycomb alerts (#26142) 2026-05-07 07:56:10 +00:00
Bence Ferdinandy 293bb422fa fix(format): restore stdout/stderr ignore for formatter processes (#26037)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-07 00:52:07 -05:00
Brendan Allan 54a78c9224 feat(desktop): move server to utilityProcess (#25962)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-05-07 13:48:56 +08:00
Jesse 9b30ee2db2 fix(desktop): add macOS settings menu entry (#26081)
Co-authored-by: jesse.mahnken <jesse.mahnken@tiefox.de>
2026-05-07 13:39:14 +08:00
carmit hershman ba1ec62caf docs: add opencode-jfrog-plugin to ecosystem list for JFrog integration (#26019) 2026-05-07 00:37:14 -05:00
Frank 72ec05d0be go: rate limit metadata 2026-05-07 00:32:33 -04:00
Frank 0b702704ae zen: nano not used for title gen 2026-05-06 23:01:16 -04:00
opencode-agent[bot] 3480cef52e chore: update nix node_modules hashes 2026-05-07 00:46:33 +00:00
opencode dcfe4b0d51 sync release versions for v1.14.40 2026-05-07 00:34:09 +00:00
Aiden Cline b2e3dc87ea feat: Update ACP support, modernize and fix misc issues (#25663) 2026-05-06 19:33:52 -05:00
André Cruz 233fc5b910 fix(provider): preserve assistant message content when reasoning blocks present (#21370)
Co-authored-by: Omer Koren <54630488+omer-koren@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-06 18:57:56 -05:00
Luke Parker 2dffdfff4a fix(server): apply cors before legacy auth (#26092) 2026-05-07 08:55:09 +10:00
Frank a4ab1408eb zen: update rate limiter 2026-05-06 15:32:08 -04:00
Frank e41843eaf7 sync 2026-05-06 15:20:38 -04:00
vimtor bf979413f9 chore: change alert type for honeycomb triggers 2026-05-06 19:27:25 +02:00
Frank 38b0cdc149 go: deprecate old models 2026-05-06 13:10:48 -04:00
Aiden Cline 344ccc647b ignore: vimtor to team members list 2026-05-06 11:45:11 -05:00
opencode-agent[bot] b9b854bf9f chore: generate 2026-05-06 15:13:34 +00:00
Dax d9c18381a6 feat(config): support well-known remote_config (#26054) 2026-05-06 15:12:23 +00:00
Kit Langton 63a175b50d fix(cli): avoid AppRuntime re-entry for network options (#26052) 2026-05-06 11:02:08 -04:00
Victor Navarro 889f979c0b chore: fix model alerts (#25990) 2026-05-06 16:57:34 +02:00
James Long 2abc4507b2 fix(tui): filter only connected workspaces in dialog; add warp synthetic message (#25915) 2026-05-06 10:25:42 -04:00
opencode-agent[bot] aa3c99a3c0 chore: update nix node_modules hashes 2026-05-06 08:50:56 +00:00
Shoubhit Dash d49d217e9d fix(tui): preserve selected model on refresh (#25993) 2026-05-06 14:14:31 +05:30
Brendan Allan 043a5c7c0d feat(desktop): implement clipboard write permission handling (#25998) 2026-05-06 08:40:45 +00:00
Brendan Allan 901d1171a6 chore(desktop): add @parcel/watcher platform packages to optionalDependencies (#25996) 2026-05-06 08:37:10 +00:00
Sergei Zharinov 518503b29b fix(ui): preserve SVG tags in DOMPurify config for KaTeX math rendering (#25866) 2026-05-06 16:06:37 +08:00
Guiii c235ba1bef docs: fix CLI attach section order (#25749) 2026-05-06 15:56:38 +08:00
Brendan Allan 754a1fb712 fix(desktop): suppress EPIPE errors in console transport (#25980) 2026-05-06 15:30:18 +08:00
Brendan Allan acca2e92dc fix(desktop): disable auto install on app quit (#25976) 2026-05-06 14:39:20 +08:00
Frank 9d178e0944 sync 2026-05-06 01:05:09 -04:00
Brendan Allan 7c8cf6ca5b fix(desktop): suppress browser API Sentry errors in prod (#25972) 2026-05-06 12:44:40 +08:00
Jack 89afac3d9d go: restore Kimi K2.6 limits (#25969) 2026-05-06 12:39:52 +08:00
opencode-agent[bot] b4c60e1b21 chore: generate 2026-05-06 04:32:49 +00:00
Brendan Allan efd8024430 feat(desktop): add OPENCODE_TEST_ONBOARDING env (#25968) 2026-05-06 04:30:20 +00:00
opencode-agent[bot] 2f05676e04 chore: generate 2026-05-06 04:17:05 +00:00
imduchuyyy 🐬 5013e8a8ec docs: update desktop app references from Tauri to Electron (#25965) 2026-05-06 12:15:59 +08:00
opencode-agent[bot] 6e7c9eb820 chore: generate 2026-05-06 01:35:13 +00:00
Kit Langton 8555de8189 Type session not-found errors (#25818) 2026-05-05 21:33:47 -04:00
Luke Parker f5c3d352a1 fix(app): require query functions for sync queries (#25939) 2026-05-06 10:09:32 +10:00
Luke Parker e117397d0f fix(server): restore web terminal CSP allowances (#25937) 2026-05-06 09:32:51 +10:00
opencode-agent[bot] 1fbc13a1b4 chore: generate 2026-05-05 23:08:37 +00:00
Aiden Cline 6409aceb1a fix: sanitize surrogates (#25934) 2026-05-05 18:07:23 -05:00
opencode-agent[bot] 837cc92586 chore: generate 2026-05-05 21:56:18 +00:00
Nathan Nguyen ca77b8f8e9 fix(cf-ai-gateway): route provider options through openaiCompatible key (#24432) (#25573) 2026-05-05 16:55:08 -05:00
opencode-agent[bot] 25547e9337 chore: generate 2026-05-05 20:41:12 +00:00
James Long 12f3d1f505 fix(core): use current workspace with /new; fix warping into local project (#25894) 2026-05-05 16:39:37 -04:00
James Long 8e182c7782 fix(core): better state handling of editor context (#25911) 2026-05-05 15:53:05 -04:00
OpeOginni 8a797ed9a1 fix(TUI): update agent create target path from "/agent" to "/agents" (#14427) 2026-05-05 14:51:40 -05:00
Aiden Cline 25ecf0af6b fix: retry server_is_overloaded errors (#25888) 2026-05-05 10:39:25 -05:00
Aiden Cline 576480b5dc fix: ensure mistral medium 3.5 has variants properly setup (#25887) 2026-05-05 10:34:20 -05:00
opencode-agent[bot] fdb4b7c4a5 chore: update nix node_modules hashes 2026-05-05 14:27:06 +00:00
Victor Navarro 726ae6f541 chore: configure alerting and monitoring (#25857) 2026-05-05 16:08:28 +02:00
opencode 773078e81f sync release versions for v1.14.39 2026-05-05 10:57:52 +00:00
Shoubhit Dash 811954880e fix(compaction): order compaction summary before retained tail (#25851) 2026-05-05 16:12:37 +05:30
Luke Parker 465c83cf82 fix(desktop): respect proxy environment (#25846) 2026-05-05 20:34:28 +10:00
Brendan Allan bb9b81aa37 fix(desktop): add error handling to store-get IPC handler (#25850) 2026-05-05 18:24:21 +08:00
opencode a20446fcc8 sync release versions for v1.14.38 2026-05-05 09:48:25 +00:00
opencode-agent[bot] 292c2aa458 chore: update nix node_modules hashes 2026-05-05 09:06:37 +00:00
Brendan Allan 52bb088753 fix(server): allow all connect-src origins in CSP for embedded UI (#25838) 2026-05-05 17:06:30 +08:00
Luke Parker b8f8f5d3a8 fix(desktop): trust system certificates (#25837) 2026-05-05 18:47:38 +10:00
opencode 8df3ef10fc sync release versions for v1.14.37 2026-05-05 07:26:49 +00:00
opencode-agent[bot] 301ab36159 chore: update nix node_modules hashes 2026-05-05 06:06:32 +00:00
Brendan Allan 03544a26cd fix(desktop): update main process (#25825) 2026-05-05 13:44:53 +08:00
Brendan Allan b4147c8d08 refactor(desktop): consolidate desktop-electron into desktop package (#25822) 2026-05-05 13:43:36 +08:00
opencode-agent[bot] 6f7d63e9ce chore: generate 2026-05-05 04:27:38 +00:00
Luke Parker 07f1c8c0ac fix(desktop): stabilize Windows titlebar zoom (#25813) 2026-05-05 04:26:35 +00:00
opencode-agent[bot] 2d0a757eb2 chore: generate 2026-05-05 02:37:07 +00:00
Kit Langton 75d141b574 fix(session): cancel subtask child sessions (#25798) 2026-05-04 22:36:06 -04:00
Dax 39c88f9afb Improve v2 session message rendering (#25634) 2026-05-05 02:35:21 +00:00
Dax Raad 0df2bb0f3b docs: restore v2 todo 2026-05-04 22:22:39 -04:00
Brendan Allan f6a3615f59 fix(console): remove Cloudflare cache config from download fetch (#25804) 2026-05-05 10:15:00 +08:00
James Long edd480f56b fix(tui): fix type error for calling workspace.warp (#25801) 2026-05-04 22:06:33 -04:00
Luke Parker 2740d398fa devex: Enable Electron MCP servers with DevTools debug port (#25795) 2026-05-05 11:37:18 +10:00
opencode-agent[bot] f33b17e8ac chore: generate 2026-05-05 01:29:49 +00:00
James Long 22a4a9df8b feat(core): session warping (#25768) 2026-05-04 21:28:38 -04:00
Brendan Allan 84afd2bef8 update: normalize download asset names to match new naming convention (#25796) 2026-05-05 09:19:13 +08:00
Luke Parker ca2411d332 Run UI unit tests in CI (#25792) 2026-05-05 11:05:53 +10:00
opencode 6b852774e1 sync release versions for v1.14.35 2026-05-05 01:01:47 +00:00
opencode-agent[bot] f14784d531 chore: generate 2026-05-05 00:35:18 +00:00
Luke Parker 6a5e329427 fix(vcs): preserve batched patch boundaries (#25787) 2026-05-05 00:34:06 +00:00
opencode 4b65b1e053 sync release versions for v1.14.34 2026-05-04 23:26:02 +00:00
Aiden Cline d431a0e4b4 fix: ensure effect server middleware properly parses errors (#25717) 2026-05-04 18:29:00 -04:00
Frank 5720883d5d sync 2026-05-04 15:51:29 -04:00
Kit Langton 007b57f078 test(agent): skip InstanceBootstrap in plugin-agent regression test (#25737) 2026-05-04 17:11:33 +00:00
Kit Langton fb07c2070c fix(server): provide fresh ConfigProvider per HttpApi listener (#25726) 2026-05-04 13:06:29 -04:00
Kit Langton 25dc6f09bc fix(worktree): fork workspace worktree boot (#25723) 2026-05-04 12:01:13 -04:00
Colby Gilbert b70e2700ef chore(docs): rename firmware provider to frogbot (#25453) 2026-05-04 10:27:03 -05:00
Frank 1aed6b1d8b sync 2026-05-04 11:16:28 -04:00
Aiden Cline c1f607d206 fix: ensure anthropic sdk properly resolves when using azure (#25721) 2026-05-04 09:58:21 -05:00
opencode-agent[bot] 2c819f290f chore: generate 2026-05-04 13:55:28 +00:00
Kit Langton 6e9f10ad3f test(server): regression reproducers for #25698 (#25714) 2026-05-04 09:54:19 -04:00
OpeOginni 1251a870cb fix(opencode): strip transfer-encoding in UI proxy and allow public manifest assets (#25698)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-05-04 09:43:03 -04:00
opencode-agent[bot] 67047fa766 chore: generate 2026-05-04 13:26:08 +00:00
Luke Parker a366128a93 fix(app): prevent terminal recovery loops (#25710) 2026-05-04 23:24:57 +10:00
opencode-agent[bot] 9f708e748a chore: generate 2026-05-04 02:57:18 +00:00
Kit Langton 7bc26dafae feat(server): pty websocket auth tickets (#25660) 2026-05-03 22:56:14 -04:00
Utkub24 ce89bcb8e2 fix: allow Codex Spark with Codex OAuth (#25640) 2026-05-03 17:58:16 -05:00
Kit Langton c2b1974ddd Effectify plugin agent regression test (#25646) 2026-05-03 22:07:10 +00:00
Kit Langton ca6150d6f0 fix(app): preserve auth token credentials (#25636) 2026-05-03 21:13:42 +00:00
Kit Langton 825ab2e38d refactor(cli): effectify provider commands (#25633) 2026-05-03 16:41:10 -04:00
opencode-agent[bot] 755cd561ec chore: generate 2026-05-03 19:45:26 +00:00
Kit Langton 6312c55d55 fix(server): serve embedded UI from bunfs (#25632) 2026-05-03 19:44:23 +00:00
opencode-agent[bot] a9dc0fae3d chore: generate 2026-05-03 18:46:50 +00:00
Dax 7749d8e85f Add v2 session failure events (#25628) 2026-05-03 14:45:48 -04:00
opencode-agent[bot] 28112fbd12 chore: generate 2026-05-03 18:24:37 +00:00
Kit Langton 387220f368 fix(server): support desktop PTY websockets with HttpApi (#25598) 2026-05-03 18:23:29 +00:00
OpeOginni adb7cb1037 fix(auth): add username option for basic auth in RunCommand (#25600)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-05-03 22:51:33 +05:30
opencode-agent[bot] c06af70ab0 chore: generate 2026-05-03 15:44:02 +00:00
Kit Langton 40dc2fa3c1 refactor(cli/providers): flatten — Effect-native handlers end-to-end (#25537) 2026-05-03 15:42:57 +00:00
Kit Langton df7dd06a0f refactor(cli/github+run): Stage 4 — drop AppRuntime.runPromise bridges (#25539) 2026-05-03 15:42:05 +00:00
opencode-agent[bot] 57d5c095d8 chore: generate 2026-05-03 15:22:38 +00:00
Kit Langton 13ac849db5 refactor(config+core): drop ConfigPaths.readFile, add AppFileSystem.readFileStringSafe, flatten TuiConfig.loadState (#25602) 2026-05-03 15:21:34 +00:00
Shoubhit Dash 8694c5b68f fix(auth): respect server username in clients (#25596) 2026-05-03 19:28:31 +05:30
Shoubhit Dash 0a7d02c87c feat: group changelog bugfixes (#25597) 2026-05-03 13:48:26 +00:00
Brendan Allan e77867ef05 ci: only build electron desktop (#19067) 2026-05-03 19:10:15 +05:30
opencode-agent[bot] fb224d8974 chore: generate 2026-05-03 13:21:15 +00:00
OpeOginni 101566131d fix(httpapi): add basic auth challenge for browser login
Adds a WWW-Authenticate challenge for unauthorized experimental HttpApi UI fallback responses so browsers open the Basic Auth prompt when a server password is configured.
2026-05-03 13:20:05 +00:00
opencode-agent[bot] 8433e8b433 chore: generate 2026-05-03 13:18:13 +00:00
Kit Langton 379600b5ab fix(sdk+cli): surface real errors instead of bare {} when server returns empty body (#25592) 2026-05-03 13:17:06 +00:00
Shoubhit Dash 7a503de606 fix(acp): pass server auth to internal client (#25591) 2026-05-03 18:42:24 +05:30
Kit Langton 2ad1eb56d3 feat(server): native HttpApi listener with Bun.serve + WS upgrade (#25547) 2026-05-03 09:09:45 -04:00
opencode-agent[bot] a43f767abb chore: generate 2026-05-03 13:07:30 +00:00
Kit Langton 0ee3b87289 feat(server): Server.openapi() backed by HttpApi spec, parity-checked against Hono output (#25545) 2026-05-03 09:06:23 -04:00
opencode-agent[bot] 3c9f3c5786 chore: generate 2026-05-03 12:59:40 +00:00
Kit Langton ca75ac6681 refactor(server): extract Hono-coupled utilities to backend-neutral modules (#25542) 2026-05-03 12:58:34 +00:00
Shoubhit Dash d1f597b5b5 fix(vcs): avoid unbounded diff memory usage (#25581) 2026-05-03 17:49:46 +05:30
Dax Raad 8299fb3e2b ignore: remove triage-unassigned.ts script
This script was used to batch-triage open GitHub issues without assignees.
Removing as the triage workflow has evolved and this batch approach is no longer needed.
2026-05-03 01:59:03 -04:00
Dax Raad 4f7f90133d ci: stop sending daily community recap notifications 2026-05-03 01:54:32 -04:00
Dax Raad b205e104f6 ci: remove vouch-based contributor filtering workflows
Removes the automated vouch system that filtered issues and PRs from non-vouched users. This simplifies the contribution process by removing the requirement for maintainers to manually vouch contributors before they can participate.
2026-05-03 01:54:32 -04:00
Dax Raad 252e2f98e6 ci: remove automatic labels from GitHub issue templates to allow manual triage 2026-05-03 01:54:32 -04:00
opencode-agent[bot] e2afdc1202 chore: generate 2026-05-03 05:22:22 +00:00
Dax Raad a08e4c9651 core: simplify triage workflow to focus on issue ownership
Switch triage agent to gpt-5.4-nano for faster issue assignment. Remove label
management from the triage tool so it only assigns owners based on team
ownership rules. This reduces noise in the issue tracker and ensures issues
get to the right team member immediately without unnecessary labels.

Update team structures to reflect current ownership and add script for
processing unassigned issues.
2026-05-03 01:21:17 -04:00
Dax Raad 7ccab8d272 core: update triage agent to use qwen3.6-plus model for improved response quality 2026-05-03 01:10:14 -04:00
Dax Raad fc57eb3b8e ci 2026-05-03 01:05:36 -04:00
Dax 9179bafd54 Add debug info command (#25550) 2026-05-03 05:04:52 +00:00
Kit Langton 2df8eda8a3 fix(cli): bridge Instance.current ALS in effectCmd handlers (regression from #25522) (#25546) 2026-05-03 04:24:33 +00:00
Kit Langton bd32252a7e refactor(cli/providers): Stage 4 — drop inline AppRuntime.runPromise calls (#25532) 2026-05-02 23:42:40 -04:00
Kit Langton 1717d636a2 refactor(cli/mcp+agent): Stage 4 — drop AppRuntime.runPromise bridges (#25530) 2026-05-02 23:40:59 -04:00
Aiden Cline 8e016b4703 fix: regression w/ auth login where stderr was ignored instead of inherited (#25529) 2026-05-02 22:36:02 -05:00
opencode-agent[bot] b89d48a2a4 chore: update nix node_modules hashes 2026-05-03 03:25:46 +00:00
Dax 33312bfd1b fix(session): encode v2 session responses (#25528) 2026-05-03 03:24:46 +00:00
opencode-agent[bot] 3f1ce36418 chore: generate 2026-05-03 03:23:47 +00:00
Kit Langton 0e13279545 refactor(cli): convert agent / providers / mcp to effectCmd (#25525) 2026-05-02 23:22:44 -04:00
Kit Langton 5f03d892c0 fix(httpapi): pagination Link header echoes request host (#25527) 2026-05-02 23:19:33 -04:00
Kit Langton bdabb102fe refactor(cli/stats): Stage 4 — fully Effect-native body (#25523) 2026-05-02 23:08:26 -04:00
Kit Langton a79a6594b0 chore: bump Effect beta (#25524) 2026-05-02 23:08:13 -04:00
opencode-agent[bot] a3d282a4c2 chore: generate 2026-05-03 03:04:40 +00:00
Kit Langton db24f89313 refactor(cli): convert mcp list, auth, auth list, logout to effectCmd (#25521) 2026-05-03 03:03:32 +00:00
opencode-agent[bot] 31cb0bfa4f chore: generate 2026-05-03 02:54:20 +00:00
Kit Langton af9fdf0a1c refactor(cli): convert github subcommands to effectCmd (#25522) 2026-05-02 22:53:20 -04:00
Youssef Achy be88cd5cb9 chore(opencode): exclude .map files from CLI binary build (#25500) 2026-05-02 22:52:32 -04:00
Luke Parker b4cc7d13b6 fix(desktop): limit zoom handler to zoom keys (#25516) 2026-05-03 02:44:52 +00:00
Aiden Cline 0ba013f8de chore: rm log statement (#25470) 2026-05-02 21:43:48 -05:00
Kit Langton 0956b15c52 refactor(acp): drop async from synchronous ACP.init (#25520) 2026-05-02 22:38:44 -04:00
opencode-agent[bot] 61150f6391 chore: generate 2026-05-03 02:36:41 +00:00
Kit Langton 7409dcc6bd refactor(cli): convert run command to effectCmd (#25519) 2026-05-02 22:35:20 -04:00
Kit Langton 2829943ad1 refactor(cli): convert debug wait, agent list, acp to effectCmd (#25518) 2026-05-02 22:31:20 -04:00
Kit Langton c4311dda31 feat(cli): allow effectCmd instance to be a function of args (#25517) 2026-05-03 02:27:41 +00:00
Kit Langton ad05a46d74 refactor(lifecycle): bootstrap as pure orchestration (#25510) 2026-05-02 22:26:54 -04:00
opencode-agent[bot] a6cadba814 chore: generate 2026-05-03 02:10:52 +00:00
Dax a3bc5d35b0 Refactor v2 session events as schemas (#24512) 2026-05-02 22:09:48 -04:00
Kit Langton 1409a0715c refactor(cli): convert web + account to effectCmd (instance: false) (#25512) 2026-05-02 21:59:35 -04:00
Kit Langton e98c291866 feat(cli): add instance: false opt-out to effectCmd (#25507) 2026-05-03 01:44:06 +00:00
Kit Langton e709dc34fb feat: default HTTP API backend to on for dev/beta channels 2026-05-02 20:43:23 -04:00
opencode-agent[bot] 9293cddb3a chore: generate 2026-05-03 00:43:16 +00:00
Kit Langton 68b3448b09 refactor(cli): drop redundant explicit Effect.ensuring(store.dispose) (#25503) 2026-05-02 20:42:09 -04:00
opencode-agent[bot] 80f2b13a55 chore: generate 2026-05-03 00:40:21 +00:00
Kit Langton 7d91d3b1ed Normalize instance lifecycle wiring (#25501) 2026-05-02 20:39:20 -04:00
opencode-agent[bot] a6464062b7 chore: generate 2026-05-03 00:32:24 +00:00
Kit Langton fd01dc9c89 test(httpapi): add route exerciser 2026-05-02 20:31:21 -04:00
opencode-agent[bot] d10fb88b66 chore: generate 2026-05-03 00:10:53 +00:00
Luke Parker 6b68b1020e docs: clarify LSP and formatter opt-in config (#25502) 2026-05-03 00:09:50 +00:00
Kit Langton 85bb9007ba feat(cli): auto-dispose InstanceContext after effectCmd handlers (#25481) 2026-05-02 19:54:13 -04:00
opencode-agent[bot] 9bef88e3b0 chore: generate 2026-05-02 23:34:40 +00:00
Kit Langton f98053c34e fix(instance): run bootstrap from instance store (#25475) 2026-05-02 19:33:38 -04:00
opencode-agent[bot] 36007aecf4 chore: generate 2026-05-02 23:23:53 +00:00
Kit Langton 4de44bbbef refactor(cli): convert debug subcommands to effectCmd (#25479) 2026-05-02 19:22:51 -04:00
opencode-agent[bot] 9d03d4419e chore: generate 2026-05-02 23:20:15 +00:00
Kit Langton 7ab1c1c74a refactor(cli): convert debug agent command to effectCmd (#25485) 2026-05-02 19:19:06 -04:00
Luke Parker 3f459819ba feat: refactor bash tool with shell-aware prompts for bash, pwsh+powershell, and cmd (#20039) 2026-05-03 09:18:48 +10:00
Kit Langton 1986a6e817 refactor(cli): convert session subcommands to effectCmd (#25483) 2026-05-02 18:15:28 -04:00
opencode-agent[bot] dfe1325fca chore: generate 2026-05-02 22:02:14 +00:00
Kit Langton c1686c6ddc refactor(cli): convert stats command to effectCmd (#25474) 2026-05-02 18:01:06 -04:00
Kit Langton 79b6ce5db4 refactor(cli): convert import command to effectCmd (#25467) 2026-05-02 21:56:32 +00:00
Kit Langton 0c816eb4b1 refactor(cli): convert plugin command to effectCmd (#25473) 2026-05-02 17:55:13 -04:00
Kit Langton e318e173d8 refactor(cli): convert export command to effectCmd (#25471) 2026-05-02 17:45:41 -04:00
opencode-agent[bot] b314781a1a chore: generate 2026-05-02 21:02:46 +00:00
Kit Langton 8396d6b016 refactor(cli): convert pr command to effectCmd (#25465) 2026-05-02 17:01:46 -04:00
opencode 43e20874f4 sync release versions for v1.14.33 2026-05-02 19:53:06 +00:00
opencode-agent[bot] c444e971b0 chore: generate 2026-05-02 19:27:24 +00:00
HyeokjaeLee 430bde9e9b fix(instance): restore InstanceBootstrap init parameter for non-Effec… (#25449)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-05-02 15:26:30 -04:00
Kit Langton 05b82a6a30 refactor(cli): drop ModelsDev Promise compat shim (#25460) 2026-05-02 15:11:01 -04:00
Kit Langton 6cd02c05c2 fix(telemetry): emit Tool.execute span for MCP and plugin tools (#25452) 2026-05-02 14:49:56 -04:00
opencode-agent[bot] b3a7513765 chore: generate 2026-05-02 18:00:11 +00:00
Kit Langton f8738c9002 feat(models): effectify ModelsDev as Service (#25434) 2026-05-02 13:59:08 -04:00
Aiden Cline b460db15d7 tweak: allow read tool to accept offset of 0 (#25431) 2026-05-02 11:12:07 -05:00
opencode-agent[bot] ff4779ca11 chore: generate 2026-05-02 16:09:04 +00:00
Kit Langton 146ff8ad85 feat(cli): add effectCmd wrapper + convert models command (#25429) 2026-05-02 12:08:04 -04:00
OpeOginni 0d0ec7dc46 docs: CLI docs for current commands and flags (#25399) 2026-05-02 11:07:22 -05:00
Jérôme Benoit 1ea6e6cd4b fix(nix): remove stale packages/shared filter (#24930) 2026-05-02 10:49:51 -05:00
opencode-agent[bot] 96061222d2 chore: generate 2026-05-02 15:45:21 +00:00
Kit Langton 3b9155714d Delete Instance.dispose and Instance.reload (#25427) 2026-05-02 11:44:16 -04:00
opencode 7371db5cc6 sync release versions for v1.14.32 2026-05-02 15:34:12 +00:00
Kit Langton b09b7d28b8 refactor(instance-store): consolidate dispose helpers (#25424) 2026-05-02 11:21:40 -04:00
opencode-agent[bot] 31ed4602e1 chore: update nix node_modules hashes 2026-05-02 15:16:12 +00:00
Sebastian 6a76346734 upgrade opentui to 0.2.2 (#25420) 2026-05-02 15:01:53 +00:00
Kit Langton 78b3000031 fix(tui): keep shell-mode prompt editable (#25419) 2026-05-02 10:56:27 -04:00
Kit Langton 4c4860fb24 Replace Instance.disposeAll/load with fixture helper (#25418) 2026-05-02 10:56:15 -04:00
Kit Langton 5242a1c6b4 fix(httpapi): install Instance ALS for adapter Promise bridge (#25417) 2026-05-02 10:49:44 -04:00
Kit Langton 075f876e6f fix(httpapi): re-land workspace create payload accepts missing extra (#25412) 2026-05-02 09:35:39 -04:00
opencode-agent[bot] a849812e9f chore: generate 2026-05-02 13:09:14 +00:00
Kit Langton d99dde6306 Migrate test inits from Promise to Effect (#25377) 2026-05-02 09:07:59 -04:00
opencode-agent[bot] becf57ee6a chore: generate 2026-05-02 04:03:59 +00:00
Kit Langton f33aec1139 Convert LoadInput.init to Effect + extract InstanceBootstrap as a Service (#25376) 2026-05-02 00:02:52 -04:00
Kit Langton 1571933096 Drop ALS fallbacks from containsPath and workspace routing (#25374) 2026-05-02 03:06:22 +00:00
Kit Langton 160928a9a9 Extract InstanceStore.provide helper (#25372) 2026-05-01 22:42:03 -04:00
opencode-agent[bot] d297c29f22 chore: generate 2026-05-02 02:19:48 +00:00
Kit Langton 0b498dd448 fix(httpapi): preserve OpenAPI parameter parity (#25291) 2026-05-01 22:18:52 -04:00
Kit Langton cec9c6122a Move instance loading into Effect service (#25277) 2026-05-01 22:18:06 -04:00
Zeke Sikelianos 51e310c9ce fix(read): prevent unsupported image formats from being sending to provider (#21114)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-01 18:14:22 -05:00
Aiden Cline 478156456e core: fix npm package detection to properly handle cached directories without installed packages (#25354) 2026-05-01 15:49:14 -05:00
opencode-agent[bot] 6252412d94 chore: generate 2026-05-01 20:03:10 +00:00
Dax Raad c2609cbf04 core: allow agents to access global tmp directory without permission prompts
Agents can now create temporary files in the global tmp directory without
triggering external_directory permission prompts. This enables agents to
freely use temporary storage for intermediate files during builds and
other operations.
2026-05-01 15:35:45 -04:00
github-actions[bot] 2115df57bf Update VOUCHED list
https://github.com/anomalyco/opencode/issues/25288#issuecomment-4360290197
2026-05-01 16:16:45 +00:00
Aiden Cline 29ec07700c fix: bedrock reasoning issue (#25303) 2026-05-01 11:15:17 -05:00
Frank bcae852d28 zen: remove hardcoded safety identifier 2026-05-01 11:12:28 -04:00
Kit Langton 16ddf5f559 fix(session): use finite archived timestamp schema (#25275) 2026-05-01 11:57:03 +00:00
Kit Langton 8c79c58c4d refactor: rename workspace adapters (#25272) 2026-05-01 07:36:52 -04:00
luo jiyin 97ed9ba624 fix: correct documentation typos (#25260) 2026-05-01 12:05:06 +02:00
Simon Klee a6b6395c8a fix(tui): gate logo subpixel rendering on truecolor support (#25265) 2026-05-01 11:33:44 +02:00
opencode 21f8027ef7 sync release versions for v1.14.31 2026-05-01 06:13:48 +00:00
Brendan Allan a5aa72bd7d fix: update provider store after loading providers in bootstrap (#25236) 2026-05-01 13:39:22 +08:00
Aiden Cline 563177c6ac fix: fix issue if tool returned image and empty text and it caused api errors (#25241) 2026-05-01 00:13:03 -05:00
opencode-agent[bot] 4eae8ec037 chore: generate 2026-05-01 04:21:51 +00:00
Aiden Cline 08895c396e docs: fix tui and keybinds documentation (#25233) 2026-04-30 23:20:54 -05:00
opencode-agent[bot] 4e451a4b0f chore: update nix node_modules hashes 2026-05-01 04:07:58 +00:00
Brendan Allan 163290bcf0 desktop: sentry integration (#15300)
Co-authored-by: Jay V <air@live.ca>
2026-05-01 11:56:31 +08:00
Aiden Cline c68c33d4fe docs: remove deprecated modes.mdx pages (#25227) 2026-04-30 22:49:32 -05:00
Dax Raad 3615d8e226 core: clarify that temp directory already exists for AI agents
The bash tool description now explicitly states that the temp directory has already been created and exists, preventing agents from unnecessarily trying to create it before use.
2026-04-30 23:48:48 -04:00
Dax 2283979199 Preapprove agent tmp directory access (#25226) 2026-04-30 23:47:15 -04:00
Aiden Cline 33f7f593ee fix: tui list jank issue (#25219) 2026-04-30 22:45:41 -05:00
opencode-agent[bot] 461e7345b3 chore: update nix node_modules hashes 2026-05-01 03:32:55 +00:00
opencode-agent[bot] 6bd91c68e8 chore: generate 2026-05-01 03:22:36 +00:00
Dax Raad ff55a40749 core: remove @effect/language-service plugin and optimize hot path type performance
- Removed @effect/language-service from both packages/core and packages/opencode tsconfig files and dependencies

- Wrapped mergeDeep calls in config loading and LLM streaming to avoid expensive remeda conditional merge type instantiations in hot paths

- Narrowed Drizzle migrate() overload signature to avoid expensive variance checks during database initialization

These changes reduce TypeScript type-checking overhead and improve startup and runtime performance for config loading, LLM streaming, and database migrations.
2026-04-30 23:21:05 -04:00
opencode-agent[bot] 8b56d77ea1 chore: generate 2026-05-01 03:02:15 +00:00
Kit Langton dd3aa96730 test(httpapi): cover more safe GET parity (#25217) 2026-04-30 23:01:11 -04:00
Kit Langton 8b56d1712f refactor(session): pass project to list (#25215) 2026-04-30 23:00:59 -04:00
Kit Langton 3c24d22d42 fix(httpapi): omit absent optional response fields (#25214) 2026-05-01 02:38:32 +00:00
Kit Langton 4c70ea28d2 fix(tui): scope Zed editor context to containing workspaces (#25211) 2026-04-30 22:33:39 -04:00
Kit Langton 5ba68a28c0 refactor(httpapi): scope async prompt fiber (#25213) 2026-04-30 22:33:02 -04:00
opencode-agent[bot] bce4def2db chore: generate 2026-05-01 02:26:56 +00:00
Kit Langton 3544ea0244 refactor(httpapi): drop session prompt bridge (#25210) 2026-04-30 22:25:52 -04:00
opencode-agent[bot] 6434918794 chore: generate 2026-05-01 01:46:55 +00:00
Kit Langton 5984d917dc refactor(session): yield instance context in system prompt (#25207) 2026-04-30 21:45:48 -04:00
Kit Langton c2a97a7a6c refactor(file): yield instance context in watcher (#25205) 2026-04-30 21:45:21 -04:00
Kit Langton a083c88e87 refactor(sync): capture instance context for publish (#25206) 2026-04-30 21:45:02 -04:00
Kit Langton ce3b0988c4 refactor(project): yield instance context in bootstrap (#25204) 2026-04-30 21:44:52 -04:00
Kit Langton e8a194a2bb test(effect): stabilize runner active shell check (#25203) 2026-04-30 21:36:19 -04:00
Kit Langton 8aa8798e07 refactor(session): yield instance context in llm (#25200) 2026-04-30 21:29:28 -04:00
opencode-agent[bot] 6d4629b566 chore: generate 2026-05-01 01:28:37 +00:00
OpeOginni a9d399699e fix(desktop): Prevent Model response Interruption when opening settings dialog (#25114) 2026-05-01 01:27:38 +00:00
Kit Langton bc805b3001 Pass CORS options to HttpApi backend (#25201) 2026-04-30 21:26:32 -04:00
Kit Langton 668d77bb4e refactor(tool): yield InstanceState context (#25199) 2026-04-30 21:01:06 -04:00
Kit Langton 5c2e06f353 Document HttpApi route patterns (#25188) 2026-04-30 20:48:14 -04:00
Kit Langton a499fe2b17 refactor(tool/read): yield InstanceState.context instead of reading ALS (#25183) 2026-04-30 20:33:04 -04:00
Kit Langton 451650b584 refactor(httpapi): preserve typed errors in session prompt handlers (#25181) 2026-04-30 20:04:00 -04:00
opencode-agent[bot] 1b76bec0e2 chore: generate 2026-05-01 00:03:55 +00:00
Kit Langton 96f4da1e1d Serve instance events through HttpApiBuilder (#25182) 2026-04-30 20:02:46 -04:00
opencode-agent[bot] 96a0dd6b04 chore: generate 2026-04-30 23:37:58 +00:00
Kit Langton 2dd1f2d453 Avoid request-time HttpApi layer provisioning (#25179) 2026-04-30 19:36:57 -04:00
opencode-agent[bot] 510f01674a chore: generate 2026-04-30 23:29:58 +00:00
Kit Langton e3134a2a99 refactor(session): align prompt input types with their schemas (#25178) 2026-04-30 19:28:46 -04:00
opencode-agent[bot] 8805104b8d chore: generate 2026-04-30 23:25:10 +00:00
Kit Langton fc155e9fc5 Build HttpApi UI route from services (#25177) 2026-04-30 19:24:10 -04:00
opencode-agent[bot] 3aaac0098e chore: generate 2026-04-30 23:06:57 +00:00
Sewer. a12333310f fix(provider): split providerOptions key on dot for openai-compatible, openai, and anthropic providers (#25145) 2026-04-30 18:05:56 -05:00
opencode-agent[bot] 247284b9af chore: generate 2026-04-30 22:51:09 +00:00
Kit Langton e0305e47f3 Protect HttpApi web UI fallback with auth (#25169) 2026-04-30 18:49:54 -04:00
Kit Langton 76a0f0f619 docs(httpapi): update migration spec to current state (#25173) 2026-04-30 18:41:27 -04:00
Aiden Cline 560baae15d fix: ensure user config takes precendence over plugin hooks for model resolution (#25167) 2026-04-30 17:15:56 -05:00
Kit Langton 5518ecaefe Fix HttpApi web UI fallback (#25163) 2026-04-30 17:43:18 -04:00
opencode-agent[bot] 924ba97055 chore: generate 2026-04-30 21:04:24 +00:00
Aiden Cline b80f52f8ad tweak: adjust codex plugin to use the models hook (#25157) 2026-04-30 16:03:07 -05:00
Kit Langton feb275d08b Remove covered workspace websocket todo (#25161) 2026-04-30 20:58:08 +00:00
Kit Langton fbcbd24063 Add SyncEvent service (#25158) 2026-04-30 16:45:26 -04:00
Kit Langton 3250b814ce Fix HttpApi raw route authorization (#25154) 2026-04-30 19:55:20 +00:00
Kit Langton 0e9d9282c6 Refactor workspace service boundaries (#25152) 2026-04-30 15:34:37 -04:00
Kit Langton b315a70773 test: use Effect test helper for agent colors (#25051) 2026-04-30 15:14:25 -04:00
Kit Langton cedff6fb89 Isolate TUI thread cwd resolution test (#25147) 2026-04-30 15:10:30 -04:00
Kit Langton 87cd9446d8 test: use testEffect for plugin triggers (#25053) 2026-04-30 14:24:53 -04:00
Kit Langton f4ce240a2e Use PTY service directly in HTTP routes (#25138) 2026-04-30 14:24:43 -04:00
Kit Langton 320527a3e4 Support multiple Zed selections in TUI context (#25140) 2026-04-30 14:15:50 -04:00
Kit Langton 19271fca2d Use workspace service in HTTP routes (#25139) 2026-04-30 13:57:25 -04:00
Kit Langton feeebbe7d4 Preserve workspace context in session HTTP routes (#25136) 2026-04-30 13:53:26 -04:00
Kit Langton f384675c01 test: use Effect test helper for run-service (#25048) 2026-04-30 17:05:10 +00:00
Kit Langton ec3ab4a00c test: use testEffect for retry policy (#25050) 2026-04-30 12:55:33 -04:00
Kit Langton e4ac936eb9 test: use testEffect for plugin workspace adaptor (#25052) 2026-04-30 12:54:53 -04:00
Kit Langton 79e23b7eb9 test: use testEffect for instance state (#25115) 2026-04-30 12:53:13 -04:00
Kit Langton 92e80b4660 test: use Effect test helper for app runtime logger (#25049) 2026-04-30 12:52:29 -04:00
Kit Langton ce63ca4d7a test: use testEffect for system prompt test (#25047) 2026-04-30 12:51:32 -04:00
Kit Langton fef7981942 test: use Effect runtime in runner deadlock case (#25045) 2026-04-30 12:45:30 -04:00
Aiden Cline ffe0314c47 fix: ensure disabling OPENCODE_DISABLE_CLAUDE_CODE_SKILLS doesnt disable external skills too (#25123) 2026-04-30 11:15:53 -05:00
opencode-agent[bot] 375444a149 chore: update nix node_modules hashes 2026-04-30 15:48:28 +00:00
Kit Langton 65c15afe9f test: use testEffect for instruction tests (#25046) 2026-04-30 11:48:13 -04:00
opencode-agent[bot] 8f57a2a462 chore: generate 2026-04-30 15:46:04 +00:00
James Long 53e9cac383 refactor(core): convert control-plane workspace to Effect (#25018) 2026-04-30 11:44:58 -04:00
Sebastian fe0c182747 upgrade opentui to 0.2.0 (#24810) 2026-04-30 17:33:54 +02:00
opencode-agent[bot] 29b1060c67 chore: generate 2026-04-30 15:08:03 +00:00
Kit Langton dddfcbf0d8 test: port instance HttpApi path/vcs read coverage to Effect 2026-04-30 11:07:00 -04:00
OpeOginni 62e1335388 fix(opencode): allow oc://renderer origin in cors middleware (#25099) 2026-04-30 12:11:42 +00:00
Brendan Allan 908e28175f fix: invert *_ready getters to fix server status indicator (#25077) 2026-04-30 15:10:39 +08:00
Brendan Allan 3398fd7719 feat(httpapi): add CORS middleware to instance routes (#25074) 2026-04-30 07:06:17 +00:00
Luke Parker 9bddf7f3ef fix app crash restoring messages without model (#25062) 2026-04-30 14:44:53 +10:00
Dax Raad 8ba374fefa ci: enable sourcemaps for beta releases
Generate linked sourcemaps when building beta releases to help users
debug issues with readable stack traces.
2026-04-30 00:42:22 -04:00
Aiden Cline 3ef0aaf768 tweak: make azure onboarding ux a bit better (#25057) 2026-04-29 23:35:59 -05:00
Tommy D. Rossi d7701dbfb6 fix(opencode): preserve external_dir and deny parent permissions in task child sessions (#23290) 2026-04-29 22:06:29 -05:00
Kit Langton c49bf0b402 test: cover ConfigService helper (#25042) 2026-04-30 02:41:59 +00:00
Kit Langton cee9610d26 refactor: use Effect config for HttpApi authorization (#25035) 2026-04-29 22:22:32 -04:00
Kit Langton 38adc13295 test: cover HttpApi authorization middleware (#25033) 2026-04-29 21:34:52 -04:00
Kit Langton 4fe14abb8c test: cover HttpApi instance context middleware (#25032) 2026-04-29 21:24:45 -04:00
Kit Langton 9052e8a1ba test: cover HttpApi workspace routing middleware (#25027) 2026-04-29 21:08:03 -04:00
github-actions[bot] de78dedceb Update VOUCHED list
https://github.com/anomalyco/opencode/issues/23890#issuecomment-4348703527
2026-04-30 00:58:42 +00:00
Kit Langton 6f508d574e test: deflake runner cancel test (#25021) 2026-04-29 20:19:52 -04:00
Kit Langton 61dfae31e7 test: cover HttpApi websocket proxy (#25017) 2026-04-29 19:37:50 -04:00
opencode ac6aa43e3b sync release versions for v1.14.30 2026-04-29 23:33:39 +00:00
Luke Parker ea89925042 fix: handle invalid mcp urls (#25019) 2026-04-30 09:32:26 +10:00
opencode-agent[bot] 12cbfe5b64 chore: generate 2026-04-29 22:40:26 +00:00
Luke Parker d7b7be1909 fix(desktop): Path mismatches cause sessions missing + strong ID + existing data fix (#25013) 2026-04-29 22:39:19 +00:00
Aiden Cline a740d2c667 fix: adjust azure defaults to closer match openai to prevent Item .. of type 'reasoning' was provided without its required following item (#25007) 2026-04-29 16:49:28 -05:00
Aiden Cline 588261076a fix: make deepseek string check a bit looser (#25012) 2026-04-29 16:47:45 -05:00
Ruben De Smet 639e27c3ce feat: add Mistral Medium 3.5 with reasoning support (#24996) 2026-04-29 16:26:24 -05:00
opencode-agent[bot] 1124ae17b4 chore: generate 2026-04-29 20:52:19 +00:00
Kit Langton 9db5890ce5 Refactor HttpApi workspace routing and proxy boundaries (#25006) 2026-04-29 16:50:54 -04:00
James Long 293877cb7e fix(core): reconnect editor context for session directory (#24984) 2026-04-29 15:11:44 -04:00
opencode-agent[bot] c480006554 chore: generate 2026-04-29 18:17:10 +00:00
Aiden Cline 6aa8e894b1 chore: rm broken codesearch tool (#24992) 2026-04-29 13:15:44 -05:00
Aiden Cline 00bb9836a6 tweak: adjust order of system prompt instructions: Global, Project, Skills (#24974) 2026-04-29 10:55:53 -05:00
github-actions[bot] 71f9189607 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/24964#issuecomment-4345349260
2026-04-29 15:54:34 +00:00
opencode-agent[bot] a3f7ea2555 chore: generate 2026-04-29 13:47:48 +00:00
Kit Langton d3df8e1180 test(httpapi): clean up SDK parity tests 2026-04-29 09:46:17 -04:00
opencode-agent[bot] df147b65fd chore: generate 2026-04-29 13:36:05 +00:00
Kit Langton 6015084fa2 Prepare Effect HttpApi backend parity (#24853) 2026-04-29 09:34:50 -04:00
Brendan Allan 65ba1f6c13 fix(download): update beta asset names from electron to desktop (#24908) 2026-04-29 14:50:06 +08:00
Brendan Allan d37e5af57d Disable Windows update code signature verification (#24905) 2026-04-29 14:40:37 +08:00
spark4862 d71b827d8c fix(session): remap compaction tail_start_id when forking (#24898)
Co-authored-by: spark4862 <spark4862@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-04-29 00:23:56 -05:00
Aiden Cline 504ca3d3d8 feat: make it easier to toggle on/off paste summary in the tui (#24869) 2026-04-28 23:55:14 -05:00
Mike a8c74c04de docs: add Atomic Chat provider section (#23069) 2026-04-28 23:10:41 -05:00
Brendan Allan f6b4f54216 refactor(app): convert getProjectAvatarSource to early returns (#24896) 2026-04-29 12:00:13 +08:00
Dax Raad fc0e3c65b3 ignore 2026-04-28 23:07:46 -04:00
Dax Raad 23b8ed788e ignore 2026-04-28 23:03:48 -04:00
Dax Raad 3bd890f46b ignore: ideas 2026-04-28 23:00:07 -04:00
Luke Parker 9fbeafb63e fix: clear timeout after promise rejection (#24864) 2026-04-28 23:37:12 +00:00
opencode-agent[bot] 91bd295209 chore: generate 2026-04-28 23:11:59 +00:00
Luke Parker d4bf70be06 fix(bash): memory leak - release parsed syntax trees (#24861)
Co-authored-by: jiwenshang <jiwenshang@xiaohongshu.com>
2026-04-28 23:10:48 +00:00
opencode-agent[bot] ae8904c4ff chore: generate 2026-04-28 20:50:56 +00:00
James Long 9209c04370 feat(core): filter sessions by path and add setting to disable (#24849) 2026-04-28 16:49:13 -04:00
Kit Langton 379e7f3f20 test(httpapi): cover sdk effect routes (#24836) 2026-04-28 16:34:06 -04:00
opencode-agent[bot] 366d11e1f8 chore: generate 2026-04-28 20:33:13 +00:00
Kit Langton 58836e75f0 fix(httpapi): wire global and control handlers (#24835) 2026-04-28 16:31:45 -04:00
Aiden Cline 0acac216ae fix(copilot): ensure available variants sync from api (#24734)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-28 14:58:51 -05:00
opencode 276d162044 sync release versions for v1.14.29 2026-04-28 18:34:48 +00:00
opencode-agent[bot] 1b0ed983c5 chore: generate 2026-04-28 18:25:23 +00:00
Kit Langton 2e8d690ab1 fix(httpapi): finish sdk openapi parity (#24827) 2026-04-28 14:24:10 -04:00
Kit Langton 1ff8d289af fix(tui): handle Zed selection byte offsets (#24825) 2026-04-28 14:09:39 -04:00
Dax d54ffbda1c tui: ignore invalid custom themes to prevent startup crashes (#24645) 2026-04-28 13:58:55 -04:00
Kit Langton c00058ed7a fix(httpapi): align request body openapi shape (#24811) 2026-04-28 12:55:37 -04:00
James Long 2c2fc3499b feat(core): store relative path for sessions (#24704) 2026-04-28 11:51:24 -04:00
Kit Langton ea3c6c3481 fix(httpapi): document instance query parameters (#24809) 2026-04-28 11:10:00 -04:00
opencode-agent[bot] 9b68b7195a chore: generate 2026-04-28 15:05:37 +00:00
Kit Langton 7739cc53b4 refactor(httpapi): fork server startup by flag (#24799) 2026-04-28 11:02:35 -04:00
David Hill 3fa78a8b01 docs: bump GitHub stars count to 150K (#24792) 2026-04-28 09:24:46 -04:00
Kit Langton e57d0c2fee fix(httpapi): document tui bad request responses
Document legacy 400 bad-request responses on TUI Effect HttpApi payload-validation endpoints and cover them with OpenAPI parity tests.
2026-04-28 09:23:54 -04:00
Kit Langton 2a4f2bf527 fix(httpapi): align sync seq validation
Reject negative and fractional sync sequence values in Effect HttpApi schemas so replay/history validation matches the legacy Hono routes.
2026-04-28 09:22:49 -04:00
Brendan Allan aa07f38b07 fix(app): preserve per-workspace icon override from localStorage (#24738)
Co-authored-by: Andrew Avsenin <snatvb@ya.ru>
2026-04-28 19:37:40 +08:00
Brendan Allan 9d1f17d836 fix(ui): remove redundant flex overrides in tool components (#24749) 2026-04-28 15:51:18 +08:00
github-actions[bot] b420952e59 Update VOUCHED list
https://github.com/anomalyco/opencode/issues/24732#issuecomment-4332442679
2026-04-28 04:53:58 +00:00
opencode-agent[bot] bb9e445257 chore: generate 2026-04-28 04:45:24 +00:00
Aiden Cline 528fb1d404 fix: sanitize tools for moonshot (#24730) 2026-04-27 23:44:21 -05:00
Brendan Allan c8d9f7aa89 refactor(app): load sync state through TanStack Query (#23792) 2026-04-28 04:24:30 +00:00
opencode-agent[bot] cd7ec93cdf chore: generate 2026-04-28 02:34:43 +00:00
Kit Langton 796b652d2b fix(httpapi): preserve mcp oauth error parity (#24706) 2026-04-27 22:33:21 -04:00
Kit Langton 4d74849c1a fix(tui): keep Zed context polling responsive (#24711) 2026-04-27 22:24:04 -04:00
opencode-agent[bot] 937a7c48a5 chore: update nix node_modules hashes 2026-04-28 02:22:33 +00:00
Kit Langton 704eb00de4 chore: bump effect beta (#24705) 2026-04-27 21:58:11 -04:00
opencode-agent[bot] bad4599bf9 chore: generate 2026-04-28 01:50:04 +00:00
Kit Langton 892fd85ba7 fix(httpapi): preserve provider oauth authorize parity (#24703) 2026-04-27 21:48:50 -04:00
opencode-agent[bot] 0eaa47d857 chore: generate 2026-04-28 00:54:28 +00:00
Kit Langton faca24d487 fix(httpapi): align session boolean query parsing (#24693) 2026-04-27 20:53:27 -04:00
Kit Langton c103202ad5 test(httpapi): cover session json parity (#24682) 2026-04-27 19:48:57 -04:00
Kit Langton ce78a4265d fix(session): remove compaction summary dividers (#24677) 2026-04-27 18:15:11 -04:00
Kit Langton c4a2353ac3 fix(session): omit undefined optional fields (#24676) 2026-04-27 17:50:09 -04:00
Kit Langton 576efed196 fix(httpapi): preserve optional session fields (#24671) 2026-04-27 21:38:28 +00:00
1627 changed files with 127310 additions and 64209 deletions
-1
View File
@@ -1,6 +1,5 @@
name: Bug report
description: Report an issue that should be fixed
labels: ["bug"]
body:
- type: textarea
id: description
@@ -1,6 +1,5 @@
name: 🚀 Feature Request
description: Suggest an idea, feature, or enhancement
labels: [discussion]
title: "[FEATURE]:"
body:
-1
View File
@@ -1,6 +1,5 @@
name: Question
description: Ask a question
labels: ["question"]
body:
- type: textarea
id: question
+1 -1
View File
@@ -11,6 +11,6 @@ MrMushrooooom
nexxeln
R44VC0RP
rekram1-node
RhysSullivan
thdxr
simonklee
vimtor
-37
View File
@@ -1,37 +0,0 @@
# Vouched contributors for this project.
#
# See https://github.com/mitchellh/vouch for details.
#
# Syntax:
# - One handle per line (without @), sorted alphabetically.
# - Optional platform prefix: platform:username (e.g., github:user).
# - Denounce with minus prefix: -username or -platform:username.
# - Optional details after a space following the handle.
adamdotdevin
-agusbasari29 AI PR slop
ariane-emory
-atharvau AI review spamming literally every PR
-borealbytes
-carycooper777
-danieljoshuanazareth
-danieljoshuanazareth
-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person
edemaine
-florianleibert
fwang
iamdavidhill
jayair
kitlangton
kommander
-opencode2026
-opencodeengineer bot that spams issues
r44vc0rp
rekram1-node
-ricardo-m-l
-robinmordasiewicz
rubdos
shantur
simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team
thdxr
-toastythebot
-170
View File
@@ -1,170 +0,0 @@
name: daily-issues-recap
on:
schedule:
# Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving)
- cron: "0 23 * * *"
workflow_dispatch: # Allow manual trigger for testing
jobs:
daily-recap:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Generate daily issues recap
id: recap
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: |
{
"bash": {
"*": "deny",
"gh issue*": "allow",
"gh search*": "allow"
},
"webfetch": "deny",
"edit": "deny",
"write": "deny"
}
run: |
# Get today's date range
TODAY=$(date -u +%Y-%m-%d)
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the OpenCode repository.
TODAY'S DATE: ${TODAY}
STEP 1: Gather today's issues
Search for all OPEN issues created today (${TODAY}) using:
gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500
IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these:
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
This recap is specifically for COMMUNITY (external) issues only.
STEP 2: Analyze and categorize
For each issue created today, categorize it:
**Severity Assessment:**
- CRITICAL: Crashes, data loss, security issues, blocks major functionality
- HIGH: Significant bugs affecting many users, important features broken
- MEDIUM: Bugs with workarounds, minor features broken
- LOW: Minor issues, cosmetic, nice-to-haves
**Activity Assessment:**
- Note issues with high comment counts or engagement
- Note issues from repeat reporters (check if author has filed before)
STEP 3: Cross-reference with existing issues
For issues that seem like feature requests or recurring bugs:
- Search for similar older issues to identify patterns
- Note if this is a frequently requested feature
- Identify any issues that are duplicates of long-standing requests
STEP 4: Generate the recap
Create a structured recap with these sections:
===DISCORD_START===
**Daily Issues Recap - ${TODAY}**
**Summary Stats**
- Total issues opened today: [count]
- By category: [bugs/features/questions]
**Critical/High Priority Issues**
[List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers]
**Most Active/Discussed**
[Issues with significant engagement or from active community members]
**Trending Topics**
[Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature']
**Duplicates & Related**
[Issues that relate to existing open issues]
===DISCORD_END===
STEP 5: Format for Discord
Format the recap as a Discord-compatible message:
- Use Discord markdown (**, __, etc.)
- BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report
- Use hyperlinked issue numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/issues/1234>)
- Group related issues on single lines where possible
- Add emoji sparingly for critical items only
- HARD LIMIT: Keep under 1800 characters total
- Skip sections that have nothing notable (e.g., if no critical issues, omit that section)
- Prioritize signal over completeness - only surface what matters
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt
# Extract only the Discord message between markers
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt
echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT
- name: Post to Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
run: |
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
cat /tmp/recap.txt
exit 0
fi
# Read the recap
RECAP_RAW=$(cat /tmp/recap.txt)
RECAP_LENGTH=${#RECAP_RAW}
echo "Recap length: ${RECAP_LENGTH} chars"
# Function to post a message to Discord
post_to_discord() {
local msg="$1"
local content=$(echo "$msg" | jq -Rs '.')
curl -s -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": ${content}}" \
"$DISCORD_WEBHOOK_URL"
sleep 1
}
# If under limit, send as single message
if [ "$RECAP_LENGTH" -le 1950 ]; then
post_to_discord "$RECAP_RAW"
else
echo "Splitting into multiple messages..."
remaining="$RECAP_RAW"
while [ ${#remaining} -gt 0 ]; do
if [ ${#remaining} -le 1950 ]; then
post_to_discord "$remaining"
break
else
chunk="${remaining:0:1900}"
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
chunk="${remaining:0:$last_newline}"
remaining="${remaining:$((last_newline+1))}"
else
chunk="${remaining:0:1900}"
remaining="${remaining:1900}"
fi
post_to_discord "$chunk"
fi
done
fi
echo "Posted daily recap to Discord"
-173
View File
@@ -1,173 +0,0 @@
name: daily-pr-recap
on:
schedule:
# Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving)
- cron: "0 22 * * *"
workflow_dispatch: # Allow manual trigger for testing
jobs:
pr-recap:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Generate daily PR recap
id: recap
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: |
{
"bash": {
"*": "deny",
"gh pr*": "allow",
"gh search*": "allow"
},
"webfetch": "deny",
"edit": "deny",
"write": "deny"
}
run: |
TODAY=$(date -u +%Y-%m-%d)
opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the OpenCode repository.
TODAY'S DATE: ${TODAY}
STEP 1: Gather PR data
Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}):
# Open PRs created today
gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
# Open PRs with activity today (updated today)
gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100
IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these:
adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr
This recap is specifically for COMMUNITY (external) contributions only.
STEP 2: For high-activity PRs, check comment counts
For promising PRs, run:
gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length'
IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts:
- copilot-pull-request-reviewer
- github-actions
STEP 3: Identify what matters (ONLY from today's PRs)
**Bug Fixes From Today:**
- PRs with 'fix' or 'bug' in title created/updated today
- Small bug fixes (< 100 lines changed) that are easy to review
- Bug fixes from community contributors
**High Activity Today:**
- PRs with significant human comments today (excluding bots listed above)
- PRs with back-and-forth discussion today
**Quick Wins:**
- Small PRs (< 50 lines) that are approved or nearly approved
- PRs that just need a final review
STEP 4: Generate the recap
Create a structured recap:
===DISCORD_START===
**Daily PR Recap - ${TODAY}**
**New PRs Today**
[PRs opened today - group by type: bug fixes, features, etc.]
**Active PRs Today**
[PRs with activity/updates today - significant discussion]
**Quick Wins**
[Small PRs ready to merge]
===DISCORD_END===
STEP 5: Format for Discord
- Use Discord markdown (**, __, etc.)
- BE EXTREMELY CONCISE - surface what we might miss
- Use hyperlinked PR numbers with suppressed embeds: [#1234](<https://github.com/${{ github.repository }}/pull/1234>)
- Include PR author: [#1234](<url>) (@author)
- For bug fixes, add brief description of what it fixes
- Show line count for quick wins: \"(+15/-3 lines)\"
- HARD LIMIT: Keep under 1800 characters total
- Skip empty sections
- Focus on PRs that need human eyes
OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt
# Extract only the Discord message between markers
sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt
echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT
- name: Post to Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }}
run: |
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post"
cat /tmp/pr_recap.txt
exit 0
fi
# Read the recap
RECAP_RAW=$(cat /tmp/pr_recap.txt)
RECAP_LENGTH=${#RECAP_RAW}
echo "Recap length: ${RECAP_LENGTH} chars"
# Function to post a message to Discord
post_to_discord() {
local msg="$1"
local content=$(echo "$msg" | jq -Rs '.')
curl -s -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": ${content}}" \
"$DISCORD_WEBHOOK_URL"
sleep 1
}
# If under limit, send as single message
if [ "$RECAP_LENGTH" -le 1950 ]; then
post_to_discord "$RECAP_RAW"
else
echo "Splitting into multiple messages..."
remaining="$RECAP_RAW"
while [ ${#remaining} -gt 0 ]; do
if [ ${#remaining} -le 1950 ]; then
post_to_discord "$remaining"
break
else
chunk="${remaining:0:1900}"
last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1)
if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then
chunk="${remaining:0:$last_newline}"
remaining="${remaining:$((last_newline+1))}"
else
chunk="${remaining:0:1900}"
remaining="${remaining:1900}"
fi
post_to_discord "$chunk"
fi
done
fi
echo "Posted daily PR recap to Discord"
+7
View File
@@ -36,3 +36,10 @@ jobs:
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
SENTRY_RELEASE: web@${{ github.sha }}
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
VITE_SENTRY_RELEASE: web@${{ github.sha }}
+51 -195
View File
@@ -88,7 +88,7 @@ jobs:
- name: Build
id: build
run: |
./packages/opencode/script/build.ts
./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }}
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
@@ -209,182 +209,6 @@ jobs:
packages/opencode/dist/opencode-windows-x64
packages/opencode/dist/opencode-windows-x64-baseline
build-tauri:
needs:
- build-cli
- version
continue-on-error: false
env:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
strategy:
fail-fast: false
matrix:
settings:
- host: macos-latest
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
# github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain
- host: windows-2025
target: aarch64-pc-windows-msvc
- host: blacksmith-4vcpu-windows-2025
target: x86_64-pc-windows-msvc
- host: blacksmith-4vcpu-ubuntu-2404
target: x86_64-unknown-linux-gnu
- host: blacksmith-8vcpu-ubuntu-2404-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v3
with:
fetch-tags: true
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
with:
keychain: build
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Verify Certificate
if: ${{ runner.os == 'macOS' }}
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported."
- name: Setup Apple API Key
if: ${{ runner.os == 'macOS' }}
run: |
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
- uses: ./.github/actions/setup-bun
- name: Azure login
if: runner.os == 'Windows'
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Cache apt packages
if: contains(matrix.settings.host, 'ubuntu')
uses: actions/cache@v4
with:
path: ~/apt-cache
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.settings.target }}-apt-
- name: install dependencies (ubuntu only)
if: contains(matrix.settings.host, 'ubuntu')
run: |
mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache
sudo apt-get update
sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
sudo chmod -R a+rw ~/apt-cache
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/desktop/src-tauri
shared-key: ${{ matrix.settings.target }}
- name: Prepare
run: |
cd packages/desktop
bun ./scripts/prepare.ts
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
- name: Resolve tauri portable SHA
if: contains(matrix.settings.host, 'ubuntu')
run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV"
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- name: Install tauri-cli from portable appimage branch
uses: taiki-e/cache-cargo-install-action@v3
if: contains(matrix.settings.host, 'ubuntu')
with:
tool: tauri-cli
git: https://github.com/tauri-apps/tauri
# branch: feat/truly-portable-appimage
rev: ${{ env.TAURI_PORTABLE_SHA }}
- name: Show tauri-cli version
if: contains(matrix.settings.host, 'ubuntu')
run: cargo tauri --version
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Build and upload artifacts
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
timeout-minutes: 60
with:
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './src-tauri/tauri.prod.conf.json' }} --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.version.outputs.release }}
tagName: ${{ needs.version.outputs.tag }}
releaseDraft: true
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }}
releaseCommitish: ${{ github.sha }}
env:
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
- name: Verify signed Windows desktop artifacts
if: runner.os == 'Windows'
shell: pwsh
run: |
$files = @(
"${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
)
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
foreach ($file in $files) {
$sig = Get-AuthenticodeSignature $file
if ($sig.Status -ne "Valid") {
throw "Invalid signature for ${file}: $($sig.Status)"
}
}
build-electron:
needs:
- build-cli
@@ -480,7 +304,7 @@ jobs:
- name: Prepare
run: bun ./scripts/prepare.ts
working-directory: packages/desktop-electron
working-directory: packages/desktop
env:
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
@@ -491,14 +315,21 @@ jobs:
- name: Build
run: bun run build
working-directory: packages/desktop-electron
working-directory: packages/desktop
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }}
VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }}
- name: Package and publish
if: needs.version.outputs.release
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts
working-directory: packages/desktop-electron
working-directory: packages/desktop
timeout-minutes: 60
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
@@ -512,19 +343,43 @@ jobs:
- name: Package (no publish)
if: ${{ !needs.version.outputs.release }}
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
working-directory: packages/desktop-electron
working-directory: packages/desktop
timeout-minutes: 60
env:
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
- name: Create and upload macOS .app.tar.gz
if: runner.os == 'macOS' && needs.version.outputs.release
working-directory: packages/desktop/dist
env:
GH_TOKEN: ${{ steps.committer.outputs.token }}
run: |
if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then
APP_DIR="mac"
OUT_NAME="opencode-desktop-mac-x64.app.tar.gz"
elif [[ "${{ matrix.settings.target }}" == "aarch64-apple-darwin" ]]; then
APP_DIR="mac-arm64"
OUT_NAME="opencode-desktop-mac-arm64.app.tar.gz"
else
echo "Unknown macOS target: ${{ matrix.settings.target }}"
exit 1
fi
APP_PATH=$(find "$APP_DIR" -maxdepth 1 -name "*.app" -type d | head -1)
if [ -z "$APP_PATH" ]; then
echo "No .app bundle found in $APP_DIR"
exit 1
fi
tar -czf "$OUT_NAME" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
gh release upload "v${{ needs.version.outputs.version }}" "$OUT_NAME" --clobber --repo "${{ needs.version.outputs.repo }}"
- name: Verify signed Windows Electron artifacts
if: runner.os == 'Windows'
shell: pwsh
run: |
$files = @()
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
foreach ($file in $files | Select-Object -Unique) {
$sig = Get-AuthenticodeSignature $file
@@ -535,21 +390,20 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: opencode-electron-${{ matrix.settings.target }}
path: packages/desktop-electron/dist/*
name: opencode-desktop-${{ matrix.settings.target }}
path: packages/desktop/dist/*
- uses: actions/upload-artifact@v4
if: needs.version.outputs.release
with:
name: latest-yml-${{ matrix.settings.target }}
path: packages/desktop-electron/dist/latest*.yml
path: packages/desktop/dist/latest*.yml
publish:
needs:
- version
- build-cli
- sign-cli-windows
- build-tauri
- build-electron
if: always() && !failure() && !cancelled()
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -576,13 +430,6 @@ jobs:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- uses: actions/download-artifact@v4
with:
name: opencode-cli
@@ -604,6 +451,13 @@ jobs:
pattern: latest-yml-*
path: /tmp/latest-yml
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Cache apt packages (AUR)
uses: actions/cache@v4
with:
@@ -632,3 +486,5 @@ jobs:
GH_REPO: ${{ needs.version.outputs.repo }}
NPM_CONFIG_PROVENANCE: false
LATEST_YML_DIR: /tmp/latest-yml
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
+5
View File
@@ -68,6 +68,11 @@ jobs:
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
- name: Run HttpApi exerciser gates
if: runner.os == 'Linux'
working-directory: packages/opencode
run: bun run test:httpapi
- name: Publish unit reports
if: always()
uses: mikepenz/action-junit-report@v6
-116
View File
@@ -1,116 +0,0 @@
name: vouch-check-issue
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check if issue author is denounced
uses: actions/github-script@v7
with:
script: |
const author = context.payload.issue.user.login;
const issueNumber = context.payload.issue.number;
// Skip bots
if (author.endsWith('[bot]')) {
core.info(`Skipping bot: ${author}`);
return;
}
// Read the VOUCHED.td file via API (no checkout needed)
let content;
try {
const response = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/VOUCHED.td',
});
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
} catch (error) {
if (error.status === 404) {
core.info('No .github/VOUCHED.td file found, skipping check.');
return;
}
throw error;
}
// Parse the .td file for vouched and denounced users
const vouched = new Set();
const denounced = new Map();
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const isDenounced = trimmed.startsWith('-');
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
if (!rest) continue;
const spaceIdx = rest.indexOf(' ');
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
// Handle platform:username or bare username
// Only match bare usernames or github: prefix (skip other platforms)
const colonIdx = handle.indexOf(':');
if (colonIdx !== -1) {
const platform = handle.slice(0, colonIdx).toLowerCase();
if (platform !== 'github') continue;
}
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
if (!username) continue;
if (isDenounced) {
denounced.set(username.toLowerCase(), reason);
continue;
}
vouched.add(username.toLowerCase());
}
// Check if the author is denounced
const reason = denounced.get(author.toLowerCase());
if (reason !== undefined) {
// Author is denounced — close the issue
const body = 'This issue has been automatically closed.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body,
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed',
state_reason: 'not_planned',
});
core.info(`Closed issue #${issueNumber} from denounced user ${author}`);
return;
}
// Author is positively vouched — add label
if (!vouched.has(author.toLowerCase())) {
core.info(`User ${author} is not denounced or vouched. Allowing issue.`);
return;
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['Vouched'],
});
core.info(`Added vouched label to issue #${issueNumber} from ${author}`);
-114
View File
@@ -1,114 +0,0 @@
name: vouch-check-pr
on:
pull_request_target:
types: [opened]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check if PR author is denounced
uses: actions/github-script@v7
with:
script: |
const author = context.payload.pull_request.user.login;
const prNumber = context.payload.pull_request.number;
// Skip bots
if (author.endsWith('[bot]')) {
core.info(`Skipping bot: ${author}`);
return;
}
// Read the VOUCHED.td file via API (no checkout needed)
let content;
try {
const response = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/VOUCHED.td',
});
content = Buffer.from(response.data.content, 'base64').toString('utf-8');
} catch (error) {
if (error.status === 404) {
core.info('No .github/VOUCHED.td file found, skipping check.');
return;
}
throw error;
}
// Parse the .td file for vouched and denounced users
const vouched = new Set();
const denounced = new Map();
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const isDenounced = trimmed.startsWith('-');
const rest = isDenounced ? trimmed.slice(1).trim() : trimmed;
if (!rest) continue;
const spaceIdx = rest.indexOf(' ');
const handle = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
const reason = spaceIdx === -1 ? null : rest.slice(spaceIdx + 1).trim();
// Handle platform:username or bare username
// Only match bare usernames or github: prefix (skip other platforms)
const colonIdx = handle.indexOf(':');
if (colonIdx !== -1) {
const platform = handle.slice(0, colonIdx).toLowerCase();
if (platform !== 'github') continue;
}
const username = colonIdx === -1 ? handle : handle.slice(colonIdx + 1);
if (!username) continue;
if (isDenounced) {
denounced.set(username.toLowerCase(), reason);
continue;
}
vouched.add(username.toLowerCase());
}
// Check if the author is denounced
const reason = denounced.get(author.toLowerCase());
if (reason !== undefined) {
// Author is denounced — close the PR
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: 'This pull request has been automatically closed.',
});
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
core.info(`Closed PR #${prNumber} from denounced user ${author}`);
return;
}
// Author is positively vouched — add label
if (!vouched.has(author.toLowerCase())) {
core.info(`User ${author} is not denounced or vouched. Allowing PR.`);
return;
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['Vouched'],
});
core.info(`Added vouched label to PR #${prNumber} from ${author}`);
@@ -1,38 +0,0 @@
name: vouch-manage-by-issue
on:
issue_comment:
types: [created]
concurrency:
group: vouch-manage
cancel-in-progress: false
permissions:
contents: write
issues: write
pull-requests: read
jobs:
manage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
- name: Setup git committer
id: committer
uses: ./.github/actions/setup-git-committer
with:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- uses: mitchellh/vouch/action/manage-by-issue@main
with:
issue-id: ${{ github.event.issue.number }}
comment-id: ${{ github.event.comment.id }}
roles: admin,maintain,write
env:
GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
+1
View File
@@ -3,6 +3,7 @@ node_modules
.worktrees
.sst
.env
.env.local
.idea
.vscode
.codex
+5
View File
@@ -0,0 +1,5 @@
# Fake secret-looking strings used by HTTP recorder redaction tests.
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:generic-api-key:69
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:generic-api-key:92
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:generic-api-key:146
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:gcp-api-key:71
+15 -112
View File
@@ -1,7 +1,7 @@
---
mode: primary
hidden: true
model: opencode/minimax-m2.5
model: opencode/gpt-5.4-nano
color: "#44BA81"
tools:
"*": false
@@ -14,127 +14,30 @@ Use your github-triage tool to triage issues.
This file is the source of truth for ownership/routing rules.
## Labels
Assign issues by choosing the team with the strongest overlap. The github-triage tool will assign a random member from that team.
### windows
Do not add labels to issues. Only assign an owner.
Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows.
When calling github-triage, pass one of these team values: tui, desktop_web, core, inference, windows.
- Use if they mention WSL too
## Teams
#### perf
### TUI
Performance-related issues:
Terminal UI issues, including rendering, keybindings, scrolling, terminal compatibility, SSH behavior, crashes in the TUI, and low-level TUI performance.
- Slow performance
- High RAM usage
- High CPU usage
### Desktop / Web
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
Desktop application and browser-based app issues, including `opencode web`, desktop-specific UI behavior, packaging, and web view problems.
#### desktop
### Core
Desktop app issues:
Core opencode server and harness issues, including sqlite, snapshots, memory, API behavior, agent context construction, tool execution, provider integrations, model behavior, documentation, and larger architectural features.
- `opencode web` command
- The desktop app itself
### Inference
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
OpenCode Zen, OpenCode Go, and billing issues.
#### nix
### Windows
**Only** add if the issue explicitly mentions nix.
If the issue does not mention nix, do not add nix.
If the issue mentions nix, assign to `rekram1-node`.
#### zen
**Only** add if the issue mentions "zen" or "opencode zen" or "opencode black".
If the issue doesn't have "zen" or "opencode black" in it then don't add zen label
#### core
Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`.
Examples:
- LSP server behavior
- Harness behavior (agent + tools)
- Feature requests for server behavior
- Agent context construction
- API endpoints
- Provider integration issues
- New, broken, or poor-quality models
#### acp
If the issue mentions acp support, assign acp label.
#### docs
Add if the issue requests better documentation or docs updates.
#### opentui
TUI issues potentially caused by our underlying TUI library:
- Keybindings not working
- Scroll speed issues (too fast/slow/laggy)
- Screen flickering
- Crashes with opentui in the log
**Do not** add for general TUI bugs.
When assigning to people here are the following rules:
Desktop / Web:
Use for desktop-labeled issues only.
- adamdotdevin
- iamdavidhill
- Brendonovich
- nexxeln
Zen:
ONLY assign if the issue will have the "zen" label.
- fwang
- MrMushrooooom
TUI (`packages/opencode/src/cli/cmd/tui/...`):
- thdxr for TUI UX/UI product decisions and interaction flow
- kommander for OpenTUI engine issues: rendering artifacts, keybind handling, terminal compatibility, SSH behavior, and low-level perf bottlenecks
- rekram1-node for TUI bugs that are not clearly OpenTUI engine issues
Core (`packages/opencode/...`, excluding TUI subtree):
- thdxr for sqlite/snapshot/memory bugs and larger architectural core features
- jlongster for opencode server + API feature work (tool currently remaps jlongster -> thdxr until assignable)
- rekram1-node for harness issues, provider issues, and other bug-squashing
For core bugs that do not clearly map, either thdxr or rekram1-node is acceptable.
Docs:
- R44VC0RP
Windows:
- Hona (assign any issue that mentions Windows or is likely Windows-specific)
Determinism rules:
- If title + body does not contain "zen", do not add the "zen" label
- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix"
- If title + body mentions nix/nixos, assign to `rekram1-node`
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
ACP:
- rekram1-node (assign any acp issues to rekram1-node)
Windows-specific issues, including native Windows behavior, WSL interactions, path handling, shell compatibility, and installation or runtime problems that only happen on Windows.
+4 -1
View File
@@ -18,9 +18,12 @@ Do not use `git log` or author metadata when deciding attribution.
Rules:
- Write the final file with sections in this order:
- Write the final file with release sections in this order:
`## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions`
- Only include sections that have at least one notable entry
- Within each release section, keep bug fixes grouped under `### Bugfixes`
- Keep other notable entries under `### Improvements` when a section has bug fixes too
- Omit empty subsections
- Keep one bullet per commit you keep
- Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing
- Start each bullet with a capital letter
+1 -1
View File
@@ -3,7 +3,7 @@
"provider": {},
"permission": {
"edit": {
"packages/opencode/migration/*": "deny",
"packages/opencode/migration/*": "ask",
},
},
"mcp": {},
+367 -285
View File
@@ -1,35 +1,62 @@
/** @jsxImportSource @opentui/solid */
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
import { RGBA, VignetteEffect } from "@opentui/core"
import type {
TuiKeybindSet,
TuiPlugin,
TuiPluginApi,
TuiPluginMeta,
TuiPluginModule,
TuiSlotPlugin,
} from "@opencode-ai/plugin/tui"
import { useTerminalDimensions, type JSX } from "@opentui/solid"
import { useBindings, useKeymapSelector } from "@opentui/keymap/solid"
import { RGBA, VignetteEffect, type KeyEvent, type Renderable } from "@opentui/core"
import { createBindingLookup, type BindingConfig } from "@opentui/keymap/extras"
import type { TuiPlugin, TuiPluginApi, TuiPluginMeta, TuiPluginModule, TuiSlotPlugin } from "@opencode-ai/plugin/tui"
const tabs = ["overview", "counter", "help"]
const bind = {
modal: "ctrl+shift+m",
screen: "ctrl+shift+o",
home: "escape,ctrl+h",
left: "left,h",
right: "right,l",
up: "up,k",
down: "down,j",
alert: "a",
confirm: "c",
prompt: "p",
select: "s",
modal_accept: "enter,return",
modal_close: "escape",
dialog_close: "escape",
local: "x",
local_push: "enter,return",
local_close: "q,backspace",
host: "z",
const command = {
modal: "smoke_modal",
screen: "smoke_screen",
alert: "smoke_alert",
confirm: "smoke_confirm",
prompt: "smoke_prompt",
select: "smoke_select",
host: "smoke_host",
home: "smoke_home",
toast: "smoke_toast",
dialog_close: "smoke_dialog_close",
local_push: "smoke_local_push",
local_pop: "smoke_local_pop",
screen_home: "smoke_screen_home",
screen_left: "smoke_screen_left",
screen_right: "smoke_screen_right",
screen_up: "smoke_screen_up",
screen_down: "smoke_screen_down",
screen_modal: "smoke_screen_modal",
screen_local: "smoke_screen_local",
screen_host: "smoke_screen_host",
screen_alert: "smoke_screen_alert",
screen_confirm: "smoke_screen_confirm",
screen_prompt: "smoke_screen_prompt",
screen_select: "smoke_screen_select",
modal_accept: "smoke_modal_accept",
modal_close: "smoke_modal_close",
}
type SmokeBindings = BindingConfig<Renderable, KeyEvent>
const defaultKeymap = {
[command.modal]: "ctrl+shift+m",
[command.screen]: "ctrl+shift+o",
[command.dialog_close]: "escape",
[command.local_push]: "enter,return",
[command.local_pop]: "escape,q,backspace",
[command.screen_home]: "escape,ctrl+h",
[command.screen_left]: "left,h",
[command.screen_right]: "right,l",
[command.screen_up]: "up,k",
[command.screen_down]: "down,j",
[command.screen_modal]: "ctrl+shift+m",
[command.screen_local]: "x",
[command.screen_host]: "z",
[command.screen_alert]: "a",
[command.screen_confirm]: "c",
[command.screen_prompt]: "p",
[command.screen_select]: "s",
[command.modal_accept]: "enter,return",
[command.modal_close]: "escape",
}
const pick = (value: unknown, fallback: string) => {
@@ -43,16 +70,14 @@ const num = (value: unknown, fallback: number) => {
return value
}
const rec = (value: unknown) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return
return Object.fromEntries(Object.entries(value))
}
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === "object" && !Array.isArray(value)
type Cfg = {
label: string
route: string
vignette: number
keybinds: Record<string, unknown> | undefined
keybinds: SmokeBindings | undefined
}
type Route = {
@@ -74,7 +99,7 @@ const cfg = (options: Record<string, unknown> | undefined) => {
label: pick(options?.label, "smoke"),
route: pick(options?.route, "workspace-smoke"),
vignette: Math.max(0, num(options?.vignette, 0.35)),
keybinds: rec(options?.keybinds),
keybinds: record(options?.keybinds) ? (options.keybinds as SmokeBindings) : undefined,
}
}
@@ -85,7 +110,12 @@ const names = (input: Cfg) => {
}
}
type Keys = TuiKeybindSet
function createKeys(input: SmokeBindings | undefined) {
return createBindingLookup({ ...defaultKeymap, ...input })
}
type Keys = ReturnType<typeof createKeys>
const ui = {
panel: "#1d1d1d",
border: "#4a4a4a",
@@ -292,125 +322,174 @@ const Screen = (props: {
}
const pop = (base?: State) => {
const next = base ?? current(props.api, props.route)
const local = Math.max(0, next.local - 1)
set(local, next)
set(Math.max(0, next.local - 1), next)
}
const show = () => {
setTimeout(() => {
open()
}, 0)
}
useKeyboard((evt) => {
if (props.api.route.current.name !== props.route.screen) return
const next = current(props.api, props.route)
if (props.api.ui.dialog.open) {
if (props.keys.match("dialog_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.ui.dialog.clear()
return
}
return
}
const screenActive = () => props.api.route.current.name === props.route.screen
if (next.local > 0) {
if (evt.name === "escape" || props.keys.match("local_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
pop(next)
return
}
useBindings(() => ({
enabled: () => screenActive() && props.api.ui.dialog.open,
commands: [
{
name: command.dialog_close,
run() {
props.api.ui.dialog.clear()
},
},
],
bindings: props.keys.gather("smoke.dialog", [command.dialog_close]),
}))
if (props.keys.match("local_push", evt)) {
evt.preventDefault()
evt.stopPropagation()
push(next)
return
}
return
}
useBindings(() => ({
enabled: () => screenActive() && !props.api.ui.dialog.open && current(props.api, props.route).local > 0,
commands: [
{
name: command.local_push,
run() {
push(current(props.api, props.route))
},
},
{
name: command.local_pop,
run() {
pop(current(props.api, props.route))
},
},
],
bindings: props.keys.gather("smoke.local", [command.local_push, command.local_pop]),
}))
if (props.keys.match("home", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate("home")
return
}
useBindings(() => ({
enabled: () => screenActive() && !props.api.ui.dialog.open && current(props.api, props.route).local === 0,
commands: [
{
name: command.screen_home,
run() {
props.api.route.navigate("home")
},
},
{
name: command.screen_left,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
},
},
{
name: command.screen_right,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
},
},
{
name: command.screen_up,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
},
},
{
name: command.screen_down,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
},
},
{
name: command.screen_modal,
run() {
props.api.route.navigate(props.route.modal, current(props.api, props.route))
},
},
{
name: command.screen_local,
run() {
open()
},
},
{
name: command.screen_host,
run() {
host(props.api, props.input, skin)
},
},
{
name: command.screen_alert,
run() {
warn(props.api, props.route, current(props.api, props.route))
},
},
{
name: command.screen_confirm,
run() {
check(props.api, props.route, current(props.api, props.route))
},
},
{
name: command.screen_prompt,
run() {
entry(props.api, props.route, current(props.api, props.route))
},
},
{
name: command.screen_select,
run() {
picker(props.api, props.route, current(props.api, props.route))
},
},
],
bindings: props.keys.gather("smoke.screen", [
command.screen_home,
command.screen_left,
command.screen_right,
command.screen_up,
command.screen_down,
command.screen_modal,
command.screen_local,
command.screen_host,
command.screen_alert,
command.screen_confirm,
command.screen_prompt,
command.screen_select,
]),
}))
const shortcuts = useKeymapSelector((keymap) => {
const bindings = keymap.getCommandBindings({
visibility: "registered",
commands: [
command.screen_home,
command.screen_up,
command.screen_down,
command.screen_modal,
command.screen_alert,
command.screen_confirm,
command.screen_prompt,
command.screen_select,
command.screen_local,
command.screen_host,
command.local_push,
command.local_pop,
],
})
if (props.keys.match("left", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
return
}
if (props.keys.match("right", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
return
}
if (props.keys.match("up", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
return
}
if (props.keys.match("down", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
return
}
if (props.keys.match("modal", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.modal, next)
return
}
if (props.keys.match("local", evt)) {
evt.preventDefault()
evt.stopPropagation()
open()
return
}
if (props.keys.match("host", evt)) {
evt.preventDefault()
evt.stopPropagation()
host(props.api, props.input, skin)
return
}
if (props.keys.match("alert", evt)) {
evt.preventDefault()
evt.stopPropagation()
warn(props.api, props.route, next)
return
}
if (props.keys.match("confirm", evt)) {
evt.preventDefault()
evt.stopPropagation()
check(props.api, props.route, next)
return
}
if (props.keys.match("prompt", evt)) {
evt.preventDefault()
evt.stopPropagation()
entry(props.api, props.route, next)
return
}
if (props.keys.match("select", evt)) {
evt.preventDefault()
evt.stopPropagation()
picker(props.api, props.route, next)
return {
screen_home: props.api.keys.formatBindings(bindings.get(command.screen_home)) ?? "",
screen_up: props.api.keys.formatBindings(bindings.get(command.screen_up)) ?? "",
screen_down: props.api.keys.formatBindings(bindings.get(command.screen_down)) ?? "",
screen_modal: props.api.keys.formatBindings(bindings.get(command.screen_modal)) ?? "",
screen_alert: props.api.keys.formatBindings(bindings.get(command.screen_alert)) ?? "",
screen_confirm: props.api.keys.formatBindings(bindings.get(command.screen_confirm)) ?? "",
screen_prompt: props.api.keys.formatBindings(bindings.get(command.screen_prompt)) ?? "",
screen_select: props.api.keys.formatBindings(bindings.get(command.screen_select)) ?? "",
screen_local: props.api.keys.formatBindings(bindings.get(command.screen_local)) ?? "",
screen_host: props.api.keys.formatBindings(bindings.get(command.screen_host)) ?? "",
local_push: props.api.keys.formatBindings(bindings.get(command.local_push)) ?? "",
local_pop: props.api.keys.formatBindings(bindings.get(command.local_pop)) ?? "",
}
})
@@ -430,7 +509,7 @@ const Screen = (props: {
<b>{props.input.label} screen</b>
<span style={{ fg: skin.muted }}> plugin route</span>
</text>
<text fg={skin.muted}>{props.keys.print("home")} home</text>
<text fg={skin.muted}>{shortcuts().screen_home} home</text>
</box>
<box flexDirection="row" gap={1} paddingBottom={1}>
@@ -477,7 +556,7 @@ const Screen = (props: {
<box flexDirection="column" gap={1}>
<text fg={skin.text}>Counter: {value.count}</text>
<text fg={skin.muted}>
{props.keys.print("up")} / {props.keys.print("down")} change value
{shortcuts().screen_up} / {shortcuts().screen_down} change value
</text>
</box>
) : null}
@@ -485,17 +564,16 @@ const Screen = (props: {
{value.tab === 2 ? (
<box flexDirection="column" gap={1}>
<text fg={skin.muted}>
{props.keys.print("modal")} modal | {props.keys.print("alert")} alert | {props.keys.print("confirm")}{" "}
confirm | {props.keys.print("prompt")} prompt | {props.keys.print("select")} select
{shortcuts().screen_modal} modal | {shortcuts().screen_alert} alert | {shortcuts().screen_confirm}{" "}
confirm | {shortcuts().screen_prompt} prompt | {shortcuts().screen_select} select
</text>
<text fg={skin.muted}>
{props.keys.print("local")} local stack | {props.keys.print("host")} host stack
{shortcuts().screen_local} local stack | {shortcuts().screen_host} host stack
</text>
<text fg={skin.muted}>
local open: {props.keys.print("local_push")} push nested · esc or {props.keys.print("local_close")}{" "}
close
local open: {shortcuts().local_push} push nested · {shortcuts().local_pop} close
</text>
<text fg={skin.muted}>{props.keys.print("home")} returns home</text>
<text fg={skin.muted}>{shortcuts().screen_home} returns home</text>
</box>
) : null}
</box>
@@ -548,7 +626,7 @@ const Screen = (props: {
</text>
<text fg={skin.muted}>Plugin-owned stack depth: {value.local}</text>
<text fg={skin.muted}>
{props.keys.print("local_push")} push nested · {props.keys.print("local_close")} pop/close
{shortcuts().local_push} push nested · {shortcuts().local_pop} pop/close
</text>
<box flexDirection="row" gap={1}>
<Btn txt="push" run={push} skin={skin} on />
@@ -571,20 +649,35 @@ const Modal = (props: {
const value = parse(props.params)
const skin = tone(props.api)
useKeyboard((evt) => {
if (props.api.route.current.name !== props.route.modal) return
useBindings(() => ({
enabled: () => props.api.route.current.name === props.route.modal,
commands: [
{
name: command.modal_accept,
run() {
props.api.route.navigate(props.route.screen, { ...parse(props.params), source: "modal" })
},
},
{
name: command.modal_close,
run() {
props.api.route.navigate("home")
},
},
],
bindings: props.keys.gather("smoke.modal", [command.modal_accept, command.modal_close]),
}))
const shortcuts = useKeymapSelector((keymap) => {
const bindings = keymap.getCommandBindings({
visibility: "registered",
commands: [command.modal, command.screen, command.modal_accept, command.modal_close],
})
if (props.keys.match("modal_accept", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...value, source: "modal" })
return
}
if (props.keys.match("modal_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate("home")
return {
modal: props.api.keys.formatBindings(bindings.get(command.modal)) ?? "",
screen: props.api.keys.formatBindings(bindings.get(command.screen)) ?? "",
modal_accept: props.api.keys.formatBindings(bindings.get(command.modal_accept)) ?? "",
modal_close: props.api.keys.formatBindings(bindings.get(command.modal_close)) ?? "",
}
})
@@ -595,10 +688,10 @@ const Modal = (props: {
<text fg={skin.text}>
<b>{props.input.label} modal</b>
</text>
<text fg={skin.muted}>{props.keys.print("modal")} modal command</text>
<text fg={skin.muted}>{props.keys.print("screen")} screen command</text>
<text fg={skin.muted}>{shortcuts().modal} modal command</text>
<text fg={skin.muted}>{shortcuts().screen} screen command</text>
<text fg={skin.muted}>
{props.keys.print("modal_accept")} opens screen · {props.keys.print("modal_close")} closes
{shortcuts().modal_accept} opens screen · {shortcuts().modal_close} closes
</text>
<box flexDirection="row" gap={1}>
<Btn
@@ -651,25 +744,8 @@ const home = (api: TuiPluginApi, input: Cfg) => ({
},
home_prompt(ctx, value) {
const skin = look(ctx.theme.current)
type Prompt = (props: {
workspaceID?: string
visible?: boolean
disabled?: boolean
onSubmit?: () => void
hint?: JSX.Element
right?: JSX.Element
showPlaceholder?: boolean
placeholders?: {
normal?: string[]
shell?: string[]
}
}) => JSX.Element
type Slot = (
props: { name: string; mode?: unknown; children?: JSX.Element } & Record<string, unknown>,
) => JSX.Element | null
const ui = api.ui as TuiPluginApi["ui"] & { Prompt: Prompt; Slot: Slot }
const Prompt = ui.Prompt
const Slot = ui.Slot
const Prompt = api.ui.Prompt
const Slot = api.ui.Slot
const normal = [
`[SMOKE] route check for ${input.label}`,
"[SMOKE] confirm home_prompt slot override",
@@ -791,109 +867,115 @@ const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => {
const route = names(input)
api.command.register(() => [
{
title: `${input.label} modal`,
value: "plugin.smoke.modal",
keybind: keys.get("modal"),
category: "Plugin",
slash: {
name: "smoke",
api.keymap.registerLayer({
commands: [
{
name: command.modal,
title: `${input.label} modal`,
category: "Plugin",
namespace: "palette",
slashName: "smoke",
run() {
api.route.navigate(route.modal, { source: "command" })
},
},
onSelect: () => {
api.route.navigate(route.modal, { source: "command" })
{
name: command.screen,
title: `${input.label} screen`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-screen",
run() {
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
},
},
},
{
title: `${input.label} screen`,
value: "plugin.smoke.screen",
keybind: keys.get("screen"),
category: "Plugin",
slash: {
name: "smoke-screen",
{
name: command.alert,
title: `${input.label} alert dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-alert",
run() {
warn(api, route, current(api, route))
},
},
onSelect: () => {
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
{
name: command.confirm,
title: `${input.label} confirm dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-confirm",
run() {
check(api, route, current(api, route))
},
},
},
{
title: `${input.label} alert dialog`,
value: "plugin.smoke.alert",
category: "Plugin",
slash: {
name: "smoke-alert",
{
name: command.prompt,
title: `${input.label} prompt dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-prompt",
run() {
entry(api, route, current(api, route))
},
},
onSelect: () => {
warn(api, route, current(api, route))
{
name: command.select,
title: `${input.label} select dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-select",
run() {
picker(api, route, current(api, route))
},
},
},
{
title: `${input.label} confirm dialog`,
value: "plugin.smoke.confirm",
category: "Plugin",
slash: {
name: "smoke-confirm",
{
name: command.host,
title: `${input.label} host overlay`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-host",
run() {
host(api, input, tone(api))
},
},
onSelect: () => {
check(api, route, current(api, route))
{
name: command.home,
title: `${input.label} go home`,
category: "Plugin",
namespace: "palette",
enabled: () => api.route.current.name !== "home",
run() {
api.route.navigate("home")
},
},
},
{
title: `${input.label} prompt dialog`,
value: "plugin.smoke.prompt",
category: "Plugin",
slash: {
name: "smoke-prompt",
{
name: command.toast,
title: `${input.label} toast`,
category: "Plugin",
namespace: "palette",
run() {
api.ui.toast({
variant: "info",
title: "Smoke",
message: "Plugin toast works",
duration: 2000,
})
},
},
onSelect: () => {
entry(api, route, current(api, route))
},
},
{
title: `${input.label} select dialog`,
value: "plugin.smoke.select",
category: "Plugin",
slash: {
name: "smoke-select",
},
onSelect: () => {
picker(api, route, current(api, route))
},
},
{
title: `${input.label} host overlay`,
value: "plugin.smoke.host",
category: "Plugin",
slash: {
name: "smoke-host",
},
onSelect: () => {
host(api, input, tone(api))
},
},
{
title: `${input.label} go home`,
value: "plugin.smoke.home",
category: "Plugin",
enabled: api.route.current.name !== "home",
onSelect: () => {
api.route.navigate("home")
},
},
{
title: `${input.label} toast`,
value: "plugin.smoke.toast",
category: "Plugin",
onSelect: () => {
api.ui.toast({
variant: "info",
title: "Smoke",
message: "Plugin toast works",
duration: 2000,
})
},
},
])
],
bindings: keys.gather("smoke.global", [
command.modal,
command.screen,
command.alert,
command.confirm,
command.prompt,
command.select,
command.host,
command.home,
command.toast,
]),
})
}
const tui: TuiPlugin = async (api, options, meta) => {
@@ -902,9 +984,9 @@ const tui: TuiPlugin = async (api, options, meta) => {
await api.theme.install("./smoke-theme.json")
api.theme.set("smoke-theme")
const value = cfg(options ?? undefined)
const value = cfg(options)
const route = names(value)
const keys = api.keybind.create(bind, value.keybinds)
const keys = createKeys(value.keybinds)
const fx = new VignetteEffect(value.vignette)
const post = fx.apply.bind(fx)
api.renderer.addPostProcessFn(post)
+8
View File
@@ -28,3 +28,11 @@ Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3
- 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.
## Testing Patterns
- Use `testEffect(...)` from `packages/opencode/test/lib/effect.ts` for tests that exercise Effect services, layers, runtime context, scoped resources, or platform integrations.
- Use `it.live(...)` for filesystem, git repositories, HTTP servers, sockets, child processes, locks, real time, and other live platform behavior.
- Run tests from package directories such as `packages/opencode`; never run package tests from the repo root.
- Prefer explicit test layers over ad hoc managed runtimes. Keep dependency provisioning visible in the test file.
- Use scoped fixtures and finalizers for resources that must be cleaned up, including temporary directories, flags, databases, fibers, servers, and global state.
+12 -68
View File
@@ -1,16 +1,14 @@
/// <reference path="../env.d.ts" />
import { tool } from "@opencode-ai/plugin"
const TEAM = {
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
zen: ["fwang", "MrMushrooooom"],
tui: ["kommander", "rekram1-node", "simonklee"],
core: ["kitlangton", "rekram1-node", "jlongster"],
docs: ["R44VC0RP"],
tui: ["kommander", "simonklee"],
desktop_web: ["Hona", "Brendonovich"],
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
inference: ["fwang", "MrMushrooooom"],
windows: ["Hona"],
} as const
const ASSIGNEES = [...new Set(Object.values(TEAM).flat())]
function pick<T>(items: readonly T[]) {
return items[Math.floor(Math.random() * items.length)]!
}
@@ -38,79 +36,25 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
}
export default tool({
description: `Use this tool to assign and/or label a GitHub issue.
description: `Use this tool to assign a GitHub issue.
Choose labels and assignee using the current triage policy and ownership rules.
Pick the most fitting labels for the issue and assign one owner.
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`,
Provide the team that should own the issue. This tool picks a random assignee from that team and does not apply labels.`,
args: {
assignee: tool.schema
.enum(ASSIGNEES as [string, ...string[]])
.describe("The username of the assignee")
.default("rekram1-node"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
.describe("The labels(s) to add to the issue")
.default([]),
team: tool.schema
.enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]])
.describe("The owning team"),
},
async execute(args) {
const issue = getIssueNumber()
const owner = "anomalyco"
const repo = "opencode"
const results: string[] = []
let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))]
const web = labels.includes("web")
const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase()
const zen = /\bzen\b/.test(text) || text.includes("opencode black")
const nix = /\bnix(os)?\b/.test(text)
if (labels.includes("nix") && !nix) {
labels = labels.filter((x) => x !== "nix")
results.push("Dropped label: nix (issue does not mention nix)")
}
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
if (labels.includes("zen") && !zen) {
throw new Error("Only add the zen label when issue title/body contains 'zen'")
}
if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) {
throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln")
}
if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) {
throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom")
}
if (assignee === "Hona" && !labels.includes("windows")) {
throw new Error("Only windows issues should be assigned to Hona")
}
if (assignee === "R44VC0RP" && !labels.includes("docs")) {
throw new Error("Only docs issues should be assigned to R44VC0RP")
}
if (assignee === "kommander" && !labels.includes("opentui")) {
throw new Error("Only opentui issues should be assigned to kommander")
}
const assignee = pick(TEAM[args.team])
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
method: "POST",
body: JSON.stringify({ assignees: [assignee] }),
})
results.push(`Assigned @${assignee} to issue #${issue}`)
if (labels.length > 0) {
await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
method: "POST",
body: JSON.stringify({ labels }),
})
results.push(`Added labels: ${labels.join(", ")}`)
}
return results.join("\n")
return `Assigned @${assignee} from ${args.team} to issue #${issue}`
},
})
+5 -4
View File
@@ -7,10 +7,11 @@
"enabled": false,
"label": "workspace",
"keybinds": {
"modal": "ctrl+alt+m",
"screen": "ctrl+alt+o",
"home": "escape,ctrl+shift+h",
"dialog_close": "escape,q"
"smoke_modal": "ctrl+alt+m",
"smoke_screen": "ctrl+alt+o",
"smoke_screen_home": "escape,ctrl+shift+h",
"smoke_screen_modal": "ctrl+alt+m",
"smoke_dialog_close": "escape,q"
}
}
]
+1
View File
@@ -9,6 +9,7 @@
### General Principles
- Keep things in one function unless composable or reusable
- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
- Avoid `try`/`catch` where possible
- Avoid using the `any` type
- Use Bun APIs when possible, like `Bun.file()`
+6 -18
View File
@@ -73,7 +73,7 @@ Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
- `packages/opencode`: OpenCode core business logic & server.
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
- `packages/app`: The shared web UI components, written in SolidJS
- `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
- `packages/desktop`: The native desktop app, built with Electron (wraps `packages/app`)
- `packages/plugin`: Source for `@opencode-ai/plugin`
### Understanding bun dev vs opencode
@@ -123,33 +123,21 @@ This starts a local dev server at http://localhost:5173 (or similar port shown i
### Running the Desktop App
The desktop app is a native Tauri application that wraps the web UI.
The desktop app is an Electron application that wraps the web UI.
To run the native desktop app:
```bash
bun run --cwd packages/desktop tauri dev
```
This starts the web dev server on http://localhost:1420 and opens the native window.
If you only want the web dev server (no native shell):
To run the desktop app in development:
```bash
bun run --cwd packages/desktop dev
```
To create a production `dist/` and build the native app bundle:
To create a production build and package the app:
```bash
bun run --cwd packages/desktop tauri build
bun run --cwd packages/desktop build
bun run --cwd packages/desktop package
```
This runs `bun run --cwd packages/desktop build` automatically via Tauris `beforeBuildCommand`.
> [!NOTE]
> Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
> [!NOTE]
> If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث
يتوفر OpenCode ايضا كتطبيق سطح مكتب. قم بالتنزيل مباشرة من [صفحة الاصدارات](https://github.com/anomalyco/opencode/releases) او من [opencode.ai/download](https://opencode.ai/download).
| المنصة | التنزيل |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb` او `.rpm` او AppImage |
| المنصة | التنزيل |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb` او `.rpm` او AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev
OpenCode ডেস্কটপ অ্যাপ্লিকেশন হিসেবেও উপলব্ধ। সরাসরি [রিলিজ পেজ](https://github.com/anomalyco/opencode/releases) অথবা [opencode.ai/download](https://opencode.ai/download) থেকে ডাউনলোড করুন।
| প্ল্যাটফর্ম | ডাউনলোড |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, or AppImage |
| প্ল্যাটফর্ম | ডাউনলোড |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, or `.AppImage` |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch
O OpenCode também está disponível como aplicativo desktop. Baixe diretamente pela [página de releases](https://github.com/anomalyco/opencode/releases) ou em [opencode.ai/download](https://opencode.ai/download).
| Plataforma | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` ou AppImage |
| Plataforma | Download |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` ou AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji
OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice izdanja](https://github.com/anomalyco/opencode/releases) ili sa [opencode.ai/download](https://opencode.ai/download).
| Platforma | Preuzimanje |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ili AppImage |
| Platforma | Preuzimanje |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ili AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste
OpenCode findes også som desktop-app. Download direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
| Platform | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, eller AppImage |
| Platform | Download |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, eller AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neu
OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Releases-Seite](https://github.com/anomalyco/opencode/releases) oder [opencode.ai/download](https://opencode.ai/download) herunter.
| Plattform | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` oder AppImage |
| Plattform | Download |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` oder AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama de
OpenCode también está disponible como aplicación de escritorio. Descárgala directamente desde la [página de releases](https://github.com/anomalyco/opencode/releases) o desde [opencode.ai/download](https://opencode.ai/download).
| Plataforma | Descarga |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, o AppImage |
| Plataforma | Descarga |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, o AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branch
OpenCode est aussi disponible en application de bureau. Téléchargez-la directement depuis la [page des releases](https://github.com/anomalyco/opencode/releases) ou [opencode.ai/download](https://opencode.ai/download).
| Plateforme | Téléchargement |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ou AppImage |
| Plateforme | Téléchargement |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ou AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ή github:anomalyco/opencode με βάση
Το OpenCode είναι επίσης διαθέσιμο ως εφαρμογή. Κατέβασε το απευθείας από τη [σελίδα εκδόσεων](https://github.com/anomalyco/opencode/releases) ή το [opencode.ai/download](https://opencode.ai/download).
| Πλατφόρμα | Λήψη |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ή AppImage |
| Πλατφόρμα | Λήψη |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, ή AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # oppure github:anomalyco/opencode per lul
OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download).
| Piattaforma | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, oppure AppImage |
| Piattaforma | Download |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, oppure AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # または github:anomalyco/opencode で最
OpenCode はデスクトップアプリとしても利用できます。[releases page](https://github.com/anomalyco/opencode/releases) から直接ダウンロードするか、[opencode.ai/download](https://opencode.ai/download) を利用してください。
| プラットフォーム | ダウンロード |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb``.rpm`、または AppImage |
| プラットフォーム | ダウンロード |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb``.rpm`、または AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신
OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https://github.com/anomalyco/opencode/releases) 에서 직접 다운로드하거나 [opencode.ai/download](https://opencode.ai/download) 를 이용하세요.
| 플랫폼 | 다운로드 |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, 또는 AppImage |
| 플랫폼 | 다운로드 |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, 또는 AppImage |
```bash
# macOS (Homebrew)
+7 -7
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
| Platform | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, or AppImage |
| Platform | Download |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, or `.AppImage` |
```bash
# macOS (Homebrew)
@@ -132,7 +132,7 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
- 100% open source
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important.
- Out-of-the-box LSP support
- Built-in opt-in LSP support
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients.
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste
OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download).
| Plattform | Nedlasting |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` eller AppImage |
| Plattform | Nedlasting |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` eller AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowsze
OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośrednio ze strony [releases](https://github.com/anomalyco/opencode/releases) lub z [opencode.ai/download](https://opencode.ai/download).
| Platforma | Pobieranie |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` lub AppImage |
| Platforma | Pobieranie |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` lub AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # или github:anomalyco/opencode для с
OpenCode также доступен как десктопное приложение. Скачайте его со [страницы релизов](https://github.com/anomalyco/opencode/releases) или с [opencode.ai/download](https://opencode.ai/download).
| Платформа | Загрузка |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` или AppImage |
| Платформа | Загрузка |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` или AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # หรือ github:anomalyco/opencode ส
OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download)
| แพลตฟอร์ม | ดาวน์โหลด |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, หรือ AppImage |
| แพลตฟอร์ม | ดาวน์โหลด |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, หรือ AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # veya en güncel geliştirme dalı için git
OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm sayfasından](https://github.com/anomalyco/opencode/releases) veya [opencode.ai/download](https://opencode.ai/download) adresinden indirebilirsiniz.
| Platform | İndirme |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` veya AppImage |
| Platform | İndirme |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` veya AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # або github:anomalyco/opencode для н
OpenCode також доступний як десктопний застосунок. Завантажуйте напряму зі [сторінки релізів](https://github.com/anomalyco/opencode/releases) або [opencode.ai/download](https://opencode.ai/download).
| Платформа | Завантаження |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` або AppImage |
| Платформа | Завантаження |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm` або AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # hoặc github:anomalyco/opencode cho nhánh
OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download).
| Nền tảng | Tải xuống |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, hoặc AppImage |
| Nền tảng | Tải xuống |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, hoặc AppImage |
```bash
# macOS (Homebrew)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最
OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。
| 平台 | 下载文件 |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb``.rpm` 或 AppImage |
| 平台 | 下载文件 |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb``.rpm` 或 AppImage |
```bash
# macOS (Homebrew Cask)
+6 -6
View File
@@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
| 平台 | 下載連結 |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, 或 AppImage |
| 平台 | 下載連結 |
| --------------------- | ---------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` |
| macOS (Intel) | `opencode-desktop-mac-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, 或 AppImage |
```bash
# macOS (Homebrew Cask)
+410 -168
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -30,6 +30,7 @@ export const api = new sst.cloudflare.Worker("Api", {
transform: {
worker: (args) => {
args.logpush = true
if ($app.stage === "vimtor") return
args.bindings = $resolve(args.bindings).apply((bindings) => [
...bindings,
{
+5
View File
@@ -1,5 +1,6 @@
import { domain } from "./stage"
import { EMAILOCTOPUS_API_KEY } from "./app"
import { SECRET } from "./secret"
////////////////
// DATABASE
@@ -221,6 +222,7 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
properties: { value: stripeWebhook.secret },
})
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
////////////////
@@ -230,6 +232,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
const bucket = new sst.cloudflare.Bucket("ZenData")
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
@@ -251,6 +254,8 @@ new sst.cloudflare.x.SolidStart("Console", {
database,
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,
DISCORD_INCIDENT_WEBHOOK_URL,
SECRET.HoneycombWebhookSecret,
STRIPE_SECRET_KEY,
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
+216
View File
@@ -0,0 +1,216 @@
import { SECRET } from "./secret"
import { domain } from "./stage"
const description = "Managed by SST (Don't edit in Honeycomb UI)"
const webhookRecipient = new honeycomb.WebhookRecipient("DiscordAlerts", {
name: $app.stage === "production" ? "Discord Alerts" : `Discord Alerts (${$app.stage})`,
url: `https://${domain}/honeycomb/webhook`,
secret: SECRET.HoneycombWebhookSecret.result,
templates: [
{
type: "trigger",
body: `{
"url": {{ .Result.URL | quote }},
"type": {{ .Vars.type | quote }},
"name": {{ .Name | quote }},
"status": {{ .Alert.Status | quote }},
"isTest": {{ .Alert.IsTest }},
"groups": {{ .Result.GroupsTriggered | toJson }}
}`,
},
],
variables: [
{
name: "type",
},
],
})
// Honeycomb can keep stale query-local calculated fields when the name is unchanged,
// so tie the field name to the expression while avoiding deploy-to-deploy churn.
// https://github.com/honeycombio/terraform-provider-honeycombio/issues/852
const calculatedField = (field: { name: string; expression: string }) => ({
...field,
name: `${field.name}_${(
Array.from(field.expression).reduce((result, char) => Math.imul(31, result) + char.charCodeAt(0), 0) >>> 0
).toString(36)}`,
})
const modelHttpErrorsQuery = (product: "go" | "zen") => {
const filters = [
{ column: "model", op: "exists" },
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
]
const failedHttpStatus = calculatedField({
name: "is_failed_http_status",
expression:
product === "go"
? `IF(AND(GTE($status, "400"), NOT(EQUALS($status, "401")), NOT(EQUALS($status, "429"))), 1, 0)`
: `IF(AND(EQUALS($status, "429"), $isFreeTier), 0, AND(GTE($status, "400"), NOT(EQUALS($status, "401"))), 1, 0)`,
})
return honeycomb.getQuerySpecificationOutput({
breakdowns: ["model"],
calculatedFields: [failedHttpStatus],
calculations: [
{ op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
{
op: "SUM",
name: "FAILED",
column: failedHttpStatus.name,
filterCombination: "AND",
filters,
},
],
formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 100), DIV($FAILED, $TOTAL), 0)" }],
timeRange: 900,
}).json
}
const providerHttpErrorsQuery = (product: "go" | "zen") => {
const filters = [
{ column: "provider", op: "exists" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
]
const successHttpStatus = calculatedField({
name: "is_success_http_status",
expression: `IF(AND(GTE($status, "200"), LT($status, "400")), 1, 0)`,
})
const failedProviderHttpStatus = calculatedField({
name: "is_failed_provider_http_status",
expression: `IF(GT($llm.error.code, "400"), 1, 0)`,
})
return honeycomb.getQuerySpecificationOutput({
breakdowns: ["provider"],
calculatedFields: [successHttpStatus, failedProviderHttpStatus],
calculations: [
{
op: "SUM",
name: "SUCCESS",
column: successHttpStatus.name,
filterCombination: "AND",
filters: [...filters, { column: "event_type", op: "=", value: "completions" }],
},
{
op: "SUM",
name: "FAILED",
column: failedProviderHttpStatus.name,
filterCombination: "AND",
filters: [...filters, { column: "event_type", op: "=", value: "llm.error" }],
},
],
formulas: [
{ name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 50), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" },
],
timeRange: 900,
}).json
}
new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
name: "Increased Model HTTP Errors [Go]",
description,
queryJson: modelHttpErrorsQuery("go"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_http_errors" }],
},
],
},
],
})
new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
name: "Increased Model HTTP Errors [Zen]",
description,
queryJson: modelHttpErrorsQuery("zen"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_http_errors" }],
},
],
},
],
})
new honeycomb.Trigger("IncreasedProviderHttpErrorsGo", {
name: "Increased Provider HTTP Errors [Go]",
description,
queryJson: providerHttpErrorsQuery("go"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "provider_http_errors" }],
},
],
},
],
})
new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", {
name: "Increased Provider HTTP Errors [Zen]",
description,
queryJson: providerHttpErrorsQuery("zen"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "provider_http_errors" }],
},
],
},
],
})
new honeycomb.Trigger("IncreasedFreeTierRequests", {
name: "Increased Free Tier Requests",
description,
queryJson: honeycomb.getQuerySpecificationOutput({
calculations: [{ op: "COUNT" }],
filters: [
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isFreeTier", op: "=", value: "true" },
],
timeRange: 3600,
}).json,
alertType: "on_change",
frequency: 900,
thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }],
baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "custom" }],
},
],
},
],
})
+7
View File
@@ -1,4 +1,11 @@
sst.Linkable.wrap(random.RandomPassword, (resource) => ({
properties: {
value: resource.result,
},
}))
export const SECRET = {
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
}
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-U/LZx/D+5JTT1LHSyZkEuqXP/ky7LkHrEYBW5pcVArk=",
"aarch64-linux": "sha256-nGZa04h4y3jbdmf87IRrlQm/E5qYR8lj5OxKgQSR2XU=",
"aarch64-darwin": "sha256-GD8pCHWMBppDaIfRKxhY2m4xWo1OrY3wOmGw+EC71mw=",
"x86_64-darwin": "sha256-KOH1ZB8pdpF7Xer6QIH7rrr9fwF/BZkCTJndPe0wypg="
"x86_64-linux": "sha256-baGxh+hk/rPhg0xI/OdMDz6dPwncgercYNBdTPnLX9o=",
"aarch64-linux": "sha256-VTWKq679B3Q4ZnAoQzC4VSCYA09wWecNJ+JajvjNB1U=",
"aarch64-darwin": "sha256-orf2zIBMTiiQrt/6qCzE+o0oKhv6u8zXF9DH1Bo3lbo=",
"x86_64-darwin": "sha256-1MZC1fadRoY4lhkmjlcUQTLYH9Q8pDI1bxd5f94f1xU="
}
}
-1
View File
@@ -55,7 +55,6 @@ stdenvNoCC.mkDerivation {
--filter './packages/opencode' \
--filter './packages/desktop' \
--filter './packages/app' \
--filter './packages/shared' \
--frozen-lockfile \
--ignore-scripts \
--no-progress
+12 -7
View File
@@ -7,12 +7,13 @@
"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",
"dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"typecheck": "bun turbo typecheck",
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
"prepare": "husky",
"random": "echo 'Random script'",
@@ -27,19 +28,20 @@
"packages/slack"
],
"catalog": {
"@effect/opentelemetry": "4.0.0-beta.48",
"@effect/platform-node": "4.0.0-beta.48",
"@effect/opentelemetry": "4.0.0-beta.57",
"@effect/platform-node": "4.0.0-beta.57",
"@npmcli/arborist": "9.4.0",
"@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.105",
"@opentui/solid": "0.1.105",
"@opentui/core": "0.2.6",
"@opentui/keymap": "0.2.6",
"@opentui/solid": "0.2.6",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@types/node": "24.12.2",
"@types/semver": "7.7.1",
"@tsconfig/node22": "22.0.2",
"@tsconfig/bun": "1.0.9",
@@ -53,7 +55,7 @@
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.48",
"effect": "4.0.0-beta.59",
"ai": "6.0.168",
"cross-spawn": "7.0.6",
"hono": "4.10.7",
@@ -77,6 +79,8 @@
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"@sentry/solid": "10.36.0",
"@sentry/vite-plugin": "4.6.0",
"solid-js": "1.9.10",
"vite-plugin-solid": "2.11.10",
"@lydell/node-pty": "1.2.0-beta.10"
@@ -129,6 +133,7 @@
},
"patchedDependencies": {
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.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"
}
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.28",
"version": "1.14.48",
"description": "",
"type": "module",
"exports": {
@@ -27,6 +27,7 @@
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
"@playwright/test": "catalog:",
"@sentry/vite-plugin": "catalog:",
"@tailwindcss/vite": "catalog:",
"@tsconfig/bun": "1.0.9",
"@types/bun": "catalog:",
@@ -40,6 +41,7 @@
},
"dependencies": {
"@kobalte/core": "catalog:",
"@sentry/solid": "catalog:",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/core": "workspace:*",
+23 -7
View File
@@ -1,4 +1,5 @@
import "@/index.css"
import * as Sentry from "@sentry/solid"
import { I18nProvider } from "@opencode-ai/ui/context"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
@@ -82,7 +83,15 @@ declare global {
}
function QueryProvider(props: ParentProps) {
const client = new QueryClient()
const client = new QueryClient({
defaultOptions: {
queries: {
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
},
},
})
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
}
@@ -140,12 +149,19 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
>
<LanguageProvider locale={props.locale}>
<UiI18nBridge>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
</MarkedProvider>
</DialogProvider>
<ErrorBoundary
fallback={(error) => {
Sentry.captureException(error)
return <ErrorPage error={error} />
}}
>
<QueryProvider>
<DialogProvider>
<MarkedProvider>
<FileComponentProvider component={File}>{props.children}</FileComponentProvider>
</MarkedProvider>
</DialogProvider>
</QueryProvider>
</ErrorBoundary>
</UiI18nBridge>
</LanguageProvider>
@@ -10,6 +10,7 @@ import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { useLayout } from "@/context/layout"
import { useLanguage } from "@/context/language"
import { projectDirectories } from "@/pages/layout/helpers"
interface DialogSelectDirectoryProps {
title?: string
@@ -284,7 +285,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
for (const project of projects) {
let at = 0
const dirs = [project.worktree, ...(project.sandboxes ?? [])]
const dirs = projectDirectories(project)
for (const directory of dirs) {
const sessions = sync.child(directory, { bootstrap: false })[0].session
for (const session of sessions) {
@@ -16,6 +16,7 @@ import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionTabs } from "@/pages/session/helpers"
import { projectDirectories } from "@/pages/layout/helpers"
import { decode64 } from "@/utils/base64"
import { getRelativeTime } from "@/utils/time"
@@ -280,14 +281,14 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const project = createMemo(() => {
const directory = projectDirectory()
if (!directory) return
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
return layout.projects.list().find((p) => p.worktree === directory || p.worktrees?.includes(directory))
})
const workspaces = createMemo(() => {
const directory = projectDirectory()
const current = project()
if (!current) return directory ? [directory] : []
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
const dirs = projectDirectories(current)
if (directory && !dirs.includes(directory)) return [...dirs, directory]
return dirs
})
@@ -1,13 +1,12 @@
import { useMutation } from "@tanstack/solid-query"
import { Component, createEffect, createMemo, on, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useMutation, useQueryClient } from "@tanstack/solid-query"
import { Component, createMemo, Show } from "solid-js"
import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
import { mcpQueryKey } from "@/context/global-sync"
const statusLabels = {
connected: "mcp.status.connected",
@@ -20,48 +19,7 @@ export const DialogSelectMcp: Component = () => {
const sync = useSync()
const sdk = useSDK()
const language = useLanguage()
const [state, setState] = createStore({
done: false,
loading: false,
})
createEffect(
on(
() => sync.data.mcp_ready,
(ready, prev) => {
if (!ready && prev) setState("done", false)
},
{ defer: true },
),
)
createEffect(() => {
if (state.done || state.loading) return
if (sync.data.mcp_ready) {
setState("done", true)
return
}
setState("loading", true)
void sdk.client.mcp
.status()
.then((result) => {
sync.set("mcp", result.data ?? {})
sync.set("mcp_ready", true)
setState("done", true)
})
.catch((err) => {
setState("done", true)
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
})
.finally(() => {
setState("loading", false)
})
})
const queryClient = useQueryClient()
const items = createMemo(() =>
Object.entries(sync.data.mcp ?? {})
@@ -71,16 +29,10 @@ export const DialogSelectMcp: Component = () => {
const toggle = useMutation(() => ({
mutationFn: async (name: string) => {
const status = sync.data.mcp[name]
if (status?.status === "connected") {
await sdk.client.mcp.disconnect({ name })
} else {
await sdk.client.mcp.connect({ name })
}
const result = await sdk.client.mcp.status()
if (result.data) sync.set("mcp", result.data)
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
else await sdk.client.mcp.connect({ name })
},
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
}))
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
+7 -1
View File
@@ -16,6 +16,7 @@ import {
} from "@/context/prompt"
import { useLayout } from "@/context/layout"
import { useSDK } from "@/context/sdk"
import { useGlobalSDK } from "@/context/global-sdk"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { Button } from "@opencode-ai/ui/button"
@@ -102,6 +103,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
export const PromptInput: Component<PromptInputProps> = (props) => {
const sdk = useSDK()
const globalSDK = useGlobalSDK()
const sync = useSync()
const local = useLocal()
@@ -1253,7 +1255,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
queries: [
loadAgentsQuery(sdk.directory, sdk.client),
loadProvidersQuery(null, globalSDK.client),
loadProvidersQuery(sdk.directory, sdk.client),
],
}))
const agentsLoading = () => agentsQuery.isLoading
@@ -342,6 +342,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
return
}
WorktreeState.pending(createdWorktree.directory)
globalSync.project.addWorktree(projectDirectory, createdWorktree.directory)
sessionDirectory = createdWorktree.directory
}
@@ -144,7 +144,7 @@ export function SessionHeader() {
const project = createMemo(() => {
const directory = projectDirectory()
if (!directory) return
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
return layout.projects.list().find((p) => p.worktree === directory || p.worktrees?.includes(directory))
})
const name = createMemo(() => {
const current = project()
@@ -20,8 +20,8 @@ export function NewSessionView(props: NewSessionViewProps) {
const sdk = useSDK()
const language = useLanguage()
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
const worktrees = createMemo(() => sync.project?.worktrees ?? [])
const options = createMemo(() => [MAIN_WORKTREE, ...worktrees(), CREATE_WORKTREE])
const current = createMemo(() => {
const selection = props.worktree
if (options().includes(selection)) return selection
@@ -329,6 +329,7 @@ export const SettingsGeneral: Component = () => {
label={(o) => o.label}
onSelect={(option) => {
if (!option) return
if (option.value === currentShell()) return
globalSync.updateConfig({ shell: option.value })
}}
variant="secondary"
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useMutation } from "@tanstack/solid-query"
import { useMutation, useQueryClient } from "@tanstack/solid-query"
import { showToast } from "@opencode-ai/ui/toast"
import { useNavigate } from "@solidjs/router"
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
@@ -15,6 +15,7 @@ import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
import { mcpQueryKey } from "@/context/global-sync"
const pollMs = 10_000
@@ -137,14 +138,14 @@ const useMcpToggleMutation = () => {
const sync = useSync()
const sdk = useSDK()
const language = useLanguage()
const queryClient = useQueryClient()
return useMutation(() => ({
mutationFn: async (name: string) => {
const status = sync.data.mcp[name]
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
const result = await sdk.client.mcp.status()
if (result.data) sync.set("mcp", result.data)
},
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
onError: (err) => {
showToast({
variant: "error",
@@ -162,14 +163,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const dialog = useDialog()
const language = useLanguage()
const navigate = useNavigate()
const sdk = useSDK()
const [load, setLoad] = createStore({
lspDone: false,
lspLoading: false,
mcpDone: false,
mcpLoading: false,
})
const fail = (err: unknown) => {
showToast({
@@ -181,40 +174,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
createEffect(() => {
if (!props.shown()) return
if (!sync.data.mcp_ready && !load.mcpDone && !load.mcpLoading) {
setLoad("mcpLoading", true)
void sdk.client.mcp
.status()
.then((result) => {
sync.set("mcp", result.data ?? {})
sync.set("mcp_ready", true)
})
.catch((err) => {
setLoad("mcpDone", true)
fail(err)
})
.finally(() => {
setLoad("mcpLoading", false)
})
}
if (!sync.data.lsp_ready && !load.lspDone && !load.lspLoading) {
setLoad("lspLoading", true)
void sdk.client.lsp
.status()
.then((result) => {
sync.set("lsp", result.data ?? [])
sync.set("lsp_ready", true)
})
.catch((err) => {
setLoad("lspDone", true)
fail(err)
})
.finally(() => {
setLoad("lspLoading", false)
})
}
})
let dialogRun = 0
+44 -22
View File
@@ -15,6 +15,7 @@ import { terminalFontFamily, useSettings } from "@/context/settings"
import type { LocalPTY } from "@/context/terminal"
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
import { terminalWriter } from "@/utils/terminal-writer"
import { terminalWebSocketURL } from "@/utils/terminal-websocket-url"
const TOGGLE_TERMINAL_ID = "terminal.toggle"
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
@@ -67,13 +68,6 @@ const debugTerminal = (...values: unknown[]) => {
console.debug("[terminal]", ...values)
}
const errorName = (err: unknown) => {
if (!err || typeof err !== "object") return
if (!("name" in err)) return
const errorName = err.name
return typeof errorName === "string" ? errorName : undefined
}
const useTerminalUiBindings = (input: {
container: HTMLDivElement
term: Term
@@ -478,14 +472,34 @@ export const Terminal = (props: TerminalProps) => {
const gone = () =>
client.pty
.get({ ptyID: id })
.then(() => false)
.get({ ptyID: id }, { throwOnError: false })
.then((result) => result.response.status === 404)
.catch((err) => {
if (errorName(err) === "NotFoundError") return true
debugTerminal("failed to inspect terminal session", err)
return false
})
const connectToken = async () => {
const result = await client.pty
.connectToken(
{ ptyID: id, directory },
{
throwOnError: false,
headers: { "x-opencode-ticket": "1" },
},
)
.catch((err: unknown) => {
if (err instanceof Error && err.message.includes("Request is not supported")) return
throw err
})
if (!result) return
if (result.response.status === 200 && result.data?.ticket) return result.data.ticket
if (result.response.status === 404 || result.response.status === 405) return
if (result.response.status === 403)
throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
throw new Error(`PTY connect ticket failed with ${result.response.status}`)
}
const retry = (err: unknown) => {
if (disposed) return
if (reconn !== undefined) return
@@ -505,22 +519,30 @@ export const Terminal = (props: TerminalProps) => {
}, ms)
}
const open = () => {
const open = async () => {
if (disposed) return
drop?.()
const next = new URL(url + `/pty/${id}/connect`)
next.searchParams.set("directory", directory)
next.searchParams.set("cursor", String(seek))
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
if (!sameOrigin && password) {
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
// For same-origin requests, let the browser reuse the page's existing auth.
next.username = username
next.password = password
}
const ticket = await connectToken().catch((err) => {
fail(err)
return undefined
})
if (once.value) return
if (disposed) return
const socket = new WebSocket(next)
const socket = new WebSocket(
terminalWebSocketURL({
url,
id,
directory,
cursor: seek,
ticket,
sameOrigin,
username,
password,
authToken: server.current?.type === "http" ? server.current.authToken : false,
}),
)
socket.binaryType = "arraybuffer"
ws = socket
+147 -132
View File
@@ -35,6 +35,9 @@ type TauriApi = {
const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__
const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.()
const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.()
const titlebarHeight = 40
const minTitlebarZoom = 0.25
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
export function Titlebar() {
const layout = useLayout()
@@ -51,7 +54,14 @@ export function Titlebar() {
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
const web = createMemo(() => platform.platform === "web")
const zoom = () => platform.webviewZoom?.() ?? 1
const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined)
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
const minHeight = () => {
if (mac()) return `${titlebarHeight / zoom()}px`
if (windows()) return `${titlebarHeight / Math.min(titlebarZoom(), 1)}px`
return undefined
}
const windowsControlsWidth = () => `${windowsControlsBaseWidth / Math.max(titlebarZoom(), 1)}px`
const [history, setHistory] = createStore({
stack: [] as string[],
@@ -165,156 +175,161 @@ export function Titlebar() {
return (
<header
class="h-10 shrink-0 bg-background-base relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
class="h-10 shrink-0 bg-background-base relative overflow-hidden"
style={{ "min-height": minHeight() }}
data-tauri-drag-region
onMouseDown={drag}
onDblClick={maximize}
>
<div
classList={{
"flex items-center min-w-0": true,
"pl-2": !mac(),
}}
class="grid h-full min-h-full w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
style={{ zoom: counterZoom() }}
>
<Show when={mac()}>
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<Show when={!mac()}>
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<div class="flex items-center gap-1 shrink-0">
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<Button
variant="ghost"
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
<div
classList={{
"flex items-center min-w-0": true,
"pl-2": !mac(),
}}
>
<Show when={mac()}>
<div class="h-full shrink-0" style={{ width: `${72 / zoom()}px` }} />
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<Show when={!mac()}>
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
<IconButton
icon="menu"
variant="ghost"
class="titlebar-icon rounded-md"
onClick={layout.mobileSidebar.toggle}
aria-label={language.t("sidebar.menu.toggle")}
aria-expanded={layout.mobileSidebar.opened()}
/>
</div>
</Show>
<div class="flex items-center gap-1 shrink-0">
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
</Button>
</TooltipKeybind>
<div class="hidden xl:flex items-center shrink-0">
<Show when={params.dir}>
<div
class="flex items-center shrink-0 w-8 mr-1"
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
<Button
variant="ghost"
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
>
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
</Button>
</TooltipKeybind>
<div class="hidden xl:flex items-center shrink-0">
<Show when={params.dir}>
<div
class="transition-opacity"
classList={{
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
}}
class="flex items-center shrink-0 w-8 mr-1"
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
>
<TooltipKeybind
placement="bottom"
title={language.t("command.session.new")}
keybind={command.keybind("session.new")}
openDelay={2000}
<div
class="transition-opacity"
classList={{
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
}}
>
<Button
variant="ghost"
icon={creating() ? "new-session-active" : "new-session"}
class="titlebar-icon w-8 h-6 p-0 box-border"
disabled={layout.sidebar.opened()}
tabIndex={layout.sidebar.opened() ? -1 : undefined}
onClick={() => {
if (!params.dir) return
navigate(`/${params.dir}/session`)
}}
aria-label={language.t("command.session.new")}
aria-current={creating() ? "page" : undefined}
/>
</TooltipKeybind>
</div>
</div>
</Show>
<div
class="flex items-center shrink-0"
classList={{
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
"duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(),
}}
>
<Show when={hasProjects() && nav()}>
<div class="flex items-center gap-0 transition-transform">
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-left"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canBack()}
onClick={back}
aria-label={language.t("common.goBack")}
/>
</Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-right"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canForward()}
onClick={forward}
aria-label={language.t("common.goForward")}
/>
</Tooltip>
<TooltipKeybind
placement="bottom"
title={language.t("command.session.new")}
keybind={command.keybind("session.new")}
openDelay={2000}
>
<Button
variant="ghost"
icon={creating() ? "new-session-active" : "new-session"}
class="titlebar-icon w-8 h-6 p-0 box-border"
disabled={layout.sidebar.opened()}
tabIndex={layout.sidebar.opened() ? -1 : undefined}
onClick={() => {
if (!params.dir) return
navigate(`/${params.dir}/session`)
}}
aria-label={language.t("command.session.new")}
aria-current={creating() ? "page" : undefined}
/>
</TooltipKeybind>
</div>
</div>
</Show>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
</div>
)}
<div
class="flex items-center shrink-0"
classList={{
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
"duration-180 ease-out": !layout.sidebar.opened(),
"duration-180 ease-in": layout.sidebar.opened(),
}}
>
<Show when={hasProjects() && nav()}>
<div class="flex items-center gap-0 transition-transform">
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-left"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canBack()}
onClick={back}
aria-label={language.t("common.goBack")}
/>
</Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
<Button
variant="ghost"
icon="chevron-right"
class="titlebar-icon w-6 h-6 p-0 box-border"
disabled={!canForward()}
onClick={forward}
aria-label={language.t("common.goForward")}
/>
</Tooltip>
</div>
</Show>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<div class="min-w-0 flex items-center justify-center pointer-events-none">
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
</div>
<div class="min-w-0 flex items-center justify-center pointer-events-none">
<div id="opencode-titlebar-center" class="pointer-events-auto min-w-0 flex justify-center w-fit max-w-full" />
</div>
<div
classList={{
"flex items-center min-w-0 justify-end": true,
"pr-2": !windows(),
}}
data-tauri-drag-region
onMouseDown={drag}
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
{!tauriApi() && <div class="w-36 shrink-0" />}
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
<div
classList={{
"flex items-center min-w-0 justify-end": true,
"pr-2": !windows(),
}}
data-tauri-drag-region
onMouseDown={drag}
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
{!tauriApi() && <div class="shrink-0" style={{ width: windowsControlsWidth() }} />}
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
</div>
</header>
)
+156 -123
View File
@@ -2,7 +2,6 @@ import type {
Config,
OpencodeClient,
Path,
Project,
ProviderAuthResponse,
ProviderListResponse,
Todo,
@@ -14,23 +13,32 @@ import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
import type { InitError } from "../pages/error"
import { useGlobalSDK } from "./global-sdk"
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
import {
bootstrapDirectory,
bootstrapGlobal,
clearProviderRev,
loadGlobalConfigQuery,
loadPathQuery,
loadProvidersQuery,
} from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
import { createRefreshQueue } from "./global-sync/queue"
import { clearSessionPrefetchDirectory } from "./global-sync/session-prefetch"
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
import { trimSessions } from "./global-sync/session-trim"
import type { ProjectMeta } from "./global-sync/types"
import type { ProjectInfo, ProjectMeta } from "./global-sync/types"
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
import { formatServerError } from "@/utils/server-errors"
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
import { createRefreshQueue } from "./global-sync/queue"
import { directoryKey } from "./global-sync/utils"
import { pathKey } from "@/utils/path-key"
type GlobalStore = {
ready: boolean
error?: InitError
path: Path
project: Project[]
project: ProjectInfo[]
session_todo: {
[sessionID: string]: Todo[]
}
@@ -40,8 +48,23 @@ type GlobalStore = {
reload: undefined | "pending" | "complete"
}
export const loadSessionsQuery = (directory: string) =>
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const
export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const
export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: mcpQueryKey(directory),
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
})
export const lspQueryKey = (directory: string) => [directory, "lsp"] as const
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: lspQueryKey(directory),
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
})
function createGlobalSync() {
const globalSDK = useGlobalSDK()
@@ -54,15 +77,38 @@ function createGlobalSync() {
const sessionLoads = new Map<string, Promise<void>>()
const sessionMeta = new Map<string, { limit: number }>()
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
queries: [
loadGlobalConfigQuery(globalSDK.client),
loadProvidersQuery(null, globalSDK.client),
loadPathQuery(null, globalSDK.client),
],
}))
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
get ready() {
return bootstrap.isPending
},
project: [],
session_todo: {},
provider: { all: [], connected: [], default: {} },
provider_auth: {},
config: {},
reload: undefined,
get path() {
const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" }
if (pathQuery.isLoading) return EMPTY
return pathQuery.data ?? EMPTY
},
get provider() {
const EMPTY = { all: [], connected: [], default: {} }
if (providerQuery.isLoading) return EMPTY
return providerQuery.data ?? EMPTY
},
get config() {
if (configQuery.isLoading) return {}
return configQuery.data ?? {}
},
get reload() {
return updateConfigMutation.isPending ? "pending" : undefined
},
})
const queryClient = useQueryClient()
@@ -76,25 +122,47 @@ function createGlobalSync() {
if (eventTimer !== undefined) clearTimeout(eventTimer)
})
const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => {
setGlobalStore("project", next)
const bootstrap = useQuery(() => ({
queryKey: ["bootstrap"],
queryFn: async () => {
await bootstrapGlobal({
globalSDK: globalSDK.client,
requestFailedTitle: language.t("common.requestFailed"),
translate: language.t,
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
setGlobalStore,
queryClient,
})
bootedAt = Date.now()
return bootedAt
},
}))
const set = setGlobalStore
const addProjectWorktree = (projectWorktree: string, directory: string) => {
setGlobalStore(
"project",
produce((draft) => {
const project = draft.find((item) => item.worktree === projectWorktree)
if (!project) return
const key = pathKey(directory)
project.worktrees = [directory, ...(project.worktrees ?? []).filter((item) => pathKey(item) !== key)]
}),
)
}
const setBootStore = ((...input: unknown[]) => {
if (input[0] === "project" && Array.isArray(input[1])) {
setProjects(input[1] as Project[])
return input[1]
}
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
}) as typeof setGlobalStore
const set = ((...input: unknown[]) => {
if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) {
setProjects(input[1] as Project[] | ((draft: Project[]) => Project[]))
return input[1]
}
return (setGlobalStore as (...args: unknown[]) => unknown)(...input)
}) as typeof setGlobalStore
const removeProjectWorktree = (projectWorktree: string, directory: string) => {
setGlobalStore(
"project",
produce((draft) => {
const project = draft.find((item) => item.worktree === projectWorktree)
if (!project) return
const key = pathKey(directory)
project.worktrees = (project.worktrees ?? []).filter((item) => pathKey(item) !== key)
}),
)
}
const setSessionTodo = (sessionID: string, todos: Todo[] | undefined) => {
if (!sessionID) return
@@ -114,10 +182,23 @@ function createGlobalSync() {
const queue = createRefreshQueue({
paused,
bootstrap,
key: directoryKey,
bootstrap: () => queryClient.fetchQuery({ queryKey: ["bootstrap"] }),
bootstrapInstance,
})
const sdkFor = (directory: string) => {
const key = directoryKey(directory)
const cached = sdkCache.get(key)
if (cached) return cached
const sdk = globalSDK.createClient({
directory,
throwOnError: true,
})
sdkCache.set(key, sdk)
return sdk
}
const children = createChildStoreManager({
owner,
isBooting: (directory) => booting.has(directory),
@@ -126,33 +207,28 @@ function createGlobalSync() {
void bootstrapInstance(directory)
},
onDispose: (directory) => {
queue.clear(directory)
sessionMeta.delete(directory)
sdkCache.delete(directory)
clearProviderRev(directory)
clearSessionPrefetchDirectory(directory)
const key = directoryKey(directory)
queue.clear(key)
sessionMeta.delete(key)
sdkCache.delete(key)
clearProviderRev(key)
clearSessionPrefetchDirectory(key)
},
translate: language.t,
getSdk: sdkFor,
global: {
provider: globalStore.provider,
},
})
const sdkFor = (directory: string) => {
const cached = sdkCache.get(directory)
if (cached) return cached
const sdk = globalSDK.createClient({
directory,
throwOnError: true,
})
sdkCache.set(directory, sdk)
return sdk
}
async function loadSessions(directory: string) {
const pending = sessionLoads.get(directory)
const key = directoryKey(directory)
const pending = sessionLoads.get(key)
if (pending) return pending
children.pin(directory)
children.pin(key)
const [store, setStore] = children.child(directory, { bootstrap: false })
const meta = sessionMeta.get(directory)
const meta = sessionMeta.get(key)
if (meta && meta.limit >= store.limit) {
const next = trimSessions(store.session, {
limit: store.limit,
@@ -162,14 +238,14 @@ function createGlobalSync() {
setStore("session", reconcile(next, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, next, setSessionTodo)
}
children.unpin(directory)
children.unpin(key)
return
}
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
const promise = queryClient
.fetchQuery({
...loadSessionsQuery(directory),
queryKey: loadSessionsQueryKey(key),
queryFn: () =>
loadRootSessionsWithFallback({
directory,
@@ -199,7 +275,7 @@ function createGlobalSync() {
setStore("session", reconcile(sessions, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
})
sessionMeta.set(directory, { limit })
sessionMeta.set(key, { limit })
})
.catch((err) => {
console.error("Failed to load sessions", err)
@@ -214,23 +290,24 @@ function createGlobalSync() {
})
.then(() => {})
sessionLoads.set(directory, promise)
sessionLoads.set(key, promise)
void promise.finally(() => {
sessionLoads.delete(directory)
children.unpin(directory)
sessionLoads.delete(key)
children.unpin(key)
})
return promise
}
async function bootstrapInstance(directory: string) {
if (!directory) return
const pending = booting.get(directory)
const key = directoryKey(directory)
if (!key) return
const pending = booting.get(key)
if (pending) return pending
children.pin(directory)
children.pin(key)
const promise = Promise.resolve().then(async () => {
const child = children.ensureChild(directory)
const cache = children.vcsCache.get(directory)
const cache = children.vcsCache.get(key)
if (!cache) return
const sdk = sdkFor(directory)
await bootstrapDirectory({
@@ -251,41 +328,29 @@ function createGlobalSync() {
})
})
booting.set(directory, promise)
booting.set(key, promise)
void promise.finally(() => {
booting.delete(directory)
children.unpin(directory)
booting.delete(key)
children.unpin(key)
})
return promise
}
const unsub = globalSDK.event.listen((e) => {
const directory = e.name
const key = directoryKey(directory)
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,
project: globalStore.project,
refresh: () => {
if (recent) return
queue.refresh()
bootstrap.refetch()
},
setGlobalProject: setProjects,
setGlobalProject: (next) => setGlobalStore("project", next),
})
if (event.type === "server.connected" || event.type === "global.disposed") {
if (recent) return
@@ -296,9 +361,9 @@ function createGlobalSync() {
return
}
const existing = children.children[directory]
const existing = children.children[key]
if (!existing) return
children.mark(directory)
children.mark(key)
const [store, setStore] = existing
applyDirectoryEvent({
event,
@@ -307,14 +372,9 @@ function createGlobalSync() {
setStore,
push: queue.push,
setSessionTodo,
vcsCache: children.vcsCache.get(directory),
vcsCache: children.vcsCache.get(key),
loadLsp: () => {
void sdkFor(directory)
.lsp.status()
.then((x) => {
setStore("lsp", x.data ?? [])
setStore("lsp_ready", true)
})
void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
},
})
})
@@ -325,27 +385,10 @@ function createGlobalSync() {
})
onCleanup(() => {
for (const directory of Object.keys(children.children)) {
children.disposeDirectory(directory)
children.disposeDirectory(directoryKey(directory))
}
})
async function bootstrap() {
bootingRoot = true
try {
await bootstrapGlobal({
globalSDK: globalSDK.client,
requestFailedTitle: language.t("common.requestFailed"),
translate: language.t,
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
setGlobalStore: setBootStore,
queryClient,
})
bootedAt = Date.now()
} finally {
bootingRoot = false
}
}
onMount(() => {
if (typeof requestAnimationFrame === "function") {
eventFrame = requestAnimationFrame(() => {
@@ -361,7 +404,6 @@ function createGlobalSync() {
void globalSDK.event.start()
}, 0)
}
void bootstrap()
})
const projectApi = {
@@ -372,23 +414,14 @@ function createGlobalSync() {
icon(directory: string, value: string | undefined) {
children.projectIcon(directory, value)
},
addWorktree: addProjectWorktree,
removeWorktree: removeProjectWorktree,
}
const updateConfig = async (config: Config) => {
setGlobalStore("reload", "pending")
return globalSDK.client.global.config
.update({ config })
.then(bootstrap)
.then(() => {
queue.refresh()
setGlobalStore("reload", undefined)
queue.refresh()
})
.catch((error) => {
setGlobalStore("reload", undefined)
throw error
})
}
const updateConfigMutation = useMutation(() => ({
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }),
onSuccess: () => bootstrap.refetch(),
}))
return {
data: globalStore,
@@ -401,8 +434,8 @@ function createGlobalSync() {
},
child: children.child,
peek: children.peek,
bootstrap,
updateConfig,
// bootstrap,
updateConfig: updateConfigMutation.mutateAsync,
project: projectApi,
todo: {
set: setSessionTodo,
+86 -132
View File
@@ -3,7 +3,6 @@ import type {
OpencodeClient,
Path,
PermissionRequest,
Project,
ProviderAuthResponse,
ProviderListResponse,
QuestionRequest,
@@ -15,15 +14,16 @@ 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 type { ProjectInfo, 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 { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery } from "../global-sync"
type GlobalStore = {
ready: boolean
path: Path
project: Project[]
project: ProjectInfo[]
session_todo: {
[sessionID: string]: Todo[]
}
@@ -66,6 +66,52 @@ function runAll(list: Array<() => Promise<unknown>>) {
return Promise.allSettled(list.map((item) => item()))
}
function showErrors(input: {
errors: unknown[]
title: string
translate: (key: string, vars?: Record<string, string | number>) => string
formatMoreCount: (count: number) => string
}) {
if (input.errors.length === 0) return
const message = formatServerError(input.errors[0], input.translate)
const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
showToast({
variant: "error",
title: input.title,
description: message + more,
})
}
export const loadGlobalConfigQuery = (sdk: OpencodeClient) =>
queryOptions({
queryKey: ["config"],
queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)),
})
export const loadProjectsQuery = (sdk: OpencodeClient) =>
queryOptions({
queryKey: ["project"],
queryFn: () =>
retry(() =>
sdk.project.list().then(async (x) => {
return Promise.all(
(x.data ?? [])
.filter((p) => !!p?.id)
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
.slice()
.sort((a, b) => cmp(a.id, b.id))
.map(async (project) => ({
...project,
worktrees: await sdk.worktree
.list({ directory: project.worktree })
.then((x) => x.data ?? [])
.catch(() => []),
})),
)
}),
),
})
export async function bootstrapGlobal(input: {
globalSDK: OpencodeClient
requestFailedTitle: string
@@ -74,53 +120,15 @@ export async function bootstrapGlobal(input: {
setGlobalStore: SetStoreFunction<GlobalStore>
queryClient: QueryClient
}) {
const fast = [
() =>
retry(() =>
input.globalSDK.global.config.get().then((x) => {
input.setGlobalStore("config", reconcile(x.data!, { merge: false }))
}),
),
]
const slow = [
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)),
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)),
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)),
() =>
input.queryClient.fetchQuery({
...loadProvidersQuery(null),
queryFn: () =>
retry(() =>
input.globalSDK.provider.list().then((x) => {
input.setGlobalStore("provider", normalizeProviderList(x.data!))
return null
}),
),
}),
() =>
retry(() =>
input.globalSDK.path.get().then((x) => {
input.setGlobalStore("path", x.data!)
}),
),
() =>
retry(() =>
input.globalSDK.project.list().then((x) => {
const projects = (x.data ?? [])
.filter((p) => !!p?.id)
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
.slice()
.sort((a, b) => cmp(a.id, b.id))
input.setGlobalStore("project", projects)
}),
),
input.queryClient
.fetchQuery(loadProjectsQuery(input.globalSDK))
.then((data) => input.setGlobalStore("project", data)),
]
await runAll(fast)
// showErrors({
// errors: errors(await runAll(fast)),
// title: input.requestFailedTitle,
// translate: input.translate,
// formatMoreCount: input.formatMoreCount,
// })
await waitForPaint()
await runAll(slow)
// showErrors({
// errors: errors(),
@@ -128,7 +136,6 @@ export async function bootstrapGlobal(input: {
// translate: input.translate,
// formatMoreCount: input.formatMoreCount,
// })
input.setGlobalStore("ready", true)
}
function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) {
@@ -141,8 +148,8 @@ function groupBySession<T extends { id: string; sessionID: string }>(input: T[])
}, {})
}
function projectID(directory: string, projects: Project[]) {
return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id
function projectID(directory: string, projects: ProjectInfo[]) {
return projects.find((project) => project.worktree === directory || project.worktrees?.includes(directory))?.id
}
function mergeSession(setStore: SetStoreFunction<State>, session: Session) {
@@ -179,45 +186,22 @@ function warmSessions(input: {
).then(() => undefined)
}
export const loadProvidersQuery = (directory: string | null) =>
queryOptions<null>({ queryKey: [directory, "providers"], 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 loadProvidersQuery = (directory: string | null, sdk: OpencodeClient) =>
queryOptions({
queryKey: [directory, "providers"],
queryFn: () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))),
})
export const loadPathQuery = (
directory: string | null,
sdk?: OpencodeClient,
transform?: (x: Awaited<ReturnType<OpencodeClient["path"]["get"]>>) => void,
) =>
export const loadAgentsQuery = (directory: string | null, sdk: OpencodeClient) =>
queryOptions({
queryKey: [directory, "agents"],
queryFn: () => retry(() => sdk.app.agents().then((x) => normalizeAgentList(x.data))),
})
export const loadPathQuery = (directory: string | null, sdk: OpencodeClient) =>
queryOptions<Path>({
queryKey: [directory, "path"],
queryFn:
sdk && transform
? () =>
retry(() =>
sdk.path.get().then(async (x) => {
transform(x)
return x.data!
}),
)
: skipToken,
queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)),
})
export async function bootstrapDirectory(input: {
@@ -231,7 +215,7 @@ export async function bootstrapDirectory(input: {
global: {
config: Config
path: Path
project: Project[]
project: ProjectInfo[]
provider: ProviderListResponse
}
queryClient: QueryClient
@@ -241,19 +225,9 @@ export async function bootstrapDirectory(input: {
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
if (seededProject) input.setStore("project", seededProject)
if (seededPath) input.setStore("path", seededPath)
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
input.setStore("provider", input.global.provider)
}
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
input.setStore("config", reconcile(input.global.config, { merge: false }))
}
if (loading || input.store.provider.all.length === 0) {
input.setStore("provider_ready", false)
}
input.setStore("mcp_ready", false)
input.setStore("mcp", {})
input.setStore("lsp_ready", false)
input.setStore("lsp", [])
if (loading) input.setStore("status", "partial")
const rev = (providerRev.get(input.directory) ?? 0) + 1
@@ -262,9 +236,9 @@ export async function bootstrapDirectory(input: {
const slow = [
() => Promise.resolve(input.loadSessions(input.directory)),
() =>
input.queryClient.ensureQueryData(
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
),
input.queryClient
.ensureQueryData(loadAgentsQuery(input.directory, input.sdk))
.then((data) => input.setStore("agent", data)),
() =>
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
@@ -272,12 +246,10 @@ export async function bootstrapDirectory(input: {
(() => 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)
}),
)),
input.queryClient.ensureQueryData(loadPathQuery(input.directory, input.sdk)).then((data) => {
const next = projectID(data.directory ?? input.directory, input.global.project)
if (next) input.setStore("project", next)
})),
() =>
retry(() =>
input.sdk.vcs.get().then((x) => {
@@ -340,33 +312,15 @@ export async function bootstrapDirectory(input: {
}),
),
() => Promise.resolve(input.loadSessions(input.directory)),
() => input.queryClient.fetchQuery(loadMcpQuery(input.directory, input.sdk)),
() =>
retry(() =>
input.sdk.mcp.status().then((x) => {
input.setStore("mcp", x.data!)
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),
input.queryClient.fetchQuery(loadProvidersQuery(input.directory, input.sdk)).catch((err) => {
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
}),
].filter(Boolean) as (() => Promise<any>)[]
@@ -22,6 +22,8 @@ describe("createChildStoreManager", () => {
onBootstrap() {},
onDispose() {},
translate: (key) => key,
getSdk: () => null!,
global: { provider: null! },
})
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
@@ -1,7 +1,7 @@
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
import {
DIR_IDLE_TTL_MS,
MAX_DIR_STORES,
@@ -14,8 +14,10 @@ import {
type VcsCache,
} from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { useQuery } from "@tanstack/solid-query"
import { loadPathQuery } from "./bootstrap"
import { useQueries } from "@tanstack/solid-query"
import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
import { loadLspQuery, loadMcpQuery } from "../global-sync"
import { directoryKey, type DirectoryKey } from "./utils"
export function createChildStoreManager(input: {
owner: Owner
@@ -24,6 +26,10 @@ export function createChildStoreManager(input: {
onBootstrap: (directory: string) => void
onDispose: (directory: string) => void
translate: (key: string, vars?: Record<string, string | number>) => string
getSdk: (directory: string) => OpencodeClient
global: {
provider: ProviderListResponse
}
}) {
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
const vcsCache = new Map<string, VcsCache>()
@@ -34,30 +40,37 @@ export function createChildStoreManager(input: {
const ownerPins = new WeakMap<object, Set<string>>()
const disposers = new Map<string, () => void>()
const markKey = (key: DirectoryKey) => {
if (!key) return
lifecycle.set(key, { lastAccessAt: Date.now() })
runEviction(key)
}
const mark = (directory: string) => {
if (!directory) return
lifecycle.set(directory, { lastAccessAt: Date.now() })
runEviction(directory)
const key = directoryKey(directory)
markKey(key)
}
const pin = (directory: string) => {
if (!directory) return
pins.set(directory, (pins.get(directory) ?? 0) + 1)
mark(directory)
const key = directoryKey(directory)
if (!key) return
pins.set(key, (pins.get(key) ?? 0) + 1)
markKey(key)
}
const unpin = (directory: string) => {
if (!directory) return
const next = (pins.get(directory) ?? 0) - 1
const key = directoryKey(directory)
if (!key) return
const next = (pins.get(key) ?? 0) - 1
if (next > 0) {
pins.set(directory, next)
pins.set(key, next)
return
}
pins.delete(directory)
pins.delete(key)
runEviction()
}
const pinned = (directory: string) => (pins.get(directory) ?? 0) > 0
const pinned = (directory: string) => (pins.get(directoryKey(directory)) ?? 0) > 0
const pinForOwner = (directory: string) => {
const current = getOwner()
@@ -79,30 +92,31 @@ export function createChildStoreManager(input: {
})
}
function disposeDirectory(directory: string) {
function disposeDirectory(directory: DirectoryKey) {
const key = directory
if (
!canDisposeDirectory({
directory,
hasStore: !!children[directory],
pinned: pinned(directory),
booting: input.isBooting(directory),
loadingSessions: input.isLoadingSessions(directory),
directory: key,
hasStore: !!children[key],
pinned: pinned(key),
booting: input.isBooting(key),
loadingSessions: input.isLoadingSessions(key),
})
) {
return false
}
vcsCache.delete(directory)
metaCache.delete(directory)
iconCache.delete(directory)
lifecycle.delete(directory)
const dispose = disposers.get(directory)
vcsCache.delete(key)
metaCache.delete(key)
iconCache.delete(key)
lifecycle.delete(key)
const dispose = disposers.get(key)
if (dispose) {
dispose()
disposers.delete(directory)
disposers.delete(key)
}
delete children[directory]
input.onDispose(directory)
delete children[key]
input.onDispose(key)
return true
}
@@ -119,13 +133,14 @@ export function createChildStoreManager(input: {
}).filter((directory) => directory !== skip)
if (list.length === 0) return
for (const directory of list) {
if (!disposeDirectory(directory)) continue
if (!disposeDirectory(directoryKey(directory))) continue
}
}
function ensureChild(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
const key = directoryKey(directory)
if (!key) console.error("No directory provided")
if (!children[key]) {
const vcs = runWithOwner(input.owner, () =>
persisted(
Persist.workspace(directory, "vcs", ["vcs.v1"]),
@@ -134,7 +149,7 @@ export function createChildStoreManager(input: {
)
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
const vcsStore = vcs[0]
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
vcsCache.set(key, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
const meta = runWithOwner(input.owner, () =>
persisted(
@@ -143,7 +158,7 @@ export function createChildStoreManager(input: {
),
)
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
metaCache.set(key, { store: meta[0], setStore: meta[1], ready: meta[3] })
const icon = runWithOwner(input.owner, () =>
persisted(
@@ -152,19 +167,38 @@ export function createChildStoreManager(input: {
),
)
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
iconCache.set(key, { store: icon[0], setStore: icon[1], ready: icon[3] })
const init = () =>
createRoot((dispose) => {
const sdk = input.getSdk(directory)
const initialMeta = meta[0].value
const initialIcon = icon[0].value
const pathQuery = useQuery(() => loadPathQuery(directory))
const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
queries: [
loadPathQuery(key, sdk),
loadMcpQuery(key, sdk),
loadLspQuery(key, sdk),
loadProvidersQuery(key, sdk),
],
}))
const child = createStore<State>({
project: "",
projectMeta: undefined,
projectMeta: initialMeta,
icon: initialIcon,
provider_ready: false,
provider: { all: [], connected: [], default: {} },
get provider_ready() {
return !providerQuery.isLoading
},
get provider() {
const EMPTY = { all: [], connected: [], default: {} }
if (providerQuery.isLoading) return EMPTY
if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0)
return input.global.provider
return providerQuery.data ?? EMPTY
},
config: {},
get path() {
if (pathQuery.isLoading || !pathQuery.data)
@@ -181,22 +215,31 @@ export function createChildStoreManager(input: {
todo: {},
permission: {},
question: {},
mcp_ready: false,
mcp: {},
lsp_ready: false,
lsp: [],
get mcp_ready() {
return !mcpQuery.isLoading
},
get mcp() {
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
},
get lsp_ready() {
return !lspQuery.isLoading
},
get lsp() {
return lspQuery.isLoading ? [] : (lspQuery.data ?? [])
},
vcs: vcsStore.value,
limit: 5,
message: {},
part: {},
part_text_accum_delta: {},
})
children[directory] = child
disposers.set(directory, dispose)
children[key] = child
disposers.set(key, dispose)
const onPersistedInit = (init: Promise<string> | string | null, run: () => void) => {
if (!(init instanceof Promise)) return
void init.then(() => {
if (children[directory] !== child) return
if (children[key] !== child) return
run()
})
}
@@ -207,6 +250,11 @@ 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)
@@ -215,15 +263,16 @@ export function createChildStoreManager(input: {
runWithOwner(input.owner, init)
}
mark(directory)
const childStore = children[directory]
markKey(key)
const childStore = children[key]
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
return childStore
}
function child(directory: string, options: ChildOptions = {}) {
const key = directoryKey(directory)
const childStore = ensureChild(directory)
pinForOwner(directory)
pinForOwner(key)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap && childStore[0].status === "loading") {
input.onBootstrap(directory)
@@ -232,6 +281,7 @@ export function createChildStoreManager(input: {
}
function peek(directory: string, options: ChildOptions = {}) {
const key = directoryKey(directory)
const childStore = ensureChild(directory)
const shouldBootstrap = options.bootstrap ?? true
if (shouldBootstrap && childStore[0].status === "loading") {
@@ -241,8 +291,9 @@ export function createChildStoreManager(input: {
}
function projectMeta(directory: string, patch: ProjectMeta) {
const key = directoryKey(directory)
const [store, setStore] = ensureChild(directory)
const cached = metaCache.get(directory)
const cached = metaCache.get(key)
if (!cached) return
const previous = store.projectMeta ?? {}
const icon = patch.icon ? { ...previous.icon, ...patch.icon } : previous.icon
@@ -258,8 +309,9 @@ export function createChildStoreManager(input: {
}
function projectIcon(directory: string, value: string | undefined) {
const key = directoryKey(directory)
const [store, setStore] = ensureChild(directory)
const cached = iconCache.get(directory)
const cached = iconCache.get(key)
if (!cached) return
if (store.icon === value) return
cached.setStore("value", value)
@@ -81,6 +81,7 @@ const baseState = (input: Partial<State> = {}) =>
limit: 10,
message: {},
part: {},
part_text_accum_delta: {},
...input,
}) as State
@@ -11,7 +11,7 @@ import type {
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types"
import type { ProjectInfo, State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache"
import { diffs as list, message as clean } from "@/utils/diffs"
@@ -20,8 +20,8 @@ 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[]) => Project[])) => void
project: ProjectInfo[]
setGlobalProject: (next: ProjectInfo[] | ((draft: ProjectInfo[]) => ProjectInfo[])) => void
refresh: () => void
}) {
if (input.event.type === "global.disposed" || input.event.type === "server.connected") {
@@ -42,7 +42,7 @@ export function applyGlobalEvent(input: {
}
input.setGlobalProject(
produce((draft) => {
draft.splice(result.index, 0, properties)
draft.splice(result.index, 0, { ...properties, worktrees: [] })
}),
)
}
@@ -211,6 +211,12 @@ export function applyDirectoryEvent(input: {
const result = Binary.search(messages, props.messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
}
const parts = draft.part[props.messageID]
if (parts) {
for (const part of parts) {
delete draft.part_text_accum_delta[part.id]
}
}
delete draft.part[props.messageID]
}),
)
@@ -219,6 +225,11 @@ export function applyDirectoryEvent(input: {
case "message.part.updated": {
const part = (event.properties as { part: Part }).part
if (SKIP_PARTS.has(part.type)) break
input.setStore(
produce((draft) => {
delete draft.part_text_accum_delta[part.id]
}),
)
const parts = input.store.part[part.messageID]
if (!parts) {
input.setStore("part", part.messageID, [part])
@@ -240,6 +251,11 @@ export function applyDirectoryEvent(input: {
}
case "message.part.removed": {
const props = event.properties as { messageID: string; partID: string }
input.setStore(
produce((draft) => {
delete draft.part_text_accum_delta[props.partID]
}),
)
const parts = input.store.part[props.messageID]
if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id)
@@ -263,6 +279,7 @@ export function applyDirectoryEvent(input: {
if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id)
if (!result.found) break
input.setStore("part_text_accum_delta", props.partID, (existing) => (existing ?? "") + props.delta)
input.setStore(
"part",
props.messageID,
@@ -0,0 +1,46 @@
import { describe, expect, test } from "bun:test"
import { createRefreshQueue } from "./queue"
import { directoryKey } from "./utils"
const tick = () => new Promise((resolve) => setTimeout(resolve, 10))
describe("createRefreshQueue", () => {
test("clears queued directories by normalized key", async () => {
const calls: string[] = []
const queue = createRefreshQueue({
paused: () => false,
key: directoryKey,
bootstrap: async () => {},
bootstrapInstance: (directory) => {
calls.push(directory)
},
})
queue.push("C:\\tmp\\demo")
queue.clear("C:/tmp/demo")
await tick()
expect(calls).toEqual([])
queue.dispose()
})
test("passes the original directory to bootstrapInstance", async () => {
const calls: string[] = []
const queue = createRefreshQueue({
paused: () => false,
key: directoryKey,
bootstrap: async () => {},
bootstrapInstance: (directory) => {
calls.push(directory)
},
})
queue.push("C:\\tmp\\demo")
await tick()
expect(calls).toEqual(["C:\\tmp\\demo"])
queue.dispose()
})
})
@@ -2,22 +2,25 @@ type QueueInput = {
paused: () => boolean
bootstrap: () => Promise<void>
bootstrapInstance: (directory: string) => Promise<void> | void
key?: (directory: string) => string
}
export function createRefreshQueue(input: QueueInput) {
const queued = new Set<string>()
const queued = new Map<string, string>()
let root = false
let running = false
let timer: ReturnType<typeof setTimeout> | undefined
const key = input.key ?? ((directory: string) => directory)
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
const take = (count: number) => {
if (queued.size === 0) return [] as string[]
const items: string[] = []
for (const item of queued) {
queued.delete(item)
items.push(item)
for (const [id, directory] of queued) {
queued.delete(id)
items.push(directory)
if (items.length >= count) break
}
return items
@@ -33,7 +36,7 @@ export function createRefreshQueue(input: QueueInput) {
const push = (directory: string) => {
if (!directory) return
queued.add(directory)
queued.set(key(directory), directory)
if (input.paused()) return
schedule()
}
@@ -73,7 +76,7 @@ export function createRefreshQueue(input: QueueInput) {
push,
refresh,
clear(directory: string) {
queued.delete(directory)
queued.delete(key(directory))
},
dispose() {
if (!timer) return
@@ -39,6 +39,7 @@ describe("app session cache", () => {
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: { ses_1: { type: "busy" } as SessionStatus },
session_diff: { ses_1: [] },
@@ -47,12 +48,14 @@ describe("app session cache", () => {
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
permission: { ses_1: [] as PermissionRequest[] },
question: { ses_1: [] as QuestionRequest[] },
part_text_accum_delta: { prt_1: "streamed text" },
}
dropSessionCaches(store, ["ses_1"])
expect(store.message.ses_1).toBeUndefined()
expect(store.part.msg_1).toBeUndefined()
expect(store.part_text_accum_delta.prt_1).toBeUndefined()
expect(store.todo.ses_1).toBeUndefined()
expect(store.session_diff.ses_1).toBeUndefined()
expect(store.session_status.ses_1).toBeUndefined()
@@ -70,6 +73,7 @@ describe("app session cache", () => {
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: {},
session_diff: {},
@@ -78,6 +82,7 @@ describe("app session cache", () => {
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
permission: {},
question: {},
part_text_accum_delta: {},
}
dropSessionCaches(store, ["ses_1"])
@@ -18,6 +18,7 @@ type SessionCache = {
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
}
export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<string>) {
@@ -27,6 +28,9 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<stri
for (const key of Object.keys(store.part)) {
const parts = store.part[key]
if (!parts?.some((part) => stale.has(part?.sessionID ?? ""))) continue
for (const part of parts) {
delete store.part_text_accum_delta[part.id]
}
delete store.part[key]
}
@@ -8,6 +8,7 @@ import type {
Part,
Path,
PermissionRequest,
Project,
ProviderListResponse,
QuestionRequest,
Session,
@@ -30,6 +31,10 @@ export type ProjectMeta = {
}
}
export type ProjectInfo = Project & {
worktrees?: string[]
}
export type State = {
status: "loading" | "partial" | "complete"
agent: Agent[]
@@ -72,6 +77,9 @@ export type State = {
part: {
[messageID: string]: Part[]
}
part_text_accum_delta: {
[partID: string]: string
}
}
export type VcsCache = {
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { Agent } from "@opencode-ai/sdk/v2/client"
import { normalizeAgentList } from "./utils"
import { directoryKey, normalizeAgentList } from "./utils"
const agent = (name = "build") =>
({
@@ -33,3 +33,20 @@ describe("normalizeAgentList", () => {
expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")])
})
})
describe("directoryKey", () => {
test("normalizes slashes", () => {
expect(String(directoryKey("C:\\Repos\\sst\\opencode"))).toBe("C:/Repos/sst/opencode")
expect(String(directoryKey("C:/Repos/sst/opencode"))).toBe("C:/Repos/sst/opencode")
})
test("preserves backslashes in posix paths", () => {
expect(String(directoryKey("/tmp/foo\\bar"))).toBe("/tmp/foo\\bar")
})
test("trims trailing slashes without breaking roots", () => {
expect(String(directoryKey("C:/Repos/sst/opencode/"))).toBe("C:/Repos/sst/opencode")
expect(String(directoryKey("C:/"))).toBe("C:/")
expect(String(directoryKey("/"))).toBe("/")
})
})
@@ -1,4 +1,6 @@
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
import type { ProjectInfo } from "./types"
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
@@ -26,7 +28,7 @@ export function normalizeProviderList(input: ProviderListResponse): ProviderList
}
}
export function sanitizeProject(project: Project) {
export function sanitizeProject(project: ProjectInfo) {
if (!project.icon?.url && !project.icon?.override) return project
return {
...project,
+12 -6
View File
@@ -6,8 +6,8 @@ import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server"
import { usePlatform } from "./platform"
import { Project } from "@opencode-ai/sdk/v2"
import { Persist, persisted, removePersisted } from "@/utils/persist"
import type { ProjectInfo } from "./global-sync/types"
import { decode64 } from "@/utils/base64"
import { same } from "@/utils/same"
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
@@ -51,7 +51,7 @@ type TabHandoff = {
at: number
}
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
export type LocalProject = Partial<ProjectInfo> & { worktree: string; expanded: boolean }
export type ReviewDiffStyle = "unified" | "split"
@@ -391,15 +391,21 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
? globalSync.data.project.find((x) => x.id === projectID)
: globalSync.data.project.find((x) => x.worktree === project.worktree)
return { ...metadata, ...project }
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
// Without this, different subdirectories of the same git repo would share the same
// icon from the database instead of using their individual overrides.
const base = { ...metadata, ...project }
if (childStore.icon) {
return { ...base, icon: { ...base.icon, override: childStore.icon } }
}
return base
}
const roots = createMemo(() => {
const map = new Map<string, string>()
for (const project of globalSync.data.project) {
const sandboxes = project.sandboxes ?? []
for (const sandbox of sandboxes) {
map.set(sandbox, project.worktree)
for (const worktree of project.worktrees ?? []) {
map.set(worktree, project.worktree)
}
}
return map
+1 -1
View File
@@ -382,7 +382,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setSaved("session", session, {
agent: msg.agent,
model: msg.model,
variant: msg.model.variant ?? null,
variant: msg.model?.variant ?? null,
})
},
},
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, test } from "bun:test"
import { resolveServerList, ServerConnection } from "./server"
describe("resolveServerList", () => {
test("lets startup auth_token credentials override a persisted same-url server", () => {
const list = resolveServerList({
stored: [{ url: "https://server.example.test" }],
props: [
{
type: "http",
authToken: true,
http: {
url: "https://server.example.test",
username: "opencode",
password: "secret",
},
},
],
})
expect(list).toHaveLength(1)
expect(list[0]?.type).toBe("http")
expect(list[0]?.http).toEqual({
url: "https://server.example.test",
username: "opencode",
password: "secret",
})
expect(list[0]?.type === "http" ? list[0].authToken : false).toBe(true)
expect(ServerConnection.key(list[0]!) as string).toBe("https://server.example.test")
})
test("keeps persisted credentials when startup has no auth_token", () => {
const list = resolveServerList({
stored: [
{
url: "https://server.example.test",
username: "opencode",
password: "saved",
},
],
props: [{ type: "http", http: { url: "https://server.example.test" } }],
})
expect(list).toHaveLength(1)
expect(list[0]?.type).toBe("http")
expect(list[0]?.http).toEqual({
url: "https://server.example.test",
username: "opencode",
password: "saved",
})
expect(list[0]?.type === "http" ? list[0].authToken : true).toBeUndefined()
})
})
+30 -21
View File
@@ -33,6 +33,33 @@ function isLocalHost(url: string) {
if (host === "localhost" || host === "127.0.0.1") return "local"
}
export function resolveServerList(input: {
props?: Array<ServerConnection.Any>
stored: StoredServer[]
}): Array<ServerConnection.Any> {
const servers = [
...input.stored.map((value) =>
typeof value === "string"
? {
type: "http" as const,
http: { url: value },
}
: value,
),
...(input.props ?? []),
]
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>()
for (const value of servers) {
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
const key = ServerConnection.key(conn)
if (deduped.has(key) && conn.type === "http" && !conn.authToken) continue
deduped.set(key, conn)
}
return [...deduped.values()]
}
export namespace ServerConnection {
type Base = { displayName?: string }
@@ -46,6 +73,7 @@ export namespace ServerConnection {
export type Http = {
type: "http"
http: HttpBase
authToken?: boolean
} & Base
export type Sidecar = {
@@ -113,26 +141,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url)
const allServers = createMemo((): Array<ServerConnection.Any> => {
const servers = [
...(props.servers ?? []),
...store.list.map((value) =>
typeof value === "string"
? {
type: "http" as const,
http: { url: value },
}
: value,
),
]
const deduped = new Map(
servers.map((value) => {
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
return [ServerConnection.key(conn), conn]
}),
)
return [...deduped.values()]
return resolveServerList({ stored: store.list, props: props.servers })
})
const [state, setState] = createStore({
@@ -174,7 +183,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
function add(input: ServerConnection.Http) {
const url_ = normalizeServerUrl(input.http.url)
if (!url_) return
const conn = { ...input, http: { ...input.http, url: url_ } }
const conn: ServerConnection.Http = { ...input, authToken: undefined, http: { ...input.http, url: url_ } }
return batch(() => {
const existing = store.list.findIndex((x) => url(x) === url_)
if (existing !== -1) {
+44 -1
View File
@@ -1,6 +1,9 @@
import { beforeAll, describe, expect, mock, test } from "bun:test"
let getWorkspaceTerminalCacheKey: (dir: string) => string
type ServerKey = Parameters<typeof import("./terminal").getTerminalServerScope>[1]
let getWorkspaceTerminalCacheKey: (dir: string, scope?: string) => string
let getTerminalServerScope: typeof import("./terminal").getTerminalServerScope
let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[]
let migrateTerminalState: (value: unknown) => unknown
@@ -17,6 +20,7 @@ beforeAll(async () => {
}))
const mod = await import("./terminal")
getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey
getTerminalServerScope = mod.getTerminalServerScope
getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys
migrateTerminalState = mod.migrateTerminalState
})
@@ -25,6 +29,45 @@ describe("getWorkspaceTerminalCacheKey", () => {
test("uses workspace-only directory cache key", () => {
expect(getWorkspaceTerminalCacheKey("/repo")).toBe("/repo:__workspace__")
})
test("can include a server scope", () => {
expect(getWorkspaceTerminalCacheKey("/repo", "wsl:Debian")).toBe("wsl:Debian:/repo:__workspace__")
})
})
describe("getTerminalServerScope", () => {
test("preserves local server keys", () => {
expect(
getTerminalServerScope(
{ type: "sidecar", variant: "base", http: { url: "http://127.0.0.1:4096" } },
"sidecar" as ServerKey,
),
).toBeUndefined()
expect(
getTerminalServerScope(
{ type: "http", http: { url: "http://localhost:4096" } },
"http://localhost:4096" as ServerKey,
),
).toBeUndefined()
expect(
getTerminalServerScope({ type: "http", http: { url: "http://[::1]:4096" } }, "http://[::1]:4096" as ServerKey),
).toBeUndefined()
})
test("scopes non-local server keys", () => {
expect(
getTerminalServerScope(
{ type: "sidecar", variant: "wsl", distro: "Debian", http: { url: "http://127.0.0.1:4096" } },
"wsl:Debian" as ServerKey,
),
).toBe("wsl:Debian" as ServerKey)
expect(
getTerminalServerScope(
{ type: "http", http: { url: "https://example.com" } },
"https://example.com" as ServerKey,
),
).toBe("https://example.com" as ServerKey)
})
})
describe("getLegacyTerminalStorageKeys", () => {
+47 -15
View File
@@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
import type { Platform } from "./platform"
import { ServerConnection, useServer } from "./server"
import { defaultTitle, titleNumber } from "./terminal-title"
import { Persist, persisted, removePersisted } from "@/utils/persist"
@@ -82,10 +83,31 @@ export function migrateTerminalState(value: unknown) {
}
}
export function getWorkspaceTerminalCacheKey(dir: string) {
export function getWorkspaceTerminalCacheKey(dir: string, scope?: string) {
if (scope) return `${scope}:${dir}:${WORKSPACE_KEY}`
return `${dir}:${WORKSPACE_KEY}`
}
export function getTerminalServerScope(conn: ServerConnection.Any | undefined, key: ServerConnection.Key) {
if (!conn) return
if (conn.type === "sidecar" && conn.variant === "base") return
if (conn.type === "http") {
try {
const url = new URL(conn.http.url)
if (
url.hostname === "localhost" ||
url.hostname === "127.0.0.1" ||
url.hostname === "::1" ||
url.hostname === "[::1]"
)
return
} catch {
return key
}
}
return key
}
export function getLegacyTerminalStorageKeys(dir: string, legacySessionID?: string) {
if (!legacySessionID) return [`${dir}/terminal.v1`]
return [`${dir}/terminal/${legacySessionID}.v1`, `${dir}/terminal.v1`]
@@ -110,15 +132,16 @@ const trimTerminal = (pty: LocalPTY) => {
}
}
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform) {
const key = getWorkspaceTerminalCacheKey(dir)
export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], platform?: Platform, scope?: string) {
const key = getWorkspaceTerminalCacheKey(dir, scope)
for (const cache of caches) {
const entry = cache.get(key)
entry?.value.clear()
}
void removePersisted(Persist.workspace(dir, "terminal"), platform)
void removePersisted(Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal"), platform)
if (scope) return
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
for (const id of sessionIDs ?? []) {
for (const key of getLegacyTerminalStorageKeys(dir, id)) {
@@ -130,12 +153,17 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
}
}
function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, legacySessionID?: string) {
const legacy = getLegacyTerminalStorageKeys(dir, legacySessionID)
function createWorkspaceTerminalSession(
sdk: ReturnType<typeof useSDK>,
dir: string,
legacySessionID?: string,
scope?: string,
) {
const legacy = scope ? [] : getLegacyTerminalStorageKeys(dir, legacySessionID)
const [store, setStore, _, ready] = persisted(
{
...Persist.workspace(dir, "terminal", legacy),
...Persist.workspace(dir, scope ? `terminal:${scope}` : "terminal", legacy),
migrate: migrateTerminalState,
},
createStore<{
@@ -357,8 +385,12 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
gate: false,
init: () => {
const sdk = useSDK()
const server = useServer()
const params = useParams()
const cache = new Map<string, TerminalCacheEntry>()
const scope = createMemo(() => {
return getTerminalServerScope(server.current, server.key)
})
caches.add(cache)
onCleanup(() => caches.delete(cache))
@@ -382,9 +414,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
}
}
const loadWorkspace = (dir: string, legacySessionID?: string) => {
const loadWorkspace = (dir: string, legacySessionID: string | undefined, serverScope: string | undefined) => {
// Terminals are workspace-scoped so tabs persist while switching sessions in the same directory.
const key = getWorkspaceTerminalCacheKey(dir)
const key = getWorkspaceTerminalCacheKey(dir, serverScope)
const existing = cache.get(key)
if (existing) {
cache.delete(key)
@@ -393,7 +425,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
}
const entry = createRoot((dispose) => ({
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID),
value: createWorkspaceTerminalSession(sdk, dir, legacySessionID, serverScope),
dispose,
}))
@@ -402,16 +434,16 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
return entry.value
}
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id))
const workspace = createMemo(() => loadWorkspace(params.dir!, params.id, scope()))
createEffect(
on(
() => ({ dir: params.dir, id: params.id }),
() => ({ dir: params.dir, id: params.id, scope: scope() }),
(next, prev) => {
if (!prev?.dir) return
if (next.dir === prev.dir && next.id === prev.id) return
if (next.dir === prev.dir && next.id) return
loadWorkspace(prev.dir, prev.id).trimAll()
if (next.dir === prev.dir && next.id === prev.id && next.scope === prev.scope) return
if (next.dir === prev.dir && next.id && next.scope === prev.scope) return
loadWorkspace(prev.dir, prev.id, prev.scope).trimAll()
},
{ defer: true },
),
+38 -1
View File
@@ -1,11 +1,13 @@
// @refresh reload
import * as Sentry from "@sentry/solid"
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface } from "@/app"
import { type Platform, PlatformProvider } from "@/context/platform"
import { dict as en } from "@/i18n/en"
import { dict as zh } from "@/i18n/zh"
import { handleNotificationClick } from "@/utils/notification-click"
import { authFromToken } from "@/utils/server"
import pkg from "../package.json"
import { ServerConnection } from "./context/server"
@@ -110,6 +112,13 @@ const getDefaultUrl = () => {
return getCurrentUrl()
}
const clearAuthToken = () => {
const params = new URLSearchParams(location.search)
if (!params.has("auth_token")) return
params.delete("auth_token")
history.replaceState(null, "", location.pathname + (params.size ? `?${params}` : "") + location.hash)
}
const platform: Platform = {
platform: "web",
version: pkg.version,
@@ -125,8 +134,36 @@ const platform: Platform = {
setDefaultServer: writeDefaultServerUrl,
}
if (import.meta.env.VITE_SENTRY_DSN) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE,
release: import.meta.env.VITE_SENTRY_RELEASE ?? `web@${pkg.version}`,
initialScope: {
tags: {
platform: "web",
},
},
integrations: (integrations) => {
return integrations.filter(
(i) =>
i.name !== "Breadcrumbs" && !(import.meta.env.OPENCODE_CHANNEL === "prod" && i.name === "GlobalHandlers"),
)
},
})
}
if (root instanceof HTMLElement) {
const server: ServerConnection.Http = { type: "http", http: { url: getCurrentUrl() } }
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
clearAuthToken()
const server: ServerConnection.Http = {
type: "http",
authToken: !!auth,
http: {
url: getCurrentUrl(),
...auth,
},
}
render(
() => (
<PlatformProvider value={platform}>
+4
View File
@@ -2,6 +2,10 @@ interface ImportMetaEnv {
readonly VITE_OPENCODE_SERVER_HOST: string
readonly VITE_OPENCODE_SERVER_PORT: string
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
readonly VITE_SENTRY_DSN?: string
readonly VITE_SENTRY_ENVIRONMENT?: string
readonly VITE_SENTRY_RELEASE?: string
}
interface ImportMeta {
+2 -2
View File
@@ -402,6 +402,8 @@ export const dict = {
"error.page.description": "حدث خطأ أثناء تحميل التطبيق.",
"error.page.details.label": "تفاصيل الخطأ",
"error.page.action.restart": "إعادة تشغيل",
"error.page.action.report": "الإبلاغ عن الخطأ",
"error.page.action.reported": "تم الإبلاغ عن الخطأ",
"error.page.action.checking": "جارٍ التحقق...",
"error.page.action.checkUpdates": "التحقق من وجود تحديثات",
"error.page.action.updateTo": "تحديث إلى {{version}}",
@@ -721,8 +723,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "جلب محتوى من عنوان URL",
"settings.permissions.tool.websearch.title": "بحث الويب",
"settings.permissions.tool.websearch.description": "البحث في الويب",
"settings.permissions.tool.codesearch.title": "بحث الكود",
"settings.permissions.tool.codesearch.description": "البحث عن كود على الويب",
"settings.permissions.tool.external_directory.title": "دليل خارجي",
"settings.permissions.tool.external_directory.description": "الوصول إلى الملفات خارج دليل المشروع",
"settings.permissions.tool.doom_loop.title": "حلقة الموت",
+2 -2
View File
@@ -403,6 +403,8 @@ export const dict = {
"error.page.description": "Ocorreu um erro ao carregar a aplicação.",
"error.page.details.label": "Detalhes do Erro",
"error.page.action.restart": "Reiniciar",
"error.page.action.report": "Reportar erro",
"error.page.action.reported": "Erro reportado",
"error.page.action.checking": "Verificando...",
"error.page.action.checkUpdates": "Verificar atualizações",
"error.page.action.updateTo": "Atualizar para {{version}}",
@@ -732,8 +734,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Buscar conteúdo de uma URL",
"settings.permissions.tool.websearch.title": "Pesquisa Web",
"settings.permissions.tool.websearch.description": "Pesquisar na web",
"settings.permissions.tool.codesearch.title": "Pesquisa de Código",
"settings.permissions.tool.codesearch.description": "Pesquisar código na web",
"settings.permissions.tool.external_directory.title": "Diretório Externo",
"settings.permissions.tool.external_directory.description": "Acessar arquivos fora do diretório do projeto",
"settings.permissions.tool.doom_loop.title": "Loop Infinito",
+2 -2
View File
@@ -449,6 +449,8 @@ export const dict = {
"error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.",
"error.page.details.label": "Detalji greške",
"error.page.action.restart": "Restartuj",
"error.page.action.report": "Prijavi grešku",
"error.page.action.reported": "Greška prijavljena",
"error.page.action.checking": "Provjera...",
"error.page.action.checkUpdates": "Provjeri ažuriranja",
"error.page.action.updateTo": "Ažuriraj na {{version}}",
@@ -806,8 +808,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a",
"settings.permissions.tool.websearch.title": "Web pretraga",
"settings.permissions.tool.websearch.description": "Pretražuj web",
"settings.permissions.tool.codesearch.title": "Pretraga koda",
"settings.permissions.tool.codesearch.description": "Pretraži kod na webu",
"settings.permissions.tool.external_directory.title": "Vanjski direktorij",
"settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta",
"settings.permissions.tool.doom_loop.title": "Beskonačna petlja",
+2 -2
View File
@@ -446,6 +446,8 @@ export const dict = {
"error.page.description": "Der opstod en fejl under indlæsning af applikationen.",
"error.page.details.label": "Fejldetaljer",
"error.page.action.restart": "Genstart",
"error.page.action.report": "Rapportér fejl",
"error.page.action.reported": "Fejl rapporteret",
"error.page.action.checking": "Tjekker...",
"error.page.action.checkUpdates": "Tjek for opdateringer",
"error.page.action.updateTo": "Opdater til {{version}}",
@@ -800,8 +802,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Hent indhold fra en URL",
"settings.permissions.tool.websearch.title": "Websøgning",
"settings.permissions.tool.websearch.description": "Søg på nettet",
"settings.permissions.tool.codesearch.title": "Kodesøgning",
"settings.permissions.tool.codesearch.description": "Søg kode på nettet",
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
"settings.permissions.tool.external_directory.description": "Få adgang til filer uden for projektmappen",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
+2 -2
View File
@@ -410,6 +410,8 @@ export const dict = {
"error.page.description": "Beim Laden der Anwendung ist ein Fehler aufgetreten.",
"error.page.details.label": "Fehlerdetails",
"error.page.action.restart": "Neustart",
"error.page.action.report": "Fehler melden",
"error.page.action.reported": "Fehler gemeldet",
"error.page.action.checking": "Prüfen...",
"error.page.action.checkUpdates": "Nach Updates suchen",
"error.page.action.updateTo": "Auf {{version}} aktualisieren",
@@ -743,8 +745,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Inhalt von einer URL abrufen",
"settings.permissions.tool.websearch.title": "Web-Suche",
"settings.permissions.tool.websearch.description": "Das Web durchsuchen",
"settings.permissions.tool.codesearch.title": "Code-Suche",
"settings.permissions.tool.codesearch.description": "Code im Web durchsuchen",
"settings.permissions.tool.external_directory.title": "Externes Verzeichnis",
"settings.permissions.tool.external_directory.description": "Zugriff auf Dateien außerhalb des Projektverzeichnisses",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
+2 -2
View File
@@ -465,6 +465,8 @@ export const dict = {
"error.page.description": "An error occurred while loading the application.",
"error.page.details.label": "Error Details",
"error.page.action.restart": "Restart",
"error.page.action.report": "Report Error",
"error.page.action.reported": "Error Reported",
"error.page.action.checking": "Checking...",
"error.page.action.checkUpdates": "Check for updates",
"error.page.action.updateTo": "Update to {{version}}",
@@ -920,8 +922,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Fetch content from a URL",
"settings.permissions.tool.websearch.title": "Web Search",
"settings.permissions.tool.websearch.description": "Search the web",
"settings.permissions.tool.codesearch.title": "Code Search",
"settings.permissions.tool.codesearch.description": "Search code on the web",
"settings.permissions.tool.external_directory.title": "External Directory",
"settings.permissions.tool.external_directory.description": "Access files outside the project directory",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
+2 -2
View File
@@ -449,6 +449,8 @@ export const dict = {
"error.page.description": "Ocurrió un error al cargar la aplicación.",
"error.page.details.label": "Detalles del error",
"error.page.action.restart": "Reiniciar",
"error.page.action.report": "Informar error",
"error.page.action.reported": "Error informado",
"error.page.action.checking": "Comprobando...",
"error.page.action.checkUpdates": "Buscar actualizaciones",
"error.page.action.updateTo": "Actualizar a {{version}}",
@@ -813,8 +815,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Obtener contenido de una URL",
"settings.permissions.tool.websearch.title": "Búsqueda Web",
"settings.permissions.tool.websearch.description": "Buscar en la web",
"settings.permissions.tool.codesearch.title": "Búsqueda de Código",
"settings.permissions.tool.codesearch.description": "Buscar código en la web",
"settings.permissions.tool.external_directory.title": "Directorio Externo",
"settings.permissions.tool.external_directory.description": "Acceder a archivos fuera del directorio del proyecto",
"settings.permissions.tool.doom_loop.title": "Bucle Infinito",
+2 -2
View File
@@ -406,6 +406,8 @@ export const dict = {
"error.page.description": "Une erreur s'est produite lors du chargement de l'application.",
"error.page.details.label": "Détails de l'erreur",
"error.page.action.restart": "Redémarrer",
"error.page.action.report": "Signaler l'erreur",
"error.page.action.reported": "Erreur signalée",
"error.page.action.checking": "Vérification...",
"error.page.action.checkUpdates": "Vérifier les mises à jour",
"error.page.action.updateTo": "Mettre à jour vers {{version}}",
@@ -741,8 +743,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL",
"settings.permissions.tool.websearch.title": "Recherche Web",
"settings.permissions.tool.websearch.description": "Rechercher sur le web",
"settings.permissions.tool.codesearch.title": "Recherche de code",
"settings.permissions.tool.codesearch.description": "Rechercher du code sur le web",
"settings.permissions.tool.external_directory.title": "Répertoire externe",
"settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet",
"settings.permissions.tool.doom_loop.title": "Boucle infernale",
+2 -2
View File
@@ -402,6 +402,8 @@ export const dict = {
"error.page.description": "アプリケーションの読み込み中にエラーが発生しました。",
"error.page.details.label": "エラー詳細",
"error.page.action.restart": "再起動",
"error.page.action.report": "エラーを報告",
"error.page.action.reported": "エラーを報告しました",
"error.page.action.checking": "確認中...",
"error.page.action.checkUpdates": "アップデートを確認",
"error.page.action.updateTo": "{{version}}にアップデート",
@@ -727,8 +729,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "URLからコンテンツを取得",
"settings.permissions.tool.websearch.title": "Web検索",
"settings.permissions.tool.websearch.description": "ウェブを検索",
"settings.permissions.tool.codesearch.title": "コード検索",
"settings.permissions.tool.codesearch.description": "ウェブ上のコードを検索",
"settings.permissions.tool.external_directory.title": "外部ディレクトリ",
"settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス",
"settings.permissions.tool.doom_loop.title": "無限ループ",
+2 -2
View File
@@ -401,6 +401,8 @@ export const dict = {
"error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.",
"error.page.details.label": "오류 세부 정보",
"error.page.action.restart": "다시 시작",
"error.page.action.report": "오류 신고",
"error.page.action.reported": "오류가 신고됨",
"error.page.action.checking": "확인 중...",
"error.page.action.checkUpdates": "업데이트 확인",
"error.page.action.updateTo": "{{version}} 버전으로 업데이트",
@@ -722,8 +724,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기",
"settings.permissions.tool.websearch.title": "웹 검색",
"settings.permissions.tool.websearch.description": "웹 검색",
"settings.permissions.tool.codesearch.title": "코드 검색",
"settings.permissions.tool.codesearch.description": "웹에서 코드 검색",
"settings.permissions.tool.external_directory.title": "외부 디렉터리",
"settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스",
"settings.permissions.tool.doom_loop.title": "무한 반복",
+2 -2
View File
@@ -450,6 +450,8 @@ export const dict = {
"error.page.description": "Det oppstod en feil under lasting av applikasjonen.",
"error.page.details.label": "Feildetaljer",
"error.page.action.restart": "Start på nytt",
"error.page.action.report": "Rapporter feil",
"error.page.action.reported": "Feil rapportert",
"error.page.action.checking": "Sjekker...",
"error.page.action.checkUpdates": "Se etter oppdateringer",
"error.page.action.updateTo": "Oppdater til {{version}}",
@@ -807,8 +809,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Hent innhold fra en URL",
"settings.permissions.tool.websearch.title": "Websøk",
"settings.permissions.tool.websearch.description": "Søk på nettet",
"settings.permissions.tool.codesearch.title": "Kodesøk",
"settings.permissions.tool.codesearch.description": "Søk etter kode på nettet",
"settings.permissions.tool.external_directory.title": "Ekstern mappe",
"settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen",
"settings.permissions.tool.doom_loop.title": "Doom Loop",
+2 -2
View File
@@ -403,6 +403,8 @@ export const dict = {
"error.page.description": "Wystąpił błąd podczas ładowania aplikacji.",
"error.page.details.label": "Szczegóły błędu",
"error.page.action.restart": "Restartuj",
"error.page.action.report": "Zgłoś błąd",
"error.page.action.reported": "Błąd zgłoszony",
"error.page.action.checking": "Sprawdzanie...",
"error.page.action.checkUpdates": "Sprawdź aktualizacje",
"error.page.action.updateTo": "Zaktualizuj do {{version}}",
@@ -729,8 +731,6 @@ export const dict = {
"settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL",
"settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci",
"settings.permissions.tool.websearch.description": "Przeszukiwanie sieci",
"settings.permissions.tool.codesearch.title": "Wyszukiwanie kodu",
"settings.permissions.tool.codesearch.description": "Przeszukiwanie kodu w sieci",
"settings.permissions.tool.external_directory.title": "Katalog zewnętrzny",
"settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu",
"settings.permissions.tool.doom_loop.title": "Zapętlenie",

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