Compare commits

...

703 Commits

Author SHA1 Message Date
Daniel Polito 3320e2bd6c Add Eyes Reaction instead of Comment Working on Github Action (#5072)
publish-github-action / publish (push) Has been cancelled
Update Nix Hashes / update (push) Has been cancelled
2025-12-10 13:35:55 -06:00
Aiden Cline 46cae6260a ignore: fix test 2025-12-10 13:31:45 -06:00
GitHub Action 001abec2f7 chore: format code 2025-12-10 19:31:11 +00:00
Dax Raad d1f87dd45e add models.dev family 2025-12-10 14:30:28 -05:00
Aiden Cline d9f443d333 more interleaved thinking fixes (#5334) 2025-12-10 13:29:26 -06:00
Aiden Cline 1cb69581b4 tweak: set gemini thinkingLevel to high by default 2025-12-10 13:09:27 -06:00
Github Action 91e936d293 Update Nix flake.lock and hashes 2025-12-10 19:04:03 +00:00
Tom Aylott 15b0da75e1 fix: upgrade OpenRouter sdk pkg (#5302)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-10 13:02:26 -06:00
Dax Raad b7a8a3012b fix /provider endpoint to return loaded providers 2025-12-10 13:49:45 -05:00
GitHub Action 41f9fd3741 chore: format code 2025-12-10 18:32:21 +00:00
Nick 36216133cd docs: add opencode-type-inject to ecosystem plugins (#5331) 2025-12-10 12:31:44 -06:00
GitHub Action dcd8a21885 chore: format code
publish-github-action / publish (push) Has been cancelled
Update Nix Hashes / update (push) Has been cancelled
2025-12-10 16:55:19 +00:00
Stanislas 5e499831ff docs: add wakatime plugin to ecosystem page (#5326)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 10:54:45 -06:00
Matt Silverlock c37febd7f2 themes: update orng theme (#5329) 2025-12-10 10:52:58 -06:00
GitHub Action d762a07d19 chore: format code 2025-12-10 16:50:49 +00:00
Noè ae80265995 docs: Added opencode-antigravity-auth to ecosystem documentation (#5303) 2025-12-10 10:50:12 -06:00
Frank 2c8841d034 zen: sync 2025-12-10 11:44:37 -05:00
arc-source-coder 950c273289 tui: only show active MCP server count (#5327) 2025-12-10 10:27:59 -06:00
Connor Adams 6437fd8ed8 docs: configure mise to use latest version instead of pinned version (#5316) 2025-12-10 10:11:48 -06:00
Aiden Cline 5828033cc7 ci: stop zed sync temporarily 2025-12-10 09:57:34 -06:00
Github Action 738af5856a Update Nix flake.lock and hashes 2025-12-10 14:04:52 +00:00
GitHub Action 2654ede6ac chore: format code 2025-12-10 14:04:08 +00:00
Sebastian Herrlinger 505b744f5d bump opentui to v0.1.60, fixing doubled key events on some older terminal emulators and add_buffer leaks for prompt input 2025-12-10 15:03:14 +01:00
Sebastian Herrlinger 56984a15b6 remove input_forward_delete special handling 2025-12-10 14:55:56 +01:00
GitHub Action 60d064eb84 chore: format code 2025-12-10 13:53:21 +00:00
opencode 7ba65ffce6 release: v1.0.143
Update Nix Hashes / update (push) Has been cancelled
2025-12-10 13:53:21 +00:00
Dax Raad 674aa65d71 docs: improve bash and grep tool documentation with clearer usage guidelines 2025-12-10 08:48:41 -05:00
opencode 3e73202ff5 release: v1.0.142
Update Nix Hashes / update (push) Has been cancelled
2025-12-10 13:25:55 +00:00
GitHub Action 98108351b0 ignore: update download stats 2025-12-10 2025-12-10 12:04:47 +00:00
Adam 274f0b8a5a fix: exit aliases 2025-12-10 02:49:54 -06:00
Aiden Cline 7718a1188e fix: revert hook try/catch that surpressed errors 2025-12-10 00:14:24 -06:00
GitHub Action a9ee71cbd7 chore: format code 2025-12-10 06:02:31 +00:00
spoj 30884c610a fix: add Windows support for shell mode (! command) (#5311) 2025-12-10 00:01:56 -06:00
Adam f8558a92e1 fix: types 2025-12-09 21:44:34 -06:00
Adam 53eb3bab9c wip(desktop): progress 2025-12-09 21:39:13 -06:00
Adam 6b4f8f07fe wip(desktop): progress 2025-12-09 21:39:13 -06:00
Adam 400cc012ab feat: provider icon component 2025-12-09 21:39:12 -06:00
GitHub Action 926e3d1746 chore: format code 2025-12-10 03:32:26 +00:00
Brendan Allan 2205184a8e tauri: don't ask to restart separately in updater 2025-12-10 11:31:50 +08:00
Brendan Allan 5730ee1f16 tauri: update macos icon 2025-12-10 11:28:40 +08:00
Brendan Allan c138a6a5da tauri: comment out restart server dialog 2025-12-10 11:18:17 +08:00
Brendan Allan ba42cc5619 fix: use project references for desktop typecheck 2025-12-10 11:15:12 +08:00
Dax Raad 0b36759a79 ci 2025-12-09 22:14:18 -05:00
GitHub Action 2a71b4f37c chore: format code 2025-12-10 03:10:18 +00:00
Timor b38d841c28 feat: add CLI arguments to agent create command for scripting (#5157) 2025-12-09 21:09:45 -06:00
Dax Raad ee71ce63e0 ci 2025-12-10 02:57:39 +00:00
opencode b87c9621c8 release: v1.0.141
Update Nix Hashes / update (push) Has been cancelled
2025-12-10 02:57:38 +00:00
Dax Raad b606c21b23 ci 2025-12-09 21:49:38 -05:00
Dax Raad 46f1477939 ci 2025-12-09 21:49:05 -05:00
Dax Raad d6e27b39ac ci 2025-12-09 21:44:13 -05:00
Dax Raad 181274c3c3 ci: publish with multiple tags instead of using dist-tag
npm dist-tag add command is broken, so publish package multiple times
with different tags directly instead
2025-12-09 21:39:05 -05:00
Dax Raad 3381781b67 core: enable project discovery for experimental builds 2025-12-09 21:23:55 -05:00
Hosenur Rahaman fcf5d84ddc docs: Add portal project to ecosystem documentation (#5300)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 20:23:14 -06:00
Dax Raad a4ed74e9fa ci 2025-12-09 21:16:36 -05:00
Dax Raad e3483d7edc ci 2025-12-09 21:16:23 -05:00
Dax Raad 35d63438f3 ci 2025-12-09 21:16:00 -05:00
Dax Raad 59c5817f6f ci 2025-12-09 21:10:58 -05:00
Dax Raad 8f25ac2e81 sync 2025-12-09 21:06:57 -05:00
Dax Raad e4fed175d4 ci 2025-12-09 21:04:06 -05:00
Dax Raad b784872d06 ci 2025-12-09 21:03:19 -05:00
Dax Raad 51b85bb657 ci 2025-12-09 21:02:04 -05:00
Dax Raad ecf4a0b6f7 ci 2025-12-09 21:01:48 -05:00
Dax Raad b898ecb17f ci 2025-12-09 21:01:39 -05:00
Dax Raad 82176070a2 ci 2025-12-09 20:59:31 -05:00
Dax Raad 73f59bbad0 ci 2025-12-09 20:58:01 -05:00
Dax Raad f350c99788 ci 2025-12-09 20:56:13 -05:00
Dax Raad 7ce319108c ci 2025-12-09 20:54:56 -05:00
Dax Raad 9f32376979 ci 2025-12-09 20:51:54 -05:00
Dax Raad 2d9e75b561 ci 2025-12-09 20:51:25 -05:00
Dax Raad f90a13f874 ci 2025-12-09 20:50:46 -05:00
Adam ee1f652b89 fix: re-enable tauri typecheck 2025-12-09 19:34:07 -06:00
Dax Raad e7f6a040de ci 2025-12-09 20:26:46 -05:00
Dax Raad 5335e80e61 ci 2025-12-09 20:24:31 -05:00
Dax Raad 1e1bc2f31b ci 2025-12-09 20:22:00 -05:00
Dax Raad 00d310b4f5 ci 2025-12-09 20:20:47 -05:00
Dax Raad a2b432e51a ci 2025-12-09 20:19:43 -05:00
Dax Raad da192cee1a ci 2025-12-09 20:17:07 -05:00
Dax Raad 9235598ba6 ci 2025-12-09 19:53:50 -05:00
Dax Raad 836f02a1b3 ci 2025-12-09 19:46:59 -05:00
Dax Raad a2a3bd31e9 ci 2025-12-09 19:46:38 -05:00
Dax Raad 74b6bdb683 ci: combine sdk and format workflows into single generate workflow 2025-12-09 19:41:23 -05:00
GitHub Action 25eeb738e7 chore: regen sdk 2025-12-10 00:33:05 +00:00
Dax Raad 89591c6911 ci 2025-12-09 19:32:30 -05:00
GitHub Action 83d529f128 chore: format code 2025-12-10 00:28:05 +00:00
GitHub Action ad5338e411 chore: regen sdk 2025-12-10 00:27:29 +00:00
Dax Raad 6d84291200 ci fix 2025-12-09 19:26:54 -05:00
Github Action 95229ddb19 Update Nix flake.lock and hashes 2025-12-09 23:56:13 +00:00
Aiden Cline 4f939708f9 ignore: rm chalk 2025-12-09 17:54:53 -06:00
Aiden Cline eab9e8f45d tweak: add OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS 2025-12-09 17:28:34 -06:00
David Hill a22b6d094a fix: add spacer before queued badge 2025-12-09 22:56:44 +00:00
Aiden Cline b9ad1b09cb better interleaved thinking support (#5298) 2025-12-09 16:32:12 -06:00
Adam 2c0101d9cb wip(desktop): progress 2025-12-09 16:23:05 -06:00
Adam f105fe4190 wip(desktop): progress 2025-12-09 16:23:05 -06:00
Dax Raad 28e015bf97 core: fix project icon update handling to preserve existing icon properties 2025-12-09 16:55:26 -05:00
Adam b55f680982 wip(desktop): progress 2025-12-09 15:55:08 -06:00
Adam 76e77d80ac wip(desktop): progress 2025-12-09 15:53:08 -06:00
Adam ffef5db0d9 wip(desktop): progress 2025-12-09 15:46:23 -06:00
Aiden Cline 8f3ce99177 add OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT 2025-12-09 15:44:56 -06:00
Adam 8fa4d39b51 wip(desktop): progress 2025-12-09 15:39:44 -06:00
Adam 7fc1058a8f wip(desktop): progress 2025-12-09 15:32:36 -06:00
ry2009 24969bb0a3 feat: add Biome LSP support (#5290) 2025-12-09 15:31:13 -06:00
Adam 582ea6d4ee wip(desktop): progress 2025-12-09 15:24:11 -06:00
Adam 43ec725d60 wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam 8de1025b54 wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam e755286636 wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam dbffa05550 wip(desktop): progress 2025-12-09 15:21:45 -06:00
Aiden Cline 034de9b81c ci: fix sdk workflow 2025-12-09 15:08:30 -06:00
Nick 55de10191b fix: resolve 'latest' to actual version when caching plugins (#5292) 2025-12-09 15:07:59 -06:00
Dax Raad a745dac54b format on write 2025-12-09 16:06:57 -05:00
GitHub Action 496675e969 chore: format code 2025-12-09 21:05:09 +00:00
GitHub Action e234824b37 chore: regen sdk 2025-12-09 21:04:37 +00:00
Dax Raad 40609faeac fix types 2025-12-09 16:03:58 -05:00
Dax Raad 3433e5bbd1 fix tests 2025-12-09 16:03:33 -05:00
GitHub Action 43dc138d88 chore: format code 2025-12-09 20:53:06 +00:00
GitHub Action 12c82fc5d6 chore: regen sdk 2025-12-09 20:52:31 +00:00
Dax Raad edbe015078 discover logic 2025-12-09 15:51:55 -05:00
GitHub Action 5d248a1d08 chore: format code 2025-12-09 20:49:25 +00:00
GitHub Action 3d8e8dcdec chore: regen sdk 2025-12-09 20:48:49 +00:00
Dax Raad f8bffeaca1 sync 2025-12-09 15:48:22 -05:00
GitHub Action eeb986153a chore: format code 2025-12-09 20:44:09 +00:00
GitHub Action 3c98711df1 chore: regen sdk 2025-12-09 20:43:35 +00:00
Dax Raad 40b2a24753 core: fix project event emission to include updated worktree data 2025-12-09 15:41:38 -05:00
Dax Raad 0dc4cc396c test fixes 2025-12-09 15:41:38 -05:00
GitHub Action 314923045f chore: format code 2025-12-09 20:26:02 +00:00
GitHub Action 49363dae9e chore: regen sdk 2025-12-09 20:25:21 +00:00
Aiden Cline dfd4e716fd fix: read when file is svg 2025-12-09 14:24:30 -06:00
GitHub Action 71e39b0648 chore: format code 2025-12-09 20:20:03 +00:00
GitHub Action 262d6e1740 chore: regen sdk 2025-12-09 20:19:27 +00:00
Dax Raad 71eced2fc9 core: fix project creation to include updated timestamp 2025-12-09 15:18:55 -05:00
Dax Raad 7b5a485d65 core: make project updated timestamp optional to support legacy project data 2025-12-09 15:18:55 -05:00
GitHub Action 59d96f6928 chore: format code 2025-12-09 20:17:37 +00:00
GitHub Action 97ae5039de chore: regen sdk 2025-12-09 20:16:59 +00:00
Aiden Cline 099816efeb ci: fix sdk gen 2025-12-09 14:16:22 -06:00
GitHub Action 5b6b2eab00 chore: format code 2025-12-09 20:12:07 +00:00
GitHub Action 4a5c27f175 chore: regen sdk 2025-12-09 20:11:32 +00:00
Dax Raad 30598c8766 core: fix project icon update to preserve existing icon properties 2025-12-09 15:11:00 -05:00
GitHub Action fad7c06ad9 chore: format code 2025-12-09 20:07:32 +00:00
GitHub Action bebbbb4856 chore: regen sdk 2025-12-09 20:06:57 +00:00
Dax Raad 31c49cd6c7 core: add automatic project icon discovery from favicon/logo files 2025-12-09 15:06:24 -05:00
GitHub Action 5f300fa9dc chore: format code 2025-12-09 19:54:54 +00:00
GitHub Action 453b445796 chore: regen sdk 2025-12-09 19:54:19 +00:00
Dax Raad 12d730ec45 fix ci 2025-12-09 14:53:47 -05:00
Dax Raad 839b8903ef rework project loading 2025-12-09 14:41:14 -05:00
Dax Raad c4d4c5cf7c rename bus 2025-12-09 14:32:09 -05:00
GitHub Action 11598fe8ba chore: format code 2025-12-09 19:31:07 +00:00
GitHub Action 8f1e439c9d chore: regen sdk 2025-12-09 19:30:32 +00:00
Adam bd3cef5398 feat: add color to project 2025-12-09 13:29:59 -06:00
Adam 98790fc143 wip(desktop): progress 2025-12-09 13:24:37 -06:00
Dax Raad 6d86da7e46 add project.name/icon 2025-12-09 13:57:18 -05:00
GitHub Action 54e67e3c3a chore: format code 2025-12-09 18:37:14 +00:00
GitHub Action 1d8b17b81e chore: regen sdk 2025-12-09 18:36:38 +00:00
Adam 057a387c34 wip(desktop): progress 2025-12-09 12:36:06 -06:00
GitHub Action d5ebb9b7f7 chore: format code 2025-12-09 18:10:31 +00:00
GitHub Action f3339a30d5 chore: regen sdk 2025-12-09 18:07:43 +00:00
Aiden Cline 3fe1711166 core: add test to prevent MCP headers regression when OAuth is enabled
Custom headers configured for remote MCP servers were being silently
dropped when OAuth was enabled (the default). This test ensures headers
are always sent to MCP servers regardless of OAuth configuration.
2025-12-09 12:06:40 -06:00
opencode 874b15d875 release: v1.0.138
Update Nix Hashes / update (push) Has been cancelled
2025-12-09 18:05:21 +00:00
GitHub Action 8f13597215 chore: format code 2025-12-09 17:53:53 +00:00
GitHub Action 517737f86d chore: regen sdk 2025-12-09 17:53:14 +00:00
Adam f223274d97 wip(desktop): progress 2025-12-09 11:52:43 -06:00
GitHub Action 58222d22d1 chore: format code 2025-12-09 17:45:35 +00:00
Jay cd8a956895 docs: Modify documentation for SDK ecosystem references
Updated link text to refer to community-built projects.
2025-12-09 12:44:33 -05:00
GitHub Action 7409aca847 chore: regen sdk 2025-12-09 17:43:58 +00:00
Jay 537068b598 docs: Update community plugins reference in documentation 2025-12-09 12:43:29 -05:00
GitHub Action 6311c89db1 chore: format code 2025-12-09 17:39:25 +00:00
Aiden Cline 75ef0772dd ci: sdk stuff 2025-12-09 11:38:22 -06:00
GitHub Action 5f983cfa1f chore: regen sdk 2025-12-09 17:38:01 +00:00
Jay a27f3af40b docs: Add submission note for OpenCode projects
Added a note about submitting projects to the list.
2025-12-09 12:37:34 -05:00
GitHub Action db9e73133a chore: format code 2025-12-09 17:37:32 +00:00
GitHub Action 4ee21b7725 chore: regen sdk 2025-12-09 17:34:45 +00:00
Aiden Cline 7a124aaf27 docs: fix name 2025-12-09 11:34:15 -06:00
GitHub Action 143d310216 chore: format code 2025-12-09 17:26:28 +00:00
Aiden Cline 8cdb0c0cf0 docs: OpenCode ecosystem (#5287)
Co-authored-by: GitHub Action <action@github.com>
2025-12-09 11:25:28 -06:00
GitHub Action 2b7a227530 chore: format code 2025-12-09 17:10:13 +00:00
GitHub Action a5c92d307f chore: regen sdk 2025-12-09 17:09:35 +00:00
Adam e099d53526 wip(desktop): progress 2025-12-09 11:09:00 -06:00
Adam 7df4d7aeed wip(desktop): progress 2025-12-09 11:09:00 -06:00
GitHub Action dda3278752 chore: format code 2025-12-09 16:53:58 +00:00
GitHub Action 1005821d6f chore: regen sdk 2025-12-09 16:53:24 +00:00
Dax Raad b2f59e9d08 core: add project update timestamps to track when projects were last modified
Projects now track when they were last updated, making it easier for users
to see recent activity and identify stale projects in their workspace.
2025-12-09 11:52:53 -05:00
GitHub Action 80799b57fe chore: format code 2025-12-09 15:53:48 +00:00
GitHub Action 5b956eee91 chore: regen sdk 2025-12-09 15:53:12 +00:00
André Cruz 0676c0c917 fix(mcp): send custom headers regardless of OAuth settings (#5273)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-09 09:52:39 -06:00
GitHub Action 5aff369b73 chore: format code 2025-12-09 15:47:54 +00:00
GitHub Action d9a3aa8a5c chore: regen sdk 2025-12-09 15:47:19 +00:00
Ravi Kumar a0c11b958b feat(telemetry): Add userId and sessionId metadata to experimental_telemetry (#5279) 2025-12-09 09:46:48 -06:00
GitHub Action 7e12e61209 chore: format code 2025-12-09 12:16:05 +00:00
GitHub Action 0c7d66b26a chore: regen sdk 2025-12-09 12:15:30 +00:00
Adam 7aa60add45 chore: cleanup 2025-12-09 06:14:58 -06:00
Adam e4cd93868b chore: cleanup 2025-12-09 06:14:22 -06:00
Adam 0963bd84d1 fix(tui): order 2025-12-09 06:13:36 -06:00
Adam 59237b71b6 wip(desktop): progress 2025-12-09 06:12:09 -06:00
Adam fcc47597b2 wip(desktop): progress 2025-12-09 06:12:09 -06:00
Adam 3ab22fbb89 fix: diff scroll gutter 2025-12-09 06:12:09 -06:00
GitHub Action d2038ee3e5 ignore: update download stats 2025-12-09 2025-12-09 12:04:56 +00:00
Github Action dcf38220dc Update Nix flake.lock and hashes 2025-12-09 11:15:34 +00:00
GitHub Action 2693eb674a chore: format code 2025-12-09 11:15:02 +00:00
GitHub Action b9a86a9abd chore: regen sdk 2025-12-09 11:14:23 +00:00
Brendan Allan 9513ab51b8 fix: use ts project references for desktop and tauri 2025-12-09 19:13:51 +08:00
Github Action 921da0663e Update Nix flake.lock and hashes 2025-12-09 09:17:58 +00:00
GitHub Action d25cea2c61 chore: format code 2025-12-09 09:17:36 +00:00
GitHub Action 853d4d6261 chore: regen sdk 2025-12-09 09:17:00 +00:00
Brendan Allan 4cca4c66f1 desktop: move updater logic to js 2025-12-09 17:16:24 +08:00
GitHub Action 71fbee39fb chore: format code 2025-12-09 07:26:55 +00:00
GitHub Action 1152eadfd4 chore: regen sdk 2025-12-09 07:26:16 +00:00
Aiden Cline 14183f06c6 core: refactor providerOptions function to accept Provider.Model for cleaner API 2025-12-09 01:25:36 -06:00
GitHub Action 54ad6cfcfe chore: format code 2025-12-09 06:42:56 +00:00
Aiden Cline 91a56095d9 ci: run format workflow after sdk workflow completion 2025-12-09 00:41:40 -06:00
GitHub Action 4cc3af0447 chore: regen sdk 2025-12-09 06:37:35 +00:00
Aiden Cline 676b5fbfa3 core: remove unused fzf dependency to address CVE
- Eliminates fzf binary dependency that was no longer used after file search overhaul
- Removes fzf from Nix package configuration and Arch Linux PKGBUILD dependencies
2025-12-09 00:36:33 -06:00
GitHub Action 48ab55231a chore: format code 2025-12-09 03:18:47 +00:00
terakael c53d6e6f4a tui: add permission indicator to footer (#4813)
Co-authored-by: terakael <terakael@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-08 21:18:17 -06:00
opencode a4df18d7c5 release: v1.0.137
Update Nix Hashes / update (push) Has been cancelled
2025-12-09 03:00:50 +00:00
GitHub Action 84d7ea095e chore: regen sdk 2025-12-09 02:17:38 +00:00
Matt Silverlock 632f32434c themes: add new orng theme (#5267)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-08 20:17:10 -06:00
GitHub Action c39bb83b78 chore: format code 2025-12-09 02:07:00 +00:00
Aiden Cline e05854a38a ci: ignore 2025-12-08 20:06:27 -06:00
GitHub Action c896b78a39 chore: regen sdk 2025-12-09 01:07:23 +00:00
Dax Raad 239fd157e5 ci 2025-12-08 20:06:36 -05:00
Github Action ec64591822 Update Nix flake.lock and hashes 2025-12-09 00:52:48 +00:00
GitHub Action bbcbbbf16d chore: format code 2025-12-09 00:52:05 +00:00
Sebastian Herrlinger 798731e2bd bump opentui to v0.1.59, allowing shift+space insert as char again 2025-12-09 01:51:27 +01:00
GitHub Action 86ef7e45e4 chore: regen sdk 2025-12-08 23:46:24 +00:00
Aiden Cline 2b6278f950 ci: ignore 2025-12-08 17:45:50 -06:00
GitHub Action 88980eb8b4 chore: format code 2025-12-08 22:44:57 +00:00
Aiden Cline af2ae4ddc4 ignore: mark ripgrep as an sst/tap/opencode depends_on 2025-12-08 16:42:32 -06:00
GitHub Action 0748f8c1f9 chore: regen sdk 2025-12-08 22:16:30 +00:00
Ariane Emory 2c79dff7e4 fix: improve sidebar working directory colour contrast with modified files (resolves #5184 (#5188) 2025-12-08 16:15:40 -06:00
Github Action cee17efd41 Update Nix flake.lock and hashes 2025-12-08 22:11:38 +00:00
GitHub Action 043ff20a83 chore: format code 2025-12-08 22:10:46 +00:00
Aiden Cline 092f609332 fix: parcel watcher musl support 2025-12-08 16:08:53 -06:00
GitHub Action 57448b9ba2 chore: regen sdk 2025-12-08 21:29:05 +00:00
Ravi Kumar 8cbfd3c161 fix: apply provider filtering to /connect endpoint (#5233) 2025-12-08 15:28:32 -06:00
GitHub Action b5df0123c1 chore: format code 2025-12-08 21:05:09 +00:00
Ariane Emory 064cfa96b6 fix: sort sessions by updated time (resolves #5074) (#5080) 2025-12-08 15:04:39 -06:00
Github Action 16873a6b8c Update Nix flake.lock and hashes 2025-12-08 21:00:43 +00:00
GitHub Action 33c5055131 chore: regen sdk 2025-12-08 20:59:55 +00:00
Sebastian Herrlinger 7471e025c1 bump opentui to v0.1.58, fixing diff color issues in light themes 2025-12-08 21:59:09 +01:00
GitHub Action e4a2bb23c7 chore: format code 2025-12-08 20:34:25 +00:00
Shoubhit Dash 921a913b2a add ocaml lsp support (#5230) 2025-12-08 14:33:45 -06:00
GitHub Action a578783797 chore: regen sdk 2025-12-08 20:06:51 +00:00
Sebastian Herrlinger 405aa27c57 fix colors for dialog prompt (like session rename prompt in light theme) 2025-12-08 21:06:03 +01:00
Sebastian Herrlinger 2a7c72f220 set theme text color for no matching items in auto complete 2025-12-08 20:59:00 +01:00
GitHub Action 323448812d chore: format code 2025-12-08 18:06:48 +00:00
Aiden Cline 75fb55e981 docs: lsp & formatters 2025-12-08 12:05:25 -06:00
GitHub Action 2cd1352451 chore: regen sdk 2025-12-08 18:02:54 +00:00
rari404 c4bc264ef9 feat: add terraform-ls language server and formatter (#5243) 2025-12-08 12:02:25 -06:00
GitHub Action d505f87c10 chore: format code 2025-12-08 17:40:24 +00:00
rari404 0505d9b8aa feat: add bash-language-server LSP (#5246) 2025-12-08 11:39:49 -06:00
GitHub Action 5a64fd79e3 chore: regen sdk 2025-12-08 17:37:58 +00:00
Shoubhit Dash 0904e20387 add ocamlformat support (#5229) 2025-12-08 11:37:30 -06:00
GitHub Action 60e2cacfa1 chore: format code 2025-12-08 17:31:56 +00:00
Daniel Polito 12db150690 feat: ability to toggle MCP Servers in TUI (#4509) 2025-12-08 11:31:22 -06:00
GitHub Action cbb9741a07 chore: regen sdk 2025-12-08 17:28:14 +00:00
Jérôme Benoit 8a00917ad5 fix: SAP AI Core Vercel AI SDK v2 support (#5180)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
2025-12-08 11:27:47 -06:00
GitHub Action 267addffeb chore: format code 2025-12-08 17:07:18 +00:00
rari404 3141b2f0a7 feat: add theme support for thinking text opacity (#5240) 2025-12-08 11:06:46 -06:00
GitHub Action 3a5161095f chore: regen sdk 2025-12-08 16:27:32 +00:00
Bishal Roy f698e3af23 feat: add catppuccin-macchiato theme (#5225) 2025-12-08 10:27:03 -06:00
Adam 7bcff6da1a feat: resize handle 2025-12-08 10:24:26 -06:00
Adam 62e2539696 wip(desktop): progress 2025-12-08 10:24:26 -06:00
GitHub Action f07347403f chore: regen sdk 2025-12-08 15:48:52 +00:00
Adam da3f48ecfc fix: opencode web command 2025-12-08 09:48:21 -06:00
Adam 3eac4f7b8c revert: opencode web command changes
This reverts commit fb8a5a79d6.
2025-12-08 09:47:43 -06:00
GitHub Action b2d798bba6 chore: format code 2025-12-08 15:40:18 +00:00
Adam fb8a5a79d6 fix: opencode web command 2025-12-08 09:39:41 -06:00
GitHub Action d70b86ef34 chore: regen sdk 2025-12-08 15:35:44 +00:00
Adam d97ae51689 fix: opencode web command 2025-12-08 09:35:12 -06:00
GitHub Action b6a648780c chore: format code 2025-12-08 15:19:26 +00:00
Adam adea39e54e fix: opencode web command 2025-12-08 09:18:46 -06:00
Adam c8e5f2b8d7 fix: bump localstorage 2025-12-08 09:05:26 -06:00
GitHub Action 20e458bb95 chore: regen sdk 2025-12-08 12:48:30 +00:00
Adam 8307f4378f chore: identity cleanup, vscode ext images 2025-12-08 06:47:57 -06:00
Github Action c63ee22905 Update Nix flake.lock and hashes 2025-12-08 12:25:44 +00:00
GitHub Action 94ba575cf7 chore: format code 2025-12-08 12:24:56 +00:00
Adam 856443c9f2 feat: better code and diff perf 2025-12-08 06:24:24 -06:00
GitHub Action 0e721f86ce ignore: update download stats 2025-12-08 2025-12-08 12:04:36 +00:00
GitHub Action 87b495018a chore: regen sdk 2025-12-08 05:46:02 +00:00
Brendan Allan 9f6e350a85 Export DesktopInterface from desktop and add PlatformContext 2025-12-08 13:43:36 +08:00
GitHub Action 20b4f36d18 chore: format code 2025-12-08 04:54:08 +00:00
Justin Vogt 7118b7d56d docs: Update "model" inheritance for agents (#4894)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-07 22:53:41 -06:00
GitHub Action ef6f5aa13e chore: regen sdk 2025-12-08 04:43:13 +00:00
Aiden Cline 7cf876df54 bump plugin versions 2025-12-07 22:42:35 -06:00
GitHub Action cd569426ec chore: format code 2025-12-08 04:22:50 +00:00
ry2009 9793546702 fix: ensure Auth.all returns valid objs (#5128) 2025-12-07 22:22:21 -06:00
GitHub Action a07b79b048 chore: regen sdk 2025-12-08 04:21:06 +00:00
Ariane Emory 7d446e570b fix: Sort themes in the /theme modal alphabetically (resolves #5217) (#5219)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-07 22:20:40 -06:00
GitHub Action ff9ba8ea95 chore: format code 2025-12-08 04:20:07 +00:00
opencode-agent[bot] 94d664ba46 docs: azure content filter note (#5212)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-12-07 22:19:39 -06:00
GitHub Action fff1d6414c chore: regen sdk 2025-12-08 03:54:26 +00:00
Aiden Cline d38bb05c69 pdf support in read tool (#5222)
Co-authored-by: ammi1378 <ammi1378@users.noreply.github.com>
2025-12-07 21:54:00 -06:00
GitHub Action 83bfba8729 chore: format code 2025-12-08 03:49:13 +00:00
Aiden Cline cd706d0999 ci: fix fmt 2025-12-07 21:48:41 -06:00
Adam e2d86e057e fix: use diff context instead of prop drilling 2025-12-07 21:21:18 -06:00
GitHub Action 92f5f4d4c1 chore: regen sdk 2025-12-08 02:56:09 +00:00
Dax Raad 090e0b5636 openapi 2025-12-07 21:55:36 -05:00
Dax Raad 1cdd92361b ci 2025-12-07 21:51:22 -05:00
GitHub Action 2c7f3a0017 chore: regen sdk 2025-12-08 02:29:26 +00:00
Aiden Cline 1b1e4a6485 ci: fix fmt 2025-12-07 20:28:50 -06:00
GitHub Action ef17940577 chore: format code 2025-12-08 02:11:26 +00:00
Aiden Cline a254b69266 tweak: additional error msg parsing case 2025-12-07 20:10:54 -06:00
GitHub Action 1c353eba0b chore: regen sdk 2025-12-08 01:48:52 +00:00
Aiden Cline cf2d21499e ignore: update description 2025-12-07 19:48:23 -06:00
GitHub Action 61a5d04573 chore: format code 2025-12-08 01:47:31 +00:00
Ariane Emory 3d012c1a87 fix: toggle timestamps now properly hides/shows timestamps when toggled (resolves #5142) (#5145) 2025-12-07 19:47:02 -06:00
GitHub Action 3d2962ce88 chore: regen sdk 2025-12-08 01:44:32 +00:00
franlol c47e33e3ed feat: add experimental.text.complete plugin hook (#4962) 2025-12-07 19:44:04 -06:00
GitHub Action 935970317d chore: format code 2025-12-08 01:13:22 +00:00
Patrick Erichsen 65fa98ab3f fix(cli): prevent help text wrapping (#5185) 2025-12-07 19:12:54 -06:00
GitHub Action 826eb61415 chore: regen sdk 2025-12-08 00:58:40 +00:00
Dax Raad 900f245e30 playing with sdk docs 2025-12-07 19:58:04 -05:00
Github Action 4341cc9fe5 Update Nix flake.lock and hashes 2025-12-08 00:05:25 +00:00
GitHub Action cd4687b3e4 chore: format code 2025-12-08 00:04:40 +00:00
Dax 616265d0ef v2 SDK (#5216)
Co-authored-by: GitHub Action <action@github.com>
2025-12-07 19:04:14 -05:00
GitHub Action 978b09c561 chore: format code 2025-12-07 20:53:12 +00:00
Dax Raad 2b6ea5d8c3 remove outdated SDKs 2025-12-07 15:52:27 -05:00
Github Action 875bc052c1 Update Nix flake.lock and hashes 2025-12-07 20:49:39 +00:00
GitHub Action 6bb0108f5f chore: regen sdk 2025-12-07 20:47:51 +00:00
André Cruz 29a40e2267 feat(mcp): add OAuth authentication support for remote MCP servers (#5014) 2025-12-07 15:47:27 -05:00
GitHub Action 4a6dbfdfea chore: regen sdk 2025-12-07 19:23:56 +00:00
rari404 26fa90c4f4 feat: add uninstall command (#5208) 2025-12-07 13:23:30 -06:00
GitHub Action e073efbb02 chore: format code 2025-12-07 19:08:48 +00:00
secretninjaman 28ffb333fb fix: use basename for shell detection to support non-standard paths (#5205)
Co-authored-by: Ayato French <a@ayatofrench.com>
2025-12-07 13:08:26 -06:00
GitHub Action d9d29597ec chore: format code 2025-12-07 19:06:37 +00:00
Aiden Cline 0eadf18120 ignore: add test for provider url case 2025-12-07 13:06:12 -06:00
GitHub Action 83b6d8f923 chore: regen sdk 2025-12-07 19:01:34 +00:00
Aiden Cline a3c2278164 fix: provider url merging logic 2025-12-07 13:01:05 -06:00
GitHub Action 81ec8e3133 chore: format code 2025-12-07 18:48:45 +00:00
Dax Raad 329251fec8 enable zoom hotkeys 2025-12-07 13:48:12 -05:00
GitHub Action 6cc646b75d chore: regen sdk 2025-12-07 18:42:48 +00:00
Brendan Allan 7d131f75e6 Desktop macOS codesigning and notarization (#5154)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Dax Raad <d@ironbay.co>
2025-12-07 13:42:23 -05:00
GitHub Action acd1488468 chore: format code 2025-12-07 18:26:05 +00:00
Mikheil Berishvili b01398d9d3 fix: autocomplete popup repositions on window resize (#5196) 2025-12-07 12:25:35 -06:00
GitHub Action ee34e486e1 chore: regen sdk 2025-12-07 18:18:10 +00:00
Aiden Cline c3636f9133 ci: fix format 2025-12-07 12:17:40 -06:00
GitHub Action 4df3ea2fdb chore: format code 2025-12-07 15:33:51 +00:00
Carsten Kragelund Jørgensen 0c96c2cc9e fix: update zed agent server linux url to tar.gz (#5194) 2025-12-07 09:33:23 -06:00
GitHub Action 80027d0a9b ignore: update download stats 2025-12-07 2025-12-07 12:04:11 +00:00
GitHub Action a0886694a4 chore: regen sdk 2025-12-07 05:55:31 +00:00
Aiden Cline 476e3ca9f0 tweak: make bash give agent more awareness of cwd, bump default timeout, drop max timeout (#5140) 2025-12-06 23:55:07 -06:00
GitHub Action 9740d97e98 chore: format code 2025-12-07 05:45:36 +00:00
Arindam Majumder c9ea340c86 docs: Nebius Token Factory provider documentation (#2997) 2025-12-06 23:45:08 -06:00
GitHub Action 89121e85d8 chore: regen sdk 2025-12-07 05:33:26 +00:00
Aiden Cline 6ff4106a1e core: prevent sessions from disappearing after git init
Previously, sessions created in a non-git directory would disappear from
the session picker after running git init and making the first commit.
This happened because the migration logic ran prematurely before a stable
project ID existed.
2025-12-06 23:32:47 -06:00
GitHub Action bf15b00501 chore: format code 2025-12-06 23:25:16 +00:00
Aiden Cline 2bb9b8d59a ci: fix review 2025-12-06 17:24:44 -06:00
GitHub Action 9888319fa1 chore: regen sdk 2025-12-06 23:23:24 +00:00
Dax Raad 7d9d39d4fb ci 2025-12-06 18:22:55 -05:00
GitHub Action 50f3dac1e6 chore: format code 2025-12-06 23:22:04 +00:00
Dax Raad 6ea4900f84 sync 2025-12-06 18:21:32 -05:00
GitHub Action f0fff56cac chore: regen sdk 2025-12-06 23:19:47 +00:00
Dax Raad 3f74236db9 tui: add visual separator between username and timestamp for better readability 2025-12-06 18:19:18 -05:00
Dax Raad e224c55556 tui: prevent deprecated models from appearing in model picker 2025-12-06 18:18:45 -05:00
GitHub Action 701c62c46a chore: format code 2025-12-06 23:15:39 +00:00
Aiden Cline b957004391 ci: fix format 2025-12-06 17:15:04 -06:00
GitHub Action b83e8b911b chore: regen sdk 2025-12-06 22:05:07 +00:00
Aiden Cline 7cdc97bc58 fix: reduce overhead of task tool metadata 2025-12-06 16:04:33 -06:00
GitHub Action f5e50e69fa chore: format code 2025-12-06 20:49:33 +00:00
Saatvik Arya edaf18830f feat(tui): add dynamic terminal window title (#5112) 2025-12-06 14:49:11 -06:00
GitHub Action 0e52ecdd26 chore: regen sdk 2025-12-06 20:48:46 +00:00
Anchit Bajaj 29b774ca91 fix: update description to lowercase for ACP command (to be consistent with other commands) (#5137) 2025-12-06 14:48:21 -06:00
GitHub Action 05df229a0d chore: format code 2025-12-06 20:43:15 +00:00
Ariane Emory 41ca76614f fix: make timestamp toggle text dynamic in command list (resolves #5106) (#5108) 2025-12-06 14:42:46 -06:00
GitHub Action ba0f759454 chore: regen sdk 2025-12-06 20:30:00 +00:00
Ben Vargas 9b787abea9 feat: restore experimental flag for websearch/codesearch tools (#5132)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-06 14:29:34 -06:00
GitHub Action a350479579 chore: format code 2025-12-06 20:28:25 +00:00
Cody Rayment 616f237fba docs: update server API reference with complete endpoint list (#5136) 2025-12-06 14:28:02 -06:00
Dak Washbrook dbf1922ef7 feat: add mercury.com theme (#5141) 2025-12-06 14:27:56 -06:00
Github Action af43e5d691 Update Nix flake.lock and hashes 2025-12-06 12:45:14 +00:00
GitHub Action 64624dafb8 chore: regen sdk 2025-12-06 12:44:22 +00:00
Adam 9409b40001 fix: terminal serialization and isolation 2025-12-06 06:43:53 -06:00
GitHub Action 9793feced8 ignore: update download stats 2025-12-06 2025-12-06 12:04:12 +00:00
GitHub Action 0b80ddd4a9 chore: format code 2025-12-06 11:53:20 +00:00
Adam a9a5688513 fix: exclude dist 2025-12-06 05:52:47 -06:00
Adam e54eb9fc77 chore: update tauri update pub key 2025-12-06 05:49:05 -06:00
GitHub Action 70a715f109 chore: regen sdk 2025-12-06 01:21:17 +00:00
Dax Raad 541f8decc4 sync 2025-12-05 20:20:47 -05:00
GitHub Action 1f3c1cd3a4 chore: format code 2025-12-06 01:14:58 +00:00
Dax fc48f669a4 Documentation edits made through Mintlify web editor 2025-12-05 20:14:29 -05:00
Dax c3cb83d1b2 Documentation edits made through Mintlify web editor 2025-12-05 20:14:10 -05:00
GitHub Action 6147cac19e chore: regen sdk 2025-12-06 01:13:23 +00:00
Dax Raad 0249d7bf98 ci 2025-12-05 20:12:52 -05:00
Dax Raad 19e3b64824 openapi route 2025-12-05 20:11:01 -05:00
GitHub Action ed42d2a4a2 chore: format code 2025-12-06 01:09:36 +00:00
Dax Raad 676e4f9f1b openapi json 2025-12-05 20:07:24 -05:00
GitHub Action 1d0e38e022 chore: format code 2025-12-06 01:02:41 +00:00
Dax Raad c825a314f4 openapi generate 2025-12-05 20:01:55 -05:00
David Hill f7e9afb975 fix: add extra line break to install page 2025-12-05 23:43:26 +00:00
Adam ad9742a60c feat: consistent (updated) social share images 2025-12-05 15:42:19 -06:00
GitHub Action 2ff967dabb chore: format code 2025-12-05 21:42:15 +00:00
Dax Raad 168b66ef2a ignore: docs test 2025-12-05 16:41:35 -05:00
GitHub Action 42f6ed6245 chore: regen sdk 2025-12-05 21:02:17 +00:00
Aiden Cline 3b0ddf3e5f ci: regen sdk instead of failing tests 2025-12-05 15:01:41 -06:00
GitHub Action a3e8d555af chore: format code 2025-12-05 20:50:13 +00:00
franlol d907c34e93 feat: add optional scrollbar to the session chat (#5116)
Co-authored-by: Sebastian <hasta84@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 14:49:05 -06:00
opencode 8b1da87a55 release: v1.0.134
Update Nix Hashes / update (push) Has been cancelled
2025-12-05 20:46:49 +00:00
Github Action 7eb4e29db5 Update Nix flake.lock and hashes 2025-12-05 20:41:00 +00:00
Brendan Allan 19d739b3ed OpenCode Desktop app (#5044)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-05 14:39:49 -06:00
Shantur Rathore 2f082043cf tweak: bash tool improve output metadata for agent consumption, fix small timeout issue (#5131) 2025-12-05 13:56:56 -06:00
Nathan Thomas dc2d711b19 feat: add max steps for supervisor and sub-agents (#4062)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 12:26:44 -06:00
Adam d731c16959 fix(desktop): terminal cursor position 2025-12-05 12:05:30 -06:00
Adam 10cd40403c fix(desktop): prompting 2025-12-05 10:51:35 -06:00
Aiden Cline 46476e594a ignore: regen sdk 2025-12-05 10:50:14 -06:00
Noam Bressler 3eaeb48de1 add experimental.open_telemetry config option to enable OTEL spans (#4978)
Co-authored-by: noamzbr <noamzbr@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 10:48:22 -06:00
Adam 49655aef0c fix(desktop): clone pty session on reconnect 2025-12-05 10:30:48 -06:00
Adam 775c78108c fix(desktop): new session not selecting tab 2025-12-05 10:30:48 -06:00
Anthony Shew 84fde5ee99 feat(theme): Vercel (#5119)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 10:01:20 -06:00
Julian Visser cccafbba58 fix: #5064 ProviderInitError github-copilot-enterprise (#5123) 2025-12-05 09:53:32 -06:00
Dax Raad 7b38b4c721 do not use required pty for local dev 2025-12-05 10:39:44 -05:00
Frank 08ddacce55 Zen: add codex max 2025-12-05 09:05:00 -05:00
GitHub Action 3c66efdd73 ignore: update download stats 2025-12-05 2025-12-05 12:04:29 +00:00
Aiden Cline e68039a889 ignore: tweak 2025-12-05 01:00:47 -06:00
Aiden Cline e3a734e9eb fix: ensure projects that go from having no commits to having commits have sessions migrated (#5105)
Co-authored-by: GitHub Action <action@github.com>
2025-12-05 00:49:07 -06:00
Aiden Cline e9f2d6878e ci: keybinds 2025-12-05 00:22:28 -06:00
ry2009 3d930c0341 tui: wrap dialog option descriptions (#5083) 2025-12-05 00:19:48 -06:00
Aiden Cline ac227be928 ignore: cmd tweak 2025-12-05 00:11:47 -06:00
GitHub Action 93d893d414 chore: format code 2025-12-05 05:14:21 +00:00
Aiden Cline 3c493dd554 ignore: rm slop commnand (only for opencode repo this isnt shipping) 2025-12-04 23:13:41 -06:00
Aiden Cline 848cfda06e fix: ensure that vcs is still set to git even if no commits in repo 2025-12-04 23:02:11 -06:00
Aiden Cline bc806f538c ci: review 2025-12-04 23:02:11 -06:00
Dax Raad 0ff47b1fb8 increase default scroll speed 2025-12-04 23:54:46 -05:00
Dax Raad 2995db2d11 fix 2025-12-04 23:48:32 -05:00
Dax Raad d81f631b55 fix pty builds 2025-12-04 23:47:57 -05:00
Jérôme Benoit 2cb12b8db2 fix: add getModel to SAP AI Core provider for correct SDK initialization (#5086)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
2025-12-04 22:43:22 -06:00
opencode-agent[bot] b658029e31 docs: llama.cpp docs: limit moved under model (#5089)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-12-04 22:39:13 -06:00
Dax Raad 0c9f6928af ignore: fix provider credentials query for BYOK
Provider credentials field was being selected from ProviderTable even when the table wasn't joined (when byokProvider was undefined). Now the join is conditional - when byokProvider exists, we join and get the credentials; when it doesn't, the join condition is always false so provider remains null.
2025-12-04 22:57:39 -05:00
Frank 5ac9d189c9 zen: fix byok 2025-12-04 21:53:33 -05:00
Github Action f5372b6081 Update Nix flake.lock and hashes 2025-12-05 02:33:39 +00:00
GitHub Action 3c73fc96d5 chore: format code 2025-12-05 02:33:01 +00:00
Adam 9b3c90e3fe Reapply "feat(desktop): terminal pane (#5081)"
This reverts commit 03a0312e5b.
2025-12-04 20:32:08 -06:00
Github Action 378cbb30e2 Update Nix flake.lock and hashes 2025-12-04 22:02:17 +00:00
opencode d88c862503 release: v1.0.133
Update Nix Hashes / update (push) Has been cancelled
2025-12-04 22:02:17 +00:00
Aiden Cline 03a0312e5b Revert "feat(desktop): terminal pane (#5081)"
This reverts commit def9fc463c.
2025-12-04 15:57:01 -06:00
Adam def9fc463c feat(desktop): terminal pane (#5081)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Dax Raad <d@ironbay.co>
2025-12-04 15:37:29 -06:00
Dax Raad 267cf11962 core: add test for custom model npm package inheritance 2025-12-04 16:30:54 -05:00
Aiden Cline 999f3dfac2 tweak: bash tool description re commit stuff 2025-12-04 15:27:23 -06:00
Cason Adams 4f734f6bd8 docs: add CodeCompanion.nvim integration instructions (#5079) 2025-12-04 14:49:51 -06:00
Dax Raad 3f1772ee9a core: ensure model npm package falls back to dev models config when not explicitly defined 2025-12-04 15:39:52 -05:00
Jérôme Benoit c2e206f03a feat: Add SAP AI Core provider support (#5023)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
2025-12-04 14:07:23 -06:00
Aiden Cline 9abf8ca055 bump: builtin plugin versions 2025-12-04 12:37:14 -06:00
Daniel Polito 40e4a0b681 add optional prompt Input to Github Action (#4828)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-04 12:10:56 -06:00
Aiden Cline 9c1ff1c9a5 tui: fix /new slash command being persisted in prompt input 2025-12-04 12:01:13 -06:00
Shantur Rathore 6688105bbb tweak: bash tool messages regarding timeouts and truncation more clear for agent (#5066) 2025-12-04 11:33:00 -06:00
Aiden Cline 68a63054d2 fix: model not being passed correctly to tool 2025-12-04 11:15:30 -06:00
Daniel Gray dd7670c48a Preserve prompt input when creating new session (#4993) 2025-12-04 11:12:58 -06:00
Adam 7e21f41892 fix(tui): cursor color 2025-12-04 06:56:48 -06:00
GitHub Action 80f2ccdd98 ignore: update download stats 2025-12-04 2025-12-04 12:04:38 +00:00
Aiden Cline 6b9989ddf4 ci: cleaner 2025-12-04 01:14:09 -06:00
Aiden Cline 1573f348e4 ci: only maintainer can trigger 2025-12-04 01:07:04 -06:00
Frank d5cfd2e8bc zen: make session provider sticky 2025-12-03 23:33:46 -05:00
wsx99outlook bffd79e719 ci: use blacksmith runners in review workflow too (#5042) 2025-12-03 22:30:00 -06:00
GitHub Action 0336bad155 chore: format code 2025-12-04 04:29:03 +00:00
YeonGyu-Kim 289475cece fix: ensure checkUpgrade sets init: (#5040) 2025-12-03 22:28:35 -06:00
opencode ec07aafa79 release: v1.0.132
Update Nix Hashes / update (push) Has been cancelled
2025-12-04 04:23:39 +00:00
GitHub Action 7c3bc4c279 chore: format code 2025-12-04 04:19:15 +00:00
Dax Raad 0466e6f36e fix anthropic api key error 2025-12-03 23:18:24 -05:00
opencode 80898220bc release: v1.0.131
Update Nix Hashes / update (push) Has been cancelled
2025-12-04 04:11:48 +00:00
Aiden Cline 345269719d fix: TypeError: undefined is not an object 2025-12-03 22:03:42 -06:00
GitHub Action 619a38e73e chore: format code 2025-12-04 03:42:08 +00:00
Frank 816759d16f zen: fix chart loading 2025-12-03 22:41:31 -05:00
opencode 9904a8cebf release: v1.0.130
Update Nix Hashes / update (push) Has been cancelled
2025-12-04 03:38:18 +00:00
Jakub Matjanowski 73c6b95c7d feat: Enhance DeepSeek reasoning content handling (#4975)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-03 21:31:36 -06:00
Jack Bisceglia d1ca2f826b docs: remove outdated theme section as system theme is now added back (#5041) 2025-12-03 21:10:26 -06:00
Aiden Cline d88fb2084a ci: add note about iife 2025-12-03 21:08:12 -06:00
Aiden Cline f3f7cb29eb fix: provider id issue 2025-12-03 20:45:55 -06:00
Aiden Cline ac54645dc1 fix: compaction type issue 2025-12-03 20:43:47 -06:00
Aiden Cline 8d83815413 fix: dax typo 2025-12-03 20:39:11 -06:00
Github Action ff94aeff29 Update Nix flake.lock and hashes 2025-12-04 02:34:26 +00:00
Aiden Cline 4bc9e710d1 bump opentui 2025-12-03 20:33:08 -06:00
Dax 85d8825cb1 core: refactor provider and model system (#5033)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <thdxr@users.noreply.github.com>
2025-12-03 21:09:03 -05:00
Dax Raad 5c0dccdc6c core: add provider test coverage for upcoming refactor
Add comprehensive test suite for Provider module to ensure safe
refactoring of provider internals. Tests cover:
- Provider loading from env vars and config
- Provider filtering (disabled_providers, enabled_providers)
- Model whitelist/blacklist
- Model aliasing and custom providers
- getModel, getProvider, closest, defaultModel functions

Also adds Env module for instance-scoped environment variable access,
enabling isolated test environments without global state pollution.
2025-12-03 18:30:42 -05:00
Frank 8123f8a8c8 zen: fix chart loading
closes #5030
2025-12-03 18:12:28 -05:00
Aiden Cline f49a17bcb1 ci: review tweak 2025-12-03 16:16:08 -06:00
Ariane Emory 154c57fe9d feat: add tool_details keybind w/ no default (#4976)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-03 13:38:09 -06:00
Aiden Cline 541bf2d5c1 ci: review ready for review action 2025-12-03 13:35:49 -06:00
Aiden Cline 8ed97ed480 ci: tweak review cmd 2025-12-03 13:34:45 -06:00
Frank 6fcd9aed2f zen: usage graph respect light/dark mode 2025-12-03 14:24:44 -05:00
Aiden Cline 6e46bb8c09 ci: make review only fire on non draft pr creation 2025-12-03 13:10:40 -06:00
Aiden Cline b905cf202c ci: add pr review 2025-12-03 12:45:06 -06:00
Ariane Emory 77df6c2ba1 feat: add messages_last_user command to scroll TUI to last user message (implements #4847) (#4855)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-03 12:30:11 -06:00
Luke Parker c7102d9cee fix: bunfs path on windows (#5011) 2025-12-03 11:21:13 -06:00
Spoon e4f11b6f5b feat: experimental.primary_tools, allow user to set the tools that should only be available to primary agents (#4913)
Co-authored-by: GitHub Action <action@github.com>
2025-12-03 11:19:43 -06:00
Aiden Cline 388c5b69ae add retry case for grok resource exhausted 2025-12-03 11:16:27 -06:00
GitHub Action cbaafbc0af chore: format code 2025-12-03 16:47:25 +00:00
Aiden Cline fec666e641 ignore: docs & style 2025-12-03 10:46:48 -06:00
GitHub Action 9198a06737 ignore: update download stats 2025-12-03 2025-12-03 12:04:36 +00:00
opencode d09ef88fd4 release: v1.0.129
Update Nix Hashes / update (push) Has been cancelled
2025-12-03 11:39:04 +00:00
Aiden Cline 25a4a855e3 core: fix GitHub Copilot Enterprise authentication failing with sdk.chat undefined error 2025-12-02 23:37:10 -06:00
Jason Cheatham 8db84c3116 tweak: adjust light/dark theme toggle (#5007) 2025-12-02 23:03:24 -06:00
Ben Vargas 2d54f98afa fix: correct Provider type in chat.params plugin hook (#5003) 2025-12-02 22:50:21 -06:00
Jason Cheatham 0abc227805 fix: handle 0 in ANSI theme color definitions (#5009) 2025-12-02 22:42:30 -06:00
Frank 627a147f7f zen: fix removing provider 2025-12-02 21:52:12 -05:00
Github Action 7617ed5a9f Update Nix flake.lock and hashes 2025-12-03 02:17:49 +00:00
Sebastian Herrlinger a08f50b1a4 actually bump opentui to v0.1.55
- fix scrollbox empty/blank last items at bottom
- fix should not insert chars with modifiers in input/textarea anymore
- do not wrap OSC4 palette sequences for tmux 3.6
2025-12-03 03:16:32 +01:00
Sebastian Herrlinger 7e045fecde Revert "bump opentui to v0.1.55"
This reverts commit e5d19f0c3f.
2025-12-03 03:14:08 +01:00
Github Action 77ce9aef61 Update Nix flake.lock and hashes 2025-12-03 02:06:35 +00:00
Sebastian Herrlinger e5d19f0c3f bump opentui to v0.1.55
- fix scrollbox empty/blank last items at bottom
- fix should not insert chars with modifiers in input/textarea anymore
- do not wrap OSC4 palette sequences for tmux 3.6
2025-12-03 03:03:52 +01:00
Aiden Cline 0816f83cbb add basic session list command 2025-12-02 19:24:05 -06:00
Aiden Cline 8993f76967 Make homebrew update check use homebrew registry version info 2025-12-02 17:43:33 -06:00
Frank a432fd69f7 wip: zen 2025-12-02 18:36:15 -05:00
Dalton Alexandre 6005e75549 fix: allow unignoring files in .ignore (#4814) 2025-12-02 17:15:12 -06:00
Jay V e8bb7265c0 ignore: commands 2025-12-02 18:09:16 -05:00
Jay V 95e9c6b26e docs: replace deprecated opencode auth login command with /connect across all documentation
Users no longer need to exit the TUI to add providers - they can now use the /connect command directly in the terminal interface. Updated all provider setup instructions to use simplified format with /connect command instead of the deprecated opencode auth login CLI command. Added /connect to TUI commands reference and streamlined provider documentation to show clearer, more concise setup steps.
2025-12-02 18:08:39 -05:00
Aiden Cline 7d654d7cc7 tweak: make message border match color of agent it was sent to 2025-12-02 16:59:31 -06:00
Aiden Cline 803af16bb7 bump default lsp server timeout 2025-12-02 16:04:08 -06:00
Adam c5c7014b72 fix: session turn margins 2025-12-02 15:50:24 -06:00
Jaga Santagostino 5269630b6e toggle to hide username in TUI (#4750)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-02 15:11:03 -06:00
GitHub Action e1fa68c89a chore: format code 2025-12-02 20:28:17 +00:00
Aiden Cline 65c7ac5dcd ci: dont forget our european designer 2025-12-02 14:27:41 -06:00
opencode-agent[bot] e1c1cbf973 add OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT (#4996)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-02 20:14:23 +00:00
opencode c9369c4bd4 release: v1.0.128
Update Nix Hashes / update (push) Has been cancelled
2025-12-02 20:14:22 +00:00
GitHub Action d9246cc25b chore: format code 2025-12-02 19:52:47 +00:00
Frank efa655c868 zen: data dumper 2025-12-02 14:51:27 -05:00
Adam 1e73bd17a6 chore: remove comment (redeploy) 2025-12-02 13:49:48 -06:00
Adam 23b47f87d1 fix: vite config 2025-12-02 13:08:59 -06:00
U Cirello 25284c39e4 fix(run): allow messages to start with dash (-) (#4904) 2025-12-02 12:52:05 -06:00
Aiden Cline 3eb43c6a0b tweak: hide [REDACTED] chunks 2025-12-02 12:29:20 -06:00
Adam 50a32254f5 chore: update landing page stats 2025-12-02 11:59:37 -06:00
Adam 504d3d0149 chore: update landing page stats 2025-12-02 11:52:53 -06:00
GitHub Action 27f9813d5d chore: format code 2025-12-02 17:07:27 +00:00
David Hill 11c036f480 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-02 17:06:54 +00:00
David Hill c4b098402b fix: add docs button 2025-12-02 17:06:37 +00:00
GitHub Action f36aa882f6 chore: format code 2025-12-02 17:02:39 +00:00
David Hill 00d1f1f949 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-02 17:00:45 +00:00
David Hill 59a86633e7 fix: website hero copy 2025-12-02 17:00:40 +00:00
GitHub Action 152b47746a chore: format code 2025-12-02 16:45:56 +00:00
Dalton Alexandre 46b4dcdb48 fix: handle ANSI color indexes in theme resolution (#4842) 2025-12-02 10:45:22 -06:00
opencode 78206bbde4 release: v1.0.127
Update Nix Hashes / update (push) Has been cancelled
2025-12-02 16:05:59 +00:00
Adam 1301fbf54f fix: share page ssr 2025-12-02 07:08:25 -06:00
Github Action 939759bc06 Update Nix flake.lock and hashes 2025-12-02 12:52:28 +00:00
GitHub Action e361aed6b7 chore: format code 2025-12-02 12:51:37 +00:00
Adam 94bb70e120 fix: type error 2025-12-02 06:51:05 -06:00
Adam c153eb9423 feat: better code and diff rendering performance 2025-12-02 06:50:21 -06:00
GitHub Action 4b2614205f ignore: update download stats 2025-12-02 2025-12-02 12:04:35 +00:00
David Hill b59e43b800 fix: update the install copy 2025-12-02 11:37:41 +00:00
Aiden Cline 77239c2a34 feat: add overridable review slash command (#4973) 2025-12-02 00:18:58 -06:00
Ariane Emory e9f3a1a3ed feat: toggle tool details visibility (resolves #4824) (#4882)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-01 19:38:44 -06:00
Adam aa907dc287 fix: install orange 2025-12-01 19:21:55 -06:00
Aiden Cline 844d326f90 fix: tui crash when no authed providers and default provider disabled (#4964) 2025-12-01 18:35:40 -06:00
Frank 8c1f684975 zen: remove unnecessary transactions 2025-12-01 18:33:32 -05:00
Stephen Collings f28249110f fix(auth): Respect disabled/enabled providers config in auth login (#4940)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-01 17:01:10 -06:00
Aiden Cline dba8059c45 fix: add .quiet 2025-12-01 16:59:32 -06:00
opencode-agent[bot] 74c16793dc fix: respect npm registry (#4958)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-01 16:58:39 -06:00
Adam 6811ba570c fix: safari icons 2025-12-01 16:27:12 -06:00
Aiden Cline f5cdf14820 fix: sanitize more invalid schema cases for gemini models 2025-12-01 16:19:10 -06:00
Dax Raad e5a31bfd50 core: fix share compaction reprocessing same events by making storage list boundaries exclusive 2025-12-01 16:58:59 -05:00
Dax Raad 0f3136faf6 core: add debug logging to troubleshoot share compaction loading 2025-12-01 16:46:57 -05:00
Github Action 329ce68161 Update Nix flake.lock and hashes 2025-12-01 21:38:38 +00:00
Dax Raad c7445952e5 core: prevent share tests from cleaning up storage used by other tests 2025-12-01 16:37:54 -05:00
Dax Raad 0cd65a5147 fix types 2025-12-01 16:36:56 -05:00
GitHub Action 28888034da chore: format code 2025-12-01 21:35:51 +00:00
Dax Raad c3585ebf72 core: reduce latency when loading shared sessions through event compaction 2025-12-01 16:35:07 -05:00
Dax Raad 2c5a3984f1 limit grep line length to 2000 2025-12-01 16:35:07 -05:00
Aiden Cline c50a1b83f3 fix: copilot responses bug w/ gpt 5 mini 2025-12-01 14:36:49 -06:00
GitHub Action ceda7de457 chore: format code 2025-12-01 19:03:13 +00:00
Aiden Cline 0399ec2808 fix case where opencode wasn't retrying 2025-12-01 13:02:33 -06:00
opencode c4510e3eb8 release: v1.0.126
Update Nix Hashes / update (push) Has been cancelled
2025-12-01 19:00:00 +00:00
blacksmith-sh[bot] 6a4018db03 .github/workflows: Migrate workflows to Blacksmith runners (#4941)
Co-authored-by: blacksmith-sh[bot] <157653362+blacksmith-sh[bot]@users.noreply.github.com>
2025-12-01 09:43:33 -05:00
GitHub Action a846e0387f chore: format code 2025-12-01 14:43:23 +00:00
Dax Raad 2cceffb565 log fetch time 2025-12-01 09:35:08 -05:00
GitHub Action b4afb8d098 chore: format code 2025-12-01 14:23:36 +00:00
opencode 3517417431 release: v1.0.125
Update Nix Hashes / update (push) Has been cancelled
2025-12-01 14:23:35 +00:00
Dax Raad 32f6c3baac docs: add rebase instructions to commit command 2025-12-01 09:14:54 -05:00
Dax Raad 9948e1c123 core: close SSE stream when instance is disposed 2025-12-01 09:14:00 -05:00
Sebastian Herrlinger 653d7fd545 no conceal for write tool output 2025-12-01 15:12:47 +01:00
GitHub Action 316586a8f4 chore: format code 2025-12-01 13:27:29 +00:00
adamelmore c40c2a39c2 chore: otf fonts 2025-12-01 07:20:53 -06:00
GitHub Action 1b5f34480d ignore: update download stats 2025-12-01 2025-12-01 12:08:56 +00:00
opencode 7a58c6cd9d release: v1.0.124
Update Nix Hashes / update (push) Has been cancelled
2025-12-01 09:20:29 +00:00
Aiden Cline 2788fac2ac tweak: better err msgs 2025-12-01 01:33:32 -06:00
Aiden Cline 78d54aad3c fix: drop absolute flag to resolve issues when using older git versions 2025-12-01 00:23:19 -06:00
GitHub Action b750f698b3 chore: format code 2025-12-01 02:53:41 +00:00
Aiden Cline 97fe935998 bump anthropic plugin to fix header issue 2025-11-30 20:52:50 -06:00
opencode f49d5927a9 release: v1.0.123
Update Nix Hashes / update (push) Has been cancelled
2025-12-01 01:07:50 +00:00
Github Action c447e67535 Update Nix flake.lock and hashes 2025-12-01 01:00:23 +00:00
Sebastian Herrlinger 1cb2b6581f bump opentui to v0.1.54, removing the cc flicker feature, fixing diffs and input event handling 2025-12-01 01:57:55 +01:00
Shantur Rathore db2a069383 tweak: compaction prompt (#4838)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-30 14:31:57 -06:00
Justin Vogt ad36dc4cff docs: explore subagent (#4893) 2025-11-30 14:04:27 -06:00
GitHub Action 40de95ec1b ignore: update download stats 2025-11-30 2025-11-30 12:04:23 +00:00
Adam 00089983d1 feat(share): split diffs on wide screens 2025-11-30 05:29:07 -06:00
Aiden Cline e20b6f6591 tweak: plan prompt 2025-11-30 01:03:35 -06:00
Aiden Cline 84798cf7f5 chore: cleanup 2025-11-30 00:32:43 -06:00
opencode-agent[bot] 098ae8c780 Added "Open docs" command to palette (#4915)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-29 23:49:45 -06:00
Justin Vogt 7191bf38ee docs: Move tip to correct tool (#4891) 2025-11-29 22:03:20 -06:00
Aiden Cline b3fa3d4475 get codex working in copilot (#4914)
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-29 21:47:29 -06:00
opencode 064a444ef7 release: v1.0.122
Update Nix Hashes / update (push) Has been cancelled
2025-11-30 00:35:34 +00:00
Github Action 63e9865d8d Update Nix flake.lock and hashes 2025-11-29 23:56:14 +00:00
Sebastian Herrlinger 85cd86fce4 bump opentui to v0.1.53, fixing an event listener leak in the line number renderable (perf) 2025-11-30 00:52:32 +01:00
Sebastian Herrlinger 211ae32359 tweak spinner to be bg independent 2025-11-30 00:50:30 +01:00
GitHub Action d84edc7637 chore: format code 2025-11-29 21:41:00 +00:00
Dax Raad 93196f2e2d core: clarify general agent can execute multiple parallel tasks 2025-11-29 16:40:15 -05:00
Dax Raad 2ef51ca6c3 ci: allow snapshot builds from any branch starting with snapshot- prefix 2025-11-29 21:30:38 +00:00
opencode c8696063c2 release: v1.0.121
Update Nix Hashes / update (push) Has been cancelled
2025-11-29 21:30:38 +00:00
Github Action 1960ecc282 Update Nix flake.lock and hashes 2025-11-29 21:20:24 +00:00
Sebastian Herrlinger ead6f57d11 upgrade opentui to v0.1.52, fixing #4906 - key repeat handling 2025-11-29 22:18:08 +01:00
kavin 42ffb4487c fix(tui): add missing fg color to permission keybind hints (#4899)
Co-authored-by: Github Action <action@github.com>
2025-11-29 14:22:20 -06:00
Adam 4981fbddf5 chore: cleanup duplicate markup 2025-11-29 06:18:51 -06:00
GitHub Action 005abeb0a7 ignore: update download stats 2025-11-29 2025-11-29 12:04:12 +00:00
Dorian Karter 99b60488de fix: minor ui bug for transparent backgrounds (#4886) 2025-11-28 23:58:44 -06:00
Dax Raad 827564401e core: improve explore agent description to clarify tool availability 2025-11-28 21:47:45 -05:00
GitHub Action 6fcc0be7ed chore: format code 2025-11-29 02:44:40 +00:00
Dax Raad 40e1f1be14 Merge remote-tracking branch 'origin/dev' into dev 2025-11-28 21:44:02 -05:00
Dax Raad 6ac9199826 core: update generated types to include explore agent configuration 2025-11-28 21:43:56 -05:00
opencode 9853c7b6d9 release: v1.0.120
Update Nix Hashes / update (push) Has been cancelled
2025-11-29 02:34:23 +00:00
Dax Raad 35b313fa40 tui: improve task display in session view to show tool names and completion status 2025-11-28 21:19:15 -05:00
Dax Raad 6a87d2fb30 Merge remote-tracking branch 'origin/dev' into dev 2025-11-28 21:13:13 -05:00
Dax Raad e3d6847112 core: add explore agent for fast codebase navigation and improve task UI display 2025-11-28 21:13:07 -05:00
Adam eade62e7d6 fix: diffs double rendering when CSR'd 2025-11-28 20:08:50 -06:00
Adam 9baa9fdc31 fix: theme-color value 2025-11-28 19:57:01 -06:00
Github Action 741449a9d4 Update Nix flake.lock and hashes 2025-11-29 01:55:02 +00:00
Sebastian Herrlinger a3f0e61161 opentui diffs 2025-11-29 02:52:17 +01:00
Dax Raad 95bc09449d ignore 2025-11-28 20:29:58 -05:00
GitHub Action c2d035be74 chore: format code 2025-11-28 22:43:33 +00:00
Justin Vogt 148e884428 docs: Add tip to tools about subagents re todos (#4875) 2025-11-28 15:49:24 -06:00
Aiden Cline 1e0e581468 ci: add community contributors to the changelog 2025-11-28 15:44:21 -06:00
Ariane Emory 7849f9713c feat: persist thinking blocks display to KV and indicate its current display state in the command_list (resolves #4582) (#4810)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-28 15:01:05 -06:00
Aiden Cline c07e6879cd ci: use haiku 2025-11-28 14:41:32 -06:00
Github Action e9036a6480 Update Nix flake.lock and hashes 2025-11-28 20:32:46 +00:00
Aiden Cline c649baa5b9 bump openrouter & google ai sdk packages 2025-11-28 14:30:22 -06:00
Aiden Cline 2be3dffb5e Revert "fix: title gen when first msg(s) are shell invocations (#4874)"
This reverts commit ae68698cd0.
2025-11-28 12:18:02 -06:00
Aiden Cline ae68698cd0 fix: title gen when first msg(s) are shell invocations (#4874)
Co-authored-by: GitHub Action <action@github.com>
2025-11-28 11:55:22 -06:00
Sergio Garcia 46e9439c4f docs: formatter: false and lsp: false (#4833)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-11-28 11:22:48 -06:00
jaov 1d9c43d9dc tweak: gemini retry message to not be explicitly about gemini 3 (#4864)
Co-authored-by: jesuso <j.ochoa@norteconecta.net>
2025-11-28 11:14:53 -06:00
Dax Raad 37c42b2cd6 ci stuff 2025-11-28 10:59:52 -05:00
Dax Raad 25ba813f35 add otui-diffs 2025-11-28 10:59:07 -05:00
GitHub Action 018fb2b0b3 ignore: update download stats 2025-11-28 2025-11-28 12:04:42 +00:00
Adam 1994f2c9a4 fix: theme-color value 2025-11-28 05:48:07 -06:00
Adam c415d21cde fix: desktop and share layouts 2025-11-28 05:35:35 -06:00
Adam b3ca55ef33 fix: unified diff as default 2025-11-28 05:35:35 -06:00
Christoph aa2a0dbd7b core: add built-in Dart LSP server and formatter (#4841) 2025-11-28 00:33:45 -06:00
Github Action 4eed4242f7 Update Nix flake.lock and hashes 2025-11-28 06:27:49 +00:00
DS 0698c072b5 fix: filter empty messages in toModelMessage (#4811) 2025-11-28 00:26:48 -06:00
Aiden Cline 58399ceafa bump anthropic package 2025-11-28 00:25:16 -06:00
Adam cb9857c162 fix(desktop): layout 2025-11-27 20:15:49 -06:00
Adam 3be19b3e1b fix: unwrap solid store part 2025-11-27 20:15:49 -06:00
Dax Raad 050cbce815 ci: add bun version check to pre-push hook to ensure version consistency 2025-11-27 15:50:23 -05:00
Dax Raad bb051b9a36 fix lock 2025-11-27 15:48:16 -05:00
Dax Raad 1fa838525d tui: simplify model dialog ordering logic to reduce complexity 2025-11-27 14:38:51 -05:00
Dax Raad 64d10c09c3 tui: improve provider dialog text clarity for better user guidance 2025-11-27 14:09:53 -05:00
Dax Raad 8c82767a56 tui: fix provider sorting to prioritize recommended options 2025-11-27 13:54:42 -05:00
Dax Raad 86d1b1dd54 tui: remove cancel keybind hint from prompt dialog to simplify UI 2025-11-27 13:48:34 -05:00
Dax Raad a1ff0556fc tui: hide favorite keybind in model dialog when disconnected to prevent errors 2025-11-27 13:42:36 -05:00
GitHub Action e554d29b38 chore: format code 2025-11-27 17:15:41 +00:00
Jay V 81bcf8e00b ignore: lock 2025-11-27 12:14:50 -05:00
Jay V 55acbb58ef ignore: add reply-to support for enterprise form emails 2025-11-27 12:12:44 -05:00
Albert O'Shea 19a27ecc38 nix: fix workflow failing on PRs (#4820)
Co-authored-by: Github Action <action@github.com>
2025-11-27 10:05:51 -06:00
Frank c6a82650e4 doc: slashing kimi k2 thinking price 2025-11-27 09:58:57 -05:00
GitHub Action 27b2414712 ignore: update download stats 2025-11-27 2025-11-27 12:04:30 +00:00
Adam 26478e641c fix: desktop layout and scroll gutters 2025-11-27 05:41:50 -06:00
Adam 6d77a07e18 fix: session turn margins 2025-11-27 05:25:39 -06:00
GitHub Action 6c2d2e0ba8 chore: format code 2025-11-27 11:06:46 +00:00
Adam 8661e5db52 fix: simpler sanitize 2025-11-27 05:05:55 -06:00
opencode 9d2bf1df73 release: v1.0.119
Update Nix Hashes / update (push) Has been cancelled
2025-11-27 03:15:40 +00:00
Dax Raad 6ca32b1d0e core: prevent codesearch and websearch tools when webfetch permission is denied 2025-11-26 22:08:50 -05:00
GitHub Action a0ea0857ca chore: format code 2025-11-27 03:08:33 +00:00
Dax Raad c0995239fa core: prevent external diff tools from interfering with snapshot generation 2025-11-26 22:07:51 -05:00
opencode 013763f44b release: v1.0.118
Update Nix Hashes / update (push) Has been cancelled
2025-11-27 02:43:07 +00:00
Dax Raad 8de0e3530c tui: prevent footer from shrinking when terminal is resized 2025-11-26 21:35:56 -05:00
Github Action bd0822d8b1 Update Nix flake.lock and hashes 2025-11-27 01:33:35 +00:00
GitHub Action 14a04f1496 chore: format code 2025-11-27 01:32:04 +00:00
Dax Raad 2b3a900c5e revert ts lsp to monorepo root 2025-11-26 20:31:26 -05:00
opencode c0c58b0737 release: v1.0.117
Update Nix Hashes / update (push) Has been cancelled
2025-11-27 01:29:00 +00:00
Dax Raad fead40328f roll back vtsls 2025-11-26 20:18:40 -05:00
Dax Raad 5ff94e4a8c temporarily restrict codesearch and websearch to opencode zen users. need to figure out how to opt out for enterprise users who do not want this 2025-11-26 20:13:20 -05:00
Dax 50ccfd7b75 tui design refinement (#4809) 2025-11-26 20:11:39 -05:00
Aiden Cline 15c8d556e1 enable parcel file watcher, expand parcel ignore patterns, replace fs watcher for git branches with parcel (#4805) 2025-11-26 17:33:43 -06:00
Aiden Cline 99726b2c89 Revert "fix: disable virtual extmarks for file/agent mentions (#4731)"
This reverts commit 4f9d27a667.
2025-11-26 14:55:39 -06:00
GitHub Action 30c7bb3783 chore: format code 2025-11-26 19:10:51 +00:00
rosmur 1d04ce9973 docs: add llama.cpp provider for local LLM inference (#4769)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-26 13:10:15 -06:00
Aiden Cline e2c1f4a29e fix: svg paste bug 2025-11-26 12:38:48 -06:00
Dax Raad 723cbf548a lil branch refactor 2025-11-26 12:34:48 -05:00
GitHub Action a50f145d17 chore: format code 2025-11-26 17:26:46 +00:00
Tommy D. Rossi a0d7653bdf fix(tui): dedupe file references in prompt (#4775) 2025-11-26 11:26:05 -06:00
opencode f111b2af66 release: v1.0.115
Update Nix Hashes / update (push) Has been cancelled
2025-11-26 16:57:35 +00:00
Jensen 75254b1afd fix(tui): show bootstrap errors instead of {} to trace (#4779)
Co-authored-by: Github Action <action@github.com>
2025-11-26 10:49:55 -06:00
Yug Agarwal 5b93c6568b docs: IO.NET Provider (#4762) 2025-11-26 10:44:05 -06:00
GitHub Action c1fc4263b6 ignore: update download stats 2025-11-26 2025-11-26 12:04:44 +00:00
Adam 7e071f2447 fix: useData 2025-11-26 06:03:13 -06:00
Adam bce0a100b2 fix: scroll gutter padding 2025-11-26 05:58:30 -06:00
Github Action f065b8bab8 Update Nix flake.lock and hashes 2025-11-26 11:31:37 +00:00
Adam 69864aec74 deps: update pierre diffs 2025-11-26 05:29:31 -06:00
opencode-agent[bot] 3aa85e85a0 Renamed vcs.changed to vcs.branch.updated (#4771)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-26 01:17:59 -06:00
Dmitry Halushka 0e7c91e29e fix: transform MCP tool schemas for Google/Gemini compatibility (#4538)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-26 00:58:20 -06:00
Aiden Cline 606d25d4d6 Remove keybinds from favorites feature, keep functionality 2025-11-26 00:45:33 -06:00
shuv 533f642e8e Add favorites to model selector (#23) (#4343)
Co-authored-by: Github Action <action@github.com>
2025-11-26 00:41:41 -06:00
Meysam Najafi Fard d80b1dd19e fix: handle remote image URLs in paste handler (#4691)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-26 00:34:22 -06:00
Aiden Cline a3abb8a019 ignore: fix type issue 2025-11-26 00:14:04 -06:00
Aiden Cline aa3120d20d Reapply "feat(github): add ability to react to PR Review Comments in Workflow (#4705)"
This reverts commit c71f709296.
2025-11-25 23:51:46 -06:00
Aiden Cline c71f709296 Revert "feat(github): add ability to react to PR Review Comments in Workflow (#4705)"
This reverts commit 64876e1189.
2025-11-25 23:51:23 -06:00
U Cirello 64876e1189 feat(github): add ability to react to PR Review Comments in Workflow (#4705)
Co-authored-by: GitHub Action <action@github.com>
2025-11-25 23:45:19 -06:00
george larson d301d6cd66 docs: add Venice.ai provider (#4748)
Co-authored-by: George Larson <georgeglarson@users.noreply.github.com>
2025-11-25 23:40:52 -06:00
Ariane Emory cc3c4277d0 feat: add a diff_style option to allow disabling columnar diffs (resolve #4677) (#4756)
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-25 23:34:58 -06:00
Frank 138291ca24 zen: trial 2025-11-25 23:44:57 -05:00
Aiden Cline 21bf5a10e5 show current git branch in tui (#4765)
Co-authored-by: GitHub Action <action@github.com>
2025-11-25 21:39:20 -06:00
Dax Raad beab4d85f4 enable exa code/websearch 2025-11-25 22:06:26 -05:00
Frank e8e6baa096 wip: zen 2025-11-25 22:01:34 -05:00
Adam aa38509d48 fix: pierre separators 2025-11-25 20:39:20 -06:00
Adam e65dcacc00 chore: cleanup pierre stuff 2025-11-25 20:39:20 -06:00
GitHub Action 06bd8087ab chore: format code 2025-11-26 02:33:36 +00:00
Dax Raad 239db60587 tui: fix event subscription cleanup in SDK context 2025-11-25 21:32:56 -05:00
opencode 4fe88dbb00 release: v1.0.114
Update Nix Hashes / update (push) Has been cancelled
2025-11-26 00:32:22 +00:00
GitHub Action c8616f411d chore: format code 2025-11-26 00:24:10 +00:00
Dax Raad 45cee03b10 switch to vtsls for typescript lsp 2025-11-25 19:23:23 -05:00
Aiden Cline 5ddaa97e63 fix: global prefix handling w/ aws bedrock (#4757) 2025-11-26 00:21:59 +00:00
1981 changed files with 43663 additions and 45774 deletions
-57
View File
@@ -1,57 +0,0 @@
#
# This file is intentionally in the wrong dir, will move and add later....
#
name: Guidelines Check
on:
# Disabled - uncomment to re-enable
# pull_request_target:
# types: [opened, synchronize]
jobs:
check-guidelines:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check PR guidelines compliance
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
<pr-number>
${{ github.event.pull_request.number }}
</pr-number>
<pr-description>
${{ github.event.pull_request.body }}
</pr-description>
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
Command MUST be like this.
```
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
```
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
auto-label:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
issues: write
+1 -1
View File
@@ -11,7 +11,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
deploy:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
+4 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
check-duplicates:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
issues: write
@@ -55,4 +55,7 @@ jobs:
Feel free to ignore if none of these address your specific case.'
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
If no clear duplicates are found, do not comment."
-29
View File
@@ -1,29 +0,0 @@
name: format
on:
push:
branches-ignore:
- production
pull_request:
branches-ignore:
- production
workflow_dispatch:
jobs:
format:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: run
run: |
./script/format.ts
env:
CI: true
+38
View File
@@ -0,0 +1,38 @@
name: generate
on:
push:
branches-ignore:
- production
pull_request:
branches-ignore:
- production
workflow_dispatch:
jobs:
generate:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Generate SDK
run: |
bun ./packages/sdk/js/script/build.ts
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
bun x prettier --write packages/sdk/openapi.json
- name: Format
run: ./script/format.ts
env:
CI: true
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
notify:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Send nicely-formatted embed to Discord
uses: SethCohen/github-releases-to-discord@v1
+4 -2
View File
@@ -3,6 +3,8 @@ name: opencode
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
opencode:
@@ -11,7 +13,7 @@ jobs:
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
id-token: write
contents: read
@@ -28,4 +30,4 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/glm-4.6
model: opencode/claude-haiku-4-5
+1 -1
View File
@@ -14,7 +14,7 @@ permissions:
jobs:
publish:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
+1 -1
View File
@@ -13,7 +13,7 @@ permissions:
jobs:
publish:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
+116 -15
View File
@@ -2,11 +2,15 @@ name: publish
run-name: "${{ format('release {0}', inputs.bump) }}"
on:
push:
branches:
- dev
- snapshot-*
workflow_dispatch:
inputs:
bump:
description: "Bump major, minor, or patch"
required: true
required: false
type: choice
options:
- major
@@ -20,12 +24,14 @@ on:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
id-token: write
contents: write
packages: write
jobs:
publish:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
steps:
- uses: actions/checkout@v3
with:
@@ -33,20 +39,13 @@ jobs:
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: ./.github/actions/setup-bun
- name: Install makepkg
- name: Setup SSH for AUR
if: inputs.bump || inputs.version
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
@@ -55,12 +54,9 @@ jobs:
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install OpenCode
if: inputs.bump || inputs.version
run: curl -fsSL https://opencode.ai/install | bash
- name: Setup npm auth
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -68,9 +64,87 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Publish
run: |
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
publish-tauri:
if: false # inputs.bump || inputs.version
continue-on-error: true
strategy:
fail-fast: false
matrix:
settings:
- host: macos-latest
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
- host: windows-latest
target: x86_64-pc-windows-msvc
- host: ubuntu-24.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- 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
- run: git fetch --force --tags
- uses: ./.github/actions/setup-bun
- name: install dependencies (ubuntu only)
if: startsWith(matrix.settings.host, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/tauri/src-tauri
shared-key: ${{ matrix.settings.target }}
- name: Prepare
run: |
cd packages/tauri
bun ./scripts/prepare.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
@@ -79,3 +153,30 @@ jobs:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
if: startsWith(matrix.settings.host, 'ubuntu')
- name: Build and upload artifacts
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_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
with:
projectPath: packages/tauri
uploadWorkflowArtifacts: true
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }}
updaterJsonPreferNsis: true
# releaseId: TODO
+79
View File
@@ -0,0 +1,79 @@
name: Guidelines Check
on:
issue_comment:
types: [created]
jobs:
check-guidelines:
if: |
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/review') &&
contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: write
steps:
- name: Get PR number
id: pr-number
run: |
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
fi
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Get PR details
id: pr-details
run: |
gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json
echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT
echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check PR guidelines compliance
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
PR_TITLE: ${{ steps.pr-details.outputs.title }}
run: |
PR_BODY=$(jq -r .body pr_data.json)
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
<pr-number>
${{ steps.pr-number.outputs.number }}
</pr-number>
<pr-description>
$PR_BODY
</pr-description>
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
Command MUST be like this.
\`\`\`
gh api \
--method POST \
-H \"Accept: application/vnd.github+json\" \
-H \"X-GitHub-Api-Version: 2022-11-28\" \
/repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \
-f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT'
\`\`\`
Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!."
-35
View File
@@ -1,35 +0,0 @@
name: snapshot
on:
push:
branches:
- dev
- fix-snapshot-2
- v0
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: ./.github/actions/setup-bun
- name: Publish
run: |
./script/publish.ts
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
stats:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
+3 -3
View File
@@ -2,13 +2,13 @@ name: "sync-zed-extension"
on:
workflow_dispatch:
release:
types: [published]
# release:
# types: [published]
jobs:
zed:
name: Release Zed Extension
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
with:
+1 -7
View File
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -28,9 +28,3 @@ jobs:
bun turbo test
env:
CI: true
- name: Check SDK is up to date
run: |
bun ./packages/sdk/js/script/build.ts
git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist
continue-on-error: false
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
typecheck:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
+3 -1
View File
@@ -18,7 +18,8 @@ on:
jobs:
update:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
SYSTEM: x86_64-linux
@@ -29,6 +30,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup Nix
uses: DeterminateSystems/nix-installer-action@v20
+2 -1
View File
@@ -6,10 +6,10 @@ node_modules
.idea
.vscode
*~
openapi.json
playground
tmp
dist
ts-dist
.turbo
**/.serena
.serena/
@@ -18,3 +18,4 @@ refs
Session.vim
opencode.json
a.out
target
+7
View File
@@ -1,2 +1,9 @@
#!/bin/sh
# Check if bun version matches package.json
EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
CURRENT_VERSION=$(bun --version)
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
exit 1
fi
bun typecheck
+5 -1
View File
@@ -1,5 +1,6 @@
---
description: Git commit and push
description: git commit and push
model: opencode/glm-4.6
---
commit and push
@@ -21,3 +22,6 @@ WHAT was done.
do not do generic messages like "improved agent experience" be very specific
about what user facing changes were made
if there are changes do a git pull --rebase
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
-8
View File
@@ -1,8 +0,0 @@
---
description: hello world iaosd ioasjdoiasjd oisadjoisajd osiajd oisaj dosaij dsoajsajdaijdoisa jdoias jdoias jdoia jois jo jdois jdoias jdoias j djoasdj
---
hey there $ARGUMENTS
!`ls`
check out @README.md
+1 -1
View File
@@ -1,5 +1,5 @@
---
description: "Find issue(s) on github"
description: "find issue(s) on github"
model: opencode/claude-haiku-4-5
---
+15
View File
@@ -0,0 +1,15 @@
---
description: Remove AI code slop
---
Check the diff against dev, and remove all AI generated slop introduced in this branch.
This includes:
- Extra comments that a human wouldn't add or is inconsistent with the rest of the file
- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted / validated codepaths)
- Casts to any to get around type issues
- Any other style that is inconsistent with the file
- Unnecessary emoji usage
Report at the end with only a 1-3 sentence summary of what you changed
+1 -1
View File
@@ -1,5 +1,5 @@
---
description: Spellcheck all markdown file changes
description: spellcheck all markdown file changes
---
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.
+15 -3
View File
@@ -2,12 +2,24 @@
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
// "enterprise": {
// "url": "http://localhost:3000",
// "url": "https://enterprise.dev.opencode.ai",
// },
"instructions": ["STYLE_GUIDE.md"],
"provider": {
"opencode": {
"options": {
// "baseURL": "http://localhost:8080",
"options": {},
},
},
"mcp": {
"exa": {
"type": "remote",
"url": "https://mcp.exa.ai/mcp",
},
"morph": {
"type": "local",
"command": ["bunx", "@morphllm/morphmcp"],
"environment": {
"ENABLED_TOOLS": "warp_grep",
},
},
},
+1
View File
@@ -0,0 +1 @@
sst-env.d.ts
-13
View File
@@ -1,16 +1,3 @@
## IMPORTANT
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use `else` statements unless necessary
- DO NOT use `try`/`catch` if it can be avoided
- AVOID `try`/`catch` where possible
- AVOID `else` statements
- AVOID using `any` type
- AVOID `let` statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
## Debugging
- To test opencode in the `packages/opencode` directory you can run `bun dev`
+2
View File
@@ -42,6 +42,8 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
Please try to follow the [style guide](./STYLE_GUIDE.md)
### Setting up a Debugger
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
+1 -1
View File
@@ -30,7 +30,7 @@ scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
mise use --pin -g ubi:sst/opencode # Any OS
mise use -g ubi:sst/opencode # Any OS
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
```
+166 -151
View File
@@ -1,153 +1,168 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ----------------- | ----------------- | ------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ------------------- | ----------------- | ------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
+12
View File
@@ -0,0 +1,12 @@
## Style Guide
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use `else` statements unless necessary
- DO NOT use `try`/`catch` if it can be avoided
- AVOID `try`/`catch` where possible
- AVOID `else` statements
- AVOID using `any` type
- AVOID `let` statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
+333 -110
View File
File diff suppressed because it is too large Load Diff
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1763934636,
"narHash": "sha256-9glbI7f1uU+yzQCq5LwLgdZqx6svOhZWkd4JRY265fc=",
"lastModified": 1765270179,
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ee09932cedcef15aaf476f9343d1dea2cb77e261",
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
"type": "github"
},
"original": {
+26
View File
@@ -30,6 +30,24 @@ Leave the following comment on a GitHub PR. opencode will implement the requeste
Delete the attachment from S3 when the note is removed /oc
```
#### Review specific code lines
Leave a comment directly on code lines in the PR's "Files" tab. opencode will automatically detect the file, line numbers, and diff context to provide precise responses.
```
[Comment on specific lines in Files tab]
/oc add error handling here
```
When commenting on specific lines, opencode receives:
- The exact file being reviewed
- The specific lines of code
- The surrounding diff context
- Line number information
This allows for more targeted requests without needing to specify file paths or line numbers manually.
## Installation
Run the following command in the terminal from your GitHub repo:
@@ -51,6 +69,8 @@ This will walk you through installing the GitHub app, creating the workflow, and
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
opencode:
@@ -135,3 +155,9 @@ Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with
```
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```
### PR review comment event
```
MOCK_EVENT='{"eventName":"pull_request_review_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"pull_request":{"number":7},"comment":{"id":1,"body":"hey opencode, add error handling","path":"src/components/Button.tsx","diff_hunk":"@@ -45,8 +45,11 @@\n- const handleClick = () => {\n- console.log('clicked')\n+ const handleClick = useCallback(() => {\n+ console.log('clicked')\n+ doSomething()\n+ }, [doSomething])","line":47,"original_line":45,"position":10,"commit_id":"abc123","original_commit_id":"def456"}}}'
```
+5
View File
@@ -13,6 +13,10 @@ inputs:
description: "Share the opencode session (defaults to true for public repos)"
required: false
prompt:
description: "Custom prompt to override the default prompt"
required: false
runs:
using: "composite"
steps:
@@ -27,3 +31,4 @@ runs:
env:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}
+44 -7
View File
@@ -5,7 +5,7 @@ import { graphql } from "@octokit/graphql"
import * as core from "@actions/core"
import * as github from "@actions/github"
import type { Context as GitHubContext } from "@actions/github/lib/context"
import type { IssueCommentEvent } from "@octokit/webhooks-types"
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { spawn } from "node:child_process"
@@ -124,7 +124,7 @@ let exitCode = 0
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
try {
assertContextEvent("issue_comment")
assertContextEvent("issue_comment", "pull_request_review_comment")
assertPayloadKeyword()
await assertOpencodeConnected()
@@ -241,19 +241,43 @@ function createOpencode() {
}
function assertPayloadKeyword() {
const payload = useContext().payload as IssueCommentEvent
const payload = useContext().payload as IssueCommentEvent | PullRequestReviewCommentEvent
const body = payload.comment.body.trim()
if (!body.match(/(?:^|\s)(?:\/opencode|\/oc)(?=$|\s)/)) {
throw new Error("Comments must mention `/opencode` or `/oc`")
}
}
function getReviewCommentContext() {
const context = useContext()
if (context.eventName !== "pull_request_review_comment") {
return null
}
const payload = context.payload as PullRequestReviewCommentEvent
return {
file: payload.comment.path,
diffHunk: payload.comment.diff_hunk,
line: payload.comment.line,
originalLine: payload.comment.original_line,
position: payload.comment.position,
commitId: payload.comment.commit_id,
originalCommitId: payload.comment.original_commit_id,
}
}
async function assertOpencodeConnected() {
let retry = 0
let connected = false
do {
try {
await client.app.get<true>()
await client.app.log<true>({
body: {
service: "github-workflow",
level: "info",
message: "Prepare to react to Github Workflow event",
},
})
connected = true
break
} catch (e) {}
@@ -383,11 +407,24 @@ async function createComment() {
}
async function getUserPrompt() {
const context = useContext()
const payload = context.payload as IssueCommentEvent | PullRequestReviewCommentEvent
const reviewContext = getReviewCommentContext()
let prompt = (() => {
const payload = useContext().payload as IssueCommentEvent
const body = payload.comment.body.trim()
if (body === "/opencode" || body === "/oc") return "Summarize this thread"
if (body.includes("/opencode") || body.includes("/oc")) return body
if (body === "/opencode" || body === "/oc") {
if (reviewContext) {
return `Review this code change and suggest improvements for the commented lines:\n\nFile: ${reviewContext.file}\nLines: ${reviewContext.line}\n\n${reviewContext.diffHunk}`
}
return "Summarize this thread"
}
if (body.includes("/opencode") || body.includes("/oc")) {
if (reviewContext) {
return `${body}\n\nContext: You are reviewing a comment on file "${reviewContext.file}" at line ${reviewContext.line}.\n\nDiff context:\n${reviewContext.diffHunk}`
}
return body
}
throw new Error("Comments must mention `/opencode` or `/oc`")
})()
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../sst-env.d.ts" />
import "sst"
export {}
export {}
+1 -1
View File
@@ -116,7 +116,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
// CONSOLE
////////////////
const bucket = new sst.cloudflare.Bucket("ConsoleData")
const bucket = new sst.cloudflare.Bucket("ZenData")
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")
+5 -5
View File
@@ -4,7 +4,7 @@ APP=opencode
MUTED='\033[0;2m'
RED='\033[0;31m'
ORANGE='\033[38;2;255;140;0m'
ORANGE='\033[38;5;214m'
NC='\033[0m' # No Color
requested_version=${VERSION:-}
@@ -353,10 +353,10 @@ echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
echo -e ""
echo -e ""
echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
echo -e "opencode ${MUTED}Use free models${NC}"
echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
echo -e "opencode help ${MUTED}List commands and options${NC}"
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
echo -e ""
echo -e "cd <project> ${MUTED}# Open directory${NC}"
echo -e "opencode ${MUTED}# Run command${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
echo -e ""
+1 -1
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-cieNNPXZd4Bg9bZtRq2H8L99e24U8p5d+d76SE7SeJc="
"nodeModules": "sha256-JT8J+Nd2kk0x46BcyotmBbM39tuKOW7VzXfOV3R3sqQ="
}
+2 -2
View File
@@ -1,4 +1,4 @@
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }:
args:
let
scripts = args.scripts;
@@ -97,7 +97,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
makeWrapper ${bun}/bin/bun $out/bin/opencode \
--add-flags "run" \
--add-flags "$out/lib/opencode/dist/src/index.js" \
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
--prefix PATH : ${lib.makeBinPath [ ripgrep ]} \
--argv0 opencode
runHook postInstall
+9 -5
View File
@@ -30,16 +30,16 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.5.5",
"@pierre/precision-diffs": "0.6.0-beta.10",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.97",
"hono": "4.7.10",
"hono-openapi": "1.1.1",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-list": "0.3.0",
@@ -63,7 +63,8 @@
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*"
"@opencode-ai/sdk": "workspace:*",
"typescript": "catalog:"
},
"repository": {
"type": "git",
@@ -85,5 +86,8 @@
"overrides": {
"@types/bun": "catalog:",
"@types/node": "catalog:"
},
"patchedDependencies": {
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.113",
"version": "1.0.143",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 50 B

+1
View File
@@ -0,0 +1 @@
../../../ui/src/assets/images/social-share-zen.png

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 46 B

+1
View File
@@ -0,0 +1 @@
../../../ui/src/assets/images/social-share.png

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 46 B

+5 -5
View File
@@ -9,8 +9,8 @@ export const config = {
github: {
repoUrl: "https://github.com/sst/opencode",
starsFormatted: {
compact: "30K",
full: "30,000",
compact: "35K",
full: "35,000",
},
},
@@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "300",
commits: "4,000",
monthlyUsers: "300,000",
contributors: "350",
commits: "5,000",
monthlyUsers: "400,000",
},
} as const
@@ -36,6 +36,7 @@ ${body.email}`.trim()
to: "contact@anoma.ly",
subject: `Enterprise Inquiry from ${body.name}`,
body: emailContent,
replyTo: body.email,
})
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
+32 -3
View File
@@ -338,6 +338,11 @@ body {
}
}
[data-slot="installation-instructions"] {
color: var(--color-text-strong);
margin-bottom: 8px;
}
[data-slot="installation"] {
width: 100%;
max-width: 100%;
@@ -348,6 +353,11 @@ body {
}
}
[data-slot="installation-options"] {
font-size: 13px;
margin-top: 12px;
}
[data-component="tabs"] {
[data-slot="tablist"] {
display: flex;
@@ -480,10 +490,10 @@ body {
}
h1 {
font-size: 28px;
font-size: 38px;
color: var(--color-text-strong);
font-weight: 500;
margin-bottom: 16px;
margin-bottom: 8px;
@media (max-width: 60rem) {
font-size: 22px;
@@ -492,7 +502,7 @@ body {
p {
color: var(--color-text);
margin-bottom: 24px;
margin-bottom: 40px;
max-width: 82%;
@media (max-width: 50rem) {
@@ -607,6 +617,25 @@ body {
padding: var(--vertical-padding) var(--padding);
color: var(--color-text);
a {
background: var(--color-background-strong);
padding: 8px 12px 8px 20px;
color: var(--color-text-inverted);
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
margin-top: 40px;
display: flex;
width: fit-content;
gap: 12px;
text-decoration: none;
}
a:hover {
background: var(--color-background-strong-hover);
}
ul {
padding: 0;
li {
+21 -15
View File
@@ -43,7 +43,7 @@ export default function Home() {
return (
<main data-page="opencode">
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<Title>OpenCode | The AI coding agent built for the terminal</Title>
<Title>OpenCode | The open source AI coding agent</Title>
<Link rel="canonical" href={config.baseUrl} />
<Meta property="og:image" content="/social-share.png" />
<Meta name="twitter:image" content="/social-share.png" />
@@ -56,23 +56,13 @@ export default function Home() {
<a data-slot="releases" href={release()?.url ?? `${config.github.repoUrl}/releases`} target="_blank">
Whats new in {release()?.name ?? "the latest release"}
</a>
<h1>The AI coding agent built for the terminal</h1>
<h1>The open source coding agent</h1>
<p>
OpenCode is fully open source, giving you control and freedom to use any provider, any model, and any
editor.
OpenCode includes free models or connect from any provider to <br />
use other models, including Claude, GPT, Gemini and more.
</p>
<a href="/docs">
<span>Read docs </span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</a>
</div>
<p data-slot="installation-instructions">Install and use. No account, no email, and no credit card.</p>
<div data-slot="installation">
<Tabs
as="section"
@@ -151,6 +141,11 @@ export default function Home() {
</div>
</Tabs>
</div>
<p data-slot="installation-options">
Available in terminal, web, and desktop (coming soon).
<br />
Extensions for VS Code, Cursor, Windsurf, and more.
</p>
</section>
<section data-component="video">
@@ -208,6 +203,17 @@ export default function Home() {
</div>
</li>
</ul>
<a href="/docs">
<span>Read docs </span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</a>
</section>
<section data-component="growth">
@@ -0,0 +1,7 @@
export async function GET() {
const response = await fetch(
"https://raw.githubusercontent.com/sst/opencode/refs/heads/dev/packages/sdk/openapi.json",
)
const json = await response.json()
return json
}
@@ -14,7 +14,7 @@ import "./workspace-picker.css"
const getWorkspaces = query(async () => {
"use server"
return withActor(async () => {
return Database.transaction((tx) =>
return Database.use((tx) =>
tx
.select({
id: WorkspaceTable.id,
@@ -3,7 +3,7 @@ import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
import { createAsync, query, useParams } from "@solidjs/router"
import { useParams } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, Show, For } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
@@ -94,8 +94,6 @@ async function getCosts(workspaceID: string, year: number, month: number) {
}, workspaceID)
}
const queryCosts = query(getCosts, "costs.get")
const MODEL_COLORS: Record<string, string> = {
"claude-sonnet-4-5": "#D4745C",
"claude-sonnet-4": "#E8B4A4",
@@ -158,32 +156,27 @@ export function GraphSection() {
model: null as string | null,
modelDropdownOpen: false,
keyDropdownOpen: false,
colorScheme: "light" as "light" | "dark",
})
const initialData = createAsync(() => queryCosts(params.id!, store.year, store.month))
const onPreviousMonth = async () => {
const month = store.month === 0 ? 11 : store.month - 1
const year = store.month === 0 ? store.year - 1 : store.year
const data = await getCosts(params.id!, year, month)
setStore({ month, year, data })
setStore({ month, year })
}
const onNextMonth = async () => {
const month = store.month === 11 ? 0 : store.month + 1
const year = store.month === 11 ? store.year + 1 : store.year
setStore({ month, year, data: await getCosts(params.id!, year, month) })
setStore({ month, year })
}
const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false })
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
const getData = createMemo(() => store.data ?? initialData())
const getModels = createMemo(() => {
const data = getData()
if (!data?.usage) return []
return Array.from(new Set(data.usage.map((row) => row.model))).sort()
if (!store.data?.usage) return []
return Array.from(new Set(store.data.usage.map((row) => row.model))).sort()
})
const getDates = createMemo(() => {
@@ -206,10 +199,19 @@ export function GraphSection() {
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
const chartConfig = createMemo((): ChartConfiguration | null => {
const data = getData()
const data = store.data
const dates = getDates()
if (!data?.usage?.length) return null
store.colorScheme
const styles = getComputedStyle(document.documentElement)
const colorTextMuted = styles.getPropertyValue("--color-text-muted").trim()
const colorBorderMuted = styles.getPropertyValue("--color-border-muted").trim()
const colorBgElevated = styles.getPropertyValue("--color-bg-elevated").trim()
const colorText = styles.getPropertyValue("--color-text").trim()
const colorTextSecondary = styles.getPropertyValue("--color-text-secondary").trim()
const colorBorder = styles.getPropertyValue("--color-border").trim()
const dailyData = new Map<string, Map<string, number>>()
for (const dateKey of dates) dailyData.set(dateKey, new Map())
@@ -252,7 +254,7 @@ export function GraphSection() {
ticks: {
maxRotation: 0,
autoSkipPadding: 20,
color: "rgba(255, 255, 255, 0.5)",
color: colorTextMuted,
font: {
family: "monospace",
size: 11,
@@ -263,10 +265,10 @@ export function GraphSection() {
stacked: true,
beginAtZero: true,
grid: {
color: "rgba(255, 255, 255, 0.1)",
color: colorBorderMuted,
},
ticks: {
color: "rgba(255, 255, 255, 0.5)",
color: colorTextMuted,
font: {
family: "monospace",
size: 11,
@@ -282,10 +284,10 @@ export function GraphSection() {
tooltip: {
mode: "index",
intersect: false,
backgroundColor: "rgba(0, 0, 0, 0.9)",
titleColor: "rgba(255, 255, 255, 0.9)",
bodyColor: "rgba(255, 255, 255, 0.8)",
borderColor: "rgba(255, 255, 255, 0.1)",
backgroundColor: colorBgElevated,
titleColor: colorText,
bodyColor: colorTextSecondary,
borderColor: colorBorder,
borderWidth: 1,
padding: 12,
displayColors: true,
@@ -301,7 +303,7 @@ export function GraphSection() {
display: true,
position: "bottom",
labels: {
color: "rgba(255, 255, 255, 0.7)",
color: colorTextSecondary,
font: {
size: 12,
},
@@ -339,15 +341,32 @@ export function GraphSection() {
}
})
createEffect(async () => {
const data = await getCosts(params.id!, store.year, store.month)
setStore({ data })
})
createEffect(() => {
const config = chartConfig()
if (!config || !canvasRef) return
if (chartInstance) chartInstance.destroy()
chartInstance = new Chart(canvasRef, config)
onCleanup(() => chartInstance?.destroy())
})
onCleanup(() => chartInstance?.destroy())
createEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
setStore({ colorScheme: mediaQuery.matches ? "dark" : "light" })
const handleColorSchemeChange = (e: MediaQueryListEvent) => {
setStore({ colorScheme: e.matches ? "dark" : "light" })
}
mediaQuery.addEventListener("change", handleColorSchemeChange)
onCleanup(() => mediaQuery.removeEventListener("change", handleColorSchemeChange))
})
return (
<section class={styles.root}>
@@ -356,55 +375,53 @@ export function GraphSection() {
<p>Usage costs broken down by model.</p>
</div>
<Show when={getData()}>
<div data-slot="filter-container">
<div data-slot="month-picker">
<button data-slot="month-button" onClick={onPreviousMonth}>
<IconChevronLeft />
</button>
<span data-slot="month-label">{formatMonthYear()}</span>
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
<IconChevronRight />
</button>
</div>
<Dropdown
trigger={store.model === null ? "All Models" : store.model}
open={store.modelDropdownOpen}
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
<span>All Models</span>
</button>
<For each={getModels()}>
{(model) => (
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
<span>{model}</span>
</button>
)}
</For>
</>
</Dropdown>
<Dropdown
trigger={getKeyName(store.key)}
open={store.keyDropdownOpen}
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
<span>All Keys</span>
</button>
<For each={getData()?.keys || []}>
{(key) => (
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
<span>{key.displayName}</span>
</button>
)}
</For>
</>
</Dropdown>
<div data-slot="filter-container">
<div data-slot="month-picker">
<button data-slot="month-button" onClick={onPreviousMonth}>
<IconChevronLeft />
</button>
<span data-slot="month-label">{formatMonthYear()}</span>
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
<IconChevronRight />
</button>
</div>
</Show>
<Dropdown
trigger={store.model === null ? "All Models" : store.model}
open={store.modelDropdownOpen}
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
<span>All Models</span>
</button>
<For each={getModels()}>
{(model) => (
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
<span>{model}</span>
</button>
)}
</For>
</>
</Dropdown>
<Dropdown
trigger={getKeyName(store.key)}
open={store.keyDropdownOpen}
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
<span>All Keys</span>
</button>
<For each={store.data?.keys || []}>
{(key) => (
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
<span>{key.displayName}</span>
</button>
)}
</For>
</>
</Dropdown>
</div>
<Show
when={chartConfig()}
@@ -1,25 +1,37 @@
import { Resource, waitUntil } from "@opencode-ai/console-resource"
export function createDataDumper(sessionId: string, requestId: string) {
export function createDataDumper(sessionId: string, requestId: string, projectId: string) {
if (Resource.App.stage !== "production") return
if (sessionId === "") return
let data: Record<string, any> = {}
let modelName: string | undefined
let data: Record<string, any> = { sessionId, requestId, projectId }
let metadata: Record<string, any> = { sessionId, requestId, projectId }
return {
provideModel: (model?: string) => (modelName = model),
provideModel: (model?: string) => {
data.modelName = model
metadata.modelName = model
},
provideRequest: (request: string) => (data.request = request),
provideResponse: (response: string) => (data.response = response),
provideStream: (chunk: string) => (data.response = (data.response ?? "") + chunk),
flush: () => {
if (!modelName) return
if (!data.modelName) return
const str = new Date().toISOString().replace(/[^0-9]/g, "")
const yyyymmdd = str.substring(0, 8)
const hh = str.substring(8, 10)
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "")
waitUntil(
Resource.ConsoleData.put(`${yyyymmdd}/${hh}/${modelName}/${sessionId}/${requestId}.json`, JSON.stringify(data)),
Resource.ZenData.put(
`data/${data.modelName}/${sessionId}/${requestId}.json`,
JSON.stringify({ timestamp, ...data }),
),
)
waitUntil(
Resource.ZenData.put(
`meta/${data.modelName}/${timestamp}/${requestId}.json`,
JSON.stringify({ timestamp, ...metadata }),
),
)
},
}
@@ -13,13 +13,7 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
import {
createBodyConverter,
createStreamPartConverter,
createResponseConverter,
ProviderHelper,
UsageInfo,
} from "./provider/provider"
import { createBodyConverter, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider"
import { anthropicHelper } from "./provider/anthropic"
import { googleHelper } from "./provider/google"
import { openaiHelper } from "./provider/openai"
@@ -27,6 +21,7 @@ import { oaCompatHelper } from "./provider/openai-compatible"
import { createRateLimiter } from "./rateLimiter"
import { createDataDumper } from "./dataDumper"
import { createTrialLimiter } from "./trialLimiter"
import { createStickyTracker } from "./stickyProviderTracker"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = {
@@ -61,6 +56,7 @@ export async function handler(
const ip = input.request.headers.get("x-real-ip") ?? ""
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
const requestId = input.request.headers.get("x-opencode-request") ?? ""
const projectId = input.request.headers.get("x-opencode-project") ?? ""
logger.metric({
is_tream: isStream,
session: sessionId,
@@ -68,15 +64,25 @@ export async function handler(
})
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, model)
const dataDumper = createDataDumper(sessionId, requestId)
const dataDumper = createDataDumper(sessionId, requestId, projectId)
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
const isTrial = await trialLimiter?.isTrial()
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
const stickyProvider = await stickyTracker?.get()
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry)
const authInfo = await authenticate(modelInfo, providerInfo)
const authInfo = await authenticate(modelInfo)
const providerInfo = selectProvider(
zenData,
authInfo,
modelInfo,
sessionId,
isTrial ?? false,
retry,
stickyProvider,
)
validateBilling(authInfo, modelInfo)
validateModelSettings(authInfo)
updateProviderKey(authInfo, providerInfo)
@@ -126,6 +132,9 @@ export async function handler(
dataDumper?.provideModel(providerInfo.storeModel)
dataDumper?.provideRequest(reqBody)
// Store sticky provider
await stickyTracker?.set(providerInfo.id)
// Scrub response headers
const resHeaders = new Headers()
const keepHeaders = ["content-type", "cache-control"]
@@ -290,16 +299,26 @@ export async function handler(
function selectProvider(
zenData: ZenData,
authInfo: AuthInfo,
modelInfo: ModelInfo,
sessionId: string,
isTrial: boolean,
retry: RetryOptions,
stickyProvider: string | undefined,
) {
const provider = (() => {
// temporarily commment out
//if (isTrial) {
// return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
//}
if (authInfo?.provider?.credentials) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
}
if (isTrial) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
}
if (stickyProvider) {
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
if (provider) return provider
}
if (retry.retryCount === MAX_RETRIES) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
@@ -336,7 +355,7 @@ export async function handler(
}
}
async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) {
async function authenticate(modelInfo: ModelInfo) {
const apiKey = opts.parseApiKey(input.request.headers)
if (!apiKey || apiKey === "public") {
if (modelInfo.allowAnonymous) return
@@ -374,7 +393,12 @@ export async function handler(
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, modelInfo.id)))
.leftJoin(
ProviderTable,
and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)),
modelInfo.byokProvider
? and(
eq(ProviderTable.workspaceID, KeyTable.workspaceID),
eq(ProviderTable.provider, modelInfo.byokProvider),
)
: sql`false`,
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]),
@@ -451,8 +475,7 @@ export async function handler(
}
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
if (!authInfo) return
if (!authInfo.provider?.credentials) return
if (!authInfo?.provider?.credentials) return
providerInfo.apiKey = authInfo.provider.credentials
}
@@ -565,7 +588,7 @@ export async function handler(
tx
.update(KeyTable)
.set({ timeUsed: sql`now()` })
.where(eq(KeyTable.id, authInfo.apiKeyId)),
.where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
)
}
@@ -0,0 +1,16 @@
import { Resource } from "@opencode-ai/console-resource"
export function createStickyTracker(stickyProvider: boolean, session: string) {
if (!stickyProvider) return
if (!session) return
const key = `sticky:${session}`
return {
get: async () => {
return await Resource.GatewayKv.get(key)
},
set: async (providerId: string) => {
await Resource.GatewayKv.put(key, providerId, { expirationTtl: 86400 })
},
}
}
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
export {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.113",
"version": "1.0.143",
"private": true,
"type": "module",
"dependencies": {
+5 -6
View File
@@ -11,7 +11,7 @@ export namespace Account {
id: z.string().optional(),
}),
async (input) =>
Database.transaction(async (tx) => {
Database.use(async (tx) => {
const id = input.id ?? Identifier.create("account")
await tx.insert(AccountTable).values({
id,
@@ -21,13 +21,12 @@ export namespace Account {
)
export const fromID = fn(z.string(), async (id) =>
Database.transaction(async (tx) => {
return tx
Database.use((tx) =>
tx
.select()
.from(AccountTable)
.where(eq(AccountTable.id, id))
.execute()
.then((rows) => rows[0])
}),
.then((rows) => rows[0]),
),
)
}
+2
View File
@@ -22,6 +22,7 @@ export namespace AWS {
to: z.string(),
subject: z.string(),
body: z.string(),
replyTo: z.string().optional(),
}),
async (input) => {
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
@@ -35,6 +36,7 @@ export namespace AWS {
Destination: {
ToAddresses: [input.to],
},
...(input.replyTo && { ReplyToAddresses: [input.replyTo] }),
Content: {
Simple: {
Subject: {
+2
View File
@@ -24,6 +24,8 @@ export namespace ZenData {
cost: ModelCostSchema,
cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(),
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
stickyProvider: z.boolean().optional(),
trial: z
.object({
limit: z.number(),
+1 -1
View File
@@ -47,7 +47,7 @@ export namespace Provider {
}),
async ({ provider }) => {
Actor.assertAdmin()
return Database.transaction((tx) =>
return Database.use((tx) =>
tx
.delete(ProviderTable)
.where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))),
+94 -90
View File
@@ -6,126 +6,130 @@
import "sst"
declare module "sst" {
export interface Resource {
ADMIN_SECRET: {
type: "sst.sst.Secret"
value: string
"ADMIN_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
AUTH_API_URL: {
type: "sst.sst.Linkable"
value: string
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
AWS_SES_ACCESS_KEY_ID: {
type: "sst.sst.Secret"
value: string
"AWS_SES_ACCESS_KEY_ID": {
"type": "sst.sst.Secret"
"value": string
}
AWS_SES_SECRET_ACCESS_KEY: {
type: "sst.sst.Secret"
value: string
"AWS_SES_SECRET_ACCESS_KEY": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_API_TOKEN": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
"type": "sst.sst.Secret"
"value": string
}
Console: {
type: "sst.cloudflare.SolidStart"
url: string
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
R2AccessKey: {
type: "sst.sst.Secret"
value: string
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
R2SecretKey: {
type: "sst.sst.Secret"
value: string
"R2AccessKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
"R2SecretKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
Web: {
type: "sst.cloudflare.Astro"
url: string
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
ZEN_MODELS2: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS3: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS4: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS4": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
Api: cloudflare.Service
AuthApi: cloudflare.Service
AuthStorage: cloudflare.KVNamespace
Bucket: cloudflare.R2Bucket
ConsoleData: cloudflare.R2Bucket
EnterpriseStorage: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.113",
"version": "1.0.143",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -194,7 +194,7 @@ export default {
// Get workspace
await Actor.provide("account", { accountID, email }, async () => {
await User.joinInvitedWorkspaces()
const workspaces = await Database.transaction(async (tx) =>
const workspaces = await Database.use((tx) =>
tx
.select({ id: WorkspaceTable.id })
.from(WorkspaceTable)
+94 -90
View File
@@ -6,126 +6,130 @@
import "sst"
declare module "sst" {
export interface Resource {
ADMIN_SECRET: {
type: "sst.sst.Secret"
value: string
"ADMIN_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
AUTH_API_URL: {
type: "sst.sst.Linkable"
value: string
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
AWS_SES_ACCESS_KEY_ID: {
type: "sst.sst.Secret"
value: string
"AWS_SES_ACCESS_KEY_ID": {
"type": "sst.sst.Secret"
"value": string
}
AWS_SES_SECRET_ACCESS_KEY: {
type: "sst.sst.Secret"
value: string
"AWS_SES_SECRET_ACCESS_KEY": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_API_TOKEN": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
"type": "sst.sst.Secret"
"value": string
}
Console: {
type: "sst.cloudflare.SolidStart"
url: string
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
R2AccessKey: {
type: "sst.sst.Secret"
value: string
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
R2SecretKey: {
type: "sst.sst.Secret"
value: string
"R2AccessKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
"R2SecretKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
Web: {
type: "sst.cloudflare.Astro"
url: string
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
ZEN_MODELS2: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS3: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS4: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS4": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
Api: cloudflare.Service
AuthApi: cloudflare.Service
AuthStorage: cloudflare.KVNamespace
Bucket: cloudflare.R2Bucket
ConsoleData: cloudflare.R2Bucket
EnterpriseStorage: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.113",
"version": "1.0.143",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
export {}
+2 -2
View File
@@ -2,8 +2,8 @@ import type { KVNamespaceListOptions, KVNamespaceListResult, KVNamespacePutOptio
import { Resource as ResourceBase } from "sst"
import Cloudflare from "cloudflare"
export const waitUntil = async (fn: () => Promise<void>) => {
await fn()
export const waitUntil = async (promise: Promise<any>) => {
await promise
}
export const Resource = new Proxy(
+94 -90
View File
@@ -6,126 +6,130 @@
import "sst"
declare module "sst" {
export interface Resource {
ADMIN_SECRET: {
type: "sst.sst.Secret"
value: string
"ADMIN_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
AUTH_API_URL: {
type: "sst.sst.Linkable"
value: string
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
AWS_SES_ACCESS_KEY_ID: {
type: "sst.sst.Secret"
value: string
"AWS_SES_ACCESS_KEY_ID": {
"type": "sst.sst.Secret"
"value": string
}
AWS_SES_SECRET_ACCESS_KEY: {
type: "sst.sst.Secret"
value: string
"AWS_SES_SECRET_ACCESS_KEY": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_API_TOKEN": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
"type": "sst.sst.Secret"
"value": string
}
Console: {
type: "sst.cloudflare.SolidStart"
url: string
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
R2AccessKey: {
type: "sst.sst.Secret"
value: string
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
R2SecretKey: {
type: "sst.sst.Secret"
value: string
"R2AccessKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
"R2SecretKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
Web: {
type: "sst.cloudflare.Astro"
url: string
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
ZEN_MODELS2: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS3: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS4: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS4": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
Api: cloudflare.Service
AuthApi: cloudflare.Service
AuthStorage: cloudflare.KVNamespace
Bucket: cloudflare.R2Bucket
ConsoleData: cloudflare.R2Bucket
EnterpriseStorage: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}
+2
View File
@@ -0,0 +1,2 @@
[test]
preload = ["./happydom.ts"]
+75
View File
@@ -0,0 +1,75 @@
import { GlobalRegistrator } from "@happy-dom/global-registrator"
GlobalRegistrator.register()
const originalGetContext = HTMLCanvasElement.prototype.getContext
// @ts-expect-error - we're overriding with a simplified mock
HTMLCanvasElement.prototype.getContext = function (contextType: string, _options?: unknown) {
if (contextType === "2d") {
return {
canvas: this,
fillStyle: "#000000",
strokeStyle: "#000000",
font: "12px monospace",
textAlign: "start",
textBaseline: "alphabetic",
globalAlpha: 1,
globalCompositeOperation: "source-over",
imageSmoothingEnabled: true,
lineWidth: 1,
lineCap: "butt",
lineJoin: "miter",
miterLimit: 10,
shadowBlur: 0,
shadowColor: "rgba(0, 0, 0, 0)",
shadowOffsetX: 0,
shadowOffsetY: 0,
fillRect: () => {},
strokeRect: () => {},
clearRect: () => {},
fillText: () => {},
strokeText: () => {},
measureText: (text: string) => ({ width: text.length * 8 }),
drawImage: () => {},
save: () => {},
restore: () => {},
scale: () => {},
rotate: () => {},
translate: () => {},
transform: () => {},
setTransform: () => {},
resetTransform: () => {},
createLinearGradient: () => ({ addColorStop: () => {} }),
createRadialGradient: () => ({ addColorStop: () => {} }),
createPattern: () => null,
beginPath: () => {},
closePath: () => {},
moveTo: () => {},
lineTo: () => {},
bezierCurveTo: () => {},
quadraticCurveTo: () => {},
arc: () => {},
arcTo: () => {},
ellipse: () => {},
rect: () => {},
fill: () => {},
stroke: () => {},
clip: () => {},
isPointInPath: () => false,
isPointInStroke: () => false,
getTransform: () => ({}),
getImageData: () => ({
data: new Uint8ClampedArray(0),
width: 0,
height: 0,
}),
putImageData: () => {},
createImageData: () => ({
data: new Uint8ClampedArray(0),
width: 0,
height: 0,
}),
} as unknown as CanvasRenderingContext2D
}
return originalGetContext.call(this, contextType as "2d", _options)
}
+3 -2
View File
@@ -9,7 +9,8 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#000000" />
<meta name="theme-color" content="#F8F7F7" />
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
@@ -22,6 +23,6 @@
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
<script src="/src/entry.tsx" type="module"></script>
</body>
</html>
+10 -2
View File
@@ -1,10 +1,14 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.113",
"version": "1.0.143",
"description": "",
"type": "module",
"exports": {
".": "./src/index.ts",
"./vite": "./vite.js"
},
"scripts": {
"typecheck": "tsgo --noEmit",
"typecheck": "tsgo -b",
"start": "vite",
"dev": "vite",
"build": "vite build",
@@ -12,8 +16,10 @@
},
"license": "MIT",
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
"@tailwindcss/vite": "catalog:",
"@tsconfig/bun": "1.0.9",
"@types/bun": "catalog:",
"@types/luxon": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -33,11 +39,13 @@
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "4.3.3",
"@solid-primitives/websocket": "1.3.1",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:",
"fuzzysort": "catalog:",
"ghostty-web": "0.3.0",
"luxon": "catalog:",
"marked": "16.2.0",
"marked-shiki": "1.2.1",
+1
View File
@@ -0,0 +1 @@
../../ui/src/assets/images/social-share-zen.png
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 43 B

+1
View File
@@ -0,0 +1 @@
../../ui/src/assets/images/social-share.png

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 43 B

@@ -0,0 +1,272 @@
import { describe, test, expect, beforeAll, afterEach } from "bun:test"
import { Terminal, Ghostty } from "ghostty-web"
import { SerializeAddon } from "./serialize"
let ghostty: Ghostty
beforeAll(async () => {
ghostty = await Ghostty.load()
})
const terminals: Terminal[] = []
afterEach(() => {
for (const term of terminals) {
term.dispose()
}
terminals.length = 0
document.body.innerHTML = ""
})
function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } {
const container = document.createElement("div")
document.body.appendChild(container)
const term = new Terminal({ cols, rows, ghostty })
const addon = new SerializeAddon()
term.loadAddon(addon)
term.open(container)
terminals.push(term)
return { term, addon, container }
}
function writeAndWait(term: Terminal, data: string): Promise<void> {
return new Promise((resolve) => {
term.write(data, resolve)
})
}
describe("SerializeAddon", () => {
describe("ANSI color preservation", () => {
test("should preserve text attributes (bold, italic, underline)", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
expect(origLine!.getCell(0)!.isBold()).toBe(1)
expect(origLine!.getCell(5)!.isItalic()).toBe(1)
expect(origLine!.getCell(12)!.isUnderline()).toBe(1)
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const boldCell = line!.getCell(0)
expect(boldCell!.getChars()).toBe("B")
expect(boldCell!.isBold()).toBe(1)
const italicCell = line!.getCell(5)
expect(italicCell!.getChars()).toBe("I")
expect(italicCell!.isItalic()).toBe(1)
const underCell = line!.getCell(12)
expect(underCell!.getChars()).toBe("U")
expect(underCell!.isUnderline()).toBe(1)
})
test("should preserve basic 16-color foreground colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRedFg = origLine!.getCell(0)!.getFgColor()
const origGreenFg = origLine!.getCell(3)!.getFgColor()
const origBlueFg = origLine!.getCell(8)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
expect(line).toBeDefined()
const redCell = line!.getCell(0)
expect(redCell!.getChars()).toBe("R")
expect(redCell!.getFgColor()).toBe(origRedFg)
const greenCell = line!.getCell(3)
expect(greenCell!.getChars()).toBe("G")
expect(greenCell!.getFgColor()).toBe(origGreenFg)
const blueCell = line!.getCell(8)
expect(blueCell!.getChars()).toBe("B")
expect(blueCell!.getFgColor()).toBe(origBlueFg)
})
test("should preserve 256-color palette colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRedFg = origLine!.getCell(0)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const redCell = line!.getCell(0)
expect(redCell!.getChars()).toBe("R")
expect(redCell!.getFgColor()).toBe(origRedFg)
})
test("should preserve RGB/truecolor colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRgbFg = origLine!.getCell(0)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const rgbCell = line!.getCell(0)
expect(rgbCell!.getChars()).toBe("R")
expect(rgbCell!.getFgColor()).toBe(origRgbFg)
})
test("should preserve background colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRedBg = origLine!.getCell(0)!.getBgColor()
const origGreenBg = origLine!.getCell(6)!.getBgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const redBgCell = line!.getCell(0)
expect(redBgCell!.getChars()).toBe("R")
expect(redBgCell!.getBgColor()).toBe(origRedBg)
const greenBgCell = line!.getCell(6)
expect(greenBgCell!.getChars()).toBe("G")
expect(greenBgCell!.getBgColor()).toBe(origGreenBg)
})
test("should handle combined colors and attributes", async () => {
const { term, addon } = createTerminal()
const input =
"\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL "
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origFg = origLine!.getCell(0)!.getFgColor()
const origBg = origLine!.getCell(0)!.getBgColor()
expect(origLine!.getCell(0)!.isBold()).toBe(1)
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "")
expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true)
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, cleanSerialized)
const line = term2.buffer.active.getLine(0)
const comboCell = line!.getCell(0)
expect(comboCell!.getChars()).toBe("C")
expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m")
})
})
describe("round-trip serialization", () => {
test("should not produce ECH sequences", async () => {
const { term, addon } = createTerminal()
await writeAndWait(term, "\x1b[31mHello\x1b[0m World")
const serialized = addon.serialize()
const hasECH = /\x1b\[\d+X/.test(serialized)
expect(hasECH).toBe(false)
})
test("multi-line content should not have garbage characters", async () => {
const { term, addon } = createTerminal()
const content = [
"\x1b[1;32m\x1b[0m \x1b[34mcd\x1b[0m /some/path",
"\x1b[1;32m\x1b[0m \x1b[34mls\x1b[0m -la",
"total 42",
].join("\r\n")
await writeAndWait(term, content)
const serialized = addon.serialize()
expect(/\x1b\[\d+X/.test(serialized)).toBe(false)
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
for (let row = 0; row < 3; row++) {
const line = term2.buffer.active.getLine(row)?.translateToString(true)
expect(line?.includes("𑼝")).toBe(false)
}
expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
})
test("serialized output written to new terminal should match original colors", async () => {
const { term, addon } = createTerminal(40, 5)
const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! "
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origHelloFg = origLine!.getCell(0)!.getFgColor()
const origWorldFg = origLine!.getCell(6)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal(40, 5)
terminals.push(term2)
await writeAndWait(term2, serialized)
const newLine = term2.buffer.active.getLine(0)
expect(newLine!.getCell(0)!.getChars()).toBe("H")
expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg)
expect(newLine!.getCell(6)!.getChars()).toBe("W")
expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg)
expect(newLine!.getCell(11)!.getChars()).toBe("!")
})
})
})
+595
View File
@@ -0,0 +1,595 @@
/**
* SerializeAddon - Serialize terminal buffer contents
*
* Port of xterm.js addon-serialize for ghostty-web.
* Enables serialization of terminal contents to a string that can
* be written back to restore terminal state.
*
* Usage:
* ```typescript
* const serializeAddon = new SerializeAddon();
* term.loadAddon(serializeAddon);
* const content = serializeAddon.serialize();
* ```
*/
import type { ITerminalAddon, ITerminalCore, IBufferRange } from "ghostty-web"
// ============================================================================
// Buffer Types (matching ghostty-web internal interfaces)
// ============================================================================
interface IBuffer {
readonly type: "normal" | "alternate"
readonly cursorX: number
readonly cursorY: number
readonly viewportY: number
readonly baseY: number
readonly length: number
getLine(y: number): IBufferLine | undefined
getNullCell(): IBufferCell
}
interface IBufferLine {
readonly length: number
readonly isWrapped: boolean
getCell(x: number): IBufferCell | undefined
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string
}
interface IBufferCell {
getChars(): string
getCode(): number
getWidth(): number
getFgColorMode(): number
getBgColorMode(): number
getFgColor(): number
getBgColor(): number
isBold(): number
isItalic(): number
isUnderline(): number
isStrikethrough(): number
isBlink(): number
isInverse(): number
isInvisible(): number
isFaint(): number
isDim(): boolean
}
// ============================================================================
// Types
// ============================================================================
export interface ISerializeOptions {
/**
* The row range to serialize. When an explicit range is specified, the cursor
* will get its final repositioning.
*/
range?: ISerializeRange
/**
* The number of rows in the scrollback buffer to serialize, starting from
* the bottom of the scrollback buffer. When not specified, all available
* rows in the scrollback buffer will be serialized.
*/
scrollback?: number
/**
* Whether to exclude the terminal modes from the serialization.
* Default: false
*/
excludeModes?: boolean
/**
* Whether to exclude the alt buffer from the serialization.
* Default: false
*/
excludeAltBuffer?: boolean
}
export interface ISerializeRange {
/**
* The line to start serializing (inclusive).
*/
start: number
/**
* The line to end serializing (inclusive).
*/
end: number
}
export interface IHTMLSerializeOptions {
/**
* The number of rows in the scrollback buffer to serialize, starting from
* the bottom of the scrollback buffer.
*/
scrollback?: number
/**
* Whether to only serialize the selection.
* Default: false
*/
onlySelection?: boolean
/**
* Whether to include the global background of the terminal.
* Default: false
*/
includeGlobalBackground?: boolean
/**
* The range to serialize. This is prioritized over onlySelection.
*/
range?: {
startLine: number
endLine: number
startCol: number
}
}
// ============================================================================
// Helper Functions
// ============================================================================
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high))
}
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor()
}
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor()
}
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
return (
!!cell1.isInverse() === !!cell2.isInverse() &&
!!cell1.isBold() === !!cell2.isBold() &&
!!cell1.isUnderline() === !!cell2.isUnderline() &&
!!cell1.isBlink() === !!cell2.isBlink() &&
!!cell1.isInvisible() === !!cell2.isInvisible() &&
!!cell1.isItalic() === !!cell2.isItalic() &&
!!cell1.isDim() === !!cell2.isDim() &&
!!cell1.isStrikethrough() === !!cell2.isStrikethrough()
)
}
// ============================================================================
// Base Serialize Handler
// ============================================================================
abstract class BaseSerializeHandler {
constructor(protected readonly _buffer: IBuffer) {}
private _isRealContent(codepoint: number): boolean {
if (codepoint === 0) return false
if (codepoint >= 0xf000) return false
return true
}
private _findLastContentColumn(line: IBufferLine): number {
let lastContent = -1
for (let col = 0; col < line.length; col++) {
const cell = line.getCell(col)
if (cell && this._isRealContent(cell.getCode())) {
lastContent = col
}
}
return lastContent + 1
}
public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
let oldCell = this._buffer.getNullCell()
const startRow = range.start.y
const endRow = range.end.y
const startColumn = range.start.x
const endColumn = range.end.x
this._beforeSerialize(endRow - startRow, startRow, endRow)
for (let row = startRow; row <= endRow; row++) {
const line = this._buffer.getLine(row)
if (line) {
const startLineColumn = row === range.start.y ? startColumn : 0
const maxColumn = row === range.end.y ? endColumn : this._findLastContentColumn(line)
const endLineColumn = Math.min(maxColumn, line.length)
for (let col = startLineColumn; col < endLineColumn; col++) {
const c = line.getCell(col)
if (!c) {
continue
}
this._nextCell(c, oldCell, row, col)
oldCell = c
}
}
this._rowEnd(row, row === endRow)
}
this._afterSerialize()
return this._serializeString(excludeFinalCursorPosition)
}
protected _nextCell(_cell: IBufferCell, _oldCell: IBufferCell, _row: number, _col: number): void {}
protected _rowEnd(_row: number, _isLastRow: boolean): void {}
protected _beforeSerialize(_rows: number, _startRow: number, _endRow: number): void {}
protected _afterSerialize(): void {}
protected _serializeString(_excludeFinalCursorPosition?: boolean): string {
return ""
}
}
// ============================================================================
// String Serialize Handler
// ============================================================================
class StringSerializeHandler extends BaseSerializeHandler {
private _rowIndex: number = 0
private _allRows: string[] = []
private _allRowSeparators: string[] = []
private _currentRow: string = ""
private _nullCellCount: number = 0
private _cursorStyle: IBufferCell
private _firstRow: number = 0
private _lastCursorRow: number = 0
private _lastCursorCol: number = 0
private _lastContentCursorRow: number = 0
private _lastContentCursorCol: number = 0
constructor(
buffer: IBuffer,
private readonly _terminal: ITerminalCore,
) {
super(buffer)
this._cursorStyle = this._buffer.getNullCell()
}
protected _beforeSerialize(rows: number, start: number, _end: number): void {
this._allRows = new Array<string>(rows)
this._lastContentCursorRow = start
this._lastCursorRow = start
this._firstRow = start
}
protected _rowEnd(row: number, isLastRow: boolean): void {
let rowSeparator = ""
if (!isLastRow) {
const nextLine = this._buffer.getLine(row + 1)
if (!nextLine?.isWrapped) {
rowSeparator = "\r\n"
this._lastCursorRow = row + 1
this._lastCursorCol = 0
}
}
this._allRows[this._rowIndex] = this._currentRow
this._allRowSeparators[this._rowIndex++] = rowSeparator
this._currentRow = ""
this._nullCellCount = 0
}
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
const sgrSeq: number[] = []
const fgChanged = !equalFg(cell, oldCell)
const bgChanged = !equalBg(cell, oldCell)
const flagsChanged = !equalFlags(cell, oldCell)
if (fgChanged || bgChanged || flagsChanged) {
if (this._isAttributeDefault(cell)) {
if (!this._isAttributeDefault(oldCell)) {
sgrSeq.push(0)
}
} else {
if (flagsChanged) {
if (!!cell.isInverse() !== !!oldCell.isInverse()) {
sgrSeq.push(cell.isInverse() ? 7 : 27)
}
if (!!cell.isBold() !== !!oldCell.isBold()) {
sgrSeq.push(cell.isBold() ? 1 : 22)
}
if (!!cell.isUnderline() !== !!oldCell.isUnderline()) {
sgrSeq.push(cell.isUnderline() ? 4 : 24)
}
if (!!cell.isBlink() !== !!oldCell.isBlink()) {
sgrSeq.push(cell.isBlink() ? 5 : 25)
}
if (!!cell.isInvisible() !== !!oldCell.isInvisible()) {
sgrSeq.push(cell.isInvisible() ? 8 : 28)
}
if (!!cell.isItalic() !== !!oldCell.isItalic()) {
sgrSeq.push(cell.isItalic() ? 3 : 23)
}
if (!!cell.isDim() !== !!oldCell.isDim()) {
sgrSeq.push(cell.isDim() ? 2 : 22)
}
if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) {
sgrSeq.push(cell.isStrikethrough() ? 9 : 29)
}
}
if (fgChanged) {
const color = cell.getFgColor()
const mode = cell.getFgColorMode()
if (mode === 2 || mode === 3 || mode === -1) {
sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
} else if (mode === 1) {
// Palette
if (color >= 16) {
sgrSeq.push(38, 5, color)
} else {
sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7))
}
} else {
sgrSeq.push(39)
}
}
if (bgChanged) {
const color = cell.getBgColor()
const mode = cell.getBgColorMode()
if (mode === 2 || mode === 3 || mode === -1) {
sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
} else if (mode === 1) {
// Palette
if (color >= 16) {
sgrSeq.push(48, 5, color)
} else {
sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7))
}
} else {
sgrSeq.push(49)
}
}
}
}
return sgrSeq
}
private _isAttributeDefault(cell: IBufferCell): boolean {
const mode = cell.getFgColorMode()
const bgMode = cell.getBgColorMode()
if (mode === 0 && bgMode === 0) {
return (
!cell.isBold() &&
!cell.isItalic() &&
!cell.isUnderline() &&
!cell.isBlink() &&
!cell.isInverse() &&
!cell.isInvisible() &&
!cell.isDim() &&
!cell.isStrikethrough()
)
}
const fgColor = cell.getFgColor()
const bgColor = cell.getBgColor()
const nullCell = this._buffer.getNullCell()
const nullFg = nullCell.getFgColor()
const nullBg = nullCell.getBgColor()
return (
fgColor === nullFg &&
bgColor === nullBg &&
!cell.isBold() &&
!cell.isItalic() &&
!cell.isUnderline() &&
!cell.isBlink() &&
!cell.isInverse() &&
!cell.isInvisible() &&
!cell.isDim() &&
!cell.isStrikethrough()
)
}
protected _nextCell(cell: IBufferCell, _oldCell: IBufferCell, row: number, col: number): void {
const isPlaceHolderCell = cell.getWidth() === 0
if (isPlaceHolderCell) {
return
}
const codepoint = cell.getCode()
const isGarbage = codepoint >= 0xf000
const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0
if (styleChanged) {
if (this._nullCellCount > 0) {
this._currentRow += `\u001b[${this._nullCellCount}C`
this._nullCellCount = 0
}
this._lastContentCursorRow = this._lastCursorRow = row
this._lastContentCursorCol = this._lastCursorCol = col
this._currentRow += `\u001b[${sgrSeq.join(";")}m`
const line = this._buffer.getLine(row)
const cellFromLine = line?.getCell(col)
if (cellFromLine) {
this._cursorStyle = cellFromLine
}
}
if (isEmptyCell) {
this._nullCellCount += cell.getWidth()
} else {
if (this._nullCellCount > 0) {
this._currentRow += `\u001b[${this._nullCellCount}C`
this._nullCellCount = 0
}
this._currentRow += cell.getChars()
this._lastContentCursorRow = this._lastCursorRow = row
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth()
}
}
protected _serializeString(excludeFinalCursorPosition?: boolean): string {
let rowEnd = this._allRows.length
if (this._buffer.length - this._firstRow <= this._terminal.rows) {
rowEnd = this._lastContentCursorRow + 1 - this._firstRow
this._lastCursorCol = this._lastContentCursorCol
this._lastCursorRow = this._lastContentCursorRow
}
let content = ""
for (let i = 0; i < rowEnd; i++) {
content += this._allRows[i]
if (i + 1 < rowEnd) {
content += this._allRowSeparators[i]
}
}
if (!excludeFinalCursorPosition) {
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
const cursorCol = this._buffer.cursorX + 1
content += `\u001b[${cursorRow};${cursorCol}H`
}
return content
}
}
// ============================================================================
// SerializeAddon Class
// ============================================================================
export class SerializeAddon implements ITerminalAddon {
private _terminal?: ITerminalCore
/**
* Activate the addon (called by Terminal.loadAddon)
*/
public activate(terminal: ITerminalCore): void {
this._terminal = terminal
}
/**
* Dispose the addon and clean up resources
*/
public dispose(): void {
this._terminal = undefined
}
/**
* Serializes terminal rows into a string that can be written back to the
* terminal to restore the state. The cursor will also be positioned to the
* correct cell.
*
* @param options Custom options to allow control over what gets serialized.
*/
public serialize(options?: ISerializeOptions): string {
if (!this._terminal) {
throw new Error("Cannot use addon until it has been loaded")
}
const terminal = this._terminal as any
const buffer = terminal.buffer
if (!buffer) {
return ""
}
const normalBuffer = buffer.normal || buffer.active
const altBuffer = buffer.alternate
if (!normalBuffer) {
return ""
}
let content = options?.range
? this._serializeBufferByRange(normalBuffer, options.range, true)
: this._serializeBufferByScrollback(normalBuffer, options?.scrollback)
if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) {
const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
content += `\u001b[?1049h\u001b[H${alternateContent}`
}
return content
}
/**
* Serializes terminal content as plain text (no escape sequences)
* @param options Custom options to allow control over what gets serialized.
*/
public serializeAsText(options?: { scrollback?: number; trimWhitespace?: boolean }): string {
if (!this._terminal) {
throw new Error("Cannot use addon until it has been loaded")
}
const terminal = this._terminal as any
const buffer = terminal.buffer
if (!buffer) {
return ""
}
const activeBuffer = buffer.active || buffer.normal
if (!activeBuffer) {
return ""
}
const maxRows = activeBuffer.length
const scrollback = options?.scrollback
const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + this._terminal.rows, 0, maxRows)
const startRow = maxRows - correctRows
const endRow = maxRows - 1
const lines: string[] = []
for (let row = startRow; row <= endRow; row++) {
const line = activeBuffer.getLine(row)
if (line) {
const text = line.translateToString(options?.trimWhitespace ?? true)
lines.push(text)
}
}
// Trim trailing empty lines if requested
if (options?.trimWhitespace) {
while (lines.length > 0 && lines[lines.length - 1] === "") {
lines.pop()
}
}
return lines.join("\n")
}
private _serializeBufferByScrollback(buffer: IBuffer, scrollback?: number): string {
const maxRows = buffer.length
const rows = this._terminal?.rows ?? 24
const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + rows, 0, maxRows)
return this._serializeBufferByRange(
buffer,
{
start: maxRows - correctRows,
end: maxRows - 1,
},
false,
)
}
private _serializeBufferByRange(
buffer: IBuffer,
range: ISerializeRange,
excludeFinalCursorPosition: boolean,
): string {
const handler = new StringSerializeHandler(buffer, this._terminal!)
const cols = this._terminal?.cols ?? 80
return handler.serialize(
{
start: { x: 0, y: range.start },
end: { x: cols, y: range.end },
},
excludeFinalCursorPosition,
)
}
}
+59
View File
@@ -0,0 +1,59 @@
import "@/index.css"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { Diff } from "@opencode-ai/ui/diff"
import { GlobalSyncProvider } from "./context/global-sync"
import Layout from "@/pages/layout"
import Home from "@/pages/home"
import DirectoryLayout from "@/pages/directory-layout"
import Session from "@/pages/session"
import { LayoutProvider } from "./context/layout"
import { GlobalSDKProvider } from "./context/global-sdk"
import { SessionProvider } from "./context/session"
import { Show } from "solid-js"
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
const url =
new URLSearchParams(document.location.search).get("url") ||
(location.hostname.includes("opencode.ai") || location.hostname.includes("localhost")
? `http://${host}:${port}`
: "/")
export function App() {
return (
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<MetaProvider>
<Font />
<Router root={Layout}>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<SessionProvider>
<Session />
</SessionProvider>
</Show>
)}
/>
</Route>
</Router>
</MetaProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</DiffComponentProvider>
</MarkedProvider>
)
}
@@ -1,5 +1,5 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
@@ -14,14 +14,44 @@ import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Select } from "@opencode-ai/ui/select"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { type IconName } from "@opencode-ai/ui/icons/provider"
interface PromptInputProps {
class?: string
ref?: (el: HTMLDivElement) => void
}
const PLACEHOLDERS = [
"Fix a TODO in the codebase",
"What is the tech stack of this project?",
"Fix broken tests",
"Explain how authentication works",
"Find and fix security vulnerabilities",
"Add unit tests for the user service",
"Refactor this function to be more readable",
"What does this error mean?",
"Help me debug this issue",
"Generate API documentation",
"Optimize database queries",
"Add input validation",
"Create a new component for...",
"How do I deploy this project?",
"Review my code for best practices",
"Add error handling to this function",
"Explain this regex pattern",
"Convert this to TypeScript",
"Add logging throughout the codebase",
"What dependencies are outdated?",
"Help me write a migration script",
"Implement caching for this endpoint",
"Add pagination to this list",
"Create a CLI command for...",
"How do environment variables work here?",
]
export const PromptInput: Component<PromptInputProps> = (props) => {
const navigate = useNavigate()
const sdk = useSDK()
@@ -36,6 +66,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
popoverIsOpen: false,
})
const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length))
onMount(() => {
const interval = setInterval(() => {
setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length)
}, 6500)
onCleanup(() => clearInterval(interval))
})
createEffect(() => {
session.id
editorRef.focus()
@@ -68,7 +107,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const handleFileSelect = (path: string | undefined) => {
if (!path) return
addPart({ type: "file", path, content: "@" + getFilename(path), start: 0, end: 0 })
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
}
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
@@ -235,9 +274,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const abort = () =>
sdk.client.session.abort({
path: {
id: session.id!,
},
sessionID: session.id!,
})
const handleKeyDown = (event: KeyboardEvent) => {
@@ -329,21 +366,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
session.prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
sdk.client.session.prompt({
path: { id: existing.id },
body: {
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
text,
},
...attachmentParts,
],
sessionID: existing.id,
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
text,
},
...attachmentParts,
],
})
}
@@ -407,7 +442,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
<Show when={!session.prompt.dirty()}>
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
Plan and build anything
Ask anything... "{PLACEHOLDERS[placeholder()]}"
</div>
</Show>
</div>
@@ -453,12 +488,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
{(i) => (
<div class="w-full flex items-center justify-between gap-x-3">
<div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0">
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
<ProviderIcon name={i.provider.id as IconName} class="size-6 p-0.5 shrink-0" />
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
<Show when={i.release_date}>
<Show when={false}>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
{DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
</Show>
</div>
@@ -0,0 +1,150 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
onSubmit?: () => void
onCleanup?: (pty: LocalPTY) => void
onConnectError?: (error: unknown) => void
}
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
let term: Term
let ghostty: Ghostty
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
onMount(async () => {
ghostty = await Ghostty.load()
ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
term = new Term({
cursorBlink: true,
fontSize: 14,
fontFamily: "TX-02, monospace",
allowTransparency: true,
theme: {
background: "#191515",
foreground: "#d4d4d4",
},
scrollback: 10_000,
ghostty,
})
term.attachCustomKeyEventHandler((event) => {
// allow for ctrl-` to toggle terminal in parent
if (event.ctrlKey && event.key.toLowerCase() === "`") {
event.preventDefault()
return true
}
return false
})
fitAddon = new FitAddon()
serializeAddon = new SerializeAddon()
term.loadAddon(serializeAddon)
term.loadAddon(fitAddon)
term.open(container)
if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
term.resize(local.pty.cols, local.pty.rows)
}
term.reset()
term.write(local.pty.buffer)
if (local.pty.scrollY) {
term.scrollToLine(local.pty.scrollY)
}
fitAddon.fit()
}
container.focus()
fitAddon.observeResize()
handleResize = () => fitAddon.fit()
window.addEventListener("resize", handleResize)
term.onResize(async (size) => {
if (ws && ws.readyState === WebSocket.OPEN) {
await sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: size.cols,
rows: size.rows,
},
})
}
})
term.onData((data) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(data)
}
})
term.onKey((key) => {
if (key.key == "Enter") {
props.onSubmit?.()
}
})
// term.onScroll((ydisp) => {
// console.log("Scroll position:", ydisp)
// })
ws.addEventListener("open", () => {
console.log("WebSocket connected")
sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: term.cols,
rows: term.rows,
},
})
})
ws.addEventListener("message", (event) => {
term.write(event.data)
})
ws.addEventListener("error", (error) => {
console.error("WebSocket error:", error)
props.onConnectError?.(error)
})
ws.addEventListener("close", () => {
console.log("WebSocket disconnected")
})
})
onCleanup(() => {
if (handleResize) {
window.removeEventListener("resize", handleResize)
}
if (serializeAddon && props.onCleanup) {
const buffer = serializeAddon.serialize()
props.onCleanup({
...local.pty,
buffer,
rows: term.rows,
cols: term.cols,
scrollY: term.getViewportY(),
})
}
ws?.close()
term?.dispose()
})
return (
<div
ref={container}
data-component="terminal"
classList={{
...(local.classList ?? {}),
"size-full px-6 py-3 font-mono": true,
[local.class ?? ""]: !!local.class,
}}
{...others}
/>
)
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
@@ -19,7 +19,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
sdk.global.event().then(async (events) => {
for await (const event of events.stream) {
// console.log("event", event)
emitter.emit(event.directory, event.payload)
emitter.emit(event.directory ?? "global", event.payload)
}
})
+68 -14
View File
@@ -12,17 +12,49 @@ import type {
FileDiff,
Todo,
SessionStatus,
} from "@opencode-ai/sdk"
} from "@opencode-ai/sdk/v2"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
const PASTEL_COLORS = [
"#FCEAFD", // pastel pink
"#FFDFBA", // pastel peach
"#FFFFBA", // pastel yellow
"#BAFFC9", // pastel green
"#EAF6FD", // pastel blue
"#EFEAFD", // pastel lavender
"#FEC8D8", // pastel rose
"#D4F0F0", // pastel cyan
"#FDF0EA", // pastel coral
"#C1E1C1", // pastel mint
]
function pickAvailableColor(usedColors: Set<string>) {
const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
return available[Math.floor(Math.random() * available.length)]
}
async function ensureProjectColor(
project: Project,
sdk: ReturnType<typeof useGlobalSDK>,
usedColors: Set<string>,
): Promise<Project> {
if (project.icon?.color) return project
const color = pickAvailableColor(usedColors)
usedColors.add(color)
const updated = { ...project, icon: { ...project.icon, color } }
sdk.client.project.update({ projectID: project.id, icon: { color } })
return updated
}
type State = {
ready: boolean
provider: Provider[]
agent: Agent[]
project: Project
project: string
config: Config
path: Path
session: Session[]
@@ -51,7 +83,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
init: () => {
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
defaultProject?: Project // TODO: remove this when we can select projects
projects: Project[]
children: Record<string, State>
}>({
@@ -61,11 +92,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!children[directory]) {
setGlobalStore("children", directory, {
project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
project: "",
config: {},
path: { state: "", config: "", worktree: "", directory: "" },
ready: false,
@@ -75,7 +105,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
session_status: {},
session_diff: {},
todo: {},
limit: 10,
limit: 5,
message: {},
part: {},
node: [],
@@ -89,9 +119,32 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const sdk = useGlobalSDK()
sdk.event.listen((e) => {
const directory = e.name
const [store, setStore] = child(directory)
const event = e.details
if (directory === "global") {
switch (event.type) {
case "project.updated": {
const usedColors = new Set(globalStore.projects.map((p) => p.icon?.color).filter(Boolean) as string[])
ensureProjectColor(event.properties, sdk, usedColors).then((project) => {
const result = Binary.search(globalStore.projects, project.id, (s) => s.id)
if (result.found) {
setGlobalStore("projects", result.index, reconcile(project))
return
}
setGlobalStore(
"projects",
produce((draft) => {
draft.splice(result.index, 0, project)
}),
)
})
break
}
}
return
}
const [store, setStore] = child(directory)
switch (event.type) {
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
@@ -162,14 +215,15 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
})
Promise.all([
sdk.client.project.list().then((x) =>
sdk.client.project.list().then(async (x) => {
const filtered = x.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
const usedColors = new Set(filtered.map((p) => p.icon?.color).filter(Boolean) as string[])
const projects = await Promise.all(filtered.map((p) => ensureProjectColor(p, sdk, usedColors)))
setGlobalStore(
"projects",
x.data!.filter((x) => !x.worktree.includes("opencode-test")),
),
),
// TODO: remove this when we can select projects
sdk.client.project.current().then((x) => setGlobalStore("defaultProject", x.data)),
projects.sort((a, b) => a.id.localeCompare(b.id)),
)
}),
]).then(() => setGlobalStore("ready", true))
return {
+73 -14
View File
@@ -1,48 +1,91 @@
import { createStore } from "solid-js/store"
import { createMemo } from "solid-js"
import { createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
projects: [] as { directory: string; expanded: boolean }[],
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
opened: true,
opened: false,
width: 280,
},
terminal: {
opened: false,
height: 280,
},
review: {
state: "pane" as "pane" | "tab",
},
}),
{
name: "___default-layout",
name: "default-layout.v6",
},
)
async function loadProjectSessions(directory: string) {
const [, setStore] = globalSync.child(directory)
globalSdk.client.session.list({ directory }).then((x) => {
const sessions = (x.data ?? [])
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, 5)
setStore("session", sessions)
})
}
onMount(() => {
Promise.all(
store.projects.map(({ worktree }) => {
return loadProjectSessions(worktree)
}),
)
})
function enrich(project: { worktree: string; expanded: boolean }) {
const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
if (!metadata) return []
return [
{
...project,
...metadata,
},
]
}
return {
projects: {
list: createMemo(() =>
globalSync.data.defaultProject
? [{ directory: globalSync.data.defaultProject!.worktree, expanded: true }, ...store.projects]
: store.projects,
),
list: createMemo(() => store.projects.flatMap(enrich)),
open(directory: string) {
if (store.projects.find((x) => x.directory === directory)) return
setStore("projects", (x) => [...x, { directory, expanded: true }])
if (store.projects.find((x) => x.worktree === directory)) return
loadProjectSessions(directory)
setStore("projects", (x) => [...x, { worktree: directory, expanded: true }])
},
close(directory: string) {
setStore("projects", (x) => x.filter((x) => x.directory !== directory))
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
},
expand(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: true } : x)))
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
},
collapse(directory: string) {
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
},
move(directory: string, toIndex: number) {
setStore("projects", (projects) => {
const fromIndex = projects.findIndex((x) => x.worktree === directory)
if (fromIndex === -1 || fromIndex === toIndex) return projects
const result = [...projects]
const [item] = result.splice(fromIndex, 1)
result.splice(toIndex, 0, item)
return result
})
},
},
sidebar: {
@@ -61,6 +104,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("sidebar", "width", width)
},
},
terminal: {
opened: createMemo(() => store.terminal.opened),
open() {
setStore("terminal", "opened", true)
},
close() {
setStore("terminal", "opened", false)
},
toggle() {
setStore("terminal", "opened", (x) => !x)
},
height: createMemo(() => store.terminal.height),
resize(height: number) {
setStore("terminal", "height", height)
},
},
review: {
state: createMemo(() => store.review?.state ?? "closed"),
pane() {
+7 -8
View File
@@ -1,11 +1,11 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo } from "solid-js"
import { uniqueBy } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { base64Encode } from "@/utils"
import { base64Encode } from "@opencode-ai/util/encode"
export type LocalFile = FileNode &
Partial<{
@@ -257,7 +257,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const load = async (path: string) => {
const relativePath = relative(path)
sdk.client.file.read({ query: { path: relativePath } }).then((x) => {
await sdk.client.file.read({ path: relativePath }).then((x) => {
setStore(
"node",
relativePath,
@@ -305,7 +305,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const list = async (path: string) => {
return sdk.client.file.list({ query: { path: path + "/" } }).then((x) => {
return sdk.client.file.list({ path: path + "/" }).then((x) => {
setStore(
"node",
produce((draft) => {
@@ -318,10 +318,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
}
const searchFiles = (query: string) =>
sdk.client.find.files({ query: { query, dirs: "false" } }).then((x) => x.data!)
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
const searchFilesAndDirectories = (query: string) =>
sdk.client.find.files({ query: { query, dirs: "true" } }).then((x) => x.data!)
sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
sdk.event.listen((e) => {
const event = e.details
@@ -336,7 +335,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return {
node: async (path: string) => {
if (!store.node[path]) {
if (!store.node[path] || !store.node[path].loaded) {
await init(path)
}
return store.node[path]
+25
View File
@@ -0,0 +1,25 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
export type Platform = {
/** Platform discriminator */
platform: "web" | "tauri"
/** Open native directory picker dialog (Tauri only) */
openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
/** Open native file picker dialog (Tauri only) */
openFilePickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
/** Save file picker dialog (Tauri only) */
saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise<string | null>
/** Open a URL in the default browser */
openLink(url: string): void
}
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
name: "Platform",
init: (props: { value: Platform }) => {
return props.value
},
})
+2 -2
View File
@@ -1,4 +1,4 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
@@ -27,6 +27,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
abort.abort()
})
return { directory: props.directory, client: sdk, event: emitter }
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
},
})
+100 -16
View File
@@ -5,34 +5,47 @@ import { useSync } from "./sync"
import { makePersisted } from "@solid-primitives/storage"
import { TextSelection } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
import { base64Encode } from "@/utils"
import { useSDK } from "./sdk"
export type LocalPTY = {
id: string
title: string
rows?: number
cols?: number
buffer?: string
scrollY?: number
}
export const { use: useSession, provider: SessionProvider } = createSimpleContext({
name: "Session",
init: () => {
const sdk = useSDK()
const params = useParams()
const sync = useSync()
const name = createMemo(
() => `___${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
)
const name = createMemo(() => `${params.dir}/session${params.id ? "/" + params.id : ""}.v3`)
const [store, setStore] = makePersisted(
createStore<{
messageId?: string
tabs: {
active?: string
opened: string[]
all: string[]
}
prompt: Prompt
cursor?: number
terminals: {
active?: string
all: LocalPTY[]
}
}>({
tabs: {
opened: [],
all: [],
},
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
terminals: { all: [] },
}),
{
name: name(),
@@ -138,7 +151,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
setStore("tabs", "active", tab)
},
setOpenedTabs(tabs: string[]) {
setStore("tabs", "opened", tabs)
setStore("tabs", "all", tabs)
},
async openTab(tab: string) {
if (tab === "chat") {
@@ -146,8 +159,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
return
}
if (tab !== "review") {
if (!store.tabs.opened.includes(tab)) {
setStore("tabs", "opened", [...store.tabs.opened, tab])
if (!store.tabs.all.includes(tab)) {
setStore("tabs", "all", [...store.tabs.all, tab])
}
}
setStore("tabs", "active", tab)
@@ -156,28 +169,99 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
batch(() => {
setStore(
"tabs",
"opened",
store.tabs.opened.filter((x) => x !== tab),
"all",
store.tabs.all.filter((x) => x !== tab),
)
if (store.tabs.active === tab) {
const index = store.tabs.opened.findIndex((f) => f === tab)
const previous = store.tabs.opened[Math.max(0, index - 1)]
const index = store.tabs.all.findIndex((f) => f === tab)
const previous = store.tabs.all[Math.max(0, index - 1)]
setStore("tabs", "active", previous)
}
})
},
moveTab(tab: string, to: number) {
const index = store.tabs.opened.findIndex((f) => f === tab)
const index = store.tabs.all.findIndex((f) => f === tab)
if (index === -1) return
setStore(
"tabs",
"opened",
"all",
produce((opened) => {
opened.splice(to, 0, opened.splice(index, 1)[0])
}),
)
},
},
terminal: {
all: createMemo(() => Object.values(store.terminals.all)),
active: createMemo(() => store.terminals.active),
new() {
sdk.client.pty.create({ title: `Terminal ${store.terminals.all.length + 1}` }).then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("terminals", "all", [
...store.terminals.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("terminals", "active", id)
})
},
update(pty: Partial<LocalPTY> & { id: string }) {
setStore("terminals", "all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
sdk.client.pty.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
})
},
async clone(id: string) {
const index = store.terminals.all.findIndex((x) => x.id === id)
const pty = store.terminals.all[index]
if (!pty) return
const clone = await sdk.client.pty.create({
title: pty.title,
})
if (!clone.data) return
setStore("terminals", "all", index, {
...pty,
...clone.data,
})
if (store.terminals.active === pty.id) {
setStore("terminals", "active", clone.data.id)
}
},
open(id: string) {
setStore("terminals", "active", id)
},
async close(id: string) {
batch(() => {
setStore(
"terminals",
"all",
store.terminals.all.filter((x) => x.id !== id),
)
if (store.terminals.active === id) {
const index = store.terminals.all.findIndex((f) => f.id === id)
const previous = store.tabs.all[Math.max(0, index - 1)]
setStore("terminals", "active", previous)
}
})
await sdk.client.pty.remove({ ptyID: id })
},
move(id: string, to: number) {
const index = store.terminals.all.findIndex((f) => f.id === id)
if (index === -1) return
setStore(
"terminals",
"all",
produce((all) => {
all.splice(to, 0, all.splice(index, 1)[0])
}),
)
},
},
}
},
})
+11 -6
View File
@@ -13,7 +13,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const [store, setStore] = globalSync.child(sdk.directory)
const load = {
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!)),
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
@@ -28,7 +28,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
@@ -41,6 +41,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
get ready() {
return store.ready
},
get project() {
const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id)
if (match.found) return globalSync.data.projects[match.index]
return undefined
},
session: {
get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id)
@@ -49,10 +54,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
sdk.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } }),
sdk.client.session.todo({ path: { id: sessionID } }),
sdk.client.session.diff({ path: { id: sessionID } }),
sdk.client.session.get({ sessionID }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }),
sdk.client.session.todo({ sessionID }),
sdk.client.session.diff({ sessionID }),
])
setStore(
produce((draft) => {
+27
View File
@@ -0,0 +1,27 @@
// @refresh reload
import { render } from "solid-js/web"
import { App } from "@/app"
import { Platform, PlatformProvider } from "@/context/platform"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
)
}
const platform: Platform = {
platform: "web",
openLink(url: string) {
window.open(url, "_blank")
},
}
render(
() => (
<PlatformProvider value={platform}>
<App />
</PlatformProvider>
),
root!,
)
+2
View File
@@ -0,0 +1,2 @@
export { PlatformProvider, type Platform } from "./context/platform"
export { App } from "./app"
-73
View File
@@ -1,73 +0,0 @@
/* @refresh reload */
import "@/index.css"
import { render } from "solid-js/web"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
import { Favicon } from "@opencode-ai/ui/favicon"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
import Layout from "@/pages/layout"
import DirectoryLayout from "@/pages/directory-layout"
import Session from "@/pages/session"
import { LayoutProvider } from "./context/layout"
import { GlobalSDKProvider } from "./context/global-sdk"
import { SessionProvider } from "./context/session"
import { base64Encode } from "./utils"
import { createMemo, Show } from "solid-js"
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
const url =
new URLSearchParams(document.location.search).get("url") ||
(location.hostname.includes("opencode.ai") || location.hostname.includes("localhost")
? `http://${host}:${port}`
: "/")
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
)
}
render(
() => (
<MarkedProvider>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<MetaProvider>
<Font />
<Router root={Layout}>
<Route
path="/"
component={() => {
const globalSync = useGlobalSync()
const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree))
return <Navigate href={`${slug()}/session`} />
}}
/>
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<SessionProvider>
<Session />
</SessionProvider>
</Show>
)}
/>
</Route>
</Router>
</MetaProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</MarkedProvider>
),
root!,
)
+17 -18
View File
@@ -1,32 +1,31 @@
import { createMemo, type ParentProps } from "solid-js"
import { createMemo, Show, type ParentProps } from "solid-js"
import { useParams } from "@solidjs/router"
import { SDKProvider } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode } from "@/utils"
import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
export default function Layout(props: ParentProps) {
const params = useParams()
const sync = useGlobalSync()
const directory = createMemo(() => {
const decoded = base64Decode(params.dir!)
return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
return base64Decode(params.dir!)
})
return (
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {
const sync = useSync()
return (
<DataProvider data={sync.data} directory={directory()}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
</SyncProvider>
</SDKProvider>
<Show when={params.dir} keyed>
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {
const sync = useSync()
return (
<DataProvider data={sync.data} directory={directory()}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
</SyncProvider>
</SDKProvider>
</Show>
)
}
+83 -12
View File
@@ -1,21 +1,92 @@
import { useGlobalSync } from "@/context/global-sync"
import { base64Encode } from "@/utils"
import { For } from "solid-js"
import { A } from "@solidjs/router"
import { For, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { getFilename } from "@opencode-ai/util/path"
import { Logo } from "@opencode-ai/ui/logo"
import { useLayout } from "@/context/layout"
import { useNavigate } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
import { usePlatform } from "@/context/platform"
import { DateTime } from "luxon"
export default function Home() {
const sync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const navigate = useNavigate()
function openProject(directory: string) {
layout.projects.open(directory)
navigate(`/${base64Encode(directory)}`)
}
async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory)
}
} else if (result) {
openProject(result)
}
}
return (
<div class="flex flex-col gap-3">
<For each={sync.data.projects}>
{(project) => (
<Button as={A} href={base64Encode(project.worktree)}>
{getFilename(project.worktree)}
</Button>
)}
</For>
<div class="mx-auto mt-55">
<Logo class="w-xl opacity-12" />
<Switch>
<Match when={sync.data.projects.length > 0}>
<div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">Recent projects</div>
<Show when={platform.openDirectoryPickerDialog}>
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
Open project
</Button>
</Show>
</div>
<ul class="flex flex-col gap-2">
<For
each={sync.data.projects
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
.slice(0, 5)}
>
{(project) => (
<Button
size="large"
variant="ghost"
class="text-14-mono text-left justify-between px-3"
onClick={() => openProject(project.worktree)}
>
{project.worktree}
<div class="text-14-regular text-text-weak">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</div>
</Button>
)}
</For>
</ul>
</div>
</Match>
<Match when={true}>
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
<Icon name="folder-add-left" size="large" />
<div class="flex flex-col gap-1 items-center justify-center">
<div class="text-14-medium text-text-strong">No recent projects</div>
<div class="text-12-regular text-text-weak">Get started by opening a local project</div>
</div>
<div />
<Show when={platform.openDirectoryPickerDialog}>
<Button class="px-3" onClick={chooseProject}>
Open project
</Button>
</Show>
</div>
</Match>
</Switch>
</div>
)
}
+409 -102
View File
@@ -1,10 +1,12 @@
import { createMemo, For, ParentProps, Show } from "solid-js"
import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js"
import { DateTime } from "luxon"
import { A, useParams } from "@solidjs/router"
import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Encode } from "@/utils"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { Mark } from "@opencode-ai/ui/logo"
import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -12,19 +14,302 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session, Project } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore } from "solid-js/store"
import {
DragDropProvider,
DragDropSensors,
DragOverlay,
SortableProvider,
closestCenter,
createSortable,
useDragDropContext,
} from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
lastSession: {} as { [directory: string]: string },
activeDraggable: undefined as string | undefined,
})
const params = useParams()
const globalSync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const navigate = useNavigate()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const handleOpenProject = async () => {
// layout.projects.open(dir.)
function navigateToProject(directory: string | undefined) {
if (!directory) return
const lastSession = store.lastSession[directory]
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
}
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
}
function openProject(directory: string, navigate = true) {
layout.projects.open(directory)
if (navigate) navigateToProject(directory)
}
function closeProject(directory: string) {
layout.projects.close(directory)
// TODO: more intelligent navigation
navigate("/")
}
async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory, false)
}
navigateToProject(result[0])
} else if (result) {
openProject(result)
}
}
createEffect(() => {
if (!params.dir || !params.id) return
const directory = base64Decode(params.dir)
setStore("lastSession", directory, params.id)
})
function getDraggableId(event: unknown): string | undefined {
if (typeof event !== "object" || event === null) return undefined
if (!("draggable" in event)) return undefined
const draggable = (event as { draggable?: { id?: unknown } }).draggable
if (!draggable) return undefined
return typeof draggable.id === "string" ? draggable.id : undefined
}
function handleDragStart(event: unknown) {
const id = getDraggableId(event)
if (!id) return
setStore("activeDraggable", id)
}
function handleDragOver(event: DragEvent) {
const { draggable, droppable } = event
if (draggable && droppable) {
const projects = layout.projects.list()
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== -1) {
layout.projects.move(draggable.id.toString(), toIndex)
}
}
}
function handleDragEnd() {
setStore("activeDraggable", undefined)
}
const ConstrainDragXAxis = (): JSX.Element => {
const context = useDragDropContext()
if (!context) return <></>
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
const transformer: Transformer = {
id: "constrain-x-axis",
order: 100,
callback: (transform) => ({ ...transform, x: 0 }),
}
onDragStart((event) => {
const id = getDraggableId(event)
if (!id) return
addTransformer("draggables", id, transformer)
})
onDragEnd((event) => {
const id = getDraggableId(event)
if (!id) return
removeTransformer("draggables", id, transformer.id)
})
return <></>
}
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
return (
<Switch>
<Match when={layout.sidebar.opened()}>
<Button
as={"div"}
variant="ghost"
data-active
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
>
<div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</div>
</Button>
</Match>
<Match when={true}>
<Button
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
data-selected={props.project.worktree === currentDirectory()}
onClick={() => navigateToProject(props.project.worktree)}
>
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
</Button>
</Match>
</Switch>
)
}
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
const sortable = createSortable(props.project.worktree)
const [projectStore] = globalSync.child(props.project.worktree)
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Switch>
<Match when={layout.sidebar.opened()}>
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
<Button
as={"div"}
variant="ghost"
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
>
<Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full group-hover/session:hidden"
/>
<Icon
name="chevron-right"
size="large"
class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
/>
</div>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</Collapsible.Trigger>
<div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
<DropdownMenu>
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
<DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Tooltip placement="top" value="New session">
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
</Tooltip>
</div>
</Button>
<Collapsible.Content>
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
<For each={projectStore.session}>
{(session) => {
const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
return (
<A
data-active={session.id === params.id}
href={`${slug()}/session/${session.id}`}
class="group/session focus:outline-none cursor-default"
>
<Tooltip placement="right" value={session.title}>
<div
class="w-full pl-4 pr-2 py-1 rounded-md
group-data-[active=true]/session:bg-surface-raised-base-hover
group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover"
>
<div class="flex items-center self-stretch gap-6 justify-between">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
{session.title}
</span>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{Math.abs(updated().diffNow().as("seconds")) < 60
? "Now"
: updated()
.toRelative({
style: "short",
unit: ["days", "hours", "minutes"],
})
?.replace(" ago", "")
?.replace(/ days?/, "d")
?.replace(" min.", "m")
?.replace(" hr.", "h")}
</span>
</div>
<div class="hidden _flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div>
</div>
</Tooltip>
</A>
)
}}
</For>
</nav>
</Collapsible.Content>
</Collapsible>
</Match>
<Match when={true}>
<Tooltip placement="right" value={props.project.worktree}>
<ProjectVisual project={props.project} />
</Tooltip>
</Match>
</Switch>
</div>
)
}
const ProjectDragOverlay = (): JSX.Element => {
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeDraggable))
return (
<Show when={project()}>
{(p) => (
<div class="bg-background-base rounded-md">
<ProjectVisual project={p()} />
</div>
)}
</Show>
)
}
return (
<div class="relative h-screen flex flex-col">
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base">
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<A
href="/"
classList={{
@@ -33,25 +318,108 @@ export default function Layout(props: ParentProps) {
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
data-tauri-drag-region
>
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<Show when={params.dir && layout.projects.list().length > 0}>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Show>
</div>
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</div>
</Show>
</div>
</header>
<div class="h-[calc(100vh-3rem)] flex">
<div
classList={{
"@container w-12 pb-5 shrink-0 bg-background-base": true,
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
>
<div class="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
<Show when={layout.sidebar.opened()}>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
min={150}
max={window.innerWidth * 0.3}
collapseThreshold={80}
onResize={layout.sidebar.resize}
onCollapse={layout.sidebar.close}
/>
</Show>
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
<Button
variant="ghost"
size="large"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@@ -78,103 +446,42 @@ export default function Layout(props: ParentProps) {
</Show>
</Button>
</Tooltip>
<div class="flex flex-col justify-center items-start gap-4 self-stretch min-h-0">
<div class="hidden @[4rem]:flex size-full flex-col grow overflow-y-auto no-scrollbar">
<For each={layout.projects.list()}>
{(project) => {
const [store] = globalSync.child(project.directory)
const slug = createMemo(() => base64Encode(project.directory))
return (
<Collapsible variant="ghost" defaultOpen class="gap-2">
<Button
as={"div"}
variant="ghost"
class="flex items-center justify-between gap-3 w-full h-8 pl-2 pr-2.25 self-stretch"
>
<Collapsible.Trigger class="p-0 text-left text-14-medium text-text-strong grow min-w-0 truncate">
{getFilename(project.directory)}
</Collapsible.Trigger>
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" size="normal" />
</Button>
<Collapsible.Content>
<nav class="w-full flex flex-col gap-1.5">
<For each={store.session}>
{(session) => {
const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
return (
<A
data-active={session.id === params.id}
href={`${slug()}/session/${session.id}`}
class="group/session focus:outline-none cursor-default"
>
<Tooltip placement="right" value={session.title}>
<div
class="w-full px-2 py-1 rounded-md
group-data-[active=true]/session:bg-surface-raised-base-hover
group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover"
>
<div class="flex items-center self-stretch gap-6 justify-between">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
{session.title}
</span>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{Math.abs(updated().diffNow().as("seconds")) < 60
? "Now"
: updated()
.toRelative({ style: "short", unit: ["days", "hours", "minutes"] })
?.replace(" ago", "")
?.replace(/ days?/, "d")
?.replace(" min.", "m")
?.replace(" hr.", "h")}
</span>
</div>
<div class="hidden _flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={session.summary}>
{(summary) => <DiffChanges changes={summary()} />}
</Show>
</div>
</div>
</Tooltip>
</A>
)
}}
</For>
</nav>
{/* <Show when={sync.session.more()}> */}
{/* <button */}
{/* class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong" */}
{/* onClick={() => sync.session.fetch()} */}
{/* > */}
{/* Show more */}
{/* </button> */}
{/* </Show> */}
</Collapsible.Content>
</Collapsible>
)
}}
</For>
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragXAxis />
<div class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar">
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
</SortableProvider>
</div>
</div>
<DragOverlay>
<ProjectDragOverlay />
</DragOverlay>
</DragDropProvider>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
disabled
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={handleOpenProject}
>
<Show when={layout.sidebar.opened()}>Open project</Show>
</Button>
</Tooltip>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={chooseProject}
>
<Show when={layout.sidebar.opened()}>Open project</Show>
</Button>
</Tooltip>
</Show>
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
disabled
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="settings-gear"
@@ -187,7 +494,7 @@ export default function Layout(props: ParentProps) {
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="bubble-5"
@@ -197,7 +504,7 @@ export default function Layout(props: ParentProps) {
</Tooltip>
</div>
</div>
<main class="size-full overflow-x-hidden">{props.children}</main>
<main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
</div>
</div>
)
+432 -288
View File
@@ -1,4 +1,4 @@
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect } from "solid-js"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
import { PromptInput } from "@/components/prompt-input"
@@ -9,10 +9,11 @@ import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Code } from "@opencode-ai/ui/code"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { MessageNav } from "@opencode-ai/ui/message-nav"
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
import {
@@ -27,9 +28,11 @@ import {
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { useSession } from "@/context/session"
import { useSession, type LocalPTY } from "@/context/session"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Terminal } from "@/components/terminal"
import { checksum } from "@opencode-ai/util/encode"
export default function Page() {
const layout = useLayout()
@@ -40,6 +43,7 @@ export default function Page() {
clickTimer: undefined as number | undefined,
fileSelectOpen: false,
activeDraggable: undefined as string | undefined,
activeTerminalDraggable: undefined as string | undefined,
})
let inputRef!: HTMLDivElement
@@ -53,6 +57,14 @@ export default function Page() {
document.removeEventListener("keydown", handleKeyDown)
})
createEffect(() => {
if (layout.terminal.opened()) {
if (session.terminal.all().length === 0) {
session.terminal.new()
}
}
})
const handleKeyDown = (event: KeyboardEvent) => {
if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") {
event.preventDefault()
@@ -72,6 +84,20 @@ export default function Page() {
document.documentElement.setAttribute("data-theme", nextTheme)
return
}
if (event.ctrlKey && event.key.toLowerCase() === "`") {
event.preventDefault()
if (event.shiftKey) {
session.terminal.new()
return
}
layout.terminal.toggle()
return
}
// @ts-expect-error
if (document.activeElement?.dataset?.component === "terminal") {
return
}
const focused = document.activeElement === inputRef
if (focused) {
@@ -123,7 +149,6 @@ export default function Page() {
const handleTabClick = async (tab: string) => {
if (store.clickTimer) {
resetClickTimer()
// local.file.update(file.path, { ...file, pinned: true })
} else {
if (tab.startsWith("file://")) {
local.file.open(tab.replace("file://", ""))
@@ -141,7 +166,7 @@ export default function Page() {
const handleDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const currentTabs = session.layout.tabs.opened
const currentTabs = session.layout.tabs.all
const fromIndex = currentTabs?.indexOf(draggable.id.toString())
const toIndex = currentTabs?.indexOf(droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== undefined) {
@@ -154,6 +179,49 @@ export default function Page() {
setStore("activeDraggable", undefined)
}
const handleTerminalDragStart = (event: unknown) => {
const id = getDraggableId(event)
if (!id) return
setStore("activeTerminalDraggable", id)
}
const handleTerminalDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const terminals = session.terminal.all()
const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString())
const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString())
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
session.terminal.move(draggable.id.toString(), toIndex)
}
}
}
const handleTerminalDragEnd = () => {
setStore("activeTerminalDraggable", undefined)
}
const SortableTerminalTab = (props: { terminal: LocalPTY }): JSX.Element => {
const sortable = createSortable(props.terminal.id)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.terminal.id}
closeButton={
session.terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(props.terminal.id)} />
)
}
>
{props.terminal.title}
</Tabs.Trigger>
</div>
</div>
)
}
const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => {
return (
<div class="flex items-center gap-x-1.5">
@@ -196,7 +264,6 @@ export default function Page() {
onTabClose: (tab: string) => void
}): JSX.Element => {
const sortable = createSortable(props.tab)
const [file] = createResource(
() => props.tab,
async (tab) => {
@@ -206,7 +273,6 @@ export default function Page() {
return undefined
},
)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
@@ -259,314 +325,392 @@ export default function Page() {
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
return (
<div class="relative bg-background-base size-full overflow-x-hidden">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Tabs.Trigger value="chat">
<div class="flex gap-x-[17px] items-center">
<div>Session</div>
<Tooltip
value={`${new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(session.usage.tokens() ?? 0)} Tokens`}
class="flex items-center gap-1.5"
>
<ProgressCircle percentage={session.usage.context() ?? 0} />
<div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
</Tooltip>
</div>
</Tabs.Trigger>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Trigger
value="review"
closeButton={
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
}
>
<div class="flex items-center gap-3">
<Show when={session.diffs()}>
<DiffChanges changes={session.diffs()} variant="bars" />
</Show>
<div class="flex items-center gap-1.5">
<div>Review</div>
<Show when={session.info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{session.info()?.summary?.files ?? 0}
</div>
</Show>
</div>
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
<div class="min-h-0 grow w-full">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Tabs.Trigger value="chat">
<div class="flex gap-x-[17px] items-center">
<div>Session</div>
<Tooltip
value={`${new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(session.usage.tokens() ?? 0)} Tokens`}
class="flex items-center gap-1.5"
>
<ProgressCircle percentage={session.usage.context() ?? 0} />
<div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
</Tooltip>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={session.layout.tabs.opened ?? []}>
<For each={session.layout.tabs.opened ?? []}>
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
<Tooltip value="Open file" class="flex items-center">
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => setStore("fileSelectOpen", true)}
/>
</Tooltip>
</div>
</Tabs.List>
</div>
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div
classList={{
"w-full flex-1 min-h-0": true,
grid: layout.review.state() === "tab",
flex: layout.review.state() === "pane",
}}
>
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-2xl mx-auto">
<Switch>
<Match when={session.id}>
<div class="flex items-start justify-start h-full min-h-0">
<Show when={session.messages.user().length > 1}>
<>
<MessageNav
class="@6xl:hidden mt-3 mr-8"
messages={session.messages.user()}
current={session.messages.active()}
onMessageSelect={session.messages.setActive}
size="compact"
working={session.working()}
/>
<MessageNav
classList={{
"hidden @6xl:flex": true,
"mt-0.5 mr-3 absolute right-full": wide(),
"mt-3 mr-8": !wide(),
}}
messages={session.messages.user()}
current={session.messages.active()}
onMessageSelect={session.messages.setActive}
size={wide() ? "normal" : "compact"}
working={session.working()}
/>
</>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Trigger
value="review"
closeButton={
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
}
>
<div class="flex items-center gap-3">
<Show when={session.diffs()}>
<DiffChanges changes={session.diffs()} variant="bars" />
</Show>
<SessionTurn
sessionID={session.id!}
messageID={session.messages.active()?.id!}
classes={{ root: "pb-20 flex-1 min-w-0", content: "pb-20" }}
/>
</div>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(sync.data.project.time.created).toRelative()}
</span>
</div>
<div class="flex items-center gap-1.5">
<div>Review</div>
<Show when={session.info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{session.info()?.summary?.files ?? 0}
</div>
</Show>
</div>
</div>
</Match>
</Switch>
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</Tabs.Trigger>
</Show>
<SortableProvider ids={session.layout.tabs.all ?? []}>
<For each={session.layout.tabs.all ?? []}>
{(tab) => (
<SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />
)}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
<Tooltip value="Open file" class="flex items-center">
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => setStore("fileSelectOpen", true)}
/>
</Tooltip>
</div>
</div>
<Show when={layout.review.state() === "pane" && session.diffs().length}>
</Tabs.List>
</div>
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div
classList={{
"w-full flex-1 min-h-0": true,
grid: layout.review.state() === "tab",
flex: layout.review.state() === "pane",
}}
>
<div
classList={{
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
"max-w-146 mx-auto": !wide(),
}}
>
<Switch>
<Match when={session.id}>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={session.messages.user()}
current={session.messages.active()}
onMessageSelect={session.messages.setActive}
working={session.working()}
wide={wide()}
/>
<SessionTurn
sessionID={session.id!}
messageID={session.messages.active()?.id!}
classes={{
root: "pb-20 flex-1 min-w-0",
content: "pb-20",
container:
"w-full " +
(wide()
? "max-w-146 mx-auto px-6"
: session.messages.user().length > 1
? "pr-6 pl-18"
: "px-6"),
}}
/>
</div>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-146 mx-auto px-6">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
</Match>
</Switch>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div class="w-full max-w-146 px-6">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</div>
</div>
<Show when={layout.review.state() === "pane" && session.diffs().length}>
<div
classList={{
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
}}
>
<SessionReview
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
}}
diffs={session.diffs()}
actions={
<Tooltip value="Open in tab">
<IconButton
icon="expand"
variant="ghost"
onClick={() => {
layout.review.tab()
session.layout.setActiveTab("review")
}}
/>
</Tooltip>
}
/>
</div>
</Show>
</div>
</Tabs.Content>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
<div
classList={{
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
}}
>
<SessionReview
classes={{
root: "pb-20",
root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={session.diffs()}
actions={
<Tooltip value="Open in tab">
<IconButton
icon="expand"
variant="ghost"
onClick={() => {
layout.review.tab()
session.layout.setActiveTab("review")
}}
/>
</Tooltip>
}
split
/>
</div>
</Show>
</div>
</Tabs.Content>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
</Tabs.Content>
</Show>
<For each={session.layout.tabs.all}>
{(tab) => {
const [file] = createResource(
() => tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<Tabs.Content value={tab} class="select-text mt-3">
<Switch>
<Match when={file()}>
{(f) => (
<Code
file={{
name: f().path,
contents: f().content?.content ?? "",
cacheKey: checksum(f().content?.content ?? ""),
}}
overflow="scroll"
class="pb-40"
/>
)}
</Match>
</Switch>
</Tabs.Content>
)
}}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedFile) => {
const [file] = createResource(
() => draggedFile(),
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
<Show when={session.layout.tabs.active}>
<div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</Show>
<div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
{/* <FileTree path="" onFileClick={ handleTabClick} /> */}
</div>
<div class="hidden shrink-0 w-56 p-2">
<Show
when={local.file.changes().length}
fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
>
<ul class="">
<For each={local.file.changes()}>
{(path) => (
<li>
<button
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
>
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
{getDirectory(path)}
</span>
</button>
</li>
)}
</For>
</ul>
</Show>
</div>
<Show when={store.fileSelectOpen}>
<SelectDialog
defaultOpen
title="Select file"
placeholder="Search files"
emptyMessage="No files found"
items={local.file.searchFiles}
key={(x) => x}
onOpenChange={(open) => setStore("fileSelectOpen", open)}
onSelect={(x) => {
if (x) {
return session.layout.openTab("file://" + x)
}
return undefined
}}
>
{(i) => (
<div
classList={{
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
"w-full flex items-center justify-between rounded-md": true,
}}
>
<SessionReview
classes={{
root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={session.diffs()}
split
/>
</div>
</Tabs.Content>
</Show>
<For each={session.layout.tabs.opened}>
{(tab) => {
const [file] = createResource(
() => tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<Tabs.Content value={tab} class="select-text mt-3">
<Switch>
<Match when={file()}>
{(f) => (
<Code
file={{ name: f().path, contents: f().content?.content ?? "" }}
overflow="scroll"
class="pb-40"
/>
)}
</Match>
</Switch>
</Tabs.Content>
)
}}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedFile) => {
const [file] = createResource(
() => draggedFile(),
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
<Show when={session.layout.tabs.active}>
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</Show>
<div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
{/* <FileTree path="" onFileClick={ handleTabClick} /> */}
</div>
<div class="hidden shrink-0 w-56 p-2">
<Show when={local.file.changes().length} fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}>
<ul class="">
<For each={local.file.changes()}>
{(path) => (
<li>
<button
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
>
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
{getDirectory(path)}
<div class="flex items-center gap-x-2 grow min-w-0">
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
</button>
</li>
)}
</For>
</ul>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
</div>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
</div>
)}
</SelectDialog>
</Show>
</div>
<Show when={store.fileSelectOpen}>
<SelectDialog
defaultOpen
title="Select file"
placeholder="Search files"
emptyMessage="No files found"
items={local.file.searchFiles}
key={(x) => x}
onOpenChange={(open) => setStore("fileSelectOpen", open)}
onSelect={(x) => {
if (x) {
local.file.open(x)
return session.layout.openTab("file://" + x)
}
return undefined
}}
<Show when={layout.terminal.opened()}>
<div
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
style={{ height: `${layout.terminal.height()}px` }}
>
{(i) => (
<div
classList={{
"w-full flex items-center justify-between rounded-md": true,
}}
>
<div class="flex items-center gap-x-2 grow min-w-0">
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
<ResizeHandle
direction="vertical"
size={layout.terminal.height()}
min={100}
max={window.innerHeight * 0.6}
collapseThreshold={50}
onResize={layout.terminal.resize}
onCollapse={layout.terminal.close}
/>
<DragDropProvider
onDragStart={handleTerminalDragStart}
onDragEnd={handleTerminalDragEnd}
onDragOver={handleTerminalDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<SortableProvider ids={session.terminal.all().map((t) => t.id)}>
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
</div>
)}
</SelectDialog>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeTerminalDraggable}>
{(draggedId) => {
const terminal = createMemo(() => session.terminal.all().find((t) => t.id === draggedId()))
return (
<Show when={terminal()}>
{(t) => (
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
{t().title}
</div>
)}
</Show>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</Show>
</div>
)
+4 -2
View File
@@ -2,7 +2,9 @@
/* tslint:disable */
/* eslint-disable */
/// <reference types="vite/client" />
interface ImportMetaEnv {}
interface ImportMetaEnv {
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
}

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