Compare commits

...

522 Commits

Author SHA1 Message Date
opencode f51bd91af4 release: v1.0.24 2025-11-05 00:12:19 +00:00
Dax Raad ebca25462e tui: fix session abort when autocomplete is visible 2025-11-04 18:45:39 -05:00
Aiden Cline 01b9148c04 fix: image reading error, also add error toast for event bus 2025-11-04 17:30:58 -06:00
Frank d3e080894c wip: zen 2025-11-04 17:54:08 -05:00
Jay V ee9aa24a55 ignore: update meta description to use proper OpenCode capitalization 2025-11-04 17:27:22 -05:00
Frank 16e2bded5b wip: zen 2025-11-04 17:24:20 -05:00
Frank 9fb49ab87b wip: zen 2025-11-04 17:24:20 -05:00
Frank 8d6a03cc89 zen: custom reload amount 2025-11-04 17:24:20 -05:00
Dax Raad 71b04ffa99 add command bar option to interrupt session 2025-11-04 17:11:07 -05:00
Aiden Cline 678ca757c9 fix: permissions not responding to esc 2025-11-04 16:08:31 -06:00
Jared A. Scheel 272349b8da Add support for uv format in formatters (#3916) 2025-11-04 15:40:29 -06:00
Ola fe94bb8e50 feat(provider): add GitHub Enterprise support for Copilot (#2522)
Co-authored-by: Jon-Mikkel Korsvik <48263282+jkorsvik@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-04 15:36:12 -06:00
opencode ba8bc1b8b4 release: v1.0.23 2025-11-04 19:38:18 +00:00
Timo Clasen 8a9a474df6 feat(TUI): add autocomplete readline style keybinds (#3717)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-04 13:28:03 -06:00
Aiden Cline 52e2b40610 fix: stop showing auto complete if user types a space 2025-11-04 13:22:18 -06:00
Dax Raad ee1ff8cc07 tui: add ability to interrupt running sessions from command palette 2025-11-04 14:07:22 -05:00
opencode 434c0ff0d7 release: v1.0.22 2025-11-04 18:41:38 +00:00
Dax Raad 7a7060ef15 fix session performance issue from large diffs 2025-11-04 13:35:44 -05:00
opencode f9af9fc221 release: v1.0.21 2025-11-04 17:47:34 +00:00
Sebastian Herrlinger 1bf1b93404 Revert "upgrade opentui to address disappearing content issues #3776, #3697"
This reverts commit 90fc3ddb02.
2025-11-04 18:38:44 +01:00
Dax Raad bc6f4aed2b local web 2025-11-04 12:33:14 -05:00
Aiden Cline 2af3f19397 respect: disable_paste_summary 2025-11-04 11:29:12 -06:00
Aiden Cline 9275665868 fix: /undo command 2025-11-04 10:59:34 -06:00
Christian Stewart 09bb819064 fix(tui): worker path resolution in dev mode (#3778)
Signed-off-by: Christian Stewart <christian@cjs.zip>
Co-authored-by: Sebastian Herrlinger <hasta84@gmail.com>
2025-11-04 17:38:11 +01:00
Err 6f0028644e fix: support scoped npm plugins (#3785)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-04 09:15:01 -06:00
Pranshu Raj aec44abcf6 [FIX]: Refocus prompt after session delete (#3882) 2025-11-04 08:57:59 -06:00
frankdierolf b41e573886 fix: correct history_next keybinding description (#3891) 2025-11-04 08:52:49 -06:00
arc-source-coder 737ddab300 tui: make /mcp an alias for /status (#3894) 2025-11-04 08:52:28 -06:00
Sebastian Herrlinger 90fc3ddb02 upgrade opentui to address disappearing content issues #3776, #3697 2025-11-04 14:57:15 +01:00
Adam 15d7eebb92 fix: lander specs 2025-11-04 06:15:12 -06:00
GitHub Action 33301c94df ignore: update download stats 2025-11-04 2025-11-04 12:05:02 +00:00
Aiden Cline d341d26e37 update brew handling 2025-11-04 00:52:26 -06:00
opencode d49b1b25d1 release: v1.0.20 2025-11-04 05:56:24 +00:00
Dax Raad 25eb100210 tui: fix tool permission lookup to use correct session ID 2025-11-04 00:50:12 -05:00
Dax Raad 9886353715 fix: persist -m model when switching agents
Add initial model from command line to fallback chain so it persists
when switching agents with tab, matching behavior of config model.

Resolves #3863
2025-11-03 23:51:52 -05:00
Aiden Cline f501501791 fix: piping 2025-11-03 22:36:10 -06:00
Dax Raad c103052f93 fix: handle parsePatch errors in TUI to prevent crashes
Wrap parsePatch calls in try-catch blocks to gracefully handle malformed
diffs that can occur when undoing after tool_use/tool_result errors or
cancelled prompts. Prevents TUI from crashing with 'Added line count did not
match for hunk' error.

Fixes #3700
2025-11-03 23:36:04 -05:00
Greg Pstrucha 68039d4c71 Fix file tagging in multi line inputs (#3865) 2025-11-03 23:22:38 -05:00
Aiden Cline d3566d3b1a ignore: delete unused code 2025-11-04 04:01:22 +00:00
Stephen Collings b275e18d28 fix: Provide OPENCODE & AGENT env vars (#3843)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-04 04:01:22 +00:00
Dax Raad af9a1797b5 tui: use keybind helper for history navigation to respect custom keybindings 2025-11-04 04:01:22 +00:00
opencode 29b3e40ddb release: v1.0.19 2025-11-04 04:01:22 +00:00
Dax Raad c49f5939a2 tui: fix model selection for models with nested paths
Users can now select models with multiple slashes like 'openrouter/google/gemini-2.5-pro'
in the TUI. Previously the TUI would only parse the first two parts of the model
path, showing 'Invalid model openrouter/google' for nested models.
2025-11-03 19:52:43 -05:00
kaanmertkoc 63862b1609 feat: implement stats command (#3832) 2025-11-03 18:41:30 -06:00
Mikhail Wahib 1cf1e88b52 fix: print the modified keybind for command_list (#3859) 2025-11-03 18:29:23 -06:00
Jay V d06afd87e5 ignore: lander 2025-11-03 18:17:32 -05:00
Frank 9fb6e81007 wip: zen 2025-11-03 17:30:18 -05:00
Adi Yeroslav 3ac82227f1 fix: update logo (#3833) 2025-11-03 16:18:38 -06:00
Aiden Cline c1f9249c84 ci: auto label web 2025-11-03 16:17:53 -06:00
Dax Raad 9bb66946db fix: correct dirs parameter type in file search 2025-11-03 17:10:31 -05:00
Dax Raad adcdbbddc7 tui: remove duplicate copy message command entry 2025-11-03 17:10:20 -05:00
Dax Raad 662435c5bb ci: stuff 2025-11-03 22:01:30 +00:00
opencode 36c1a05eaa release: v1.0.18 2025-11-03 22:01:30 +00:00
Dax Raad 5708e3bf1e ci: tweak 2025-11-03 16:56:41 -05:00
Dax Raad 0da1ed3fc8 tui: add copy last assistant message to session menu 2025-11-03 16:47:18 -05:00
Adam d5179c8b63 wip: desktop work 2025-11-03 15:42:39 -06:00
Adam bd0a4f7bbe wip: desktop work 2025-11-03 15:42:10 -06:00
Adam 3d43214075 wip: desktop work 2025-11-03 15:42:10 -06:00
Dax Raad 178a14ce3e fix dirs query param 2025-11-03 16:35:55 -05:00
Timo Clasen 8e1010dc3f feat(TUI): don't show /share hint if sharing is disabled (#3835) 2025-11-03 15:30:55 -06:00
Adi Yeroslav 9c82f1f5e9 fix: session rename functionality (#3840) 2025-11-03 15:26:30 -06:00
Dmytro Tiapukhin e5a651eef7 fix: better mcp sanitization (#3842)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-03 15:19:49 -06:00
Dax Raad d26605aa56 tui: add support for Ctrl+_ key combination in keybind parser 2025-11-03 16:16:09 -05:00
opencode 5cc0d337b1 release: v1.0.17 2025-11-03 21:14:52 +00:00
Dax Raad 902763b47d web command 2025-11-03 16:10:23 -05:00
Aiden Cline 55d07a139c fix: mcp error (#3847) 2025-11-03 15:04:53 -06:00
Frank 05232ead93 zen: wip 2025-11-03 15:44:06 -05:00
Tyler Gannon 7652a96064 fix: wait for stdout to flush in generate command (#3821) 2025-11-03 14:05:48 -06:00
Frank 901aae09f7 zen: filter out alpha models 2025-11-03 15:04:59 -05:00
opencode f95799f17c release: v1.0.16 2025-11-03 17:08:32 +00:00
Dax Raad 99a6c5e44d regen sdk 2025-11-03 11:55:19 -05:00
Dax Raad 07bb75f086 core: add optional dirs parameter to file search API
Allow users to exclude directories from file search results by setting dirs=false parameter in /find/file endpoint
2025-11-03 11:53:41 -05:00
Frank 66eb846e6f zen: wip 2025-11-03 11:30:53 -05:00
Adam 34f11c699e wip: desktop work 2025-11-03 08:29:13 -06:00
Adam 7a32fec008 wip: desktop work 2025-11-03 08:29:13 -06:00
James Alexander 37a6b5177e Add unit tests for util functions: iife, lazy, timeout (#3791) 2025-11-03 09:24:45 -05:00
Haris Gušić 573ffe186b fix(tui): Show correct keybind in session delete confirmation message (#3805) 2025-11-03 09:22:05 -05:00
Alex Knight 0f7ff3fcb1 Log share link immediately after session creation (#3811) 2025-11-03 09:21:43 -05:00
frankdierolf 2c3aa330b9 fix: correct clipboard image encoding and binary handling (#3817) 2025-11-03 09:21:13 -05:00
Pranshu Raj 47b2fb79dc docs: add session_child_cycle and session_child_cycle_reverse keybinds (#3807) 2025-11-03 09:20:35 -05:00
Sebastian Herrlinger 6deaf54bb3 use new opentui getTextRange method and Bun.stringWidth instead of value.length to mitigate issues like #3734 2025-11-03 15:15:55 +01:00
GitHub Action d549cd3213 ignore: update download stats 2025-11-03 2025-11-03 12:04:38 +00:00
Ivan Starkov 93e52f7ecf feat: Enhance task display with [subagent type] (#3772) 2025-11-03 01:09:31 -06:00
Aiden Cline 88f12b0822 core: prevent TypeError when error handling encounters non-object errors
When API errors like token limit exceeded errors are passed as strings to error checking methods, the 'in' operator would throw a TypeError. This fix adds a type guard to check that the input is an object before attempting to access its properties, allowing proper error classification even when encountering unexpected error formats from providers.
2025-11-02 23:38:56 -06:00
Zeldris 54af7f9e18 docs: use brew official formula (#3733) 2025-11-02 21:00:23 -06:00
Dax Raad be685e95a3 docs 2025-11-03 01:57:36 +00:00
Dax Raad dc2ab75fca ci: eventualy consistency 2025-11-03 01:57:36 +00:00
opencode f1324e886f release: v1.0.15 2025-11-03 01:57:36 +00:00
opencode c47fde2ca4 release: v1.0.14 2025-11-03 00:12:08 +00:00
Dax Raad f42e1c6375 tui: fix focus management and dialog interactions 2025-11-02 19:07:22 -05:00
Dax Raad f68374ad22 DELETE GO BUBBLETEA CRAP HOORAY 2025-11-02 18:43:33 -05:00
opencode 5e86c9b791 release: v1.0.13 2025-11-02 23:31:25 +00:00
Dax Raad 94658c31c5 add back child session cycle 2025-11-02 18:26:38 -05:00
Dax Raad 9fd672a1cb undo 2025-11-02 16:31:32 -05:00
Dax Raad 10523c4372 move dialog select keybind to input 2025-11-02 15:47:04 -05:00
Dax Raad d1cd7d0344 ci: centralize Bun version to package.json to ensure consistent builds across CI and local development 2025-11-02 15:42:15 -05:00
Dax Raad 06ac1be226 upgrade to bun 1.3.1 2025-11-02 14:00:50 -05:00
Dax Raad 05489bc843 tui: fix file path handling when pasting images with spaces in filename
- Fixes issue where files with spaces in their names couldn't be pasted as images
- Prevents default paste behavior to avoid conflicts with image insertion
- Improves error handling for file reading operations
2025-11-02 13:45:44 -05:00
Dax Raad 3f02eecf22 tui: add /timeline command to quickly navigate to specific messages in session history 2025-11-02 18:27:42 +00:00
opencode f5ca78ed7b release: v1.0.12 2025-11-02 18:27:42 +00:00
Dax Raad 894cbaa51e fix duplicate plugin subscriptions 2025-11-02 13:22:58 -05:00
John Eismeier 8b70b89fde fix: typos (#3757)
Signed-off-by: John E <jeis4wpi@outlook.com>
2025-11-02 09:56:40 -06:00
Aditya Mathur f9dbc586dc chore: update hono-openapi to version 1.1.1 (#3738) 2025-11-02 09:21:55 -06:00
GitHub Action ffeef63ca1 ignore: update download stats 2025-11-02 2025-11-02 12:04:05 +00:00
kcrommett 4da58294d9 add nightowl theme back after opentui release (#3732) 2025-11-02 04:29:14 -05:00
opencode fa2e88f49b release: v1.0.11 2025-11-02 08:18:59 +00:00
Dax Raad 28e765ef0a fix dialog 2025-11-02 02:53:55 -05:00
Dax Raad bfbcb5f200 tui: prevent default Enter key behavior when selecting dialog options to avoid conflicts 2025-11-02 01:19:30 -05:00
Aiden Cline 89492b3002 ci: fix regex 2025-11-01 20:23:10 -05:00
opencode-agent[bot] 2663415d47 github action: truncate PR titles to 256 chars to avoid GH api errors (#3727)
publish-github-action / publish (push) Has been cancelled
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-01 20:09:35 -05:00
Aiden Cline 51be67cc14 ci: stop auto assigning 2025-11-01 19:57:09 -05:00
Sebastian Herrlinger 92a1943771 upgrade to opentui 0.1.32, activates kitty keyboard 2025-11-02 01:45:38 +01:00
opencode 1e15fc273a release: v1.0.10 2025-11-01 18:06:28 +00:00
Dax 104a895a71 Light mode (#3709) 2025-11-01 13:54:01 -04:00
Dax Raad f98e730405 docs update 2025-11-01 13:23:03 -04:00
Dax Raad b12bef05d3 docs: update keybinds documentation with current defaults and remove deprecated bindings 2025-11-01 12:32:22 -04:00
opencode 2f1d001cc5 release: v1.0.9 2025-11-01 16:22:49 +00:00
Dax Raad 65d0b3ed6d sync 2025-11-01 12:14:15 -04:00
Haris Gušić 22a34d7958 feat: tui: Port /exit command and all command aliases (#3665) 2025-11-01 12:13:10 -04:00
Aiden Cline cb4401ec92 ignore: update contributing md 2025-11-01 11:08:07 -05:00
opencode febf467b03 release: v1.0.8 2025-11-01 15:58:23 +00:00
Dax Raad d55a2fd56c tui: change delete keybind to ctrl+d in session list dialog 2025-11-01 11:53:46 -04:00
Dax Raad 40f577e5e7 fix modified files being empty 2025-11-01 11:48:47 -04:00
Dax Raad 9e49870118 remember sidebar position 2025-11-01 11:40:33 -04:00
Daniel van Strien fe38e3ab02 docs: add Hugging Face Inference Providers documentation (#3505)
Co-authored-by: célina <hanouticelina@gmail.com>
2025-11-01 10:33:17 -05:00
Haris Gušić 0170577743 feat: tui: Add --prompt option (#3668) 2025-11-01 11:18:31 -04:00
Giuseppe Rota 7de6ea5922 fix: fix typo in commit message guidelines (#3702) 2025-11-01 10:14:53 -05:00
Yuku Kotani 2fe7d13e69 Add formatter status display to TUI status dialog (#3701) 2025-11-01 11:14:39 -04:00
Dax Raad 1bc3c98ae7 ensure wl-copy is available 2025-11-01 11:10:39 -04:00
Haris Gušić 55787f2caa fix: tui: Handle Clipboard.copy errors properly (#3685) 2025-11-01 15:34:21 +01:00
Haris Gušić 7df61a74a0 fix: tui: add toast for /share url copy (#3686) 2025-11-01 08:06:56 -05:00
GitHub Action 4f23110880 ignore: update download stats 2025-11-01 2025-11-01 12:04:18 +00:00
Aiden Cline 041353f4ff make /init a default slash command on server side (#3677) 2025-11-01 01:14:09 -05:00
Haris Gušić c72f8b17c6 fix: tui: Fix /editor command (#3663) 2025-11-01 00:16:06 +00:00
opencode eb304f4115 release: v1.0.7 2025-11-01 00:16:05 +00:00
Dax Raad 5565f14ef5 tab to accept autocomplete 2025-10-31 20:10:01 -04:00
Dax Raad 10a4455c6f tui: fix prompt text aggregation to exclude synthetic content 2025-10-31 20:01:27 -04:00
Dax Raad 5ded6d6ad7 docs: sync 2025-10-31 23:58:57 +00:00
opencode 849a38c30c release: v1.0.6 2025-10-31 23:58:57 +00:00
Dax Raad 68050ab802 tui: prevent clipboard operations from throwing errors on process exit 2025-10-31 19:54:15 -04:00
opencode 91d01fd4cc release: v1.0.5 2025-10-31 23:51:36 +00:00
Dax Raad 9beb0f8512 tui: improve keyboard navigation and MCP server status display 2025-10-31 19:47:08 -04:00
Dax Raad d4cb47eadc tui: add keyboard shortcuts to cycle through recently used models
Users can now press F2 to cycle forward and Shift+F2 to cycle backward through their recently used models, making it faster to switch between commonly used AI models without opening the model selection dialog.
2025-10-31 19:42:41 -04:00
Dax Raad 261ff416a9 sync 2025-10-31 23:05:11 +00:00
opencode d0a70cb217 release: v1.0.4 2025-10-31 23:05:10 +00:00
Aiden Cline 20fc56d020 Revert "opentui: fix: Make worker.ts path independent from cwd (#3600)"
This reverts commit d473d4ffc8.
2025-10-31 17:57:56 -05:00
opencode a57ae3ec93 release: v1.0.3 2025-10-31 22:52:57 +00:00
Dax Raad 30f9fa12d9 tui: add session rename functionality with /rename command
- Add /rename command to autocomplete when a session is active
- Add rename dialog component for changing session names
- Add rename option to session list dialog with 'r' keybind
- Add session rename command to command registry
2025-10-31 18:44:33 -04:00
Haris Gušić d473d4ffc8 opentui: fix: Make worker.ts path independent from cwd (#3600) 2025-10-31 17:37:31 -05:00
Haris Gušić af50596529 fix: grep failing when pattern started with a dash 2025-10-31 17:20:22 -05:00
Dax Raad 3823d8d50e tui: simplify theme selection API by renaming setSelectedTheme to set 2025-10-31 18:11:36 -04:00
Dax Raad 7a926b32ce respect theme in config 2025-10-31 18:04:38 -04:00
Haris Gušić a5ede68241 fix: Remove conflicting "-h" aliases in TUI spawn and thread commands (#3651) 2025-10-31 16:59:59 -05:00
Aiden Cline 60dc38050d fix: unsupported option 2025-10-31 16:53:08 -05:00
Dax Raad 31d0caee38 tui: add /editor command to autocomplete for opening external editor 2025-10-31 17:47:08 -04:00
Dax Raad 2a7ab45605 add /theme 2025-10-31 17:44:41 -04:00
Aiden Cline 019054dd1e Revert "fix: ensure flags & docs match (#3638)"
This reverts commit a018a15f32.
2025-10-31 16:43:29 -05:00
opencode-agent[bot] a018a15f32 fix: ensure flags & docs match (#3638)
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-10-31 16:33:46 -05:00
Nathan Thomas e630d680dd feat: allow ctrl+d to exit the app (#3636) 2025-10-31 16:27:41 -05:00
Haris Gušić 9e392f25a6 feat: Improve error boundary add button to easily create issue in github (#3639) 2025-10-31 16:20:32 -05:00
Aiden Cline 2cc4e6ad7c ci: change model 2025-10-31 16:06:22 -05:00
Adam 70d8d1ab1e wip: desktop work 2025-10-31 15:57:21 -05:00
Adam 342aa27e03 wip: desktop work 2025-10-31 15:37:50 -05:00
Adam e1aed0cd01 wip: desktop work 2025-10-31 15:37:50 -05:00
opencode c8ea2c5ce0 release: v1.0.2 2025-10-31 20:33:50 +00:00
Dax Raad 5e8309a353 tui: update hello command with test content 2025-10-31 16:21:30 -04:00
Dax Raad aae0ce9921 tui: improve autocomplete component styling and update test command 2025-10-31 16:21:30 -04:00
Dax Raad 81b94d84dc ignore 2025-10-31 20:17:40 +00:00
opencode ceab70f8d9 release: v1.0.1 2025-10-31 20:17:39 +00:00
Dax Raad afe8cecc2b tui: add persistent key-value storage for user preferences
- Add KVProvider context for storing user preferences like theme and warnings
- Update theme context to use KV storage instead of sync config
- Move openrouter warning to persistent KV storage
- Refactor theme selection to persist user choice across sessions
2025-10-31 16:13:02 -04:00
Aiden Cline 4a292bf977 ci: auto assign 2025-10-31 14:58:18 -05:00
Aiden Cline e249b41513 ci: autolabel action 2025-10-31 14:55:33 -05:00
Dax Raad 9021dd60a1 tui: add /session command to list available sessions 2025-10-31 15:41:36 -04:00
opencode b9a39b816c release: v1.0.0 2025-10-31 19:28:07 +00:00
Dax Raad 1eeba770b1 docs: add v1.0 upgrade guide with UX changes documentation 2025-10-31 15:15:52 -04:00
Frank 6cff306be1 wip: zen 2025-10-31 15:09:48 -04:00
Dax 96bdeb3c7b OpenTUI is here (#2685) 2025-10-31 15:07:36 -04:00
opencode 81c617770d release: v0.15.31 2025-10-31 18:56:26 +00:00
Frank 021334509e sync 2025-10-31 14:43:51 -04:00
Frank 4bde3f7b15 zen: billing page layout 2025-10-31 14:43:51 -04:00
Haris Gušić 4355027408 fix: Missing @opencode-ai/plugin causes crash (#3615) 2025-10-31 13:41:04 -05:00
David Hill b022cf0ed6 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-31 17:02:53 +00:00
David Hill a529b0324d wip: Tweaking select styles
This ghost style is placeholder for now until we style this properly with an icon and all the states.
2025-10-31 17:02:44 +00:00
David Hill 16f5e16395 wip: Tweaking button styles 2025-10-31 17:02:12 +00:00
Adam 76e080b2cb wip: desktop work 2025-10-31 12:00:44 -05:00
Adam ffc889b99e wip: desktop work 2025-10-31 12:00:44 -05:00
Steven Martin 36b48a44ac tweak - normalise unix-like identifiers to support git bash for windows (#2100)
Co-authored-by: Steven Martin <smartin@clearcom.com>
2025-10-31 11:41:44 -05:00
David Hill 5379abe330 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-31 16:03:36 +00:00
David Hill a5bcb76bbf Added border as shadow and updated button 2025-10-31 16:03:33 +00:00
Aiden Cline b628c580c2 update types 2025-10-31 10:49:52 -05:00
Aiden Cline 46d675b980 tweak: filter out deprecated models 2025-10-31 10:48:51 -05:00
Adam a8bf1ad40f wip: desktop work 2025-10-31 10:08:24 -05:00
Adam 0ac943de90 wip: desktop work 2025-10-31 09:45:57 -05:00
Adam 485135cf5c wip: desktop work 2025-10-31 07:30:02 -05:00
Adam 543eee78a6 wip: desktop work 2025-10-31 07:24:47 -05:00
GitHub Action dafb63cfb3 ignore: update download stats 2025-10-31 2025-10-31 12:04:46 +00:00
opencode 504a599473 release: v0.15.30 2025-10-31 06:12:16 +00:00
Dax Raad 750b9f80a5 sync 2025-10-31 02:00:11 -04:00
Aiden Cline dfdd009750 fix: bash permission case 2025-10-31 00:52:46 -05:00
Haris Gušić c1ada302f9 fix: Opencode hangs after exit (#3481)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-30 23:57:58 -05:00
Filip 51e4c9fc4c add optional headers field to model config (#3546) 2025-10-30 23:35:26 -05:00
Jay V 43e272e6c4 ignore: refactor header context menu to use CSS styling and router navigation 2025-10-30 20:24:29 -04:00
David Hill 2f9f189f39 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-30 22:53:00 +00:00
David Hill f3c70f4ea8 Asset updates 2025-10-30 22:52:56 +00:00
David Hill 5d4441cd2b Fix download zip 2025-10-30 22:41:32 +00:00
David Hill bf5f34ace7 Brand Assets Zip 2025-10-30 22:38:12 +00:00
David Hill 9589657d21 Configure png and svg downloads 2025-10-30 22:25:07 +00:00
David Hill 37baed99c1 Brand page 2025-10-30 22:13:48 +00:00
Ritoban Dutta a3ba740de4 fix: resolve hanging permission prompts in headless mode (#3522)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-30 15:37:41 -05:00
Adam dc96664578 chore: sanitize tool parts locally 2025-10-30 14:57:38 -05:00
Jay V 4dafc532a8 ignore: update project stats to reflect current growth 2025-10-30 15:47:41 -04:00
Adam 984fe4b769 wip: desktop work 2025-10-30 14:46:38 -05:00
Adam 48f50cf55e wip: desktop work 2025-10-30 14:41:15 -05:00
Frank ba13f8da08 wip: fix 2025-10-30 15:15:46 -04:00
Frank 1a8b494055 wip: zen 2025-10-30 15:10:29 -04:00
Frank 4f02d7d424 zen: allow byok requests w/o a balance 2025-10-30 15:10:29 -04:00
Adam 4cebd69bf0 wip: desktop work 2025-10-30 13:54:52 -05:00
Adam dc6e54503c wip: desktop work 2025-10-30 13:49:29 -05:00
David Hill f18847d739 Adding links to legal 2025-10-30 17:37:30 +00:00
Adam 2a0b67d84f fix: lander space 2025-10-30 12:32:25 -05:00
Adam 89eac737a5 wip: desktop work 2025-10-30 12:30:45 -05:00
Brandon c68607fb2b feat: Adds session id to Agent context metadata (#3559)
Co-authored-by: Brandon Wisnicki <bwisnicki@palantir.com>
2025-10-30 12:15:21 -05:00
Adam e944ff0286 wip: desktop work 2025-10-30 12:13:02 -05:00
Adam ee7612a31c wip: desktop work 2025-10-30 12:02:51 -05:00
Adam 582ed7c363 wip: desktop work 2025-10-30 12:02:50 -05:00
Adam dce287a42d wip: desktop work 2025-10-30 12:02:50 -05:00
Adam 19974daa67 wip: desktop work 2025-10-30 12:02:50 -05:00
Adam dcf865a889 wip: desktop work 2025-10-30 12:02:50 -05:00
Adam 3b20935959 wip: desktop work 2025-10-30 12:02:50 -05:00
Adam 30f4c2cf4c wip: desktop work 2025-10-30 12:02:49 -05:00
Aiden Cline 3541fdcb20 tweak: adjust deno lsp (#3581) 2025-10-30 11:59:54 -05:00
David Hill 15de97c10f Change position 2025-10-30 16:55:23 +00:00
Jay V ee3fd3f7be ignore:lander 2025-10-30 12:40:07 -04:00
Haris Gušić dc87659791 tweak: move zod validation for tools to ensure it always runs (#3565) 2025-10-30 11:31:44 -05:00
Andrew Pashynnyk 149f5eaa2e fix: preserve metadata from MCP tool results in tool.execute.after hook (#3573) 2025-10-30 11:10:42 -05:00
Aiden Cline 42e0b47a7d fix: better frontmatter errors 2025-10-30 10:56:40 -05:00
oribi 2d5df3ad76 fix: agent model selection priority issue (#3572) 2025-10-30 09:32:39 -05:00
David Hill f202fa0d89 Theme aware svgs to be copied 2025-10-30 13:01:48 +00:00
David Hill 0abffdb8f8 Icon size update 2025-10-30 12:46:34 +00:00
David Hill e533d48b51 New assets 2025-10-30 12:36:41 +00:00
David Hill 439372704d Close content menu with escape 2025-10-30 12:19:26 +00:00
David Hill d7277fd305 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-30 12:16:15 +00:00
GitHub Action 5ae73637d3 ignore: update download stats 2025-10-30 2025-10-30 12:04:36 +00:00
David Hill bf0cbf2bfa Adding polish 2025-10-30 11:37:00 +00:00
opencode 4b3a841dd9 release: v0.15.29 2025-10-30 06:49:34 +00:00
Aiden Cline aca32eaa1c fix: tui not showing some errors 2025-10-30 01:41:28 -05:00
Matt Gillard 3ae75d7031 add optional timeout field to mcp config to allow users to use responding servers (#3558)
Co-authored-by: Matt Gillard <matt-github@gillard.biz>
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-10-30 01:25:24 -05:00
Khang Ha (Kelvin) 4b5e447961 Fix "bufio.Scanner token too long" error by replacing Scanner with Reader in SSE (#3531) 2025-10-30 01:04:06 -05:00
Aiden Cline 7a2b8eae76 tweak: catch err 2025-10-30 00:29:30 -05:00
ElecTwix d983b9485d fix: add doom loop detection (#3445)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-30 00:13:18 -05:00
GitHub Action 14836de276 ignore: update download stats 2025-10-30 2025-10-30 02:28:13 +00:00
David Hill e265efec09 Add right click menu to logo 2025-10-30 00:40:24 +00:00
David Hill 5ae00ba567 Brand page 2025-10-30 00:22:49 +00:00
Jay V a0f032c9b9 ignore: fix email formatting to ensure proper line breaks between message and signature 2025-10-29 19:19:28 -04:00
Jay V e6132fc6a4 docs: enterprise 2025-10-29 19:07:43 -04:00
Frank 950b608c4d zen: show browser default time 2025-10-29 17:14:28 -04:00
Adam 3210df7428 wip: desktop work 2025-10-29 16:04:34 -05:00
Adam cdeb82e9ca wip: desktop work 2025-10-29 16:04:34 -05:00
rienkim a9cae7b335 feat: add positional argument support to slash commands (#3456)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-29 14:54:24 -05:00
Frank 972c0893dd zen: fix unified endpoint for codex 2025-10-29 15:06:37 -04:00
opencode e5d89ca567 release: v0.15.28 2025-10-29 19:00:56 +00:00
Aiden Cline 4ae70d4b0d fix: parcel watcher issue (#3544) 2025-10-29 13:52:38 -05:00
opencode 935cd7481b release: v0.15.27 2025-10-29 18:41:24 +00:00
Dax Raad 5553efea5e only generate user message summary if no diffs 2025-10-29 14:28:43 -04:00
Adam 0ff73ed8a6 wip: desktop work 2025-10-29 16:59:45 +00:00
opencode 5e792d7ac5 release: v0.15.26 2025-10-29 16:59:45 +00:00
David Hill 4a77e94e3c Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-29 16:16:56 +00:00
David Hill 4c563ea405 Enron update 2025-10-29 16:16:48 +00:00
Aiden Cline 5875257462 ignore: add label 2025-10-29 11:08:46 -05:00
David Hill 9701891e94 testimonial and other polish 2025-10-29 16:02:25 +00:00
opencode a2ab37c1b6 release: v0.15.25 2025-10-29 16:01:53 +00:00
Dax Raad 4d6e2d8efc autoupgrade latest major only 2025-10-29 11:53:25 -04:00
Aiden Cline 4407d5d96f fix: ensure tool inputs are zod validated 2025-10-29 10:46:57 -05:00
Adam 244945c0e7 fix: desktop error 2025-10-29 10:43:34 -05:00
opencode c652b2b4e8 release: v0.15.24 2025-10-29 15:38:55 +00:00
David Hill aabeeb1431 Adding polish 2025-10-29 15:34:45 +00:00
David Hill 0fbedc5e19 Fix form submission 2025-10-29 15:34:39 +00:00
Dax Raad 12782fff14 remove log 2025-10-29 11:27:51 -04:00
Dax Raad ca463a2346 session diff only include modified files 2025-10-29 11:26:21 -04:00
Aiden Cline 7265cdf817 ignore: rm 2025-10-29 10:19:02 -05:00
David Hill 7baa751351 First pass at adding an enterprise page 2025-10-29 15:16:17 +00:00
Adam 5b86fa9109 wip: desktop work 2025-10-29 07:32:01 -05:00
Adam aa7e008fe1 wip: desktop work 2025-10-29 07:32:00 -05:00
GitHub Action 792664071c ignore: update download stats 2025-10-29 2025-10-29 12:04:57 +00:00
Aiden Cline a0541ba57a zen: fix models endpoint to be openai compatible 2025-10-28 22:48:57 -05:00
Aiden Cline 4994bf1b46 ignore: rename type 2025-10-28 22:39:57 -05:00
Tyler Gannon 1e24514d61 add OpenAPI annotations to tui.ts control endpoints (#3519) 2025-10-28 22:39:22 -05:00
opencode 4b1c6300a0 release: v0.15.23 2025-10-29 01:35:27 +00:00
Dax Raad db3fb9d316 ci: stuff 2025-10-28 21:28:44 -04:00
Dax Raad cd79676b42 sync 2025-10-28 20:35:18 -04:00
Dax Raad 09e7e0ab70 tag majors 2025-10-28 20:28:30 -04:00
Kevin King 0e60f66604 ignore: python sdk (#2779)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-28 18:32:45 -05:00
Aiden Cline fc8db6cdf9 fix: ensure timeout param passed to bash tool is positive 2025-10-28 17:32:39 -05:00
kcrommett 5cc37c4ea0 mcp: fix status() to not overwrite connected with failed (#3514) 2025-10-28 16:16:03 -05:00
Adam 46ad456718 wip: desktop work 2025-10-28 15:39:41 -05:00
Haris Gušić 832ffd2303 fix: Use process.stdout.write instead of console.log (#3508) 2025-10-28 15:38:08 -05:00
GitHub Action b261430880 chore: format code 2025-10-28 20:29:47 +00:00
Adam 545f345848 wip: desktop work 2025-10-28 15:29:16 -05:00
Adam 77ae0b527e wip: desktop work 2025-10-28 15:29:16 -05:00
Adam c1278109c9 wip: desktop work 2025-10-28 15:29:16 -05:00
Adam a7a88d01ef wip: desktop work 2025-10-28 15:29:16 -05:00
Adam 4e0ab6b634 wip: desktop work 2025-10-28 15:29:16 -05:00
Adam d36485b7af wip: desktop work 2025-10-28 15:29:15 -05:00
Adam 1da24f6adb wip: desktop work 2025-10-28 15:29:15 -05:00
Frank e29dd27632 zen: provider affinity 2025-10-28 15:55:03 -04:00
Aiden Cline 37380e1f94 add --title flag to opencode run (#3507) 2025-10-28 13:32:36 -05:00
Dax Raad 1309ca7a81 ignore 2025-10-28 14:13:47 -04:00
Dax Raad c1515316f5 core: fix additions and deletions counting in edit tool filediff 2025-10-28 14:08:10 -04:00
Danilo Favato b66e7b6fce tweak: add experimental chatMaxRetries to config (#2116)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-28 12:09:41 -05:00
oribi eb398f1951 add OPENCODE_CONFIG_DIR to allow loading a custom config directory (#3504)
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-10-28 11:50:09 -05:00
Aiden Cline 643c22d21f add catch for mcp tool execution 2025-10-28 10:23:04 -05:00
Aiden Cline 74acd08ead add catch for mcp tool execution 2025-10-28 10:23:03 -05:00
opencode 49ea5aa2ad release: v0.15.20 2025-10-28 15:12:37 +00:00
Aiden Cline ee1af0fe80 fix: blank version issue 2025-10-28 10:03:53 -05:00
GitHub Action dfebf40471 ignore: update download stats 2025-10-28 2025-10-28 12:04:31 +00:00
opencode 6af6a1295f release: v0.15.19 2025-10-28 08:12:32 +00:00
Dax Raad 22821744ef feat: add OPENCODE_FAKE_VCS flag for VCS testing and update todo tracking instructions 2025-10-28 02:54:29 -04:00
Aiden Cline 872c9467b2 chore: rm unused import 2025-10-28 00:43:29 -05:00
Dax Raad d8249f32a8 do not set temperature for claude models 2025-10-28 01:14:25 -04:00
Aiden Cline 982954cc1b feat (acp): mcp server support, file diffs, some default slash commands (/init, /compact), show todos properly (#3490)
The mcp server support does not mean acp didn't allow u to use mcp servers previously, it means that now you can connect new servers via ACP instead of relying on the opencode defined ones
2025-10-28 00:08:30 -05:00
Frank 4caa458232 acp: fix type error 2025-10-27 21:40:08 -04:00
Frank 6fe8e3973c zen: support 1M claude context 2025-10-27 21:36:10 -04:00
Frank 7816901713 wip: zen doc 2025-10-27 21:25:56 -04:00
Frank 71abca9571 wip: zen 2025-10-27 19:26:28 -04:00
kcrommett 7216a8c86d fix: editor paste functionality for text attachments (#3489) 2025-10-27 17:51:33 -05:00
Jay V e3e16e58c5 docs: edit 2025-10-27 18:16:48 -04:00
Dax Raad a2951a2702 Remove typecheck script from desktop package 2025-10-27 18:03:32 -04:00
Jay V 55453dc606 Add missing dependencies for desktop package 2025-10-27 17:49:31 -04:00
Jay V 198d7f7e5f Merge branch 'doc-acp' into dev 2025-10-27 17:49:24 -04:00
Jay V e3e9fd7aa8 docs: edit 2025-10-27 17:48:17 -04:00
Aiden Cline 3c56dbcf58 chore: rm comment 2025-10-27 16:15:13 -05:00
Aiden Cline ee07ed2dc4 chore: delete unused file 2025-10-27 15:44:12 -05:00
Adam 485e4520e7 wip: desktop work 2025-10-27 15:37:07 -05:00
Adam fc115ea367 wip: desktop work 2025-10-27 15:37:07 -05:00
Adam d03b79e61e wip: desktop work 2025-10-27 15:37:06 -05:00
Adam 0acae8211a wip: desktop work 2025-10-27 15:37:06 -05:00
Aiden Cline 0af4505756 fix: litellm error tool= param must be specified 2025-10-27 14:03:42 -05:00
Aurelien Ribon a606e1d2ec fix: dont set reasoning effort to medium for gpt-5-pro (#3474) 2025-10-27 10:50:57 -05:00
Aiden Cline 0e65700183 update sdk 2025-10-27 10:47:04 -05:00
Aiden Cline e6301ca5d5 tweak: rename event 2025-10-27 10:42:47 -05:00
Bernat Pericàs b562863fcc feat: add session.started event that triggers when a new session is created (#3413) 2025-10-27 07:18:23 -05:00
GitHub Action db85f01eff ignore: update download stats 2025-10-27 2025-10-27 12:04:50 +00:00
Aiden Cline 1a6fd018f6 Revert "fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)"
This reverts commit a9624c0fff.
2025-10-27 01:30:13 -05:00
Aiden Cline fdb5bae3c6 docs: acp 2025-10-27 00:56:00 -05:00
Haris Gušić a9624c0fff fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-27 00:03:10 -05:00
Aiden Cline 316d4c9197 wip 2025-10-26 23:49:56 -05:00
Denys Rybalka 5e886c35d5 chore: use stable URLs in PKGBUILD (#3448) 2025-10-26 19:50:45 -05:00
Aiden Cline 5162268f9d docs: update agent frontmatter permission example 2025-10-26 15:04:48 -05:00
Jérôme Benoit 0eb899a950 chore: cleanup versioned zod imports (#3460)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.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-10-26 14:50:41 -05:00
Dan McGuirk 3241f6b8bb docs: fix typos (#3454) 2025-10-26 14:37:25 -05:00
GitHub Action 2c792f17e6 ignore: update download stats 2025-10-26 2025-10-26 12:04:22 +00:00
Joscha Götzer 7d0c6860cd fix: make build script work cross-platform (#3430)
Co-authored-by: JosXa <info@josxa.dev>
2025-10-26 01:40:17 -05:00
Aiden Cline c70e393c81 Remove claude-haiku-4.5 from default priority for GitHub Copilot session title generation 2025-10-26 01:21:34 -05:00
opencode 20963c4186 release: v0.15.18 2025-10-26 03:49:21 +00:00
Aiden Cline 0a778a2789 make title gen more reliable 2025-10-25 22:14:29 -05:00
Aiden Cline 42c1e61bf4 fix: $ invocation not .quiet() (#3449) 2025-10-25 16:27:09 -05:00
Dax Raad 795b845782 update anthropic prompt 2025-10-25 17:26:27 -04:00
Mohammad Alhashemi 2e434a459a feat: add noReply parameter (#3433)
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-10-25 14:56:54 -05:00
Paulo Edgar Castro ae62bc8b1f fix: timeout param that allows user to disable provider timeout (#3443) 2025-10-25 12:08:27 -05:00
GitHub Action 187a5fe301 ignore: update download stats 2025-10-25 2025-10-25 12:04:18 +00:00
opencode fc2afdc92f release: v0.15.17 2025-10-25 06:45:23 +00:00
Aiden Cline fe5e7cfd1b ignore: rm change 2025-10-25 01:35:43 -05:00
Aiden Cline 98d51dde6a acp: slash commands, agents, permissions, @ references, code cleanup (#3403)
Co-authored-by: yetone <yetoneful@gmail.com>
2025-10-25 01:32:46 -05:00
Aiden Cline 5fec5ff424 fix: bedrock reasoning 2025-10-24 17:35:08 -05:00
Adam fea6a357bc wip: desktop work 2025-10-24 16:04:44 -05:00
Adam 6b82153263 wip: desktop work 2025-10-24 15:51:31 -05:00
Adam fa8e714d69 wip: desktop work 2025-10-24 15:43:47 -05:00
Adam 90515bc8c3 wip: desktop work 2025-10-24 15:02:31 -05:00
Adam e34042e17a wip: accordion css not going to keep me down 2025-10-24 15:01:28 -05:00
Dax Raad 6ff0ce8bc5 ignore: improve session timeline debugging and message display functionality 2025-10-24 15:45:37 -04:00
Aiden Cline e88b659545 make plan agent whitelist more conservative (#3424) 2025-10-24 14:40:36 -05:00
Dax Raad 74048ece2d ignore: fix new session message loading with retry logic to handle server processing delays 2025-10-24 15:35:53 -04:00
Dax Raad 6646f7264a ignore: highlight active session in sidebar to improve visual feedback 2025-10-24 15:27:20 -04:00
Dax Raad 18e549a474 ignore: fix session activation after creation to ensure proper state management 2025-10-24 15:24:17 -04:00
Adam 82249754e7 fix: pierre dep 2025-10-24 13:51:24 -05:00
Aiden Cline 5a0228897b ignore: reword 2025-10-24 13:46:47 -05:00
Adam e2920c06a3 wip: desktop work 2025-10-24 13:01:22 -05:00
Dax Raad 4da3aa2eb2 add missing dep 2025-10-24 13:43:33 -04:00
Adam efe7f01f41 wip: desktop work 2025-10-24 12:38:00 -05:00
Adam 9ae3d74adc wip: desktop work 2025-10-24 12:32:02 -05:00
Adam 477b6c584d wip: desktop work 2025-10-24 12:16:33 -05:00
Adam 86447b5764 wip: desktop work 2025-10-24 12:16:33 -05:00
Adam fe8f6d7a3e wip: desktop work 2025-10-24 12:16:33 -05:00
Adam 59b5f53509 wip: desktop work 2025-10-24 12:16:32 -05:00
Adam 3eb2db98ed wip: desktop work 2025-10-24 12:16:32 -05:00
Adam 35dec0649d wip: desktop work 2025-10-24 12:16:32 -05:00
Adam 78a7f79143 wip: ui package demo page 2025-10-24 12:16:32 -05:00
Aiden Cline 707ed72381 adjust edit tool multiple matches error wording (#3418) 2025-10-24 12:14:49 -05:00
Dax Raad 21880e199d mroe summary tweaks 2025-10-24 12:37:23 -04:00
Dax Raad 736a85d427 track finish reason 2025-10-24 12:23:24 -04:00
Dax Raad fb40dc6b20 generate user message title and body 2025-10-24 11:50:42 -04:00
Hieu Nguyen 483fcdaddb feat: support lua lsp (#3402) 2025-10-24 10:37:11 -05:00
Andrew Pashynnyk 883b71ac36 fix: respect local config for autoupdate settings (#3408)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-24 09:50:09 -05:00
Dax Raad 3e574c71cb potentially fix fetch failed timeout errors 2025-10-24 09:46:28 -04:00
Dax Raad 4cab66da6c test cleanup 2025-10-24 09:29:38 -04:00
Dax Raad 7003efd2da fix summary 2025-10-24 09:24:58 -04:00
Dax Raad 06fe87b361 fix failing migration 2025-10-24 09:20:15 -04:00
GitHub Action 944fda45e6 ignore: update download stats 2025-10-24 2025-10-24 12:05:02 +00:00
opencode 343471b98d release: v0.15.16 2025-10-24 04:52:27 +00:00
Dax Raad 56528493dc codex does not have reasoning effort set 2025-10-24 00:29:34 -04:00
opencode e66156c86e release: v0.15.15 2025-10-24 03:51:17 +00:00
Dax Raad 8b9b8ca15b update codex prompt 2025-10-23 23:36:32 -04:00
Haris Gušić 50cc641288 fix: Opencode fails with ENOENT posix_spawn '/usr/bin/rg' (#3396) 2025-10-23 18:05:01 -05:00
Bernat Pericàs 4c90bf3e07 refactor: whitelist some safe bash tools in Plan agent (#3288)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-23 17:11:08 -05:00
Aiden Cline 4216c1c2a9 adjust changelog generation 2025-10-23 16:16:11 -05:00
Dax Raad 4bd7646ccb regen sdk 2025-10-23 16:33:00 -04:00
Dax Raad cee7106054 session summaries in data 2025-10-23 16:28:20 -04:00
Dax Raad f4dfae0bb0 ignore: diff stuff 2025-10-23 16:04:58 -04:00
Aiden Cline 9b5fe10df6 add flag wildcard parsing support for bash tool (#3390) 2025-10-23 13:35:09 -05:00
Aiden Cline b5f336c0ea test: rm flaky test 2025-10-23 12:52:08 -05:00
Aiden Cline 913c3ae799 tweak: split out title before newline 2025-10-23 12:44:43 -05:00
Thierry Delafontaine a68111ca77 fix: move zod-to-json-schema to dependencies (#3387) 2025-10-23 12:16:03 -05:00
Aiden Cline 5f8a3a574e docs: fix numbers 2025-10-23 11:53:21 -05:00
Aiden Cline d69e8e5528 docs: tweak google vertex 2025-10-23 11:49:53 -05:00
Yuku Kotani e5df43f9b7 docs: Add Google Vertex AI provider documentation (#3349) 2025-10-23 11:44:06 -05:00
Andrew Pashynnyk 3c7b229d8b fix: allow tool.execute.after hook to modify MCP tool output (#3381) 2025-10-23 10:38:55 -05:00
Mani Sundararajan 9ab4414aef docs: rm winget as a recommended installation method under windows (#3382)
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-10-23 09:25:42 -05:00
GitHub Action c2cf6fb904 ignore: update download stats 2025-10-23 2025-10-23 12:04:55 +00:00
opencode 5e69bdbef4 release: v0.15.14 2025-10-23 05:55:28 +00:00
Yesh Yendamuri f81e28c673 feat: add model management to ACP sessions (#3358) 2025-10-23 00:43:28 -05:00
Aiden Cline 61899d4fa7 regen sdk 2025-10-22 23:00:03 -05:00
Aiden Cline 7c7ebb0a9d feat: retry parts (#3369) 2025-10-22 18:31:36 -05:00
Dax Raad 9def7cff2d summary tweaks 2025-10-22 19:03:08 -04:00
geril07 c2ef930d2a add option to allow agent switches to not change model (#3356) 2025-10-22 17:51:46 -05:00
Adam 3c3d2f5a6e wip: desktop work 2025-10-22 17:51:16 -05:00
Dax Raad f435049d36 sync 2025-10-22 18:49:57 -04:00
Dax Raad 1f80de2fa6 core: add experimental turn summarization to compact conversation history 2025-10-22 18:33:46 -04:00
Adam f194a784b0 wip: desktop work 2025-10-22 17:33:08 -05:00
Adam 89b703c387 wip: desktop work 2025-10-22 17:31:49 -05:00
Aiden Cline eff12cb484 vscode: eslint fix 2025-10-22 17:17:47 -05:00
Aiden Cline 593e89b4f4 vscode: fix script
publish-vscode / publish (push) Has been cancelled
2025-10-22 17:11:51 -05:00
Aiden Cline 4d3f703715 vscode: adjust tsconfig
publish-vscode / publish (push) Has been cancelled
2025-10-22 16:43:06 -05:00
Aiden Cline 123dcc10cc ignore: cleanup bun.lock w/ bun i 2025-10-22 15:34:41 -05:00
Dax Raad 28d8af48a0 add parent id to assistant messages
publish-vscode / publish (push) Has been cancelled
2025-10-22 15:01:13 -04:00
Kyle Galbraith 10ff6e9830 docs: fix typo in SDK documentation (#3355) 2025-10-22 11:43:28 -05:00
theVinchi a7b43d82ab add Amazon Nova models to us-* prefix requirement list (#3357) 2025-10-22 11:41:38 -05:00
Aiden Cline 9005fd31ed tweak 2025-10-22 11:29:10 -05:00
Aiden Cline d2bded23c3 tweak 2025-10-22 11:28:06 -05:00
Aiden Cline c0cbc37f85 tweak: model priority 2025-10-22 11:12:32 -05:00
Aiden Cline 9df61055e2 change default title model 2025-10-22 10:41:08 -05:00
GitHub Action 074136b1e8 ignore: update download stats 2025-10-22 2025-10-22 12:04:58 +00:00
Affaan Mustafa 8db5951287 feat: Improve editor detection with auto-discovery and better error messages (#3155)
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-10-21 20:49:42 -05:00
Haris Gušić 97c7e941eb fix: opencode run shouldn't print to stderr (#3341) 2025-10-21 19:53:09 -05:00
Aiden Cline 354f5c3281 ignore: fix acp test (#3339) 2025-10-21 15:27:59 -05:00
opencode 833706cda4 release: v0.15.13 2025-10-21 20:24:20 +00:00
Aiden Cline 2a951cea38 ignore: reword 2025-10-21 14:43:34 -05:00
Dax Raad d9a8d2032a fix sourcemapping so errors show proper stack trace 2025-10-21 15:32:24 -04:00
Dax Raad d7cdabe8b7 refactor acp args 2025-10-21 15:24:09 -04:00
Aiden Cline e7c74d13cc ignore: reword contributing.md 2025-10-21 14:12:33 -05:00
opencode 6ac5a447c2 release: v0.15.12 2025-10-21 17:31:56 +00:00
Aiden Cline cb4670e6de ignore: add label 2025-10-21 11:12:52 -05:00
Aiden Cline ca0f3902b7 fix: provider option transforms (#3331) 2025-10-21 11:08:21 -05:00
Dax Raad e9996342a7 core: provide line-level statistics in file diffs to help users understand the scale of changes 2025-10-21 11:54:41 -04:00
opencode a84826061d release: v0.15.11 2025-10-21 15:04:22 +00:00
Dax Raad 7a20f77ebf core: improve error handling with console output for debugging 2025-10-21 10:55:10 -04:00
Giuseppe Rota 731122bf99 fix(acp): use newer acp package (#3317) 2025-10-21 09:35:09 -05:00
GitHub Action f9036734eb ignore: update download stats 2025-10-21 2025-10-21 12:06:20 +00:00
kcrommett a99bd3aa2c tweak: adjust file api to encode images (#3292) 2025-10-21 00:52:39 -05:00
zWing 96efede846 fix: openai options when using @ai-sdk/azure (#3315) 2025-10-20 23:30:13 -05:00
Cheol Kang 2f66055d25 feat: add -f/--file flag to opencode run command (#3295)
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-10-20 23:25:54 -05:00
opencode 6995dab1dc release: v0.15.10 2025-10-20 22:21:03 +00:00
Dax Raad a0a09f421c core: add session diff API to show file changes between snapshots 2025-10-20 17:59:26 -04:00
Joe Schmitt f3f21194ae feat: Add ACP (Agent Client Protocol) support (#2947)
Co-authored-by: opencode-bot <devnull@opencode.local>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: GitHub Action <action@github.com>
2025-10-20 16:55:22 -05:00
opencode 835fa9fb81 release: v0.15.9 2025-10-20 21:16:57 +00:00
Aiden Cline 96ae6d51aa ignore: link labels 2025-10-20 16:02:23 -05:00
Frank 075ef0fa34 wip: share 2025-10-20 16:17:51 -04:00
Aiden Cline 89b72e4442 fix: bash tool permission merges (#3302) 2025-10-20 15:15:14 -05:00
Frank 7a7b3c6315 wip: zen 2025-10-20 13:22:43 -04:00
Rui Ferrão 3d48c14d29 docs: clarify the effects of the subtask command option with primary … (#3299) 2025-10-20 12:16:09 -05:00
GitHub Action bfa79ed44b ignore: update download stats 2025-10-20 2025-10-20 12:04:59 +00:00
Hieu Nguyen 4d8268c818 feat: support astro lsp (#3242) 2025-10-19 22:49:06 -05:00
Squibid 95d413bec6 theme: add mellow theme (#3198) 2025-10-19 22:34:04 -05:00
Joscha Götzer 1cb5a70382 feat(theme): Add Night Owl theme (#3269) 2025-10-19 22:30:04 -05:00
Walter 6adc16ca8a fix(tui): Allow modals to handle ESC key before force closing (#3279) 2025-10-19 22:16:54 -05:00
Zeldris 10ebe9ae09 docs: add scoop & winget to readme (#3215) 2025-10-19 21:50:51 -05:00
Aiden Cline 43a07c6aca hide invalid option 2025-10-19 20:51:33 -05:00
Aiden Cline 5d3a88f34f fix: snapshot undo bug (#3290) 2025-10-19 18:42:34 -05:00
Ivan Uhalin e47edfffe4 docs: fix Grep by Vercel link in documentation (#3280) 2025-10-19 11:51:57 -05:00
GitHub Action 141097fc73 ignore: update download stats 2025-10-19 2025-10-19 12:04:24 +00:00
Aiden Cline c8898463a7 ignore: adjust template 2025-10-18 23:45:01 -05:00
Aiden Cline 1c7bd6365e replace all $ARGUMENTS (#3276) 2025-10-18 23:37:17 -05:00
Aiden Cline 290d15a80f ignore: add version to bug template 2025-10-18 14:56:06 -05:00
Aiden Cline 233a018fe5 docs: contributing.md (#3248)
Co-authored-by: Jay <air@live.ca>
2025-10-18 14:10:14 -05:00
opencode d69beec087 release: v0.15.8 2025-10-18 18:36:52 +00:00
Aiden Cline 1f869bccc1 make compact interruptable (#3251) 2025-10-18 11:49:29 -05:00
GitHub Action 8da8c9e78c ignore: update download stats 2025-10-18 2025-10-18 12:04:14 +00:00
Adam 335d833655 wip: desktop work 2025-10-17 15:22:11 -05:00
Haris Gušić 1dba01e057 Improve typo directory error (#3247) 2025-10-17 14:34:37 -05:00
Jay V a3de43f3de ignore: clarify that workspaces are free for teams during beta 2025-10-17 15:19:19 -04:00
Jay V 22ad4f5365 ignore: help users discover that teams feature is free during beta 2025-10-17 14:33:21 -04:00
Jay V 5bfbec60b5 docs: clarify team collaboration features and pricing 2025-10-17 13:58:10 -04:00
Adam cc18b58ff9 wip: desktop work 2025-10-17 12:07:58 -05:00
Adam 887a819f24 wip: desktop work 2025-10-17 12:06:36 -05:00
mgrachten fe8b3a2515 set 755 permissions (#3237) 2025-10-17 10:42:42 -05:00
GitHub Action 86079353ef ignore: update download stats 2025-10-17 2025-10-17 12:04:43 +00:00
Frank b7c8690414 wip: zen 2025-10-17 02:23:45 -04:00
Frank c25b9bf65a wip: zen 2025-10-17 01:15:25 -04:00
Matt Gillard ddb2e6957c added AU inference for bedrock haiku 4.5 (#3206)
Co-authored-by: Matt Gillard <matt-github@gillard.biz>
2025-10-16 23:48:41 -05:00
Frank a590b32a10 wip: zen 2025-10-17 00:26:00 -04:00
Frank 5f7bba11fd wip: zen 2025-10-16 23:43:49 -04:00
Frank 4663ea5faa wip: zen 2025-10-16 23:19:25 -04:00
Frank dd581e8577 wip: zen 2025-10-17 03:17:07 +00:00
opencode bad01d76de release: v0.15.7 2025-10-17 03:17:07 +00:00
Aiden Cline d69366b00c Revert "try to avoid persisting empty thinking/text blocks"
This reverts commit d8a15e7bc9.
2025-10-16 22:04:55 -05:00
Frank 1947580b08 wip: zen 2025-10-16 22:28:34 -04:00
Frank ca9b13e8a2 wip: zen 2025-10-16 22:28:09 -04:00
Hieu Nguyen 92d9a0ec61 feat: deno lsp (#3210)
Co-authored-by: hiunguynx <hieu.nm1@teko.vn>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-10-16 18:08:36 -05:00
opencode 2be9ed2590 release: v0.15.6 2025-10-16 23:02:59 +00:00
Aiden Cline 25861f6d0d sync 2025-10-16 17:49:02 -05:00
Aiden Cline b24f4e3d2c fix: timeout option (#3229) 2025-10-16 17:47:41 -05:00
Frank 729ad1cb75 wip: zen 2025-10-16 17:50:32 -04:00
Haris Gušić fb4105a46c docs: fix Ctrl+K styling in documentation (#3021) 2025-10-16 15:30:28 -05:00
Frank 7abc3e9794 ci: fix 2025-10-16 16:04:25 -04:00
Frank 88fef05923 wip: zen 2025-10-16 15:58:51 -04:00
GitHub Action 8552f3555e chore: format code 2025-10-16 19:55:30 +00:00
Adam 47d9e01765 wip: css/ui and desktop work 2025-10-16 14:53:44 -05:00
Aiden Cline fc18fc8a08 fix: bash hangs & orphans (#3225) 2025-10-16 14:39:36 -05:00
Dax Raad 7474788778 ci: fix 2025-10-16 15:36:12 -04:00
GitHub Action 26d0d20e4d chore: format code 2025-10-16 19:08:21 +00:00
Adam 20229f147b wip: css/ui and desktop work 2025-10-16 14:07:37 -05:00
Aiden Cline 149cb6a9ec fix: connection closed
publish-github-action / publish (push) Has been cancelled
2025-10-16 14:00:07 -05:00
Frank 7ec5e49e19 zen: support stripe link 2025-10-16 14:59:46 -04:00
874 changed files with 62950 additions and 37749 deletions
+59
View File
@@ -0,0 +1,59 @@
name: Bug report
description: Report an issue that should be fixed
labels: ["bug"]
body:
- type: textarea
id: description
attributes:
label: Description
description: Describe the bug you encountered
placeholder: What happened?
validations:
required: true
- type: input
id: opencode-version
attributes:
label: OpenCode version
description: What version of OpenCode are you using?
validations:
required: false
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: How can we reproduce this issue?
placeholder: |
1.
2.
3.
validations:
required: false
- type: textarea
id: screenshot-or-link
attributes:
label: Screenshot and/or share link
description: Run `/share` to get a share link, or attach a screenshot
placeholder: Paste link or drag and drop screenshot here
validations:
required: false
- type: input
id: os
attributes:
label: Operating System
description: what OS are you using?
placeholder: e.g., macOS 26.0.1, Ubuntu 22.04, Windows 11
validations:
required: false
- type: input
id: terminal
attributes:
label: Terminal
description: what terminal are you using?
placeholder: e.g., iTerm2, Ghostty, Alacritty, Windows Terminal
validations:
required: false
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: 💬 Discord Community
url: https://discord.gg/opencode
about: For quick questions or real-time discussion. Note that issues are searchable and help others with the same question.
@@ -0,0 +1,20 @@
name: 🚀 Feature Request
description: Suggest an idea, feature, or enhancement
labels: [discussion]
title: "[FEATURE]:"
body:
- type: checkboxes
id: verified
attributes:
label: Feature hasn't been suggested before.
options:
- label: I have verified this feature I'm about to request hasn't been suggested before.
required: true
- type: textarea
attributes:
label: Describe the enhancement you want to request
description: What do you want to change or add? What are the benefits of implementing this? Try to be detailed so we can understand your request better :)
validations:
required: true
+11
View File
@@ -0,0 +1,11 @@
name: Question
description: Ask a question
labels: ["question"]
body:
- type: textarea
id: question
attributes:
label: Question
description: What's your question?
validations:
required: true
+2
View File
@@ -5,6 +5,8 @@ runs:
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Cache ~/.bun
id: cache-bun
+71
View File
@@ -0,0 +1,71 @@
#
# This file is intentionally in the wrong dir, will move and add later....
#
# name: publish-python-sdk
# on:
# release:
# types: [published]
# workflow_dispatch:
# jobs:
# publish:
# runs-on: ubuntu-latest
# permissions:
# contents: read
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# - name: Setup Bun
# uses: oven-sh/setup-bun@v1
# with:
# bun-version: 1.2.21
# - name: Install dependencies (JS/Bun)
# run: bun install
# - name: Install uv
# shell: bash
# run: curl -LsSf https://astral.sh/uv/install.sh | sh
# - name: Generate Python SDK from OpenAPI (CLI)
# shell: bash
# run: |
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/generate.py --source cli
# - name: Sync Python dependencies
# shell: bash
# run: |
# ~/.local/bin/uv sync --dev --project packages/sdk/python
# - name: Set version from release tag
# shell: bash
# run: |
# TAG="${GITHUB_REF_NAME:-}"
# if [ -z "$TAG" ]; then
# TAG="$(git describe --tags --abbrev=0 || echo 0.0.0)"
# fi
# echo "Using version: $TAG"
# VERSION="$TAG" ~/.local/bin/uv run --project packages/sdk/python python - <<'PY'
# import os, re, pathlib
# root = pathlib.Path('packages/sdk/python')
# pt = (root / 'pyproject.toml').read_text()
# version = os.environ.get('VERSION','0.0.0').lstrip('v')
# pt = re.sub(r'(?m)^(version\s*=\s*")[^"]+("\s*)$', f"\\1{version}\\2", pt)
# (root / 'pyproject.toml').write_text(pt)
# # Also update generator config override for consistency
# cfgp = root / 'openapi-python-client.yaml'
# if cfgp.exists():
# cfg = cfgp.read_text()
# cfg = re.sub(r'(?m)^(package_version_override:\s*)\S+$', f"\\1{version}", cfg)
# cfgp.write_text(cfg)
# PY
# - name: Build and publish to PyPI
# env:
# PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
# shell: bash
# run: |
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/publish.py
+55
View File
@@ -0,0 +1,55 @@
name: Auto-label TUI Issues
on:
issues:
types: [opened]
jobs:
auto-label:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Auto-label and assign issues
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const issue = context.payload.issue;
const title = issue.title;
const description = issue.body || '';
// Check for web/desktop keywords
const webPattern = /\b(web|desktop)\b/i;
const isWebRelated = webPattern.test(title) || webPattern.test(description);
// Check for version patterns like v1.0.x or 1.0.x
const versionPattern = /[v]?1\.0\./i;
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
if (isWebRelated) {
// Add web label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['web']
});
// Assign to adamdotdevin
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: ['adamdotdevin']
});
} else if (isVersionRelated) {
// Only add opentui if NOT web-related
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['opentui']
});
}
+1 -1
View File
@@ -26,4 +26,4 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/kimi-k2
model: opencode/glm-4.6
+4
View File
@@ -24,6 +24,10 @@ jobs:
- run: git fetch --force --tags
- run: bun install -g @vscode/vsce
- name: Install extension dependencies
run: bun install
working-directory: ./sdks/vscode
- name: Publish
run: |
./script/publish
+5 -1
View File
@@ -53,13 +53,17 @@ jobs:
- name: Install OpenCode
run: curl -fsSL https://opencode.ai/install | bash
- name: Setup npm auth
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Publish
run: |
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_CHANNEL: latest
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+1
View File
@@ -5,6 +5,7 @@ on:
branches:
- dev
- opentui
- v0
concurrency: ${{ github.workflow }}-${{ github.ref }}
+3
View File
@@ -5,8 +5,11 @@ node_modules
.env
.idea
.vscode
*~
openapi.json
playground
tmp
dist
.turbo
**/.serena
.serena/
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh
bun run typecheck
bun typecheck
+3
View File
@@ -18,3 +18,6 @@ For anything in the packages/app use the ignore: prefix.
prefer to explain WHY something was done from an end user perspective instead of
WHAT was done.
do not do generic messages like "improved agent experience" be very specific
about what user facing changes were made
+1 -1
View File
@@ -1,5 +1,5 @@
---
description: hello world
description: hello world iaosd ioasjdoiasjd oisadjoisajd osiajd oisaj dosaij dsoajsajdaijdoisa jdoias jdoias jdoia jois jo jdois jdoias jdoias j djoasdj
---
hey there $ARGUMENTS
+1 -1
View File
@@ -17,7 +17,7 @@
## Tool Calling
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environnement:
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
json
{
+68
View File
@@ -0,0 +1,68 @@
# Contributing to OpenCode
We want to make it easy for you to contribute to OpenCode. Here are the most common type of changes that get merged:
- Bug fixes
- Additional LSPs / Formatters
- Improvements to LLM performance
- Support for new providers
- Fixes for environment-specific quirks
- Missing standard behavior
- Documentation improvements
However, any UI or core product feature must go through a design review with the core team before implementation.
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
> [!NOTE]
> PRs that ignore these guardrails will likely be closed.
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
## Developing OpenCode
- Requirements: Bun 1.3+
- Install dependencies and start the dev server from the repo root:
```bash
bun install
bun dev
```
- Core pieces:
- `packages/opencode`: OpenCode core business logic & server.
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
- `packages/plugin`: Source for `@opencode-ai/plugin`
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
## Pull Request Expectations
- Try to keep pull requests small and focused.
- Link relevant issue(s) in the description
- Explain the issue and why your change fixes it
- Avoid having verbose LLM generated PR descriptions
- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
### Style Preferences
These are not strictly enforced, they are just general guidelines:
- **Functions:** Keep logic within a single function unless breaking it out adds clear reuse or composition benefits.
- **Destructuring:** Do not do unnecessary destructuring of variables.
- **Control flow:** Avoid `else` statements.
- **Error handling:** Prefer `.catch(...)` instead of `try`/`catch` when possible.
- **Types:** Reach for precise types and avoid `any`.
- **Variables:** Stick to immutable patterns and avoid `let`.
- **Naming:** Choose concise single-word identifiers when they remain descriptive.
- **Runtime APIs:** Use Bun helpers such as `Bun.file()` when they fit the use case.
## Feature Requests
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
+4 -36
View File
@@ -26,7 +26,9 @@ curl -fsSL https://opencode.ai/install | bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
brew install sst/tap/opencode # macOS and Linux
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
```
@@ -54,41 +56,7 @@ For more info on how to configure OpenCode [**head over to our docs**](https://o
### Contributing
OpenCode is an opinionated tool so any fundamental feature needs to go through a
design process with the core team.
> [!IMPORTANT]
> We do not accept PRs for core features.
However we still merge a ton of PRs - you can contribute:
- Bug fixes
- Improvements to LLM performance
- Support for new providers
- Fixes for env specific quirks
- Missing standard behavior
- Documentation
Take a look at the git history to see what kind of PRs we end up merging.
> [!NOTE]
> If you do not follow the above guidelines we might close your PR.
To run OpenCode locally you need.
- Bun 1.3 or higher
- Golang 1.24.x
And run.
```bash
$ bun install
$ bun dev
```
#### Development Notes
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the OpenCode team to generate a new stainless sdk for the clients.
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
### FAQ
+130 -110
View File
@@ -1,112 +1,132 @@
# 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) |
| 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) |
+536 -233
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -104,7 +104,7 @@ To test locally:
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
- `MOCK_TOKEN`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `MOCK_TOKEN`: A GitHub personal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `MOCK_EVENT`: Mock GitHub event payload (see templates below).
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/github/index.ts` runs your local version of `opencode`.
@@ -118,7 +118,7 @@ Replace:
- `"owner":"sst"` with repo owner
- `"repo":"hello-world"` with repo name
- `"actor":"fwang"` with the GitHub username of commentor
- `"actor":"fwang"` with the GitHub username of commenter
- `"number":4` with the GitHub issue id
- `"body":"hey opencode, summarize thread"` with comment body
+48 -15
View File
@@ -152,6 +152,9 @@ try {
return session.id.slice(-8)
})()
console.log("opencode session", session.id)
if (shareId) {
console.log("Share link:", `${useShareUrl()}/s/${shareId}`)
}
// Handle 3 cases
// 1. Issue
@@ -168,7 +171,9 @@ try {
const summary = await summarize(response)
await pushToLocalBranch(summary)
}
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
const hasShared = prData.comments.nodes.some((c) =>
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`)
}
// Fork PR
@@ -180,7 +185,9 @@ try {
const summary = await summarize(response)
await pushToForkBranch(summary, prData)
}
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
const hasShared = prData.comments.nodes.some((c) =>
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
await updateComment(`${response}${footer({ image: !hasShared })}`)
}
}
@@ -361,7 +368,9 @@ async function getAccessToken() {
if (!response.ok) {
const responseJson = (await response.json()) as { error?: string }
throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`)
throw new Error(
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`,
)
}
const responseJson = (await response.json()) as { token: string }
@@ -402,8 +411,12 @@ async function getUserPrompt() {
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
// ie. ![Image](https://github.com/user-attachments/assets/xxxx)
const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi)
const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi)
const mdMatches = prompt.matchAll(
/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi,
)
const tagMatches = prompt.matchAll(
/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi,
)
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
console.log("Images", JSON.stringify(matches, null, 2))
@@ -430,7 +443,8 @@ async function getUserPrompt() {
// Replace img tag with file path, ie. @image.png
const replacement = `@${filename}`
prompt = prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
prompt =
prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
offset += replacement.length - tag.length
const contentType = res.headers.get("content-type")
@@ -498,7 +512,12 @@ async function subscribeSessionEvents() {
? JSON.stringify(part.state.input)
: "Unknown"
console.log()
console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title)
console.log(
color + `|`,
"\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`,
"",
"\x1b[0m" + title,
)
}
if (part.type === "text") {
@@ -710,7 +729,8 @@ async function assertPermissions() {
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
}
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
if (!["admin", "write"].includes(permission))
throw new Error(`User ${actor} does not have write permissions`)
}
async function updateComment(body: string) {
@@ -730,12 +750,13 @@ async function updateComment(body: string) {
async function createPR(base: string, branch: string, title: string, body: string) {
console.log("Creating pull request...")
const { repo } = useContext()
const truncatedTitle = title.length > 256 ? title.slice(0, 253) + "..." : title
const pr = await octoRest.rest.pulls.create({
owner: repo.owner,
repo: repo.repo,
head: branch,
base,
title,
title: truncatedTitle,
body,
})
return pr.data.number
@@ -753,7 +774,9 @@ function footer(opts?: { image?: boolean }) {
return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
})()
const shareUrl = shareId ? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;` : ""
const shareUrl = shareId
? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;`
: ""
return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})`
}
@@ -936,9 +959,13 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
})
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
const files = (pr.files.nodes || []).map((f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
const files = (pr.files.nodes || []).map(
(f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`,
)
const reviewData = (pr.reviews.nodes || []).map((r) => {
const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`)
const comments = (r.comments.nodes || []).map(
(c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`,
)
return [
`- ${r.author.login} at ${r.submittedAt}:`,
` - Review body: ${r.body}`,
@@ -960,9 +987,15 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
`Deletions: ${pr.deletions}`,
`Total Commits: ${pr.commits.totalCount}`,
`Changed Files: ${pr.files.nodes.length} files`,
...(comments.length > 0 ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] : []),
...(files.length > 0 ? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"] : []),
...(reviewData.length > 0 ? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"] : []),
...(comments.length > 0
? ["<pull_request_comments>", ...comments, "</pull_request_comments>"]
: []),
...(files.length > 0
? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"]
: []),
...(reviewData.length > 0
? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"]
: []),
"</pull_request>",
].join("\n")
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { domain } from "./stage"
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
export const EMAILOCTOPUS_API_KEY = new sst.Secret("EMAILOCTOPUS_API_KEY")
const ADMIN_SECRET = new sst.Secret("ADMIN_SECRET")
const bucket = new sst.cloudflare.Bucket("Bucket")
export const api = new sst.cloudflare.Worker("Api", {
@@ -12,7 +13,7 @@ export const api = new sst.cloudflare.Worker("Api", {
WEB_DOMAIN: domain,
},
url: true,
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY],
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, ADMIN_SECRET],
transform: {
worker: (args) => {
args.logpush = true
+11 -3
View File
@@ -61,7 +61,13 @@ export const auth = new sst.cloudflare.Worker("AuthApi", {
domain: `auth.${domain}`,
handler: "packages/console/function/src/auth.ts",
url: true,
link: [database, authStorage, GITHUB_CLIENT_ID_CONSOLE, GITHUB_CLIENT_SECRET_CONSOLE, GOOGLE_CLIENT_ID],
link: [
database,
authStorage,
GITHUB_CLIENT_ID_CONSOLE,
GITHUB_CLIENT_SECRET_CONSOLE,
GOOGLE_CLIENT_ID,
],
})
////////////////
@@ -97,7 +103,8 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
],
})
const ZEN_MODELS = new sst.Secret("ZEN_MODELS")
const ZEN_MODELS1 = new sst.Secret("ZEN_MODELS1")
const ZEN_MODELS2 = new sst.Secret("ZEN_MODELS2")
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
properties: { value: auth.url.apply((url) => url!) },
@@ -130,7 +137,8 @@ new sst.cloudflare.x.SolidStart("Console", {
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,
STRIPE_SECRET_KEY,
ZEN_MODELS,
ZEN_MODELS1,
ZEN_MODELS2,
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
AWS_SES_SECRET_ACCESS_KEY,
+9 -4
View File
@@ -10,10 +10,14 @@ NC='\033[0m' # No Color
requested_version=${VERSION:-}
os=$(uname -s | tr '[:upper:]' '[:lower:]')
if [[ "$os" == "darwin" ]]; then
os="darwin"
fi
raw_os=$(uname -s)
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
# Normalize various Unix-like identifiers
case "$raw_os" in
Darwin*) os="darwin" ;;
Linux*) os="linux" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
esac
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]]; then
@@ -96,6 +100,7 @@ download_and_install() {
curl -# -L -o "$filename" "$url"
unzip -q "$filename"
mv opencode "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/opencode"
cd .. && rm -rf opencodetmp
}
@@ -0,0 +1,15 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
"files": [
{
"date": 1759827172859,
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
}
],
"hashType": "sha256"
}
+48
View File
@@ -0,0 +1,48 @@
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"}
{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"}
{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.120"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:37.121"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.490"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:38:52.491"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.524"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:39:25.525"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.126"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:40:57.127"}
{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.175"}
{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 22:42:24.176"}
+15 -1
View File
@@ -1,3 +1,17 @@
{
"$schema": "https://opencode.ai/config.json"
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
"mcp": {
"weather": {
"type": "local",
"command": ["bun", "x", "@h1deya/mcp-server-weather"]
},
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
"headers": {
"CONTEXT7_API_KEY": "{env:CONTEXT7_API_KEY}"
}
}
}
}
+14 -7
View File
@@ -1,13 +1,15 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.0",
"packageManager": "bun@1.3.1",
"scripts": {
"dev": "bun run packages/opencode/src/index.ts",
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
"prepare": "husky"
"prepare": "husky",
"random": "echo 'Random script'"
},
"workspaces": {
"packages": [
@@ -19,12 +21,16 @@
"catalog": {
"@types/bun": "1.3.0",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/node": "22.13.9",
"@tsconfig/node22": "22.0.2",
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.4.1",
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.8",
"hono": "4.7.10",
@@ -35,8 +41,9 @@
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-js": "1.9.9",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"@tailwindcss/vite": "4.1.11",
"virtua": "0.42.3",
"vite": "7.1.4",
"vite-plugin-solid": "2.11.8"
}
@@ -49,8 +56,8 @@
"turbo": "2.5.6"
},
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/script": "workspace:*"
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*"
},
"repository": {
"type": "git",
@@ -59,7 +66,7 @@
"license": "MIT",
"prettier": {
"semi": false,
"printWidth": 120
"printWidth": 100
},
"trustedDependencies": [
"esbuild",
+5 -3
View File
@@ -7,13 +7,15 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.15.5"
"version": "1.0.24"
},
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode-ai/console-core": "workspace:*",
"@opencode-ai/console-mail": "workspace:*",
"@openauthjs/openauth": "catalog:",
"@kobalte/core": "catalog:",
"@jsx-email/render": "1.1.1",
"@opencode-ai/console-resource": "workspace:*",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
+4 -1
View File
@@ -12,7 +12,10 @@ export default function App() {
root={(props) => (
<MetaProvider>
<Title>opencode</Title>
<Meta name="description" content="opencode - The AI coding agent built for the terminal." />
<Meta
name="description"
content="OpenCode - The AI coding agent built for the terminal."
/>
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,16 @@
<svg width="240" height="300" viewBox="0 0 240 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1401_86283)">
<mask id="mask0_1401_86283" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
<path d="M240 0H0V300H240V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86283)">
<path d="M180 240H60V120H180V240Z" fill="#4B4646"/>
<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#F1ECEC"/>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86283">
<rect width="240" height="300" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,16 @@
<svg width="240" height="300" viewBox="0 0 240 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1401_86274)">
<mask id="mask0_1401_86274" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
<path d="M240 0H0V300H240V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86274)">
<path d="M180 240H60V120H180V240Z" fill="#CFCECD"/>
<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#211E1E"/>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86274">
<rect width="240" height="300" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,30 @@
<svg width="641" height="115" viewBox="0 0 641 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1401_86292)">
<mask id="mask0_1401_86292" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="641" height="115">
<path d="M640.714 0H0V115H640.714V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86292)">
<path d="M49.2868 82.1433H16.4297V49.2861H49.2868V82.1433Z" fill="#4B4646"/>
<path d="M49.2857 32.8573H16.4286V82.143H49.2857V32.8573ZM65.7143 98.5716H0V16.4287H65.7143V98.5716Z" fill="#B7B1B1"/>
<path d="M131.427 82.1433H98.5703V49.2861H131.427V82.1433Z" fill="#4B4646"/>
<path d="M98.5692 82.143H131.426V32.8573H98.5692V82.143ZM147.855 98.5716H98.5692V115H82.1406V16.4287H147.855V98.5716Z" fill="#B7B1B1"/>
<path d="M229.997 65.7139V82.1424H180.711V65.7139H229.997Z" fill="#4B4646"/>
<path d="M230.003 65.7144H180.718V82.143H230.003V98.5716H164.289V16.4287H230.003V65.7144ZM180.718 49.2859H213.575V32.8573H180.718V49.2859Z" fill="#B7B1B1"/>
<path d="M295.717 98.5718H262.859V49.2861H295.717V98.5718Z" fill="#4B4646"/>
<path d="M295.715 32.8573H262.858V98.5716H246.43V16.4287H295.715V32.8573ZM312.144 98.5716H295.715V32.8573H312.144V98.5716Z" fill="#B7B1B1"/>
<path d="M394.286 82.1433H345V49.2861H394.286V82.1433Z" fill="#4B4646"/>
<path d="M394.285 32.8573H344.999V82.143H394.285V98.5716H328.57V16.4287H394.285V32.8573Z" fill="#F1ECEC"/>
<path d="M459.998 82.1433H427.141V49.2861H459.998V82.1433Z" fill="#4B4646"/>
<path d="M459.997 32.8573H427.14V82.143H459.997V32.8573ZM476.425 98.5716H410.711V16.4287H476.425V98.5716Z" fill="#F1ECEC"/>
<path d="M542.146 82.1433H509.289V49.2861H542.146V82.1433Z" fill="#4B4646"/>
<path d="M542.145 32.8571H509.288V82.1429H542.145V32.8571ZM558.574 98.5714H492.859V16.4286H542.145V0H558.574V98.5714Z" fill="#F1ECEC"/>
<path d="M640.715 65.7139V82.1424H591.43V65.7139H640.715Z" fill="#4B4646"/>
<path d="M591.429 32.8573V49.2859H624.286V32.8573H591.429ZM640.714 65.7144H591.429V82.143H640.714V98.5716H575V16.4287H640.714V65.7144Z" fill="#F1ECEC"/>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86292">
<rect width="640.714" height="115" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,30 @@
<svg width="640" height="115" viewBox="0 0 640 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1401_86330)">
<mask id="mask0_1401_86330" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="640" height="115">
<path d="M640 0H0V115H640V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86330)">
<path d="M49.2346 82.1433H16.4141V49.2861H49.2346V82.1433Z" fill="#CFCECD"/>
<path d="M49.2308 32.8573H16.4103V82.143H49.2308V32.8573ZM65.641 98.5716H0V16.4287H65.641V98.5716Z" fill="#656363"/>
<path d="M131.281 82.1433H98.4609V49.2861H131.281V82.1433Z" fill="#CFCECD"/>
<path d="M98.4649 82.143H131.285V32.8573H98.4649V82.143ZM147.696 98.5716H98.4649V115H82.0547V16.4287H147.696V98.5716Z" fill="#656363"/>
<path d="M229.746 65.7139V82.1424H180.516V65.7139H229.746Z" fill="#CFCECD"/>
<path d="M229.743 65.7144H180.512V82.143H229.743V98.5716H164.102V16.4287H229.743V65.7144ZM180.512 49.2859H213.332V32.8573H180.512V49.2859Z" fill="#656363"/>
<path d="M295.383 98.5718H262.562V49.2861H295.383V98.5718Z" fill="#CFCECD"/>
<path d="M295.387 32.8573H262.567V98.5716H246.156V16.4287H295.387V32.8573ZM311.797 98.5716H295.387V32.8573H311.797V98.5716Z" fill="#656363"/>
<path d="M393.848 82.1433H344.617V49.2861H393.848V82.1433Z" fill="#CFCECD"/>
<path d="M393.844 32.8573H344.613V82.143H393.844V98.5716H328.203V16.4287H393.844V32.8573Z" fill="#211E1E"/>
<path d="M459.485 82.1433H426.664V49.2861H459.485V82.1433Z" fill="#CFCECD"/>
<path d="M459.489 32.8573H426.668V82.143H459.489V32.8573ZM475.899 98.5716H410.258V16.4287H475.899V98.5716Z" fill="#211E1E"/>
<path d="M541.539 82.1433H508.719V49.2861H541.539V82.1433Z" fill="#CFCECD"/>
<path d="M541.535 32.8571H508.715V82.1428H541.535V32.8571ZM557.946 98.5714H492.305V16.4286H541.535V0H557.946V98.5714Z" fill="#211E1E"/>
<path d="M639.996 65.7139V82.1424H590.766V65.7139H639.996Z" fill="#CFCECD"/>
<path d="M590.77 32.8573V49.2859H623.59V32.8573H590.77ZM640 65.7144H590.77V82.143H640V98.5716H574.359V16.4287H640V65.7144Z" fill="#211E1E"/>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86330">
<rect width="640" height="115" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,22 @@
<svg width="641" height="115" viewBox="0 0 641 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1401_86315)">
<mask id="mask0_1401_86315" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="641" height="115">
<path d="M640.714 0H0V115H640.714V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86315)">
<path d="M49.2857 32.8573H16.4286V82.143H49.2857V32.8573ZM65.7143 98.5716H0V16.4287H65.7143V98.5716Z" fill="white"/>
<path d="M98.5692 82.143H131.426V32.8573H98.5692V82.143ZM147.855 98.5716H98.5692V115H82.1406V16.4287H147.855V98.5716Z" fill="white"/>
<path d="M230.003 65.7144H180.718V82.143H230.003V98.5716H164.289V16.4287H230.003V65.7144ZM180.718 49.2859H213.575V32.8573H180.718V49.2859Z" fill="white"/>
<path d="M295.715 32.8573H262.858V98.5716H246.43V16.4287H295.715V32.8573ZM312.144 98.5716H295.715V32.8573H312.144V98.5716Z" fill="white"/>
<path d="M394.285 32.8573H344.999V82.143H394.285V98.5716H328.57V16.4287H394.285V32.8573Z" fill="white"/>
<path d="M459.997 32.8573H427.14V82.143H459.997V32.8573ZM476.425 98.5716H410.711V16.4287H476.425V98.5716Z" fill="white"/>
<path d="M542.145 32.8571H509.288V82.1429H542.145V32.8571ZM558.574 98.5714H492.859V16.4286H542.145V0H558.574V98.5714Z" fill="white"/>
<path d="M591.429 32.8573V49.2859H624.286V32.8573H591.429ZM640.714 65.7144H591.429V82.143H640.714V98.5716H575V16.4287H640.714V65.7144Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86315">
<rect width="640.714" height="115" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,22 @@
<svg width="640" height="115" viewBox="0 0 640 115" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1401_86353)">
<mask id="mask0_1401_86353" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="640" height="115">
<path d="M640 0H0V115H640V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1401_86353)">
<path d="M49.2308 32.8573H16.4103V82.143H49.2308V32.8573ZM65.641 98.5716H0V16.4287H65.641V98.5716Z" fill="black"/>
<path d="M98.4649 82.143H131.285V32.8573H98.4649V82.143ZM147.696 98.5716H98.4649V115H82.0547V16.4287H147.696V98.5716Z" fill="black"/>
<path d="M229.743 65.7144H180.512V82.143H229.743V98.5716H164.102V16.4287H229.743V65.7144ZM180.512 49.2859H213.332V32.8573H180.512V49.2859Z" fill="black"/>
<path d="M295.387 32.8573H262.567V98.5716H246.156V16.4287H295.387V32.8573ZM311.797 98.5716H295.387V32.8573H311.797V98.5716Z" fill="black"/>
<path d="M393.844 32.8573H344.613V82.143H393.844V98.5716H328.203V16.4287H393.844V32.8573Z" fill="black"/>
<path d="M459.489 32.8573H426.668V82.143H459.489V32.8573ZM475.899 98.5716H410.258V16.4287H475.899V98.5716Z" fill="black"/>
<path d="M541.535 32.8571H508.715V82.1428H541.535V32.8571ZM557.946 98.5714H492.305V16.4286H541.535V0H557.946V98.5714Z" fill="black"/>
<path d="M590.77 32.8573V49.2859H623.59V32.8573H590.77ZM640 65.7144H590.77V82.143H640V98.5716H574.359V16.4287H640V65.7144Z" fill="black"/>
</g>
</g>
<defs>
<clipPath id="clip0_1401_86353">
<rect width="640" height="115" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,10 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 7H7V19H15V7ZM19 23H3V3H19V23Z" fill="url(#paint0_linear_1311_94922)" stroke="#F1ECEC"/>
<path d="M3 0V26M19 0V26M15 0V26M7 0V26M0 3H22M0 7H22M0 19H22M0 23H22" stroke="#4B4646" stroke-opacity="0.4"/>
<defs>
<linearGradient id="paint0_linear_1311_94922" x1="11" y1="3" x2="11" y2="23" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B1818"/>
<stop offset="1" stop-color="#2D2828"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 526 B

@@ -0,0 +1,10 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 7H7V19H15V7ZM19 23H3V3H19V23Z" fill="url(#paint0_linear_1311_94913)" stroke="#8E8B8B"/>
<path d="M3 0V26M19 0V26M15 0V26M7 0V26M0 3H22M0 7H22M0 19H22M0 23H22" stroke="#110000" stroke-opacity="0.121569"/>
<defs>
<linearGradient id="paint0_linear_1311_94913" x1="11" y1="3" x2="11" y2="23" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9F8F8"/>
<stop offset="1" stop-color="#E9E8E8"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,11 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1311_94916)">
<path d="M15 19H7V11H15V19Z" fill="#4B4646"/>
<path d="M15 7H7V19H15V7ZM19 23H3V3H19V23Z" fill="#F1ECEC"/>
</g>
<defs>
<clipPath id="clip0_1311_94916">
<rect width="16" height="20" fill="white" transform="translate(3 3)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 385 B

@@ -0,0 +1,11 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1311_94907)">
<path d="M15 19H7V11H15V19Z" fill="#BCBBBB"/>
<path d="M15 7H7V19H15V7ZM19 23H3V3H19V23Z" fill="#211E1E"/>
</g>
<defs>
<clipPath id="clip0_1311_94907">
<rect width="16" height="20" fill="white" transform="translate(3 3)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 385 B

@@ -0,0 +1,11 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1311_94973)">
<path d="M24 32H8V16H24V32Z" fill="#4B4646"/>
<path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#F1ECEC"/>
</g>
<defs>
<clipPath id="clip0_1311_94973">
<rect width="32" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 358 B

@@ -0,0 +1,11 @@
<svg width="32" height="40" viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1311_94969)">
<path d="M24 32H8V16H24V32Z" fill="#BCBBBB"/>
<path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#211E1E"/>
</g>
<defs>
<clipPath id="clip0_1311_94969">
<rect width="32" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 358 B

@@ -0,0 +1,25 @@
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1311_95032)">
<path d="M18 30H6V18H18V30Z" fill="#4B4646"/>
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#B7B1B1"/>
<path d="M48 30H36V18H48V30Z" fill="#4B4646"/>
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#B7B1B1"/>
<path d="M84 24V30H66V24H84Z" fill="#4B4646"/>
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#B7B1B1"/>
<path d="M108 36H96V18H108V36Z" fill="#4B4646"/>
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#B7B1B1"/>
<path d="M144 30H126V18H144V30Z" fill="#4B4646"/>
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#F1ECEC"/>
<path d="M168 30H156V18H168V30Z" fill="#4B4646"/>
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#F1ECEC"/>
<path d="M198 30H186V18H198V30Z" fill="#4B4646"/>
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#F1ECEC"/>
<path d="M234 24V30H216V24H234Z" fill="#4B4646"/>
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#F1ECEC"/>
</g>
<defs>
<clipPath id="clip0_1311_95032">
<rect width="234" height="42" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,25 @@
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1311_95049)">
<path d="M18 30H6V18H18V30Z" fill="#CFCECD"/>
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#656363"/>
<path d="M48 30H36V18H48V30Z" fill="#CFCECD"/>
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#656363"/>
<path d="M84 24V30H66V24H84Z" fill="#CFCECD"/>
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#656363"/>
<path d="M108 36H96V18H108V36Z" fill="#CFCECD"/>
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#656363"/>
<path d="M144 30H126V18H144V30Z" fill="#CFCECD"/>
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#211E1E"/>
<path d="M168 30H156V18H168V30Z" fill="#CFCECD"/>
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#211E1E"/>
<path d="M198 30H186V18H198V30Z" fill="#CFCECD"/>
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#211E1E"/>
<path d="M234 24V30H216V24H234Z" fill="#CFCECD"/>
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#211E1E"/>
</g>
<defs>
<clipPath id="clip0_1311_95049">
<rect width="234" height="42" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.33203 7.99967V6.33301H10.9987M17.6654 7.99967V6.33301H10.9987M10.9987 6.33301V19.6663M10.9987 19.6663H9.33203M10.9987 19.6663H12.6654" stroke="#F1ECEC" stroke-width="2" stroke-linecap="square"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

@@ -0,0 +1,3 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.33203 7.99967V6.33301H10.9987M17.6654 7.99967V6.33301H10.9987M10.9987 6.33301V19.6663M10.9987 19.6663H9.33203M10.9987 19.6663H12.6654" stroke="black" stroke-width="2" stroke-linecap="square"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

@@ -1,6 +1,7 @@
import { createAsync } from "@solidjs/router"
import { createMemo } from "solid-js"
import { github } from "~/lib/github"
import { config } from "~/config"
export function Footer() {
const githubData = createAsync(() => github())
@@ -10,13 +11,13 @@ export function Footer() {
notation: "compact",
compactDisplay: "short",
}).format(githubData()!.stars!)
: "25K",
: config.github.starsFormatted.compact,
)
return (
<footer data-component="footer">
<div data-slot="cell">
<a href="https://github.com/sst/opencode" target="_blank">
<a href={config.github.repoUrl} target="_blank">
GitHub <span>[{starCount()}]</span>
</a>
</div>
@@ -27,7 +28,7 @@ export function Footer() {
<a href="/discord">Discord</a>
</div>
<div data-slot="cell">
<a href="https://x.com/opencode">X</a>
<a href={config.social.twitter}>X</a>
</div>
</footer>
)
@@ -0,0 +1,63 @@
.context-menu {
position: fixed;
z-index: 1000;
min-width: 160px;
border-radius: 8px;
background-color: var(--color-background);
box-shadow:
0 0 0 1px rgba(19, 16, 16, 0.08),
0 6px 8px -4px rgba(19, 16, 16, 0.12),
0 4px 3px -2px rgba(19, 16, 16, 0.12),
0 1px 2px -1px rgba(19, 16, 16, 0.12);
padding: 6px;
@media (prefers-color-scheme: dark) {
box-shadow: 0 0 0 1px rgba(247, 237, 237, 0.1);
}
}
.context-menu-item {
display: flex;
gap: 12px;
width: 100%;
padding: 8px 16px 8px 8px;
font-weight: 500;
cursor: pointer;
background: none;
border: none;
align-items: center;
color: var(--color-text);
font-size: var(--font-size-sm);
text-align: left;
border-radius: 2px;
transition: background-color 0.2s ease;
[data-slot="copy dark"] {
display: none;
}
@media (prefers-color-scheme: dark) {
[data-slot="copy light"] {
display: none;
}
[data-slot="copy dark"] {
display: block;
}
}
&:hover {
background-color: var(--color-background-weak-hover);
color: var(--color-text-strong);
}
img {
width: 22px;
height: 26px;
}
}
.context-menu-divider {
border: none;
border-top: 1px solid var(--color-border);
margin: var(--space-1) 0;
}
+136 -12
View File
@@ -1,47 +1,168 @@
import logoLight from "../asset/logo-ornate-light.svg"
import logoDark from "../asset/logo-ornate-dark.svg"
import { A, createAsync } from "@solidjs/router"
import copyLogoLight from "../asset/lander/logo-light.svg"
import copyLogoDark from "../asset/lander/logo-dark.svg"
import copyWordmarkLight from "../asset/lander/wordmark-light.svg"
import copyWordmarkDark from "../asset/lander/wordmark-dark.svg"
import copyBrandAssetsLight from "../asset/lander/brand-assets-light.svg"
import copyBrandAssetsDark from "../asset/lander/brand-assets-dark.svg"
// SVG files for copying (separate from button icons)
// Replace these with your actual SVG files for copying
import copyLogoSvgLight from "../asset/lander/opencode-logo-light.svg"
import copyLogoSvgDark from "../asset/lander/opencode-logo-dark.svg"
import copyWordmarkSvgLight from "../asset/lander/opencode-wordmark-light.svg"
import copyWordmarkSvgDark from "../asset/lander/opencode-wordmark-dark.svg"
import { A, createAsync, useNavigate } from "@solidjs/router"
import { createMemo, Match, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { github } from "~/lib/github"
import { queryIsLoggedIn } from "~/routes/workspace/common"
import { createEffect, onCleanup } from "solid-js"
import { config } from "~/config"
import "./header-context-menu.css"
const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches
const fetchSvgContent = async (svgPath: string): Promise<string> => {
try {
const response = await fetch(svgPath)
const svgText = await response.text()
return svgText
} catch (err) {
console.error("Failed to fetch SVG content:", err)
throw err
}
}
export function Header(props: { zen?: boolean }) {
const navigate = useNavigate()
const githubData = createAsync(() => github())
const isLoggedIn = createAsync(() => queryIsLoggedIn())
const starCount = createMemo(() =>
githubData()?.stars
? new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(githubData()?.stars!)
: "25K",
: config.github.starsFormatted.compact,
)
const [store, setStore] = createStore({
mobileMenuOpen: false,
contextMenuOpen: false,
contextMenuPosition: { x: 0, y: 0 },
})
createEffect(() => {
const handleClickOutside = () => {
setStore("contextMenuOpen", false)
}
const handleContextMenu = (event: MouseEvent) => {
event.preventDefault()
setStore("contextMenuOpen", false)
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setStore("contextMenuOpen", false)
}
}
if (store.contextMenuOpen) {
document.addEventListener("click", handleClickOutside)
document.addEventListener("contextmenu", handleContextMenu)
document.addEventListener("keydown", handleKeyDown)
onCleanup(() => {
document.removeEventListener("click", handleClickOutside)
document.removeEventListener("contextmenu", handleContextMenu)
document.removeEventListener("keydown", handleKeyDown)
})
}
})
const handleLogoContextMenu = (event: MouseEvent) => {
event.preventDefault()
const logoElement = (event.currentTarget as HTMLElement).querySelector("a")
if (logoElement) {
const rect = logoElement.getBoundingClientRect()
setStore("contextMenuPosition", {
x: rect.left - 16,
y: rect.bottom + 8,
})
}
setStore("contextMenuOpen", true)
}
const copyWordmarkToClipboard = async () => {
try {
const isDark = isDarkMode()
const wordmarkSvgPath = isDark ? copyWordmarkSvgDark : copyWordmarkSvgLight
const wordmarkSvg = await fetchSvgContent(wordmarkSvgPath)
await navigator.clipboard.writeText(wordmarkSvg)
} catch (err) {
console.error("Failed to copy wordmark to clipboard:", err)
}
}
const copyLogoToClipboard = async () => {
try {
const isDark = isDarkMode()
const logoSvgPath = isDark ? copyLogoSvgDark : copyLogoSvgLight
const logoSvg = await fetchSvgContent(logoSvgPath)
await navigator.clipboard.writeText(logoSvg)
} catch (err) {
console.error("Failed to copy logo to clipboard:", err)
}
}
return (
<section data-component="top">
<A href="/">
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
</A>
<div onContextMenu={handleLogoContextMenu}>
<A href="/">
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
</A>
</div>
<Show when={store.contextMenuOpen}>
<div
class="context-menu"
style={`left: ${store.contextMenuPosition.x}px; top: ${store.contextMenuPosition.y}px;`}
>
<button class="context-menu-item" onClick={copyLogoToClipboard}>
<img data-slot="copy light" src={copyLogoLight} alt="Logo" />
<img data-slot="copy dark" src={copyLogoDark} alt="Logo" />
Copy logo as SVG
</button>
<button class="context-menu-item" onClick={copyWordmarkToClipboard}>
<img data-slot="copy light" src={copyWordmarkLight} alt="Wordmark" />
<img data-slot="copy dark" src={copyWordmarkDark} alt="Wordmark" />
Copy wordmark as SVG
</button>
<button class="context-menu-item" onClick={() => navigate("/brand")}>
<img data-slot="copy light" src={copyBrandAssetsLight} alt="Brand Assets" />
<img data-slot="copy dark" src={copyBrandAssetsDark} alt="Brand Assets" />
Brand assets
</button>
</div>
</Show>
<nav data-component="nav-desktop">
<ul>
<li>
<a href="https://github.com/sst/opencode" target="_blank">
<a href={config.github.repoUrl} target="_blank">
GitHub <span>[{starCount()}]</span>
</a>
</li>
<li>
<a href="/docs">Docs</a>
</li>
<li>
<A href="/enterprise">Enterprise</A>
</li>
<li>
<Switch>
<Match when={props.zen}>
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
<a href="/auth">Login</a>
</Match>
<Match when={!props.zen}>
<A href="/zen">Zen</A>
@@ -102,17 +223,20 @@ export function Header(props: { zen?: boolean }) {
<A href="/">Home</A>
</li>
<li>
<a href="https://github.com/sst/opencode" target="_blank">
<a href={config.github.repoUrl} target="_blank">
GitHub <span>[{starCount()}]</span>
</a>
</li>
<li>
<a href="/docs">Docs</a>
</li>
<li>
<A href="/enterprise">Enterprise</A>
</li>
<li>
<Switch>
<Match when={props.zen}>
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
<a href="/auth">Login</a>
</Match>
<Match when={!props.zen}>
<A href="/zen">Zen</A>
@@ -100,6 +100,17 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconStripe(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M15.827 12.506c0 .672-.31 1.175-.771 1.175-.293 0-.468-.106-.589-.237l-.007-1.855c.13-.143.31-.247.596-.247.456-.001.771.51.771 1.164zm3.36-1.253c-.312 0-.659.236-.659.798h1.291c0-.562-.325-.798-.632-.798zm4.813-5.253v12c0 1.104-.896 2-2 2h-20c-1.104 0-2-.896-2-2v-12c0-1.104.896-2 2-2h20c1.104 0 2 .896 2 2zm-17.829 7.372c0-1.489-1.909-1.222-1.909-1.784 0-.195.162-.271.424-.271.38 0 .862.116 1.242.321v-1.176c-.414-.165-.827-.228-1.241-.228-1.012.001-1.687.53-1.687 1.414 0 1.382 1.898 1.158 1.898 1.754 0 .231-.201.305-.479.305-.414 0-.947-.171-1.366-.399v1.192c.464.2.935.283 1.365.283 1.038.001 1.753-.512 1.753-1.411zm2.422-3.054h-.949l-.001-1.084-1.219.259-.005 4.006c0 .739.556 1.285 1.297 1.285.408 0 .71-.074.876-.165v-1.016c-.16.064-.948.293-.948-.443v-1.776h.948v-1.066zm2.596 0c-.166-.06-.75-.169-1.042.369l-.078-.369h-1.079v4.377h1.248v-2.967c.295-.388.793-.313.952-.262v-1.148zm1.554 0h-1.253v4.377h1.253v-4.377zm0-1.664l-1.253.266v1.017l1.253-.266v-1.017zm4.314 3.824c0-1.454-.826-2.244-1.703-2.243-.489 0-.805.23-.978.392l-.065-.309h-1.099v5.828l1.249-.265.003-1.413c.179.131.446.316.883.316.893 0 1.71-.719 1.71-2.306zm3.943.045c0-1.279-.619-2.288-1.805-2.288-1.188 0-1.911 1.01-1.911 2.281 0 1.506.852 2.267 2.068 2.267.597 0 1.045-.136 1.384-.324v-1.006c-.34.172-.731.276-1.227.276-.487 0-.915-.172-.971-.758h2.444l.018-.448z"
/>
</svg>
)
}
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1,9 +1,14 @@
import { A } from "@solidjs/router"
export function Legal() {
return (
<div data-component="legal">
<span>
©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
</span>
<span>
<A href="/brand">Brand</A>
</span>
</div>
)
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Application-wide constants and configuration
*/
export const config = {
// GitHub
github: {
repoUrl: "https://github.com/sst/opencode",
starsFormatted: {
compact: "30K",
full: "30,000",
},
},
// Social links
social: {
twitter: "https://x.com/opencode",
discord: "https://discord.gg/opencode",
},
// Static stats (used on landing page)
stats: {
contributors: "250",
commits: "3,500",
monthlyUsers: "300,000",
},
} as const
+8 -2
View File
@@ -1,5 +1,5 @@
import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { and, Database, eq, inArray, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { redirect } from "@solidjs/router"
import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -56,7 +56,13 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
tx
.select()
.from(UserTable)
.where(and(eq(UserTable.workspaceID, workspace), inArray(UserTable.accountID, accounts)))
.where(
and(
eq(UserTable.workspaceID, workspace),
isNull(UserTable.timeDeleted),
inArray(UserTable.accountID, accounts),
),
)
.limit(1)
.execute()
.then((x) => x[0]),
+8 -3
View File
@@ -1,4 +1,5 @@
import { query } from "@solidjs/router"
import { config } from "~/config"
export const github = query(async () => {
"use server"
@@ -6,11 +7,15 @@ export const github = query(async () => {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
}
const apiBaseUrl = config.github.repoUrl.replace(
"https://github.com/",
"https://api.github.com/repos/",
)
try {
const [meta, releases, contributors] = await Promise.all([
fetch("https://api.github.com/repos/sst/opencode", { headers }).then((res) => res.json()),
fetch("https://api.github.com/repos/sst/opencode/releases", { headers }).then((res) => res.json()),
fetch("https://api.github.com/repos/sst/opencode/contributors?per_page=1", { headers }),
fetch(apiBaseUrl, { headers }).then((res) => res.json()),
fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
])
const [release] = releases
const contributorCount = Number.parseInt(
@@ -0,0 +1,46 @@
import type { APIEvent } from "@solidjs/start/server"
import { AWS } from "@opencode-ai/console-core/aws.js"
interface EnterpriseFormData {
name: string
role: string
email: string
message: string
}
export async function POST(event: APIEvent) {
try {
const body = (await event.request.json()) as EnterpriseFormData
// Validate required fields
if (!body.name || !body.role || !body.email || !body.message) {
return Response.json({ error: "All fields are required" }, { status: 400 })
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(body.email)) {
return Response.json({ error: "Invalid email format" }, { status: 400 })
}
// Create email content
const emailContent = `
${body.message}<br><br>
--<br>
${body.name}<br>
${body.role}<br>
${body.email}`.trim()
// Send email using AWS SES
await AWS.sendEmail({
to: "contact@anoma.ly",
subject: `Enterprise Inquiry from ${body.name}`,
body: emailContent,
})
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
} catch (error) {
console.error("Error processing enterprise form:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
}
}
+3 -22
View File
@@ -1,30 +1,11 @@
import { Actor } from "@opencode-ai/console-core/actor.js"
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { redirect } from "@solidjs/router"
import type { APIEvent } from "@solidjs/start/server"
import { withActor } from "~/context/auth.withActor"
import { getLastSeenWorkspaceID } from "../workspace/common"
export async function GET(input: APIEvent) {
try {
const workspaces = await withActor(async () => {
const actor = Actor.assert("account")
return Database.transaction(async (tx) =>
tx
.select({ id: WorkspaceTable.id })
.from(UserTable)
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(
and(
eq(UserTable.accountID, actor.properties.accountID),
isNull(UserTable.timeDeleted),
isNull(WorkspaceTable.timeDeleted),
),
),
)
})
return redirect(`/workspace/${workspaces[0].id}`)
const workspaceID = await getLastSeenWorkspaceID()
return redirect(`/workspace/${workspaceID}`)
} catch {
return redirect("/auth/authorize")
}
@@ -0,0 +1,529 @@
::selection {
background: var(--color-background-interactive);
color: var(--color-text-strong);
@media (prefers-color-scheme: dark) {
background: var(--color-background-interactive);
color: var(--color-text-inverted);
}
}
[data-page="enterprise"] {
--color-background: hsl(0, 20%, 99%);
--color-background-weak: hsl(0, 8%, 97%);
--color-background-weak-hover: hsl(0, 8%, 94%);
--color-background-strong: hsl(0, 5%, 12%);
--color-background-strong-hover: hsl(0, 5%, 18%);
--color-background-interactive: hsl(62, 84%, 88%);
--color-background-interactive-weaker: hsl(64, 74%, 95%);
--color-text: hsl(0, 1%, 39%);
--color-text-weak: hsl(0, 1%, 60%);
--color-text-weaker: hsl(30, 2%, 81%);
--color-text-strong: hsl(0, 5%, 12%);
--color-text-inverted: hsl(0, 20%, 99%);
--color-text-success: hsl(119, 100%, 35%);
--color-border: hsl(30, 2%, 81%);
--color-border-weak: hsl(0, 1%, 85%);
--color-icon: hsl(0, 1%, 55%);
--color-success: hsl(142, 76%, 36%);
background: var(--color-background);
font-family: var(--font-mono);
color: var(--color-text);
padding-bottom: 5rem;
@media (prefers-color-scheme: dark) {
--color-background: hsl(0, 9%, 7%);
--color-background-weak: hsl(0, 6%, 10%);
--color-background-weak-hover: hsl(0, 6%, 15%);
--color-background-strong: hsl(0, 15%, 94%);
--color-background-strong-hover: hsl(0, 15%, 97%);
--color-background-interactive: hsl(62, 100%, 90%);
--color-background-interactive-weaker: hsl(60, 20%, 8%);
--color-text: hsl(0, 4%, 71%);
--color-text-weak: hsl(0, 2%, 49%);
--color-text-weaker: hsl(0, 3%, 28%);
--color-text-strong: hsl(0, 15%, 94%);
--color-text-inverted: hsl(0, 9%, 7%);
--color-text-success: hsl(119, 60%, 72%);
--color-border: hsl(0, 3%, 28%);
--color-border-weak: hsl(0, 4%, 23%);
--color-icon: hsl(10, 3%, 43%);
--color-success: hsl(142, 76%, 46%);
}
/* Header and Footer styles - copied from index.css */
[data-component="top"] {
padding: 24px 5rem;
height: 80px;
position: sticky;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-background);
border-bottom: 1px solid var(--color-border-weak);
z-index: 10;
@media (max-width: 60rem) {
padding: 24px 1.5rem;
}
img {
height: 34px;
width: auto;
}
[data-component="nav-desktop"] {
ul {
display: flex;
justify-content: space-between;
gap: 48px;
li {
display: inline-block;
a {
text-decoration: none;
span {
color: var(--color-text-weak);
}
}
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
}
@media (max-width: 40rem) {
display: none;
}
}
[data-component="nav-mobile"] {
button > svg {
color: var(--color-icon);
}
}
[data-component="nav-mobile-toggle"] {
border: none;
background: none;
outline: none;
height: 40px;
width: 40px;
cursor: pointer;
margin-right: -8px;
}
[data-component="nav-mobile-toggle"]:hover {
background: var(--color-background-weak);
}
[data-component="nav-mobile"] {
display: none;
@media (max-width: 40rem) {
display: block;
[data-component="nav-mobile-icon"] {
cursor: pointer;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
[data-component="nav-mobile-menu-list"] {
position: fixed;
background: var(--color-background);
top: 80px;
left: 0;
right: 0;
height: 100vh;
ul {
list-style: none;
padding: 20px 0;
li {
a {
text-decoration: none;
padding: 20px;
display: block;
span {
color: var(--color-text-weak);
}
}
a:hover {
background: var(--color-background-weak);
}
}
}
}
}
}
[data-slot="logo dark"] {
display: none;
}
@media (prefers-color-scheme: dark) {
[data-slot="logo light"] {
display: none;
}
[data-slot="logo dark"] {
display: block;
}
}
}
[data-component="footer"] {
border-top: 1px solid var(--color-border-weak);
display: flex;
flex-direction: row;
@media (max-width: 65rem) {
border-bottom: 1px solid var(--color-border-weak);
}
[data-slot="cell"] {
flex: 1;
text-align: center;
a {
text-decoration: none;
padding: 2rem 0;
width: 100%;
display: block;
span {
color: var(--color-text-weak);
@media (max-width: 40rem) {
display: none;
}
}
}
a:hover {
background: var(--color-background-weak);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
[data-slot="cell"] + [data-slot="cell"] {
border-left: 1px solid var(--color-border-weak);
@media (max-width: 40rem) {
border-left: none;
}
}
/* Mobile: third column on its own row */
@media (max-width: 25rem) {
flex-wrap: wrap;
[data-slot="cell"] {
flex: 1 0 100%;
border-left: none;
border-top: 1px solid var(--color-border-weak);
}
[data-slot="cell"]:nth-child(1) {
border-top: none;
}
}
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
border: 1px solid var(--color-border-weak);
border-top: none;
@media (max-width: 65rem) {
border: none;
}
}
[data-component="content"] {
}
[data-component="brand-content"] {
padding: 4rem 5rem;
h2 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 1rem;
}
h3 {
font-size: 1.25rem;
font-weight: 500;
color: var(--color-text-strong);
margin: 2rem 0 1rem 0;
}
p {
line-height: 1.6;
margin-bottom: 2.5rem;
color: var(--color-text);
}
[data-component="download-button"] {
padding: 8px 12px 8px 20px;
background: var(--color-background-strong);
color: var(--color-text-inverted);
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
display: flex;
width: fit-content;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
text-decoration: none;
&:hover:not(:disabled) {
background: var(--color-background-strong-hover);
}
&:active {
transform: scale(0.98);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
[data-component="brand-grid"] {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 4rem;
margin-bottom: 2rem;
}
[data-component="brand-grid"] img {
width: 100%;
height: auto;
display: block;
border-radius: 4px;
border: 1px solid var(--color-border-weak);
}
[data-component="brand-grid"] > div {
position: relative;
}
[data-component="actions"] {
position: absolute;
background: rgba(4, 0, 0, 0.08);
border-radius: 4px;
bottom: 0;
right: 0;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
opacity: 0;
transition: opacity 0.2s ease;
@media (max-width: 40rem) {
position: static;
opacity: 1;
background: none;
margin-top: 1rem;
justify-content: start;
}
}
[data-component="brand-grid"] > div:hover [data-component="actions"] {
opacity: 1;
@media (max-width: 40rem) {
opacity: 1;
}
}
[data-component="actions"] button {
padding: 6px 12px;
background: var(--color-background);
color: var(--color-text);
border: none;
border-radius: 4px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s ease;
cursor: pointer;
box-shadow:
0 0 0 1px rgba(19, 16, 16, 0.08),
0 6px 8px -4px rgba(19, 16, 16, 0.12),
0 4px 3px -2px rgba(19, 16, 16, 0.12),
0 1px 2px -1px rgba(19, 16, 16, 0.12);
@media (max-width: 40rem) {
box-shadow:
0 0 0 1px rgba(19, 16, 16, 0.16)
}
&:hover {
background: var(--color-background);
}
&:active {
transform: scale(0.98);
box-shadow: 0 0 0 1px rgba(19, 16, 16, 0.08), 0 6px 8px -8px rgba(19, 16, 16, 0.50);
}
}
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
}
[data-component="faq"] {
border-top: 1px solid var(--color-border-weak);
padding: 4rem 5rem;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
[data-slot="section-title"] {
margin-bottom: 24px;
h3 {
font-size: 16px;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 12px;
}
p {
margin-bottom: 12px;
color: var(--color-text);
}
}
ul {
padding: 0;
li {
list-style: none;
margin-bottom: 24px;
line-height: 200%;
@media (max-width: 60rem) {
line-height: 180%;
}
}
}
[data-slot="faq-question"] {
display: flex;
gap: 16px;
margin-bottom: 8px;
color: var(--color-text-strong);
font-weight: 500;
cursor: pointer;
background: none;
border: none;
padding: 0;
[data-slot="faq-icon-plus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: block;
}
[data-expanded] & {
display: none;
}
}
[data-slot="faq-icon-minus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: none;
}
[data-expanded] & {
display: block;
}
}
[data-slot="faq-question-text"] {
flex-grow: 1;
text-align: left;
}
}
[data-slot="faq-answer"] {
margin-left: 40px;
margin-bottom: 32px;
color: var(--color-text);
}
}
[data-component="legal"] {
color: var(--color-text-weak);
text-align: center;
padding: 2rem 5rem;
display: flex;
gap: 32px;
justify-content: center;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
a {
color: var(--color-text-weak);
text-decoration: none;
}
a:hover {
color: var(--color-text);
text-decoration: underline;
}
}
a {
color: var(--color-text-strong);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
&:hover {
text-decoration-thickness: 2px;
}
}
}
@@ -0,0 +1,352 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png"
import previewLogoDark from "../../asset/brand/preview-opencode-logo-dark.png"
import previewWordmarkLight from "../../asset/brand/preview-opencode-wordmark-light.png"
import previewWordmarkDark from "../../asset/brand/preview-opencode-wordmark-dark.png"
import previewWordmarkSimpleLight from "../../asset/brand/preview-opencode-wordmark-simple-light.png"
import previewWordmarkSimpleDark from "../../asset/brand/preview-opencode-wordmark-simple-dark.png"
import logoLightPng from "../../asset/brand/opencode-logo-light.png"
import logoDarkPng from "../../asset/brand/opencode-logo-dark.png"
import wordmarkLightPng from "../../asset/brand/opencode-wordmark-light.png"
import wordmarkDarkPng from "../../asset/brand/opencode-wordmark-dark.png"
import wordmarkSimpleLightPng from "../../asset/brand/opencode-wordmark-simple-light.png"
import wordmarkSimpleDarkPng from "../../asset/brand/opencode-wordmark-simple-dark.png"
import logoLightSvg from "../../asset/brand/opencode-logo-light.svg"
import logoDarkSvg from "../../asset/brand/opencode-logo-dark.svg"
import wordmarkLightSvg from "../../asset/brand/opencode-wordmark-light.svg"
import wordmarkDarkSvg from "../../asset/brand/opencode-wordmark-dark.svg"
import wordmarkSimpleLightSvg from "../../asset/brand/opencode-wordmark-simple-light.svg"
import wordmarkSimpleDarkSvg from "../../asset/brand/opencode-wordmark-simple-dark.svg"
const brandAssets = "/opencode-brand-assets.zip"
export default function Brand() {
const downloadFile = async (url: string, filename: string) => {
try {
const response = await fetch(url)
const blob = await response.blob()
const blobUrl = window.URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = blobUrl
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(blobUrl)
} catch (error) {
console.error("Download failed:", error)
const link = document.createElement("a")
link.href = url
link.target = "_blank"
link.rel = "noopener noreferrer"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}
return (
<main data-page="enterprise">
<Title>OpenCode | Brand</Title>
<Meta name="description" content="OpenCode brand guidelines" />
<div data-component="container">
<Header />
<div data-component="content">
<section data-component="brand-content">
<h2>Brand guidelines</h2>
<p>Resources and assets to help you work with the OpenCode brand.</p>
<button
data-component="download-button"
onClick={() => downloadFile(brandAssets, "opencode-brand-assets.zip")}
>
Download all assets
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<div data-component="brand-grid">
<div>
<img src={previewLogoLight} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button onClick={() => downloadFile(logoLightSvg, "opencode-logo-light.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
<div>
<img src={previewLogoDark} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button onClick={() => downloadFile(logoDarkSvg, "opencode-logo-dark.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
<div>
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}
>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button
onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}
>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
<div>
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}
>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button
onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}
>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
<div>
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() =>
downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")
}
>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button
onClick={() =>
downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")
}
>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
<div>
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() =>
downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")
}
>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
<button
onClick={() =>
downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")
}
>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</button>
</div>
</div>
</div>
</section>
</div>
<Footer />
</div>
<Legal />
</main>
)
}
@@ -0,0 +1,550 @@
::selection {
background: var(--color-background-interactive);
color: var(--color-text-strong);
@media (prefers-color-scheme: dark) {
background: var(--color-background-interactive);
color: var(--color-text-inverted);
}
}
[data-page="enterprise"] {
--color-background: hsl(0, 20%, 99%);
--color-background-weak: hsl(0, 8%, 97%);
--color-background-weak-hover: hsl(0, 8%, 94%);
--color-background-strong: hsl(0, 5%, 12%);
--color-background-strong-hover: hsl(0, 5%, 18%);
--color-background-interactive: hsl(62, 84%, 88%);
--color-background-interactive-weaker: hsl(64, 74%, 95%);
--color-text: hsl(0, 1%, 39%);
--color-text-weak: hsl(0, 1%, 60%);
--color-text-weaker: hsl(30, 2%, 81%);
--color-text-strong: hsl(0, 5%, 12%);
--color-text-inverted: hsl(0, 20%, 99%);
--color-text-success: hsl(119, 100%, 35%);
--color-border: hsl(30, 2%, 81%);
--color-border-weak: hsl(0, 1%, 85%);
--color-icon: hsl(0, 1%, 55%);
--color-success: hsl(142, 76%, 36%);
background: var(--color-background);
font-family: var(--font-mono);
color: var(--color-text);
padding-bottom: 5rem;
@media (prefers-color-scheme: dark) {
--color-background: hsl(0, 9%, 7%);
--color-background-weak: hsl(0, 6%, 10%);
--color-background-weak-hover: hsl(0, 6%, 15%);
--color-background-strong: hsl(0, 15%, 94%);
--color-background-strong-hover: hsl(0, 15%, 97%);
--color-background-interactive: hsl(62, 100%, 90%);
--color-background-interactive-weaker: hsl(60, 20%, 8%);
--color-text: hsl(0, 4%, 71%);
--color-text-weak: hsl(0, 2%, 49%);
--color-text-weaker: hsl(0, 3%, 28%);
--color-text-strong: hsl(0, 15%, 94%);
--color-text-inverted: hsl(0, 9%, 7%);
--color-text-success: hsl(119, 60%, 72%);
--color-border: hsl(0, 3%, 28%);
--color-border-weak: hsl(0, 4%, 23%);
--color-icon: hsl(10, 3%, 43%);
--color-success: hsl(142, 76%, 46%);
}
/* Header and Footer styles - copied from index.css */
[data-component="top"] {
padding: 24px 5rem;
height: 80px;
position: sticky;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-background);
border-bottom: 1px solid var(--color-border-weak);
z-index: 10;
@media (max-width: 60rem) {
padding: 24px 1.5rem;
}
img {
height: 34px;
width: auto;
}
[data-component="nav-desktop"] {
ul {
display: flex;
justify-content: space-between;
gap: 48px;
li {
display: inline-block;
a {
text-decoration: none;
span {
color: var(--color-text-weak);
}
}
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
}
@media (max-width: 40rem) {
display: none;
}
}
[data-component="nav-mobile"] {
button > svg {
color: var(--color-icon);
}
}
[data-component="nav-mobile-toggle"] {
border: none;
background: none;
outline: none;
height: 40px;
width: 40px;
cursor: pointer;
margin-right: -8px;
}
[data-component="nav-mobile-toggle"]:hover {
background: var(--color-background-weak);
}
[data-component="nav-mobile"] {
display: none;
@media (max-width: 40rem) {
display: block;
[data-component="nav-mobile-icon"] {
cursor: pointer;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
[data-component="nav-mobile-menu-list"] {
position: fixed;
background: var(--color-background);
top: 80px;
left: 0;
right: 0;
height: 100vh;
ul {
list-style: none;
padding: 20px 0;
li {
a {
text-decoration: none;
padding: 20px;
display: block;
span {
color: var(--color-text-weak);
}
}
a:hover {
background: var(--color-background-weak);
}
}
}
}
}
}
[data-slot="logo dark"] {
display: none;
}
@media (prefers-color-scheme: dark) {
[data-slot="logo light"] {
display: none;
}
[data-slot="logo dark"] {
display: block;
}
}
}
[data-component="footer"] {
border-top: 1px solid var(--color-border-weak);
display: flex;
flex-direction: row;
@media (max-width: 65rem) {
border-bottom: 1px solid var(--color-border-weak);
}
[data-slot="cell"] {
flex: 1;
text-align: center;
a {
text-decoration: none;
padding: 2rem 0;
width: 100%;
display: block;
span {
color: var(--color-text-weak);
@media (max-width: 40rem) {
display: none;
}
}
}
a:hover {
background: var(--color-background-weak);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
[data-slot="cell"] + [data-slot="cell"] {
border-left: 1px solid var(--color-border-weak);
@media (max-width: 40rem) {
border-left: none;
}
}
/* Mobile: third column on its own row */
@media (max-width: 25rem) {
flex-wrap: wrap;
[data-slot="cell"] {
flex: 1 0 100%;
border-left: none;
border-top: 1px solid var(--color-border-weak);
}
[data-slot="cell"]:nth-child(1) {
border-top: none;
}
}
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
border: 1px solid var(--color-border-weak);
border-top: none;
@media (max-width: 65rem) {
border: none;
}
}
[data-component="content"] {
}
[data-component="enterprise-content"] {
padding: 4rem 0;
@media (max-width: 60rem) {
padding: 2rem 0;
}
}
[data-component="enterprise-columns"] {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
padding: 4rem 5rem;
@media (max-width: 80rem) {
gap: 3rem;
}
@media (max-width: 60rem) {
grid-template-columns: 1fr;
gap: 3rem;
padding: 2rem 1.5rem;
}
}
[data-component="enterprise-column-1"] {
h2 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 1rem;
}
h3 {
font-size: 1.25rem;
font-weight: 500;
color: var(--color-text-strong);
margin: 2rem 0 1rem 0;
}
p {
line-height: 1.6;
margin-bottom: 1.5rem;
color: var(--color-text);
}
[data-component="testimonial"] {
margin-top: 4rem;
font-weight: 500;
color: var(--color-text-strong);
[data-component="quotation"] {
svg {
margin-bottom: 1rem;
opacity: 20%;
}
}
[data-component="testimonial-logo"] {
svg {
margin-top: 1.5rem;
}
}
}
}
[data-component="enterprise-column-2"] {
[data-component="enterprise-form"] {
padding: 0;
h2 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 1.5rem;
}
[data-component="form-group"] {
margin-bottom: 1.5rem;
label {
display: block;
font-weight: 500;
color: var(--color-text-weak);
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
transition: background-color 5000000s ease-in-out 0s;
}
input:-webkit-autofill {
-webkit-text-fill-color: var(--color-text-strong) !important;
}
input:-moz-autofill {
-moz-text-fill-color: var(--color-text-strong) !important;
}
input,
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border-weak);
border-radius: 4px;
background: var(--color-background-weak);
color: var(--color-text-strong);
font-family: inherit;
&::placeholder {
color: var(--color-text-weak);
}
&:focus {
background: var(--color-background-interactive-weaker);
outline: none;
border: none;
color: var(--color-text-strong);
border: 1px solid var(--color-background-strong);
box-shadow: 0 0 0 3px var(--color-background-interactive);
@media (prefers-color-scheme: dark) {
box-shadow: none;
border: 1px solid var(--color-background-interactive);
}
}
}
textarea {
resize: vertical;
min-height: 120px;
}
}
[data-component="submit-button"] {
padding: 0.5rem 1.5rem;
background: var(--color-background-strong);
color: var(--color-text-inverted);
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover:not(:disabled) {
background: var(--color-background-strong-hover);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
[data-component="success-message"] {
margin-top: 1rem;
padding: 1rem 0;
color: var(--color-text-success);
text-align: left;
}
}
}
[data-component="faq"] {
border-top: 1px solid var(--color-border-weak);
padding: 4rem 5rem;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
[data-slot="section-title"] {
margin-bottom: 24px;
h3 {
font-size: 16px;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 12px;
}
p {
margin-bottom: 12px;
color: var(--color-text);
}
}
ul {
padding: 0;
li {
list-style: none;
margin-bottom: 24px;
line-height: 200%;
@media (max-width: 60rem) {
line-height: 180%;
}
}
}
[data-slot="faq-question"] {
display: flex;
gap: 16px;
margin-bottom: 8px;
color: var(--color-text-strong);
font-weight: 500;
cursor: pointer;
background: none;
border: none;
padding: 0;
[data-slot="faq-icon-plus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: block;
}
[data-expanded] & {
display: none;
}
}
[data-slot="faq-icon-minus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: none;
}
[data-expanded] & {
display: block;
}
}
[data-slot="faq-question-text"] {
flex-grow: 1;
text-align: left;
}
}
[data-slot="faq-answer"] {
margin-left: 40px;
margin-bottom: 32px;
color: var(--color-text);
}
}
[data-component="legal"] {
color: var(--color-text-weak);
text-align: center;
padding: 2rem 5rem;
display: flex;
gap: 32px;
justify-content: center;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
a {
color: var(--color-text-weak);
text-decoration: none;
}
a:hover {
color: var(--color-text);
text-decoration: underline;
}
}
a {
color: var(--color-text-strong);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
&:hover {
text-decoration-thickness: 2px;
}
}
}
@@ -0,0 +1,268 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { createSignal, Show } from "solid-js"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import { Faq } from "~/component/faq"
export default function Enterprise() {
const [formData, setFormData] = createSignal({
name: "",
role: "",
email: "",
message: "",
})
const [isSubmitting, setIsSubmitting] = createSignal(false)
const [showSuccess, setShowSuccess] = createSignal(false)
const handleInputChange = (field: string) => (e: Event) => {
const target = e.target as HTMLInputElement | HTMLTextAreaElement
setFormData((prev) => ({ ...prev, [field]: target.value }))
}
const handleSubmit = async (e: Event) => {
e.preventDefault()
setIsSubmitting(true)
try {
const response = await fetch("/api/enterprise", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData()),
})
if (response.ok) {
setShowSuccess(true)
setFormData({
name: "",
role: "",
email: "",
message: "",
})
setTimeout(() => setShowSuccess(false), 5000)
}
} catch (error) {
console.error("Failed to submit form:", error)
} finally {
setIsSubmitting(false)
}
}
return (
<main data-page="enterprise">
<Title>OpenCode | Enterprise solutions for your organisation</Title>
<Meta name="description" content="Contact OpenCode for enterprise solutions" />
<div data-component="container">
<Header />
<div data-component="content">
<section data-component="enterprise-content">
<div data-component="enterprise-columns">
<div data-component="enterprise-column-1">
<h2>Your code is yours</h2>
<p>
OpenCode operates securely inside your organization with no data or context stored
and no licensing restrictions or ownership claims. Start a trial with your team,
then deploy it across your organization by integrating it with your SSO and
internal AI gateway.
</p>
<p>Let us know and how we can help.</p>
<Show when={false}>
<div data-component="testimonial">
<div data-component="quotation">
<svg
width="20"
height="17"
viewBox="0 0 20 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
fill="currentColor"
/>
</svg>
</div>
Thanks to OpenCode, we found a way to create software to track all our assets
even the imaginary ones.
<div data-component="testimonial-logo">
<svg
width="80"
height="79"
viewBox="0 0 80 79"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 39.3087L10.0579 29.251L15.6862 34.7868L13.7488 36.7248L10.3345 33.2186L8.48897 35.0639L11.8111 38.4781L9.96557 40.4156L6.55181 37.0018L4.06028 39.4928L7.56674 42.9991L5.62884 44.845L0 39.3087Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.7182 36.8164L20.2094 39.4003L16.6108 46.9666L22.2393 41.3374L24.3615 43.46L14.2118 53.5179L11.9047 51.1187L15.4112 43.3677L9.78254 49.0888L7.66016 46.9666L17.7182 36.8164Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M42.8139 61.915L45.3055 64.4064L41.6145 71.9731L47.243 66.3441L49.3652 68.4663L39.3077 78.5244L36.9088 76.1252L40.5072 68.374L34.7866 74.0953L32.6641 71.9731L42.8139 61.915Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.4258 55.7324L26.4833 45.582L28.6061 47.7042C31.0049 50.1034 32.3892 51.9497 30.1746 54.1642C28.7902 55.548 27.6831 56.0094 26.1145 54.9016L26.0222 54.994C27.2218 56.1941 26.9448 57.1162 25.4688 58.5931L23.9 60.1615C23.4383 60.6232 22.8847 61.2693 22.7927 62.0067L20.6705 59.8845C20.7625 59.146 21.3161 58.5008 21.778 58.1316L23.5307 56.3788C24.269 55.6403 23.715 54.2555 23.254 53.8872L22.8847 53.4256L18.548 57.7623L16.4258 55.7324ZM24.3611 51.9495C25.4689 53.0563 26.4833 53.3332 27.4984 52.3178C28.5134 51.3957 28.2367 50.3802 27.1295 49.1812L24.3611 51.9495Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M33.4952 66.9899C31.096 69.3891 28.8815 68.4659 27.4047 66.9899C26.021 65.6062 25.0978 63.3907 27.4972 60.9003L31.8336 56.6548C34.2333 54.2556 36.4478 55.0864 37.9241 56.5635C39.308 58.0396 40.2311 60.2541 37.8315 62.6531L33.4952 66.9899ZM29.0659 63.5752C28.6048 64.0369 28.6048 64.7753 29.1583 65.3292C29.6196 65.8821 30.4502 65.7897 30.8194 65.4215L36.2633 59.9769C36.7246 59.6076 36.7246 58.7779 36.171 58.3164C35.7097 57.7626 34.8791 57.7626 34.5101 58.2241L29.0659 63.5752Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M78.5267 39.308L68.2845 29.0654L47.5231 49.735L49.6453 51.8572L68.2845 33.2179L74.3746 39.308L47.2461 66.3435L49.3683 68.4657L78.5267 39.308Z"
fill="#0083C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M49.6443 51.8577L43.3695 45.4902L64.0386 24.8215L53.7969 14.4873L33.0352 35.2482L35.1574 37.3705L53.7969 18.7315L59.7947 24.8215L39.1251 45.4902L47.5221 53.9799L49.6443 51.8577Z"
fill="#2D9C5C"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M35.1564 37.3706L28.7896 31.0038L49.5515 10.3347L39.3088 0L10.0586 29.2507L12.1804 31.2804L39.3088 4.24476L45.3066 10.3347L24.6377 31.0038L33.0342 39.4008L35.1564 37.3706Z"
fill="#E92A35"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M77.2332 52.4105C76.0336 52.4105 75.111 51.4884 75.111 50.196C75.111 48.9046 76.0336 47.9814 77.2332 47.9814C78.3405 47.9814 79.263 48.9046 79.263 50.196C79.263 51.4884 78.3405 52.4105 77.2332 52.4105ZM77.2332 52.9643C78.7098 52.9643 80.0015 51.6729 80.0015 50.196C80.0015 48.6276 78.7096 47.4287 77.2332 47.4287C75.6644 47.4287 74.4648 48.6278 74.4648 50.196C74.4647 51.6731 75.6643 52.9643 77.2332 52.9643ZM76.1259 51.7653H76.6797V50.3804H77.0485L77.8788 51.7653H78.4332L77.6023 50.3804C78.1558 50.2881 78.4332 50.0122 78.4332 49.5507C78.4332 48.9046 78.0633 48.6276 77.3253 48.6276H76.1257V51.7653H76.1259ZM76.6797 49.0892H77.2332C77.5102 49.0892 77.8788 49.0892 77.8788 49.4586C77.8788 49.9202 77.6023 49.9202 77.2332 49.9202H76.6797V49.0892Z"
fill="#0083C6"
/>
</svg>
</div>
</div>
</Show>
</div>
<div data-component="enterprise-column-2">
<div data-component="enterprise-form">
<form onSubmit={handleSubmit}>
<div data-component="form-group">
<label for="name">Full name</label>
<input
id="name"
type="text"
required
value={formData().name}
onInput={handleInputChange("name")}
placeholder="Jeff Bezos"
/>
</div>
<div data-component="form-group">
<label for="role">Role</label>
<input
id="role"
type="text"
required
value={formData().role}
onInput={handleInputChange("role")}
placeholder="Executive Chairman"
/>
</div>
<div data-component="form-group">
<label for="email">Company email</label>
<input
id="email"
type="email"
required
value={formData().email}
onInput={handleInputChange("email")}
placeholder="jeff@amazon.com"
/>
</div>
<div data-component="form-group">
<label for="message">What problem are you trying to solve?</label>
<textarea
id="message"
required
rows={5}
value={formData().message}
onInput={handleInputChange("message")}
placeholder="We need help with..."
/>
</div>
<button type="submit" disabled={isSubmitting()} data-component="submit-button">
{isSubmitting() ? "Sending..." : "Send"}
</button>
</form>
{showSuccess() && (
<div data-component="success-message">
Message sent, we'll be in touch soon.
</div>
)}
</div>
</div>
</div>
</section>
<section data-component="faq">
<div data-slot="section-title">
<h3>FAQ</h3>
</div>
<ul>
<li>
<Faq question="What is OpenCode Enterprise?">
OpenCode Enterprise is for organizations that want to ensure that their code and
data never leaves their infrastructure. It can do this by using a centralized
config that integrates with your SSO and internal AI gateway.
</Faq>
</li>
<li>
<Faq question="How do I get started with OpenCode Enterprise?">
Simply start with an internal trial with your team. OpenCode by default does not
store your code or context data, making it easy to get started. Then contact us to
discuss pricing and implementation options.
</Faq>
</li>
<li>
<Faq question="How does enterprise pricing work?">
We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not
charge for tokens used. For further details, contact us for a custom quote based
on your organization's needs.
</Faq>
</li>
<li>
<Faq question="Is my data secure with OpenCode Enterprise?">
Yes. OpenCode does not store your code or context data. All processing happens
locally or through direct API calls to your AI provider. With central config and
SSO integration, your data remains secure within your organization's
infrastructure.
</Faq>
</li>
</ul>
</section>
</div>
<Footer />
</div>
<Legal />
</main>
)
}
+17 -28
View File
@@ -56,7 +56,6 @@ body {
}
}
@supports (background: -webkit-named-image(i)) {
[data-page="opencode"] {
border-top: 1px solid var(--color-border-weak);
@@ -118,7 +117,6 @@ body {
display: none !important;
}
[data-component="growth-stat"] {
display: flex;
flex-direction: column;
@@ -149,14 +147,12 @@ body {
display: none;
}
}
}
[data-component="stat-illustration"] {
width: 100%;
height: 100%;
display: block;
width: 100%;
height: 100%;
display: block;
svg {
margin: 0;
@@ -164,10 +160,9 @@ body {
height: auto;
display: block;
}
}
}
}
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
@@ -184,7 +179,6 @@ body {
-moz-text-fill-color: var(--color-text-strong) !important;
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
@@ -328,10 +322,10 @@ body {
display: flex;
flex-direction: column;
max-width: 100%;
padding: calc(var(--vertical-padding)*2) var(--padding);
padding: calc(var(--vertical-padding) * 2) var(--padding);
@media (max-width: 30rem) {
padding: var(--vertical-padding) var(--padding)
padding: var(--vertical-padding) var(--padding);
}
@media (prefers-color-scheme: dark) {
@@ -352,7 +346,6 @@ body {
@media (max-width: 550px) {
width: calc(100vw - 48px);
}
}
[data-component="tabs"] {
@@ -368,7 +361,7 @@ body {
padding: 0 20px;
@media (max-width: 60rem) {
gap: 20px;
gap: 32px;
overflow-x: auto;
width: 100%;
}
@@ -443,7 +436,6 @@ body {
@media (max-width: 35rem) {
width: calc(100% - 40px) !important;
}
}
[data-slot="highlight"] {
@@ -496,7 +488,6 @@ body {
@media (max-width: 60rem) {
font-size: 22px;
}
}
p {
@@ -579,7 +570,6 @@ body {
margin-bottom: 24px;
max-width: 100%;
div {
display: flex;
gap: 12px;
@@ -827,16 +817,12 @@ body {
outline: none;
border: none;
color: var(--color-text-strong);
border: 1px solid var(--color-background-strong); /* Tailwind blue-600 as example */
/* Tailwind-style ring */
border: 1px solid var(--color-background-strong);
box-shadow: 0 0 0 3px var(--color-background-interactive);
/* mimics "ring-2 ring-blue-600/50" */
@media (prefers-color-scheme: dark) {
box-shadow: none;
border: 1px solid var(--color-background-interactive)
border: 1px solid var(--color-background-interactive);
}
}
@@ -896,7 +882,6 @@ body {
margin-top: 2px;
}
[data-slot="faq-icon-plus"] {
flex-shrink: 0;
color: var(--color-text-weak);
@@ -1025,7 +1010,6 @@ body {
}
[data-component="copy-status"] {
[data-slot="copy"] {
display: block;
width: var(--space-4);
@@ -1058,12 +1042,10 @@ body {
border-bottom: 1px solid var(--color-border-weak);
}
[data-slot="cell"] {
flex: 1;
text-align: center;
a {
text-decoration: none;
padding: 2rem 0;
@@ -1076,7 +1058,6 @@ body {
@media (max-width: 40rem) {
display: none;
}
}
}
@@ -1115,10 +1096,18 @@ body {
[data-component="legal"] {
color: var(--color-text-weak);
text-align: center;
display: flex;
gap: 32px;
justify-content: center;
a {
color: var(--color-text-weak);
text-decoration: none;
}
a:hover {
color: var(--color-text);
text-decoration: underline;
}
}
}
File diff suppressed because it is too large Load Diff
+140 -105
View File
@@ -13,122 +13,157 @@ export async function POST(input: APIEvent) {
input.request.headers.get("stripe-signature")!,
Resource.STRIPE_WEBHOOK_SECRET.value,
)
console.log(body.type, JSON.stringify(body, null, 2))
if (body.type === "customer.updated") {
// check default payment method changed
const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
if (!("default_payment_method" in prevInvoiceSettings)) return
const customerID = body.data.object.id
const paymentMethodID = body.data.object.invoice_settings.default_payment_method as string
return (async () => {
if (body.type === "customer.updated") {
// check default payment method changed
const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
if (!("default_payment_method" in prevInvoiceSettings)) return "ignored"
if (!customerID) throw new Error("Customer ID not found")
if (!paymentMethodID) throw new Error("Payment method ID not found")
const customerID = body.data.object.id
const paymentMethodID = body.data.object.invoice_settings.default_payment_method as string
const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
await Database.use(async (tx) => {
await tx
.update(BillingTable)
.set({
paymentMethodID,
paymentMethodLast4: paymentMethod.card!.last4,
})
.where(eq(BillingTable.customerID, customerID))
})
}
if (body.type === "checkout.session.completed") {
const workspaceID = body.data.object.metadata?.workspaceID
const customerID = body.data.object.customer as string
const paymentID = body.data.object.payment_intent as string
const invoiceID = body.data.object.invoice as string
const amount = body.data.object.amount_total
if (!customerID) throw new Error("Customer ID not found")
if (!paymentMethodID) throw new Error("Payment method ID not found")
if (!workspaceID) throw new Error("Workspace ID not found")
if (!customerID) throw new Error("Customer ID not found")
if (!amount) throw new Error("Amount not found")
if (!paymentID) throw new Error("Payment ID not found")
if (!invoiceID) throw new Error("Invoice ID not found")
await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
// set customer metadata
if (!customer?.customerID) {
await Billing.stripe().customers.update(customerID, {
metadata: {
workspaceID,
},
})
}
// get payment method for the payment intent
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
expand: ["payment_method"],
})
const paymentMethod = paymentIntent.payment_method
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
await Database.transaction(async (tx) => {
const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
await Database.use(async (tx) => {
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} + ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
customerID,
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card!.last4,
reload: true,
reloadError: null,
timeReloadError: null,
paymentMethodID,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
})
.where(eq(BillingTable.customerID, customerID))
})
}
if (body.type === "checkout.session.completed") {
const workspaceID = body.data.object.metadata?.workspaceID
const amountInCents =
body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
const customerID = body.data.object.customer as string
const paymentID = body.data.object.payment_intent as string
const invoiceID = body.data.object.invoice as string
if (!workspaceID) throw new Error("Workspace ID not found")
if (!customerID) throw new Error("Customer ID not found")
if (!amountInCents) throw new Error("Amount not found")
if (!paymentID) throw new Error("Payment ID not found")
if (!invoiceID) throw new Error("Invoice ID not found")
await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID)
throw new Error("Customer ID mismatch")
// set customer metadata
if (!customer?.customerID) {
await Billing.stripe().customers.update(customerID, {
metadata: {
workspaceID,
},
})
}
// get payment method for the payment intent
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
expand: ["payment_method"],
})
const paymentMethod = paymentIntent.payment_method
if (!paymentMethod || typeof paymentMethod === "string")
throw new Error("Payment method not expanded")
await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amountInCents)}`,
customerID,
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
// enable reload if first time enabling billing
...(customer?.customerID
? {}
: {
reload: true,
reloadError: null,
timeReloadError: null,
}),
})
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(amountInCents),
paymentID,
invoiceID,
customerID,
})
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(Billing.CHARGE_AMOUNT),
paymentID,
invoiceID,
customerID,
})
})
}
if (body.type === "charge.refunded") {
const customerID = body.data.object.customer as string
const paymentIntentID = body.data.object.payment_intent as string
if (!customerID) throw new Error("Customer ID not found")
if (!paymentIntentID) throw new Error("Payment ID not found")
const workspaceID = await Database.use((tx) =>
tx
.select({
workspaceID: BillingTable.workspaceID,
})
.from(BillingTable)
.where(eq(BillingTable.customerID, customerID))
.then((rows) => rows[0]?.workspaceID),
)
if (!workspaceID) throw new Error("Workspace ID not found")
const amount = await Database.use((tx) =>
tx
.select({
amount: PaymentTable.amount,
})
.from(PaymentTable)
.where(
and(
eq(PaymentTable.paymentID, paymentIntentID),
eq(PaymentTable.workspaceID, workspaceID),
),
)
.then((rows) => rows[0]?.amount),
)
if (!amount) throw new Error("Payment not found")
await Database.transaction(async (tx) => {
await tx
.update(PaymentTable)
.set({
timeRefunded: new Date(body.created * 1000),
})
.where(
and(
eq(PaymentTable.paymentID, paymentIntentID),
eq(PaymentTable.workspaceID, workspaceID),
),
)
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${amount}`,
})
.where(eq(BillingTable.workspaceID, workspaceID))
})
}
})()
.then((message) => {
return Response.json({ message: message ?? "done" }, { status: 200 })
})
}
if (body.type === "charge.refunded") {
const customerID = body.data.object.customer as string
const paymentIntentID = body.data.object.payment_intent as string
if (!customerID) throw new Error("Customer ID not found")
if (!paymentIntentID) throw new Error("Payment ID not found")
const workspaceID = await Database.use((tx) =>
tx
.select({
workspaceID: BillingTable.workspaceID,
})
.from(BillingTable)
.where(eq(BillingTable.customerID, customerID))
.then((rows) => rows[0]?.workspaceID),
)
if (!workspaceID) throw new Error("Workspace ID not found")
await Database.transaction(async (tx) => {
await tx
.update(PaymentTable)
.set({
timeRefunded: new Date(body.created * 1000),
})
.where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
})
.where(eq(BillingTable.workspaceID, workspaceID))
.catch((error: any) => {
return Response.json({ message: error.message }, { status: 500 })
})
}
console.log("finished handling")
return Response.json("ok", { status: 200 })
}
+7 -5
View File
@@ -79,17 +79,19 @@ export default function Home() {
<strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
</li>
<li>
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a> provided by opencode{" "}
<label>New</label>
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a>{" "}
provided by opencode <label>New</label>
</li>
<li>
<strong>Multi-session</strong> Start multiple agents in parallel on the same project
</li>
<li>
<strong>Shareable links</strong> Share a link to any sessions for reference or to debug
<strong>Shareable links</strong> Share a link to any sessions for reference or to
debug
</li>
<li>
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max
account
</li>
<li>
<strong>Use any model</strong> Supports 75+ LLM providers through{" "}
@@ -121,7 +123,7 @@ export default function Home() {
<h3 data-component="title">homebrew</h3>
<button data-copy data-slot="button">
<span>
brew install <strong>sst/tap/opencode</strong>
brew install <strong>opencode</strong>
</span>
<CopyStatus />
</button>
@@ -23,7 +23,13 @@ const getWorkspaces = query(async () => {
})
.from(UserTable)
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(and(eq(UserTable.accountID, Actor.account()), isNull(WorkspaceTable.timeDeleted))),
.where(
and(
eq(UserTable.accountID, Actor.account()),
isNull(WorkspaceTable.timeDeleted),
isNull(UserTable.timeDeleted),
),
),
)
})
}, "workspaces")
@@ -12,7 +12,7 @@ const getUserEmail = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
const actor = Actor.assert("user")
const email = await User.getAccountEmail(actor.properties.userID)
const email = await User.getAuthEmail(actor.properties.userID)
return email
}, workspaceID)
}, "userEmail")
@@ -16,38 +16,110 @@
justify-content: flex-end;
}
[data-component="workspace-nav-items"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
/* Desktop Navigation */
[data-component="nav-desktop"] {
display: block;
[data-nav-button] {
padding: var(--space-3) var(--space-4);
border-radius: var(--border-radius-sm);
color: var(--color-text-muted);
text-decoration: none;
font-size: var(--font-size-sm);
font-weight: 500;
transition: all 0.15s ease;
@media (max-width: 48rem) {
display: none;
}
&:hover {
color: var(--color-text);
[data-component="workspace-nav-items"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
[data-nav-button] {
padding: var(--space-3) var(--space-4);
border-radius: var(--border-radius-sm);
color: var(--color-text-muted);
text-decoration: none;
font-size: var(--font-size-sm);
font-weight: 500;
transition: all 0.15s ease;
&:hover {
color: var(--color-text);
}
&.active {
color: var(--color-text);
font-weight: 700;
position: relative;
&::before {
content: "";
position: absolute;
left: calc(-1 * var(--space-0-5));
top: 0;
bottom: 0;
width: 2px;
background-color: var(--color-text);
border-radius: 0 2px 2px 0;
}
}
}
}
}
&.active {
color: var(--color-text);
font-weight: 700;
/* Mobile Navigation */
[data-component="nav-mobile"] {
display: none;
@media (max-width: 48rem) {
display: flex;
align-items: stretch;
justify-content: flex-start;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
[data-component="workspace-nav-items"] {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--space-1);
min-width: max-content;
height: 100%;
[data-nav-button] {
padding: var(--space-2) var(--space-3);
padding-bottom: calc(var(--space-2) + 4px);
border-radius: var(--border-radius-sm);
color: var(--color-text-muted);
text-decoration: none;
font-size: var(--font-size-sm);
font-weight: 500;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
position: relative;
&::before {
content: '';
position: absolute;
left: calc(-1 * var(--space-0-5));
top: 0;
bottom: 0;
width: 2px;
background-color: var(--color-text);
border-radius: 0 2px 2px 0;
&:hover {
color: var(--color-text);
}
&.active {
color: var(--color-text);
font-weight: 700;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: var(--color-text);
border-radius: 2px 2px 0 0;
}
}
}
}
@@ -221,5 +293,16 @@
border-right: none;
border-bottom: 1px solid var(--color-border);
padding: var(--space-4);
justify-content: flex-start;
overflow-x: auto;
overflow-y: hidden;
flex-shrink: 0;
min-height: fit-content;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
}
}
@@ -6,29 +6,54 @@ import "./[id].css"
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
return (
<main data-page="workspace">
<div data-component="workspace-container">
<nav data-component="workspace-nav">
<div data-component="workspace-nav-items">
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
Zen
</A>
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
API Keys
</A>
<A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
Members
</A>
<Show when={userInfo()?.isAdmin}>
<A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
Billing
<nav data-component="nav-desktop">
<div data-component="workspace-nav-items">
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
Zen
</A>
<A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
Settings
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
API Keys
</A>
</Show>
</div>
<A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
Members
</A>
<Show when={userInfo()?.isAdmin}>
<A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
Billing
</A>
<A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
Settings
</A>
</Show>
</div>
</nav>
<nav data-component="nav-mobile">
<div data-component="workspace-nav-items">
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
Zen
</A>
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
API Keys
</A>
<A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
Members
</A>
<Show when={userInfo()?.isAdmin}>
<A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
Billing
</A>
<A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
Settings
</A>
</Show>
</div>
</nav>
</nav>
<div data-component="workspace-content">{props.children}</div>
</div>
@@ -4,9 +4,6 @@
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
p {
color: var(--color-danger);
@@ -24,27 +21,116 @@
}
}
[data-slot="payment"] {
[data-slot="section-content"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
min-width: 14.5rem;
width: fit-content;
}
[data-slot="balance-display"] {
display: flex;
align-items: flex-start;
gap: var(--space-3);
@media (max-width: 30rem) {
width: 100%;
flex-direction: column;
align-items: flex-start;
gap: var(--space-2);
}
[data-slot="balance-amount"] {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg-surface);
align-self: stretch;
[data-slot="balance-label"] {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin-top: var(--space-2);
font-weight: 400;
}
[data-slot="balance-value"] {
font-size: var(--font-size-2xl);
font-weight: 600;
color: var(--color-text);
}
}
[data-slot="balance-right-section"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
flex: 1;
}
[data-slot="add-balance-form-container"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
[data-slot="add-balance-form"] {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--space-3);
label {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text-muted);
white-space: nowrap;
}
input[data-component="input"] {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
line-height: 1.5;
&:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-alpha);
}
&::placeholder {
color: var(--color-text-disabled);
}
}
[data-slot="form-actions"] {
display: flex;
gap: var(--space-2);
}
}
[data-slot="form-error"] {
color: var(--color-danger);
font-size: var(--font-size-sm);
line-height: 1.4;
}
[data-slot="credit-card"] {
padding: var(--space-3-5) var(--space-4);
padding: var(--space-2) var(--space-4);
background-color: var(--color-bg-surface);
border-radius: var(--border-radius-sm);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
min-width: 150px;
align-self: flex-start;
[data-slot="card-icon"] {
display: flex;
@@ -56,56 +142,44 @@
display: flex;
align-items: baseline;
gap: var(--space-1);
flex: 1;
justify-content: flex-end;
[data-slot="secret"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
font-weight: 400;
}
[data-slot="number"] {
font-size: var(--font-size-3xl);
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text);
color: var(--color-text-muted);
}
[data-slot="type"] {
font-size: var(--font-size-sm);
font-weight: 400;
color: var(--color-text-muted);
}
}
button {
white-space: nowrap;
flex-shrink: 0;
}
}
[data-slot="button-row"] {
display: flex;
gap: var(--space-2);
align-items: center;
@media (max-width: 30rem) {
flex-direction: column;
>button {
width: 100%;
}
}
[data-slot="create-form"] {
margin: 0;
}
/* Make Enable Billing button full width when it's the only button */
>button {
flex: 1;
}
button {
align-self: flex-start;
white-space: nowrap;
flex-shrink: 0;
}
}
[data-slot="usage"] {
p {
font-size: var(--font-size-sm);
line-height: 1.5;
color: var(--color-text-secondary);
b {
font-weight: 600;
}
}
[data-slot="enable-billing-button"] {
align-self: flex-start;
padding: var(--space-4);
min-width: 150px;
}
}
@@ -1,66 +1,87 @@
import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
import { createMemo, Show } from "solid-js"
import { action, useParams, useAction, createAsync, useSubmission, json } from "@solidjs/router"
import { createMemo, Match, Show, Switch, createEffect } from "solid-js"
import { createStore } from "solid-js/store"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { withActor } from "~/context/auth.withActor"
import { IconCreditCard } from "~/component/icon"
import { IconCreditCard, IconStripe } from "~/component/icon"
import styles from "./billing-section.module.css"
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { createCheckoutUrl } from "../../common"
const reload = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key })
}, "billing.reload")
const setReload = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
const reload = form.get("reload")?.toString() === "true"
return json(
await Database.use((tx) =>
tx
.update(BillingTable)
.set({
reload,
})
.where(eq(BillingTable.workspaceID, workspaceID)),
),
{ revalidate: getBillingInfo.key },
)
}, "billing.setReload")
import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common"
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
"use server"
return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID)
return json(
await withActor(
() =>
Billing.generateSessionUrl({ returnUrl })
.then((data) => ({ error: undefined, data }))
.catch((e) => ({
error: e.message as string,
data: undefined,
})),
workspaceID,
),
{ revalidate: queryBillingInfo.key },
)
}, "sessionUrl")
const getBillingInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return await Billing.get()
}, workspaceID)
}, "billing.get")
export function BillingSection() {
const params = useParams()
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
const balanceInfo = createAsync(() => getBillingInfo(params.id))
const createCheckoutUrlAction = useAction(createCheckoutUrl)
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
const createSessionUrlAction = useAction(createSessionUrl)
const createSessionUrlSubmission = useSubmission(createSessionUrl)
const setReloadSubmission = useSubmission(setReload)
const reloadSubmission = useSubmission(reload)
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const checkoutAction = useAction(createCheckoutUrl)
const checkoutSubmission = useSubmission(createCheckoutUrl)
const sessionAction = useAction(createSessionUrl)
const sessionSubmission = useSubmission(createSessionUrl)
const [store, setStore] = createStore({
showAddBalanceForm: false,
addBalanceAmount: "",
checkoutRedirecting: false,
sessionRedirecting: false,
})
const balance = createMemo(() => formatBalance(billingInfo()?.balance ?? 0))
async function onClickCheckout() {
const amount = parseInt(store.addBalanceAmount)
const baseUrl = window.location.href
const checkout = await checkoutAction(params.id, amount, baseUrl, baseUrl)
if (checkout && checkout.data) {
setStore("checkoutRedirecting", true)
window.location.href = checkout.data
}
}
async function onClickSession() {
const baseUrl = window.location.href
const sessionUrl = await sessionAction(params.id, baseUrl)
if (sessionUrl && sessionUrl.data) {
setStore("sessionRedirecting", true)
window.location.href = sessionUrl.data
}
}
function showAddBalanceForm() {
while (true) {
checkoutSubmission.clear()
if (!checkoutSubmission.result) break
}
setStore({
showAddBalanceForm: true,
addBalanceAmount: billingInfo()!.reloadAmount.toString(),
})
}
function hideAddBalanceForm() {
setStore("showAddBalanceForm", false)
checkoutSubmission.clear()
}
// DUMMY DATA FOR TESTING - UNCOMMENT ONE OF THE SCENARIOS BELOW
// Scenario 1: User has not added billing details and has no balance
// const balanceInfo = () => ({
// balance: 0,
// paymentMethodType: null as string | null,
// paymentMethodLast4: null as string | null,
// reload: false,
// reloadError: null as string | null,
@@ -70,6 +91,7 @@ export function BillingSection() {
// Scenario 2: User has not added billing details but has a balance
// const balanceInfo = () => ({
// balance: 1500000000, // $15.00
// paymentMethodType: null as string | null,
// paymentMethodLast4: null as string | null,
// reload: false,
// reloadError: null as string | null,
@@ -79,6 +101,7 @@ export function BillingSection() {
// Scenario 3: User has added billing details (reload enabled)
// const balanceInfo = () => ({
// balance: 750000000, // $7.50
// paymentMethodType: "card",
// paymentMethodLast4: "4242",
// reload: true,
// reloadError: null as string | null,
@@ -88,141 +111,141 @@ export function BillingSection() {
// Scenario 4: User has billing details but reload failed
// const balanceInfo = () => ({
// balance: 250000000, // $2.50
// paymentMethodType: "card",
// paymentMethodLast4: "4242",
// reload: true,
// reloadError: "Your card was declined." as string,
// timeReloadError: new Date(Date.now() - 3600000) as Date // 1 hour ago
// })
const balanceAmount = createMemo(() => {
return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
})
const hasBalance = createMemo(() => {
return (balanceInfo()?.balance ?? 0) > 0 && balanceAmount() !== "0.00"
})
// Scenario 5: User has Link payment method
// const balanceInfo = () => ({
// balance: 500000000, // $5.00
// paymentMethodType: "link",
// paymentMethodLast4: null as string | null,
// reload: true,
// reloadError: null as string | null,
// timeReloadError: null as Date | null
// })
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>Billing</h2>
<p>
Manage payments methods. <a href="mailto:contact@anoma.ly">Contact us</a> if you have any questions.
Manage payments methods. <a href="mailto:contact@anoma.ly">Contact us</a> if you have any
questions.
</p>
</div>
<div data-slot="section-content">
<Show when={balanceInfo()?.reloadError}>
<div data-slot="reload-error">
<p>
Reload failed at{" "}
{balanceInfo()?.timeReloadError!.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
})}
. Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try
again.
</p>
<form action={reload} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} />
<button data-color="primary" type="submit" disabled={reloadSubmission.pending}>
{reloadSubmission.pending ? "Reloading..." : "Reload"}
</button>
</form>
<div data-slot="balance-display">
<div data-slot="balance-amount">
<span data-slot="balance-value">${balance()}</span>
<span data-slot="balance-label">Current Balance</span>
</div>
</Show>
<div data-slot="payment">
<div data-slot="credit-card">
<div data-slot="card-icon">
<IconCreditCard style={{ width: "32px", height: "32px" }} />
</div>
<div data-slot="card-details">
<Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
<span data-slot="secret"></span>
<span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span>
</Show>
</div>
</div>
<div data-slot="button-row">
<Show
when={balanceInfo()?.reload}
fallback={
<Show
when={hasBalance()}
fallback={
<button
data-color="primary"
disabled={createCheckoutUrlSubmission.pending}
onClick={async () => {
const baseUrl = window.location.href
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
if (checkoutUrl) {
window.location.href = checkoutUrl
}
}}
<Show when={billingInfo()?.customerID}>
<div data-slot="balance-right-section">
<Show
when={!store.showAddBalanceForm}
fallback={
<div data-slot="add-balance-form-container">
<div data-slot="add-balance-form">
<label>Add $</label>
<input
data-component="input"
type="number"
min={billingInfo()?.reloadAmountMin.toString()}
step="1"
value={store.addBalanceAmount}
onInput={(e) => {
setStore("addBalanceAmount", e.currentTarget.value)
checkoutSubmission.clear()
}}
placeholder="Enter amount"
/>
<div data-slot="form-actions">
<button
data-color="ghost"
type="button"
onClick={() => hideAddBalanceForm()}
>
Cancel
</button>
<button
data-color="primary"
type="button"
disabled={
!store.addBalanceAmount ||
checkoutSubmission.pending ||
store.checkoutRedirecting
}
onClick={onClickCheckout}
>
{checkoutSubmission.pending || store.checkoutRedirecting
? "Loading..."
: "Add"}
</button>
</div>
</div>
<Show
when={checkoutSubmission.result && (checkoutSubmission.result as any).error}
>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"}
</button>
}
>
<form action={setReload} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="reload" value="true" />
<button data-color="primary" type="submit" disabled={setReloadSubmission.pending}>
{setReloadSubmission.pending ? "Enabling..." : "Enable Billing"}
</button>
</form>
</Show>
}
>
<button
data-color="primary"
disabled={createSessionUrlSubmission.pending}
onClick={async () => {
const baseUrl = window.location.href
const sessionUrl = await createSessionUrlAction(params.id, baseUrl)
if (sessionUrl) {
window.location.href = sessionUrl
}
}}
{(err: any) => <div data-slot="form-error">{err()}</div>}
</Show>
</div>
}
>
{createSessionUrlSubmission.pending ? "Loading..." : "Manage Payment Methods"}
</button>
<form action={setReload} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="reload" value="false" />
<button data-color="ghost" type="submit" disabled={setReloadSubmission.pending}>
{setReloadSubmission.pending ? "Disabling..." : "Disable"}
<button data-color="primary" onClick={() => showAddBalanceForm()}>
Add Balance
</button>
</form>
</Show>
</div>
</div>
<div data-slot="usage">
<Show when={!balanceInfo()?.reload}>
<Show
when={hasBalance()}
fallback={
<p>
We'll load <b>$20</b> (+$1.23 processing fee) and reload it when it reaches <b>$5</b>.
</p>
}
>
<p>
You have <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> remaining in
your account. You can continue using the API with your remaining balance.
</p>
</Show>
</Show>
<Show when={balanceInfo()?.reload && !balanceInfo()?.reloadError}>
<p>
Your current balance is <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
. We'll automatically reload <b>$20</b> (+$1.23 processing fee) when it reaches <b>$5</b>.
</p>
</Show>
<div data-slot="credit-card">
<div data-slot="card-icon">
<Switch fallback={<IconCreditCard style={{ width: "24px", height: "24px" }} />}>
<Match when={billingInfo()?.paymentMethodType === "link"}>
<IconStripe style={{ width: "24px", height: "24px" }} />
</Match>
</Switch>
</div>
<div data-slot="card-details">
<Switch>
<Match when={billingInfo()?.paymentMethodType === "card"}>
<Show
when={billingInfo()?.paymentMethodLast4}
fallback={<span data-slot="number">----</span>}
>
<span data-slot="secret"></span>
<span data-slot="number">{billingInfo()?.paymentMethodLast4}</span>
</Show>
</Match>
<Match when={billingInfo()?.paymentMethodType === "link"}>
<span data-slot="type">Linked to Stripe</span>
</Match>
</Switch>
</div>
<button
data-color="ghost"
disabled={sessionSubmission.pending || store.sessionRedirecting}
onClick={onClickSession}
>
{sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage"}
</button>
</div>
</div>
</Show>
</div>
<Show when={!billingInfo()?.customerID}>
<button
data-slot="enable-billing-button"
data-color="primary"
disabled={checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout}
>
{checkoutSubmission.pending || store.checkoutRedirecting
? "Loading..."
: "Enable Billing"}
</button>
</Show>
</div>
</section>
)
@@ -1,21 +1,26 @@
import { MonthlyLimitSection } from "./monthly-limit-section"
import { BillingSection } from "./billing-section"
import { ReloadSection } from "./reload-section"
import { PaymentSection } from "./payment-section"
import { Show } from "solid-js"
import { createAsync, useParams } from "@solidjs/router"
import { querySessionInfo } from "../../common"
import { queryBillingInfo, querySessionInfo } from "../../common"
export default function () {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id))
return (
<div data-page="workspace-[id]">
<div data-slot="sections">
<Show when={userInfo()?.isAdmin}>
<BillingSection />
<MonthlyLimitSection />
<PaymentSection />
<Show when={billingInfo()?.customerID}>
<ReloadSection />
<MonthlyLimitSection />
<PaymentSection />
</Show>
</Show>
</div>
</div>
@@ -1,16 +1,10 @@
import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { json, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { createEffect, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode-ai/console-core/billing.js"
import styles from "./monthly-limit-section.module.css"
const getBillingInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return await Billing.get()
}, workspaceID)
}, "billing.get")
import { queryBillingInfo } from "../../common"
const setMonthlyLimit = action(async (form: FormData) => {
"use server"
@@ -28,7 +22,7 @@ const setMonthlyLimit = action(async (form: FormData) => {
.catch((e) => ({ error: e.message as string })),
workspaceID,
),
{ revalidate: getBillingInfo.key },
{ revalidate: queryBillingInfo.key },
)
}, "billing.setMonthlyLimit")
@@ -36,7 +30,7 @@ export function MonthlyLimitSection() {
const params = useParams()
const submission = useSubmission(setMonthlyLimit)
const [store, setStore] = createStore({ show: false })
const balanceInfo = createAsync(() => getBillingInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id))
let input: HTMLInputElement
@@ -68,13 +62,13 @@ export function MonthlyLimitSection() {
<section class={styles.root}>
<div data-slot="section-title">
<h2>Monthly Limit</h2>
<p>Set a monthly spending limit for your account.</p>
<p>Set a monthly usage limit for your account.</p>
</div>
<div data-slot="section-content">
<div data-slot="balance">
<div data-slot="amount">
{balanceInfo()?.monthlyLimit ? <span data-slot="currency">$</span> : null}
<span data-slot="value">{balanceInfo()?.monthlyLimit ?? "-"}</span>
{billingInfo()?.monthlyLimit ? <span data-slot="currency">$</span> : null}
<span data-slot="value">{billingInfo()?.monthlyLimit ?? "-"}</span>
</div>
<Show
when={!store.show}
@@ -106,15 +100,19 @@ export function MonthlyLimitSection() {
}
>
<button data-color="primary" onClick={() => show()}>
{balanceInfo()?.monthlyLimit ? "Edit Limit" : "Set Limit"}
{billingInfo()?.monthlyLimit ? "Edit Limit" : "Set Limit"}
</button>
</Show>
</div>
<Show when={balanceInfo()?.monthlyLimit} fallback={<p data-slot="usage-status">No spending limit set.</p>}>
<Show
when={billingInfo()?.monthlyLimit}
fallback={<p data-slot="usage-status">No usage limit set.</p>}
>
<p data-slot="usage-status">
Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
Current usage for{" "}
{new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
{(() => {
const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated
const dateLastUsed = billingInfo()?.timeMonthlyUsageUpdated
if (!dateLastUsed) return "0"
const current = new Date().toLocaleDateString("en-US", {
@@ -128,7 +126,7 @@ export function MonthlyLimitSection() {
timeZone: "UTC",
})
if (current !== lastUsed) return "0"
return ((balanceInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2)
return ((billingInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2)
})()}
.
</p>
@@ -0,0 +1,262 @@
.root {
[data-slot="title-row"] {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
}
[data-slot="section-content"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
[data-slot="setting-row"] {
display: flex;
align-items: center;
gap: var(--space-3);
p {
flex: 1;
font-size: var(--font-size-sm);
line-height: 1.5;
color: var(--color-text-secondary);
margin: 0;
b {
font-weight: 600;
}
}
[data-slot="create-form"] {
margin: 0;
}
}
[data-slot="create-form"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
margin-top: var(--space-4);
[data-slot="form-field"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
label {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
[data-slot="field-label"] {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text-muted);
}
[data-slot="toggle-container"] {
display: flex;
align-items: center;
}
input[data-component="input"] {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-mono);
&:focus {
outline: none;
border-color: var(--color-accent);
}
&::placeholder {
color: var(--color-text-disabled);
}
}
}
[data-slot="input-row"] {
display: flex;
flex-direction: row;
gap: var(--space-3);
@media (max-width: 40rem) {
flex-direction: column;
gap: var(--space-2);
}
}
[data-slot="input-field"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
flex: 1;
p {
line-height: 1.2;
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
input[data-component="input"] {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
line-height: 1.5;
min-width: 0;
&:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-alpha);
}
&::placeholder {
color: var(--color-text-disabled);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--color-bg-surface);
}
}
[data-slot="field-with-connector"] {
display: flex;
align-items: center;
gap: var(--space-2);
[data-slot="field-connector"] {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
white-space: nowrap;
}
input[data-component="input"] {
flex: 1;
min-width: 80px;
}
}
}
[data-slot="form-actions"] {
display: flex;
gap: var(--space-2);
margin-top: var(--space-1);
}
[data-slot="form-error"] {
color: var(--color-danger);
font-size: var(--font-size-sm);
line-height: 1.4;
margin-top: calc(var(--space-1) * -1);
}
[data-slot="model-toggle-label"] {
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.5rem;
cursor: pointer;
input {
opacity: 0;
width: 0;
height: 0;
}
span {
position: absolute;
inset: 0;
background-color: #ccc;
border: 1px solid #bbb;
border-radius: 1.5rem;
transition: all 0.3s ease;
cursor: pointer;
&::before {
content: "";
position: absolute;
top: 50%;
left: 0.125rem;
width: 1.25rem;
height: 1.25rem;
background-color: white;
border: 1px solid #ddd;
border-radius: 50%;
transform: translateY(-50%);
transition: all 0.3s ease;
}
}
input:checked + span {
background-color: #21ad0e;
border-color: #148605;
&::before {
transform: translateX(1rem) translateY(-50%);
}
}
&:hover span {
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
}
input:checked:hover + span {
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
}
&:has(input:disabled) {
cursor: not-allowed;
}
input:disabled + span {
opacity: 0.5;
cursor: not-allowed;
}
}
}
[data-slot="reload-error"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
margin-top: var(--space-4);
p {
color: var(--color-danger);
font-size: var(--font-size-sm);
line-height: 1.4;
margin: 0;
flex: 1;
}
[data-slot="create-form"] {
display: flex;
gap: var(--space-2);
margin: 0;
flex-shrink: 0;
padding: 0;
border: none;
}
}
}
@@ -0,0 +1,211 @@
import { json, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { createEffect, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import styles from "./reload-section.module.css"
import { queryBillingInfo } from "../../common"
const reload = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
return json(await withActor(() => Billing.reload(), workspaceID), {
revalidate: queryBillingInfo.key,
})
}, "billing.reload")
const setReload = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
const reloadValue = form.get("reload")?.toString() === "true"
const amountStr = form.get("reloadAmount")?.toString()
const triggerStr = form.get("reloadTrigger")?.toString()
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
if (reloadValue) {
if (reloadAmount === null || reloadAmount < Billing.RELOAD_AMOUNT_MIN)
return { error: `Reload amount must be at least $${Billing.RELOAD_AMOUNT_MIN}` }
if (reloadTrigger === null || reloadTrigger < Billing.RELOAD_TRIGGER_MIN)
return { error: `Balance trigger must be at least $${Billing.RELOAD_TRIGGER_MIN}` }
}
return json(
await Database.use((tx) =>
tx
.update(BillingTable)
.set({
reload: reloadValue,
...(reloadAmount !== null ? { reloadAmount } : {}),
...(reloadTrigger !== null ? { reloadTrigger } : {}),
...(reloadValue
? {
reloadError: null,
timeReloadError: null,
}
: {}),
})
.where(eq(BillingTable.workspaceID, workspaceID)),
),
{ revalidate: queryBillingInfo.key },
)
}, "billing.setReload")
export function ReloadSection() {
const params = useParams()
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const setReloadSubmission = useSubmission(setReload)
const reloadSubmission = useSubmission(reload)
const [store, setStore] = createStore({
show: false,
reload: false,
reloadAmount: "",
reloadTrigger: "",
})
createEffect(() => {
if (
!setReloadSubmission.pending &&
setReloadSubmission.result &&
!(setReloadSubmission.result as any).error
) {
setStore("show", false)
}
})
function show() {
while (true) {
setReloadSubmission.clear()
if (!setReloadSubmission.result) break
}
const info = billingInfo()!
setStore("show", true)
setStore("reload", info.reload ? true : true)
setStore("reloadAmount", info.reloadAmount.toString())
setStore("reloadTrigger", info.reloadTrigger.toString())
}
function hide() {
setStore("show", false)
}
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>Auto Reload</h2>
<div data-slot="title-row">
<Show
when={billingInfo()?.reload}
fallback={
<p>
Auto reload is <b>disabled</b>. Enable to automatically reload when balance is low.
</p>
}
>
<p>
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b>{" "}
(+$1.23 processing fee) when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
</p>
</Show>
<button data-color="primary" type="button" onClick={() => show()}>
{billingInfo()?.reload ? "Edit" : "Enable"}
</button>
</div>
</div>
<Show when={store.show}>
<form action={setReload} method="post" data-slot="create-form">
<div data-slot="form-field">
<label>
<span data-slot="field-label">Enable Auto Reload</span>
<div data-slot="toggle-container">
<label data-slot="model-toggle-label">
<input
type="checkbox"
name="reload"
value="true"
checked={store.reload}
onChange={(e) => setStore("reload", e.currentTarget.checked)}
/>
<span></span>
</label>
</div>
</label>
</div>
<div data-slot="input-row">
<div data-slot="input-field">
<p>Reload $</p>
<input
data-component="input"
name="reloadAmount"
type="number"
min={billingInfo()?.reloadAmountMin.toString()}
step="1"
value={store.reloadAmount}
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
placeholder={billingInfo()?.reloadAmount.toString()}
disabled={!store.reload}
/>
</div>
<div data-slot="input-field">
<p>When balance reaches $</p>
<input
data-component="input"
name="reloadTrigger"
type="number"
min={billingInfo()?.reloadTriggerMin.toString()}
step="1"
value={store.reloadTrigger}
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
placeholder={billingInfo()?.reloadTrigger.toString()}
disabled={!store.reload}
/>
</div>
</div>
<Show when={setReloadSubmission.result && (setReloadSubmission.result as any).error}>
{(err: any) => <div data-slot="form-error">{err()}</div>}
</Show>
<input type="hidden" name="workspaceID" value={params.id} />
<div data-slot="form-actions">
<button type="button" data-color="ghost" onClick={() => hide()}>
Cancel
</button>
<button type="submit" data-color="primary" disabled={setReloadSubmission.pending}>
{setReloadSubmission.pending ? "Saving..." : "Save"}
</button>
</div>
</form>
</Show>
<Show when={billingInfo()?.reload && billingInfo()?.reloadError}>
<div data-slot="section-content">
<div data-slot="reload-error">
<p>
Reload failed at{" "}
{billingInfo()?.timeReloadError!.toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
})}
. Reason: {billingInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment
method and try again.
</p>
<form action={reload} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} />
<button data-color="ghost" type="submit" disabled={reloadSubmission.pending}>
{reloadSubmission.pending ? "Retrying..." : "Retry"}
</button>
</form>
</div>
</div>
</Show>
</section>
)
}
@@ -1,22 +1,32 @@
import { Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router"
import { NewUserSection } from "./new-user-section"
import { UsageSection } from "./usage-section"
import { ModelSection } from "./model-section"
import { ProviderSection } from "./provider-section"
import { IconLogo } from "~/component/icon"
import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router"
import { querySessionInfo, queryBillingInfo, createCheckoutUrl } from "../common"
import { Show, createMemo } from "solid-js"
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
export default function () {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const createCheckoutUrlAction = useAction(createCheckoutUrl)
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
const balanceAmount = createMemo(() => {
return ((billingInfo()?.balance ?? 0) / 100000000).toFixed(2)
const checkoutAction = useAction(createCheckoutUrl)
const checkoutSubmission = useSubmission(createCheckoutUrl)
const [store, setStore] = createStore({
checkoutRedirecting: false,
})
const balance = createMemo(() => formatBalance(billingInfo()?.balance ?? 0))
async function onClickCheckout() {
const baseUrl = window.location.href
const checkout = await checkoutAction(params.id, billingInfo()!.reloadAmount, baseUrl, baseUrl)
if (checkout && checkout.data) {
setStore("checkoutRedirecting", true)
window.location.href = checkout.data
}
}
return (
<div data-page="workspace-[id]">
@@ -38,21 +48,17 @@ export default function () {
<button
data-color="primary"
data-size="sm"
disabled={createCheckoutUrlSubmission.pending}
onClick={async () => {
const baseUrl = window.location.href
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
if (checkoutUrl) {
window.location.href = checkoutUrl
}
}}
disabled={checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout}
>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Enable billing"}
{checkoutSubmission.pending || store.checkoutRedirecting
? "Loading..."
: "Enable billing"}
</button>
}
>
<span data-slot="balance">
Current balance <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
Current balance <b>${balance()}</b>
</span>
</Show>
</span>
@@ -22,6 +22,22 @@
}
}
[data-slot="beta-notice"] {
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg-surface);
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: var(--space-3);
a {
color: var(--color-accent);
text-decoration: none;
}
}
[data-slot="create-form"] {
display: flex;
flex-direction: column;
@@ -81,7 +97,7 @@
display: flex;
gap: var(--space-2);
>button[type="reset"] {
> button[type="reset"] {
align-self: flex-start;
}
}
@@ -207,7 +223,6 @@
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
@@ -217,8 +232,7 @@
th {
&:nth-child(3)
/* Date */
{
/* Date */ {
display: none;
}
}
@@ -226,11 +240,10 @@
td {
&:nth-child(3)
/* Date */
{
/* Date */ {
display: none;
}
}
}
}
}
}
@@ -139,7 +139,7 @@ function MemberRow(props: { member: any; workspaceID: string; actorID: string; a
return (
<tr>
<td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td>
<td data-slot="member-email">{props.member.authEmail ?? props.member.email}</td>
<td data-slot="member-role">
<Show when={store.editing && !isCurrentUser()} fallback={<span>{props.member.role}</span>}>
<RoleDropdown
@@ -253,6 +253,13 @@ export function MemberSection() {
</Show>
</div>
</div>
<div data-slot="beta-notice">
Workspaces are free for teams during the beta.{" "}
<a href="/docs/zen/#for-teams" target="_blank" rel="noopener noreferrer">
Learn more
</a>
.
</div>
<Show when={store.show}>
<form action={inviteMember} method="post" data-slot="create-form">
<div data-slot="input-row">
@@ -2,10 +2,18 @@ import { Model } from "@opencode-ai/console-core/model.js"
import { query, action, useParams, createAsync, json } from "@solidjs/router"
import { createMemo, For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { ZenModel } from "@opencode-ai/console-core/model.js"
import { ZenData } from "@opencode-ai/console-core/model.js"
import styles from "./model-section.module.css"
import { querySessionInfo } from "../common"
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
import {
IconAlibaba,
IconAnthropic,
IconMoonshotAI,
IconOpenAI,
IconStealth,
IconXai,
IconZai,
} from "~/component/icon"
const getModelLab = (modelId: string) => {
if (modelId.startsWith("claude")) return "Anthropic"
@@ -21,8 +29,8 @@ const getModelsInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return {
all: Object.entries(ZenModel.list())
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
all: Object.entries(ZenData.list().models)
.filter(([id, _model]) => !["claude-3-5-haiku", "minimax-m2"].includes(id))
.filter(([id, _model]) => !id.startsWith("an-"))
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
.map(([id, model]) => ({ id, name: model.name })),
@@ -68,7 +76,8 @@ export function ModelSection() {
<div data-slot="section-title">
<h2>Models</h2>
<p>
Manage which models workspace members can access. <a href="/docs/zen#pricing ">Learn more</a>.
Manage which models workspace members can access.{" "}
<a href="/docs/zen#pricing ">Learn more</a>.
</p>
</div>
<div data-slot="models-list">
@@ -1,8 +1,12 @@
import { Resource } from "@opencode-ai/console-resource"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { action, query } from "@solidjs/router"
import { action, json, query } from "@solidjs/router"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { User } from "@opencode-ai/console-core/user.js"
import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
export function formatDateForTable(date: Date) {
const options: Intl.DateTimeFormatOptions = {
@@ -12,7 +16,7 @@ export function formatDateForTable(date: Date) {
minute: "2-digit",
hour12: true,
}
return date.toLocaleDateString("en-GB", options).replace(",", ",")
return date.toLocaleDateString(undefined, options).replace(",", ",")
}
export function formatDateUTC(date: Date) {
@@ -30,34 +34,76 @@ export function formatDateUTC(date: Date) {
return date.toLocaleDateString("en-US", options)
}
export const queryIsLoggedIn = query(async () => {
export function formatBalance(amount: number) {
const balance = ((amount ?? 0) / 100000000).toFixed(2)
return balance === "-0.00" ? "0.00" : balance
}
export async function getLastSeenWorkspaceID() {
"use server"
return withActor(() => {
try {
Actor.assert("account")
return true
} catch {
return false
}
return withActor(async () => {
const actor = Actor.assert("account")
return Database.use(async (tx) =>
tx
.select({ id: WorkspaceTable.id })
.from(UserTable)
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(
and(
eq(UserTable.accountID, actor.properties.accountID),
isNull(UserTable.timeDeleted),
isNull(WorkspaceTable.timeDeleted),
),
)
.orderBy(desc(UserTable.timeSeen))
.limit(1)
.then((x) => x[0]?.id),
)
})
}, "isLoggedIn.get")
}
export const querySessionInfo = query(async (workspaceID: string) => {
"use server"
return withActor(() => {
return {
isAdmin: Actor.userRole() === "admin",
isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
isBeta:
Resource.App.stage === "production"
? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y"
: true,
}
}, workspaceID)
}, "session.get")
export const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
"use server"
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
}, "checkoutUrl")
export const createCheckoutUrl = action(
async (workspaceID: string, amount: number, successUrl: string, cancelUrl: string) => {
"use server"
return json(
await withActor(
() =>
Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl })
.then((data) => ({ error: undefined, data }))
.catch((e) => ({
error: e.message as string,
data: undefined,
})),
workspaceID,
),
)
},
"checkoutUrl",
)
export const queryBillingInfo = query(async (workspaceID: string) => {
"use server"
return withActor(() => Billing.get(), workspaceID)
return withActor(async () => {
const billing = await Billing.get()
return {
...billing,
reloadAmount: billing.reloadAmount ?? Billing.RELOAD_AMOUNT,
reloadAmountMin: Billing.RELOAD_AMOUNT_MIN,
reloadTrigger: billing.reloadTrigger ?? Billing.RELOAD_TRIGGER,
reloadTriggerMin: Billing.RELOAD_TRIGGER_MIN,
}
}, workspaceID)
}, "billing.get")
+10 -13
View File
@@ -60,7 +60,6 @@ body {
}
}
[data-page="zen"] {
background: var(--color-background);
--padding: 5rem;
@@ -112,9 +111,6 @@ body {
-moz-text-fill-color: var(--color-text-strong) !important;
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
@@ -179,8 +175,6 @@ body {
}
}
[data-component="nav-mobile-toggle"] {
border: none;
background: none;
@@ -258,14 +252,14 @@ body {
[data-component="hero"] {
display: flex;
flex-direction: column;
padding: calc(var(--vertical-padding)*2) var(--padding);
padding: calc(var(--vertical-padding) * 2) var(--padding);
[data-slot="zen logo dark"] {
display: none;
}
@media (max-width: 30rem) {
padding: var(--vertical-padding) var(--padding)
padding: var(--vertical-padding) var(--padding);
}
@media (prefers-color-scheme: dark) {
@@ -295,8 +289,6 @@ body {
}
}
p {
color: var(--color-text);
margin-bottom: 24px;
@@ -778,12 +770,10 @@ body {
border-bottom: 1px solid var(--color-border-weak);
}
[data-slot="cell"] {
flex: 1;
text-align: center;
a {
text-decoration: none;
padding: 2rem 0;
@@ -796,7 +786,6 @@ body {
@media (max-width: 40rem) {
display: none;
}
}
}
@@ -835,10 +824,18 @@ body {
[data-component="legal"] {
color: var(--color-text-weak);
text-align: center;
display: flex;
gap: 32px;
justify-content: center;
a {
color: var(--color-text-weak);
text-decoration: none;
}
a:hover {
color: var(--color-text);
text-decoration: underline;
}
}
}
+10 -4
View File
@@ -1,5 +1,5 @@
import "./index.css"
import { createAsync } from "@solidjs/router"
import { createAsync, query, redirect } from "@solidjs/router"
import { Title, Meta, Link } from "@solidjs/meta"
import { HttpHeader } from "@solidjs/start"
import zenLogoLight from "../../asset/zen-ornate-light.svg"
@@ -16,10 +16,16 @@ import { Faq } from "~/component/faq"
import { Legal } from "~/component/legal"
import { Footer } from "~/component/footer"
import { Header } from "~/component/header"
import { queryIsLoggedIn } from "~/routes/workspace/common"
import { getLastSeenWorkspaceID } from "../workspace/common"
const checkLoggedIn = query(async () => {
"use server"
const workspaceID = await getLastSeenWorkspaceID()
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
}, "checkLoggedIn.get")
export default function Home() {
const isLoggedIn = createAsync(() => queryIsLoggedIn())
createAsync(() => checkLoggedIn())
return (
<main data-page="zen">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
@@ -105,7 +111,7 @@ export default function Home() {
</div>
</div>
<a href="/auth">
<span>{isLoggedIn() ? "Go to workspace " : "Get started with Zen "}</span>
<span>Get started with Zen </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"
@@ -0,0 +1,5 @@
export class AuthError extends Error {}
export class CreditsError extends Error {}
export class MonthlyLimitError extends Error {}
export class UserLimitError extends Error {}
export class ModelError extends Error {}
@@ -1,73 +1,56 @@
import { z } from "zod"
import type { APIEvent } from "@solidjs/start/server"
import path from "node:path"
import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { BillingTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { Resource } from "@opencode-ai/console-resource"
import { Billing } from "../../../../core/src/billing"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { ZenModel } from "@opencode-ai/console-core/model.js"
import { ZenData } from "@opencode-ai/console-core/model.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
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 } from "./error"
import {
createBodyConverter,
createStreamPartConverter,
createResponseConverter,
} from "./provider/provider"
import { anthropicHelper } from "./provider/anthropic"
import { openaiHelper } from "./provider/openai"
import { oaCompatHelper } from "./provider/openai-compatible"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type Model = ZenData["models"][string]
export async function handler(
input: APIEvent,
opts: {
modifyBody?: (body: any) => any
setAuthHeader: (headers: Headers, apiKey: string) => void
format: ZenData.Format
parseApiKey: (headers: Headers) => string | undefined
onStreamPart: (chunk: string) => void
getStreamUsage: () => any
normalizeUsage: (body: any) => {
inputTokens: number
outputTokens: number
reasoningTokens?: number
cacheReadTokens?: number
cacheWrite5mTokens?: number
cacheWrite1hTokens?: number
}
},
) {
class AuthError extends Error {}
class CreditsError extends Error {}
class MonthlyLimitError extends Error {}
class UserLimitError extends Error {}
class ModelError extends Error {}
type Model = z.infer<typeof ZenModel.ModelSchema>
const FREE_WORKSPACES = [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
]
const logger = {
metric: (values: Record<string, any>) => {
console.log(`_metric:${JSON.stringify(values)}`)
},
log: console.log,
debug: (message: string) => {
if (Resource.App.stage === "production") return
console.debug(message)
},
}
try {
const url = new URL(input.request.url)
const body = await input.request.json()
logger.debug(JSON.stringify(body))
logger.metric({
is_tream: !!body.stream,
session: input.request.headers.get("x-opencode-session"),
request: input.request.headers.get("x-opencode-request"),
})
const modelInfo = validateModel(body.model)
const providerInfo = selectProvider(modelInfo)
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, body.model)
const providerInfo = selectProvider(
zenData,
modelInfo,
input.request.headers.get("x-real-ip") ?? "",
)
const authInfo = await authenticate(modelInfo, providerInfo)
validateBilling(modelInfo, authInfo)
validateModelSettings(authInfo)
@@ -76,22 +59,28 @@ export async function handler(
// Request to model provider
const startTimestamp = Date.now()
const res = await fetch(path.posix.join(providerInfo.api, url.pathname.replace(/^\/zen\/v1/, "") + url.search), {
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
const reqBody = JSON.stringify(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
model: providerInfo.model,
}),
)
logger.debug("REQUEST URL: " + reqUrl)
logger.debug("REQUEST: " + reqBody)
const res = await fetch(reqUrl, {
method: "POST",
headers: (() => {
const headers = input.request.headers
headers.delete("host")
headers.delete("content-length")
opts.setAuthHeader(headers, providerInfo.apiKey)
providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
})
return headers
})(),
body: JSON.stringify({
...(opts.modifyBody?.(body) ?? body),
model: providerInfo.model,
}),
body: reqBody,
})
// Scrub response headers
@@ -102,14 +91,19 @@ export async function handler(
resHeaders.set(k, v)
}
}
logger.debug("STATUS: " + res.status + " " + res.statusText)
if (res.status === 400 || res.status === 503) {
logger.debug("RESPONSE: " + (await res.text()))
}
// Handle non-streaming response
if (!body.stream) {
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
const json = await res.json()
const body = JSON.stringify(json)
const body = JSON.stringify(responseConverter(json))
logger.metric({ response_length: body.length })
logger.debug(body)
await trackUsage(authInfo, modelInfo, providerInfo.id, json.usage)
logger.debug("RESPONSE: " + body)
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
await reload(authInfo)
return new Response(body, {
status: res.status,
@@ -119,10 +113,13 @@ export async function handler(
}
// Handle streaming response
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
const usageParser = providerInfo.createUsageParser()
const stream = new ReadableStream({
start(c) {
const reader = res.body?.getReader()
const decoder = new TextDecoder()
const encoder = new TextEncoder()
let buffer = ""
let responseLength = 0
@@ -134,9 +131,9 @@ export async function handler(
response_length: responseLength,
"timestamp.last_byte": Date.now(),
})
const usage = opts.getStreamUsage()
const usage = usageParser.retrieve()
if (usage) {
await trackUsage(authInfo, modelInfo, providerInfo.id, usage)
await trackUsage(authInfo, modelInfo, providerInfo, usage)
await reload(authInfo)
}
c.close()
@@ -156,12 +153,21 @@ export async function handler(
const parts = buffer.split("\n\n")
buffer = parts.pop() ?? ""
for (const part of parts) {
logger.debug(part)
opts.onStreamPart(part.trim())
for (let part of parts) {
logger.debug("PART: " + part)
part = part.trim()
usageParser.parse(part)
if (providerInfo.format !== opts.format) {
part = streamConverter(part)
c.enqueue(encoder.encode(part + "\n\n"))
}
}
c.enqueue(value)
if (providerInfo.format === opts.format) {
c.enqueue(value)
}
return pump()
}) || Promise.resolve()
@@ -211,27 +217,47 @@ export async function handler(
)
}
function validateModel(reqModel: string) {
const json = JSON.parse(Resource.ZEN_MODELS.value)
const allModels = ZenModel.ModelsSchema.parse(json)
if (!(reqModel in allModels)) {
function validateModel(zenData: ZenData, reqModel: string) {
if (!(reqModel in zenData.models)) {
throw new ModelError(`Model ${reqModel} not supported`)
}
const modelId = reqModel as keyof typeof allModels
const modelData = allModels[modelId]
const modelId = reqModel as keyof typeof zenData.models
const modelData = zenData.models[modelId]
logger.metric({ model: modelId })
return { id: modelId, ...modelData }
}
function selectProvider(model: Awaited<ReturnType<typeof validateModel>>) {
function selectProvider(
zenData: ZenData,
model: Awaited<ReturnType<typeof validateModel>>,
ip: string,
) {
const providers = model.providers
.filter((provider) => !provider.disabled)
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
return providers[Math.floor(Math.random() * providers.length)]
// Use the last 2 characters of IP address to select a provider
const lastChars = ip.slice(-2)
const index = parseInt(lastChars, 16) % providers.length
const provider = providers[index || 0]
if (!(provider.id in zenData.providers)) {
throw new ModelError(`Provider ${provider.id} not supported`)
}
const format = zenData.providers[provider.id].format
return {
...provider,
...zenData.providers[provider.id],
...(format === "anthropic"
? anthropicHelper
: format === "openai"
? openaiHelper
: oaCompatHelper),
}
}
async function authenticate(
@@ -255,6 +281,7 @@ export async function handler(
monthlyLimit: BillingTable.monthlyLimit,
monthlyUsage: BillingTable.monthlyUsage,
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
reloadTrigger: BillingTable.reloadTrigger,
},
user: {
id: UserTable.id,
@@ -270,11 +297,20 @@ export async function handler(
.from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))
.innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)))
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)))
.innerJoin(
UserTable,
and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)),
)
.leftJoin(
ModelTable,
and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)),
)
.leftJoin(
ProviderTable,
and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)),
and(
eq(ProviderTable.workspaceID, KeyTable.workspaceID),
eq(ProviderTable.provider, providerInfo.id),
),
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]),
@@ -298,12 +334,20 @@ export async function handler(
}
function validateBilling(model: Model, authInfo: Awaited<ReturnType<typeof authenticate>>) {
if (!authInfo || authInfo.isFree) return
if (!authInfo) return
if (authInfo.provider?.credentials) return
if (authInfo.isFree) return
if (model.allowAnonymous) return
const billing = authInfo.billing
if (!billing.paymentMethodID) throw new CreditsError("No payment method")
if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
if (!billing.paymentMethodID)
throw new CreditsError(
`No payment method. Add a payment method here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
)
if (billing.balance <= 0)
throw new CreditsError(
`Insufficient balance. Manage your billing here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
)
const now = new Date()
const currentYear = now.getUTCFullYear()
@@ -318,7 +362,7 @@ export async function handler(
const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
if (currentYear === dateYear && currentMonth === dateMonth)
throw new MonthlyLimitError(
`Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}.`,
`Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
)
}
@@ -331,7 +375,9 @@ export async function handler(
const dateYear = authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear()
const dateMonth = authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
if (currentYear === dateYear && currentMonth === dateMonth)
throw new UserLimitError(`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}.`)
throw new UserLimitError(
`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
)
}
}
@@ -352,15 +398,22 @@ export async function handler(
async function trackUsage(
authInfo: Awaited<ReturnType<typeof authenticate>>,
modelInfo: ReturnType<typeof validateModel>,
providerId: string,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
usage: any,
) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
opts.normalizeUsage(usage)
const {
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWrite5mTokens,
cacheWrite1hTokens,
} = providerInfo.normalizeUsage(usage)
const modelCost =
modelInfo.cost200K &&
inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000
inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) >
200_000
? modelInfo.cost200K
: modelInfo.cost
@@ -411,13 +464,14 @@ export async function handler(
if (!authInfo) return
const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
const cost =
authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID: authInfo.workspaceID,
id: Identifier.create("usage"),
model: modelInfo.id,
provider: providerId,
provider: providerInfo.id,
inputTokens,
outputTokens,
reasoningTokens,
@@ -451,7 +505,9 @@ export async function handler(
`,
timeMonthlyUsageUpdated: sql`now()`,
})
.where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)))
.where(
and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)),
)
})
await Database.use((tx) =>
@@ -477,8 +533,14 @@ export async function handler(
and(
eq(BillingTable.workspaceID, authInfo.workspaceID),
eq(BillingTable.reload, true),
lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)),
or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
lt(
BillingTable.balance,
centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100),
),
or(
isNull(BillingTable.timeReloadLockedTill),
lt(BillingTable.timeReloadLockedTill, sql`now()`),
),
),
),
)

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