Compare commits

...

548 Commits

Author SHA1 Message Date
opencode 008ccb4729 release: v1.0.71 2025-11-18 01:59:40 +00:00
Dax Raad bc232045a1 respect server suggestion for default model 2025-11-17 20:53:48 -05:00
GitHub Action 16cab556df chore: format code 2025-11-18 01:27:22 +00:00
Jay V 66148df74b docs: clarify custom tools can execute scripts in any language with Python example 2025-11-17 20:26:27 -05:00
opencode 4611e08f09 release: v1.0.70 2025-11-17 23:45:49 +00:00
Sebastian Herrlinger bf6204f577 upgrade opentui to v0.1.46
- enable bracketed paste (and more) on win
- fix word wrapping with CJK and at wrap/chunk boundaries
- old style meta+arrow
- allow <1 scroll speed for slowdown
2025-11-18 00:22:21 +01:00
Daniel Hofheinz 17cde9feb7 docs: add built-in agents reference to README (#3047)
Co-authored-by: Jay V <air@live.ca>
2025-11-17 17:19:14 -05:00
Aiden Cline 7eccbdc4ac fix /exit 2025-11-17 16:13:41 -06:00
Aiden Cline ab072290fc Revert "fix: system theme background to use 'none' for terminal transparency (#4408)"
This reverts commit f4a4514a9f.
2025-11-17 16:03:27 -06:00
GitHub Action ad9d83748c chore: format code 2025-11-17 21:46:10 +00:00
Aiden Cline 55b57e1aae ci: tweak 2025-11-17 15:45:23 -06:00
Aiden Cline 21b7877beb docs: tweak wording 2025-11-17 21:26:46 +00:00
opencode de50234a1a release: v1.0.69 2025-11-17 21:26:46 +00:00
opencode-agent[bot] d60102ba52 Added /thinking slash command to toggle thinking blocks visibility in OpenTUI. (#4424)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-17 15:16:35 -06:00
Haris Gušić 066a876f3d docs(contributing): Add "Setting up a Debugger" section (#4421)
Co-authored-by: GitHub Action <action@github.com>
2025-11-17 14:28:06 -06:00
Haris Gušić c07a241ca8 chore: Remove obsolete 'any' type annotation (#4423) 2025-11-17 14:27:43 -06:00
Aiden Cline 0a2fffa9b5 tweak: whitelist 2025-11-17 13:18:13 -06:00
Dax Raad bdfa213ccf deprecated session.idle event 2025-11-17 11:42:45 -05:00
Aiden Cline 7f0b2ce1ac Reapply "fix: system theme background to use 'none' for terminal transparency" (#4415)
This reverts commit a5365ce294.
2025-11-17 10:39:53 -06:00
Dax Raad 0a2d7af179 core: honor retry-after values exceeding 10 minutes instead of discarding them 2025-11-17 11:33:28 -05:00
Dax Raad 37652f48fb ignore 2025-11-17 11:32:07 -05:00
Dax Raad 8b19c6c7e4 better retry display 2025-11-17 11:31:10 -05:00
Aiden Cline a5365ce294 Revert "fix: system theme background to use 'none' for terminal transparency" (#4415) 2025-11-17 10:24:20 -06:00
Jensen f4a4514a9f fix: system theme background to use 'none' for terminal transparency (#4408) 2025-11-17 10:22:31 -06:00
opencode-agent[bot] 154006469c Updated help dialog to use dynamic keybind (#4414)
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-17 10:18:59 -06:00
Dax a1214fff2e Refactor agent loop (#4412) 2025-11-17 10:57:18 -05:00
GitHub Action 9fd43ec616 ignore: update download stats 2025-11-17 2025-11-17 12:04:41 +00:00
Luke Parker 5731c268b6 fix: Line count on win (#4401) 2025-11-17 01:08:22 -06:00
Keath Milligan f4d892d4e1 fix: handle Git Bash path mapping on windows (#4380) 2025-11-17 01:06:44 -06:00
Aiden Cline 10b3702938 chore: update type 2025-11-17 00:07:23 -06:00
Tyler Gannon e96442310c chore: replace z.union with z.enum for cleaner OpenAPI generation (#4394) 2025-11-17 00:06:40 -06:00
Spoon 5c722bf8c4 fix(batch): simple UX feedback (#4396) 2025-11-17 00:02:05 -06:00
Youssef Achy 58cc5cdf2a add support for azure cognitive services provider (#4397) 2025-11-17 00:01:45 -06:00
opencode-agent[bot] 3c6dcad2af Fixed OPENCODE_CONFIG_DIR to load config files. (#4400)
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-16 23:48:36 -06:00
Jay 2535f9febf docs: Add clarification for projects using 'opencode' name
Added a section to clarify the affiliation of related projects.
2025-11-16 20:51:41 -05:00
Aiden Cline 25678fa504 fix: vercel gateway options 2025-11-16 18:39:31 -06:00
Sebastian Herrlinger d7f4f3ec1f bump opentui version to 0.1.45, fixing highlighting on windows 2025-11-16 23:56:11 +01:00
Aiden Cline 16ccb39459 docs: permissions 2025-11-16 16:40:48 -06:00
Aiden Cline f8630fb188 ignore: rm 2025-11-16 16:32:04 -06:00
Baptiste Cavallo 72e604744d fix(batch): restore per-tool UI feedback + UX improvements (#4387) 2025-11-16 16:31:41 -06:00
opencode-agent[bot] 832be6e7eb Added copy option to message context menu (#4389)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-16 15:35:05 -06:00
opencode 8ba48ed71d release: v1.0.68 2025-11-16 20:38:48 +00:00
Aiden Cline cf266f6162 fix: promptCacheKey set unnecessarily 2025-11-16 14:32:57 -06:00
GitHub Action 1e6589526d ignore: update download stats 2025-11-16 2025-11-16 12:04:11 +00:00
Frank f6b3ffaf64 wip: zen 2025-11-16 03:32:13 -05:00
GitHub Action 5d765d63d4 chore: format code 2025-11-16 08:30:36 +00:00
Frank 0e12dd62a3 zen: usage paging 2025-11-16 03:29:52 -05:00
opencode 2b957b5d1c release: v1.0.67 2025-11-16 07:49:52 +00:00
GitHub Action 31c7a0157c chore: format code 2025-11-16 07:44:06 +00:00
Aiden Cline e728b94bca fix: panic when theme has 'none' 2025-11-16 01:43:23 -06:00
opencode 49040c0130 release: v1.0.66 2025-11-16 07:27:25 +00:00
Aiden Cline 0d05238ee6 fix: initial val 2025-11-16 01:14:49 -06:00
Aiden Cline 9b8a7da1e6 fix: history jsonl file corruption cases (#4364) 2025-11-16 00:50:13 -06:00
Zeno Jiricek 61fd21182c docs: mise installation command (#2938) 2025-11-15 21:44:28 -06:00
GitHub Action 487c2b5e76 chore: format code 2025-11-16 03:38:13 +00:00
xiaojie.zj 0e4703b227 add: add zenmux doc and header (#3597)
Co-authored-by: xiaojie.zj <xiaojie.zj@antgroup.com>
2025-11-15 21:37:30 -06:00
Alvin Johansson 84e0232bd5 Add Flexoki theme (#3986) 2025-11-15 21:28:13 -06:00
Luke Parker 35fbb011b2 fix: Diff view now ignores line endings changes/windows autocrlf (#4356) 2025-11-15 21:18:39 -06:00
Aiden Cline 6527a123f0 fix aur build (#4359) 2025-11-15 20:16:19 -06:00
Aiden Cline 0377cfd37c fix: omit ref for todo tool 2025-11-15 19:19:36 -06:00
Aiden Cline edc933d816 tweak: make zod error more prompty 2025-11-15 13:19:24 -06:00
GitHub Action 0d608f6014 ignore: update download stats 2025-11-15 2025-11-15 12:04:09 +00:00
Chris Olszewski 69a45ef7d7 fix: snapshot history when running from git worktrees (#4312) 2025-11-15 01:02:00 -06:00
Baptiste Cavallo 1056b36eae experimental batch tool (#2983)
Co-authored-by: GitHub Action <action@github.com>
2025-11-15 00:54:36 -06:00
Aiden Cline 35c737ac68 tweak: only show dropdown for 3+ items (#4345) 2025-11-14 23:45:48 -06:00
Abílio Costa 725a2c2e95 docs: clarify that config files are merged, not replaced (#4342)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-14 17:49:47 -06:00
Tyler Gannon c724d2392f fix: replace union type with enum "true"/"false" in /find/file endpoint (#4338) 2025-11-14 17:48:23 -06:00
Frank f5230d1f02 fix: incorrect sonnet price calculation 2025-11-14 18:46:43 -05:00
GitHub Action 078111bd96 chore: format code 2025-11-14 22:44:36 +00:00
sredfern 736f8882f5 fix(provider): support local file paths for custom providers (#4323) 2025-11-14 16:43:59 -06:00
Brian Cheung 37cf365927 feat: support images in mcp tool responses (#4100)
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-14 15:00:52 -06:00
Aiden Cline b939470302 fix: add azure exclusion 2025-11-14 11:54:00 -06:00
Aiden Cline ef4b2baedc set verbosity to low for gpt-5.1 (match codex) 2025-11-14 11:52:29 -06:00
Dax Raad 64d28ea457 fix sdk types 2025-11-14 12:42:46 -05:00
Dax Raad 2520780846 fix sdk types 2025-11-14 12:42:46 -05:00
Shantur Rathore 986c60353e set promptCacheKey for openai compatible providers (#4203)
Co-authored-by: GitHub Action <action@github.com>
2025-11-14 11:41:01 -06:00
Dax Raad 5fc26c958a add global.event.subscribe() to sdk 2025-11-14 12:32:43 -05:00
Frank c1cf9cda6a doc: add baseten provider 2025-11-14 12:19:58 -05:00
GitHub Action 10d376eab2 ignore: update download stats 2025-11-14 2025-11-14 12:04:48 +00:00
Frank 53fc8a861b zen: add gpt-5-nano model 2025-11-14 00:59:42 -05:00
Frank 1d8330331c zen: use gpt-5-nano as small model 2025-11-14 00:59:00 -05:00
Frank 7a03c7fe38 zen: add gpt5.1 to docs 2025-11-13 23:47:38 -05:00
Frank 09bd32169c zen: hide alpha models 2025-11-13 23:10:06 -05:00
Dax Raad 7ec32f834e improve read tool end-of-file detection to prevent infinite loops 2025-11-13 21:41:06 -05:00
GitHub Action 205492c7e8 chore: format code 2025-11-14 01:16:58 +00:00
Aiden Cline 4c2e888709 no mr llm, you may not read that 2025-11-13 19:16:07 -06:00
opencode c78fd097d1 release: v1.0.65 2025-11-14 00:10:30 +00:00
Dax Raad 340966195b handle config errors gracefully 2025-11-13 18:59:09 -05:00
GitHub Action 92604b391b chore: format code 2025-11-13 22:39:53 +00:00
Aiden Cline 0c51feb9c2 fix: max tokens when using models like opus with providers other than anthropic (#4307) 2025-11-13 16:39:09 -06:00
opencode d0b4169a6b release: v1.0.64 2025-11-13 22:12:44 +00:00
Aiden Cline 1fc6c6fb2a fix: typeerror case 2025-11-13 15:51:23 -06:00
Adam 14f9b95557 fix(desktop): default theme 2025-11-13 15:26:36 -06:00
GitHub Action d3bf1fa1fa chore: format code 2025-11-13 20:48:10 +00:00
Adam a8836c5615 wip(desktop): layout improvements 2025-11-13 14:47:29 -06:00
Aiden Cline 779a27693a fix: opencode run timeout 2025-11-13 14:27:33 -06:00
GitHub Action 829d86840a chore: format code 2025-11-13 19:42:31 +00:00
Valerio Di Maggio e225294dd4 Fix: unreadable texts in light mode (#4301) 2025-11-13 13:41:56 -06:00
opencode a673e3650d release: v1.0.63 2025-11-13 19:00:14 +00:00
Aiden Cline ff462dfd7a fix: windows install (#4293)
Co-authored-by: GitHub Action <action@github.com>
2025-11-13 12:22:07 -06:00
Luke Parker 73443585e5 fix: resolve bun/pnpm global install failures on Windows (#4275)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-13 10:38:57 -06:00
Tommy D. Rossi 609ab069a9 Add scroll acceleration support to TUI (#4289) 2025-11-13 17:02:10 +01:00
GitHub Action ec3579d7cb ignore: update download stats 2025-11-13 2025-11-13 12:04:32 +00:00
Aiden Cline f80a3fea31 fixes 2025-11-12 22:05:07 -06:00
Luke Parker 43a8d1b1ae fix: Enable Windows builds and fix bun+pnpm install on Windows (#4273) 2025-11-12 21:57:44 -06:00
Aiden Cline 09fa84ccfc fix: dirty check 2025-11-12 19:03:46 -06:00
GitHub Action b981f0a205 chore: format code 2025-11-13 00:53:22 +00:00
Aiden Cline 767038afc3 ci: update zed sync 2025-11-12 18:52:39 -06:00
opencode a7774115c5 release: v1.0.62 2025-11-13 00:13:18 +00:00
Luke Parker 288bc88e40 fix: Tool calling on windows (#4234) 2025-11-12 17:47:39 -06:00
Aiden Cline 6d36dbf9de fix: github action dirty check (#4262) 2025-11-12 16:16:07 -06:00
OpeOginni 4ab4baf3a4 feat(sidebar): add expandable sections for sidebar (#4132)
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 16:15:17 -06:00
phantomreactor 90f05eb9c2 paste images in wsl using ctrl+v (#4123)
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 15:10:23 -06:00
Melih Mucuk b63b6d04c6 Fix usage & billing for custom model aliases and cached/reasoning tokens (#4222)
Co-authored-by: Melih Mucuk <melih@monkeysteam.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-12 13:59:35 -06:00
Aiden Cline 8addaa7e08 fix: custom model name merging 2025-11-12 13:55:13 -06:00
Elias a96bf8e62d docs: OVHcloud AI Endpoints provider (#4257) 2025-11-12 13:28:35 -06:00
Ivan c8bda598f5 fix: correct cache cost for OpenRouter and other OpenAI-compatible providers (#4256) 2025-11-12 12:41:44 -06:00
Adam c857cff585 fix(desktop): double listing dir 2025-11-12 12:17:54 -06:00
Aiden Cline fd9d2db755 ci: update zed sync 2025-11-12 10:52:20 -06:00
Aiden Cline b19fd14f80 ignore: make issue button send opencode version too 2025-11-12 10:40:48 -06:00
Sebastian Herrlinger a0f469095c upgrade opentui to 0.1.42, fixing some CJK/grapheme issues with prompt extmarks and char corruption 2025-11-12 15:35:16 +01:00
Adam 0ccb26df94 feat(desktop): sticky diff headers 2025-11-12 07:03:39 -06:00
Adam 71fd5966ad fix(desktop): styling tweaks 2025-11-12 07:03:38 -06:00
GitHub Action c02230de4f ignore: update download stats 2025-11-12 2025-11-12 12:05:15 +00:00
Filip aa2e2c76c0 fix: clangd hanging fixed (#3611)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-12 00:21:55 -06:00
opencode 7c2d4ee79a release: v1.0.61 2025-11-12 03:10:55 +00:00
Dax Raad e3a2728fa3 tui: add double-esc interrupt mechanism for long-running operations
Users can now press escape twice within 5 seconds to interrupt long-running
operations in the TUI. The first press shows a visual hint, and the second
press aborts the current session.
2025-11-11 22:04:00 -05:00
Boston Cartwright 18260b037b feat: add SourceKit LSP support (#1545)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-11 20:51:33 -06:00
Dax Raad ad83dd3ad9 tui: fix autocomplete display to prevent long file paths from breaking layout 2025-11-12 02:36:43 +00:00
opencode 6f37315cd1 release: v1.0.60 2025-11-12 02:36:42 +00:00
Dax d81dce6a82 fix: add support for loading custom themes from .opencode/themes directory (#4229)
Co-authored-by: GitHub Action <action@github.com>
2025-11-11 21:30:38 -05:00
opencode 0bd11e970b release: v1.0.59 2025-11-12 02:07:41 +00:00
Dax Raad 7e29e1dd23 better errors on initial tui boot 2025-11-11 21:01:45 -05:00
Rafał Krzyważnia 491a2adf8d fix: resolve @file references in slash commands with subagents (#4221) 2025-11-11 19:38:50 -06:00
Aiden Cline c07d6487a8 fix config ordering (#4228) 2025-11-11 19:27:34 -06:00
Aiden Cline 9990e84d37 fix: ensure revert dialog moves that prompt to input box (#4227) 2025-11-11 19:08:59 -06:00
Aiden Cline 0b86adbe99 feat: agent color cfg (#4226)
Co-authored-by: 0xrin <0xrin1@protonmail.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-11 18:32:44 -06:00
Frank 834a2c09d5 wip: poc pr command 2025-11-11 18:50:28 -05:00
Frank f13c17e654 wip: poc pr command 2025-11-11 18:50:28 -05:00
Julian LaNeve a0611d92e4 docs: Update config references to latest Sonnet & Haiku models (#4210) 2025-11-11 16:52:45 -06:00
Aiden Cline 0b001c3e80 tweak: make todos appear list of modified files 2025-11-11 16:05:23 -06:00
Sebastian Herrlinger 53b7cb62c4 upgrade opentui to 0.1.41:
- enables modifyOtherKeys to get CSI u sequences in terminals that support it
- uses Private Mode 2026 for synced rendering to fix cursor flickering in terminals like iTerm2
- lazy highlighting for code renderables (perf)
- linear scroll acceleration by default
- align textarea default bindings more with readline
- fix vertical cursor movement in textarea
- introduce stdin buffer to handle chunked sequences
- improve capability detection (async)
- renderer emits focus/blur events when app is focused/blurred (if supported by terminal)
2025-11-11 23:00:31 +01:00
Aiden Cline c5e096c76a fix: costs being 0 when using custom model id overrides (#4219) 2025-11-11 15:58:14 -06:00
Aiden Cline e1fc4a756b Hide /share if disabled (#4215) 2025-11-11 14:47:39 -06:00
Aiden Cline e5bc4cbbcf ci: update changelog script 2025-11-11 14:27:13 -06:00
GitHub Action 459d5ec19b chore: format code 2025-11-11 20:21:00 +00:00
Aiden Cline 8baa222621 ci: update script 2025-11-11 14:20:19 -06:00
Dax Raad ce1397cc34 core: add test to verify OpenCode doesn't crash when starting in git repositories with no commit history 2025-11-11 20:17:36 +00:00
Ron Suhodrev dc7c5ced4c tui: restore full text when editing prompts with summarized content (#4030) 2025-11-11 20:17:36 +00:00
Corwin Marsh b8e8fe7e31 docs: Update dead Context7 mcp server link (#4207)
Co-authored-by: Corwin Marsh <corwinm@users.noreply.github.com>
2025-11-11 20:17:36 +00:00
opencode 890085758f release: v1.0.58 2025-11-11 20:17:36 +00:00
Dax Raad 85f15893bc core: prevent crash when starting in repositories without any commits yet 2025-11-11 15:11:42 -05:00
Adam 98be75b17c fix(desktop): give review pane more width 2025-11-11 13:02:59 -06:00
GitHub Action b5cc27b8ea chore: format code 2025-11-11 18:38:23 +00:00
Frank 05937b52cc chore: format code 2025-11-11 13:37:36 -05:00
GitHub Action 62b82570e1 chore: format code 2025-11-11 17:34:09 +00:00
Dax Raad 4bf75c0b44 core: remove unused experimental flags for turn summary and no-bootstrap to simplify feature flag management 2025-11-11 12:33:26 -05:00
opencode a8a06c4983 release: v1.0.57 2025-11-11 17:30:26 +00:00
Dax Raad b0b7fd143b tui: show LSP diagnostics inline when viewing files so users can see type errors and compilation issues without leaving the interface 2025-11-11 12:15:40 -05:00
GitHub Action 140498eb4f chore: format code 2025-11-11 16:59:37 +00:00
Haris Gušić ca5126e24d fix: TUI spawn: reset BUN_OPTIONS (#3606) 2025-11-11 10:58:59 -06:00
Josiah Witt fb2b3e567c docs: update keymap.json bindings for OpenCode command (#4192) 2025-11-11 10:48:10 -06:00
Adam c672a1963b fix(desktop): prompt clearing inconsistent 2025-11-11 09:35:08 -06:00
Adam 54bff6b120 fix(desktop): code/diff number container width 2025-11-11 09:22:35 -06:00
Adam ab3f198fab fix(desktop): session show more hidden on new session 2025-11-11 09:11:34 -06:00
Adam 0057ef6336 fix(desktop): prompt input not clearing, attachments flaky 2025-11-11 09:01:28 -06:00
Adam 4f604b3839 fix(desktop): color grouping 2025-11-11 09:01:27 -06:00
GitHub Action a20489584e ignore: update download stats 2025-11-11 2025-11-11 12:04:42 +00:00
Dax Raad a6b066bd47 ci 2025-11-11 02:15:33 -05:00
Dax Raad 37fdcac05a ci 2025-11-11 02:13:26 -05:00
Dax Raad 299bf1dca8 ci 2025-11-11 01:59:10 -05:00
Dax Raad d685aa38ef type checks 2025-11-11 01:56:01 -05:00
Dax Raad 995b23787c ci 2025-11-11 01:48:29 -05:00
Dax Raad ed8e663e13 ignore 2025-11-11 01:41:58 -05:00
Dax Raad 38cee3b848 ci: sync 2025-11-11 01:37:10 -05:00
Dax Raad 6d116d4b54 ci: fix 2025-11-11 01:35:50 -05:00
Aiden Cline 7c4f111b34 ignore: run bun i 2025-11-11 00:34:09 -06:00
Dax Raad f2fac29270 ci 2025-11-11 01:33:02 -05:00
Dax Raad 12892f0e12 ci: improve bun caching to invalidate when bun version changes in package.json 2025-11-11 01:31:24 -05:00
GitHub Action 9714a3558e chore: format code 2025-11-11 06:28:19 +00:00
Dax Raad e49a1d1f39 ci: fix 2025-11-11 01:27:39 -05:00
Dax Raad 528565510d sync 2025-11-11 01:25:39 -05:00
GitHub Action 36cfda933d chore: format code 2025-11-11 06:24:58 +00:00
Dax Raad ecf5040966 tui: update @opentui/core to v0.1.39 and fix build script for new target format 2025-11-11 01:24:17 -05:00
Frank 7d56603c26 zen: failover on error 2025-11-11 00:29:44 -05:00
Aiden Cline 02b7cc8313 keep session dot in list for current active (#4185) 2025-11-10 22:20:35 -06:00
Dax Raad c9a52c9a85 cache project id in root git folder 2025-11-10 21:57:55 -05:00
Dax Raad dea668b0ea tui: help users read thinking blocks and trust todo syncs 2025-11-10 20:34:04 -05:00
Aiden Cline 1bc3e92376 fix: undefined check 2025-11-10 19:21:57 -06:00
Dax Raad 3f5acc3dff add web and codesearch tools 2025-11-10 16:39:54 -05:00
Aiden Cline 0588011476 ignore: bump copilot plugin version 2025-11-10 13:40:15 -06:00
OpeOginni bba72c82ae Fix/google vertex configs (#4169)
Co-authored-by: GitHub Action <action@github.com>
2025-11-10 13:25:03 -06:00
denesbeck e95181a551 Refactor/redundant toast comp (#4163)
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-10 11:27:19 -06:00
GitHub Action 74e8c2e50f chore: format code 2025-11-10 17:19:20 +00:00
David Hill cdabafa264 wip code theme inc light 2025-11-10 17:18:37 +00:00
denesbeck 0a92af60a0 fix: upgrade toast notification (#4159) 2025-11-10 10:28:28 -06:00
David Hill c7808a4b01 wip code theme 2025-11-10 16:16:50 +00:00
David Hill 7f978e07ff wip code theme 2025-11-10 16:14:24 +00:00
David Hill a4ae1bb9eb wip code theme 2025-11-10 16:00:05 +00:00
David Hill 96a39803cc wip code theme 2025-11-10 15:58:56 +00:00
GitHub Action 16f8f20b31 chore: format code 2025-11-10 14:51:24 +00:00
David Hill 06b1684ddb wip code editor update dark mode 2025-11-10 14:50:36 +00:00
David Hill c6e830c954 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-11-10 13:44:12 +00:00
GitHub Action fc78c28df6 ignore: update download stats 2025-11-10 2025-11-10 12:04:53 +00:00
opencode 930a1bf358 release: v1.0.55 2025-11-10 06:47:55 +00:00
GitHub Action 6cf7f18cc9 chore: format code 2025-11-10 06:41:40 +00:00
Aiden Cline 3f59570ee6 fix: add null check 2025-11-10 00:40:42 -06:00
GitHub Action 304e956b5d chore: format code 2025-11-10 06:00:38 +00:00
Wankong 538eaa42aa docs: format installation commands for madkrown code block (#4151)
Co-authored-by: GitHub Action <action@github.com>
2025-11-09 23:59:57 -06:00
opencode 67c41fd389 release: v1.0.54 2025-11-10 05:50:33 +00:00
GitHub Action 83ea19770a chore: format code 2025-11-10 05:44:26 +00:00
Dax Raad 3ace8543b2 tui: auto-scroll to bottom when switching between sessions 2025-11-10 00:43:48 -05:00
opencode eb855e1e31 release: v1.0.53 2025-11-10 05:42:55 +00:00
Dax Raad 5e53f054c6 make reasoning parts less ugly 2025-11-10 00:37:35 -05:00
Dax Raad 5d5e184329 tui: improve session UI with better sidebar toggle and message handling 2025-11-10 00:21:31 -05:00
Dax Raad 2fbb49ac30 tui: render reasoning parts with syntax highlighting instead of plain text 2025-11-10 00:08:05 -05:00
Dax Raad c56b407e1d tui: display 'Free' badge for zero-cost models in model selection dialog 2025-11-09 23:56:24 -05:00
lenstr bdaa0e8b8c fix: --model flag being ignored in TUI mode (#4147) 2025-11-09 20:21:52 -06:00
Aiden Cline 4e549b1c05 fix: allow user to configure doom loop & external dir perms (#4095) 2025-11-09 20:21:38 -06:00
Aiden Cline 7be8e16c33 ci: sync zed 2025-11-09 13:54:14 -06:00
Frank d1588f93a1 doc: add big pickle to doc 2025-11-09 14:12:54 -05:00
Aiden Cline 576696a370 ci: update description 2025-11-09 13:09:52 -06:00
Aiden Cline 2c6f9043e8 ci: fix 2025-11-09 13:06:32 -06:00
Aiden Cline 9f771ef0ae ci: fix 2025-11-09 13:05:51 -06:00
Aiden Cline 356715f67d ci: fix regex 2025-11-09 13:03:10 -06:00
Aiden Cline 540421267a fix 2025-11-09 12:56:17 -06:00
Aiden Cline e253398936 ci: ignore 2025-11-09 12:54:59 -06:00
Aiden Cline ee87e1f139 ci: ignore 2025-11-09 12:54:22 -06:00
Aiden Cline 8887616457 ci: sync zed 2025-11-09 12:47:36 -06:00
Aiden Cline 905c034885 ci: zed sync 2025-11-09 12:00:08 -06:00
Aiden Cline 92f7c4943f ci: zed sync 2025-11-09 11:59:00 -06:00
Aiden Cline 10bde356b1 chore: rm comment 2025-11-09 11:50:02 -06:00
Aiden Cline f7cc46cd9f set cap for max time to wait between retries (#4135)
Co-authored-by: GitHub Action <action@github.com>
2025-11-09 11:46:58 -06:00
Mathias Beugnon d9ffe07391 fix: messageID type in chat.message (#4128)
Co-authored-by: GitHub Action <action@github.com>
2025-11-09 09:39:50 -06:00
GitHub Action c0702ed8bd ignore: update download stats 2025-11-09 2025-11-09 12:04:16 +00:00
opencode b927b9dca6 release: v1.0.51
sync-zed-extension / Release Zed Extension (push) Has been cancelled
2025-11-09 06:46:42 +00:00
Dax Raad 4b7231be68 fix race condition 2025-11-09 01:41:49 -05:00
GitHub Action 70a6fe96ea chore: format code 2025-11-09 06:01:58 +00:00
Aiden Cline 6e5971dff2 ci: update sync zed 2025-11-09 00:01:13 -06:00
opencode d48d6b3577 release: v1.0.50
/ Release Zed Extension (push) Has been cancelled
2025-11-09 05:53:26 +00:00
Aiden Cline 4b1668c3ef Revert "tui: display 'Free' badge for zero-cost models in model selection dialog"
This reverts commit ce9b758d0a.
2025-11-08 23:48:18 -06:00
Mathias Beugnon d85eb1b880 feat: add input context to chat.params and chat.message (#4085) 2025-11-08 23:29:27 -06:00
Ivan Starkov 9637d70407 fix: UI Freezes for a few minutes if repo has binary files (#4109) 2025-11-08 23:28:09 -06:00
Aiden Cline cfbbdc2e14 ci: add job to sync zed extension 2025-11-08 23:23:43 -06:00
GitHub Action feb65201f6 chore: format code 2025-11-09 01:58:05 +00:00
opencode f1f07a56d8 release: v1.0.49 2025-11-09 01:58:04 +00:00
Dax Raad 0fe313bd87 tui: fix continue session navigation to wait for sync completion before redirecting
Previously, the continue session navigation would immediately try to redirect
to the most recent session before the sync data was fully loaded, causing
navigation to fail. Now it waits for sync status to be complete before
attempting the redirect, ensuring the session data is available.
2025-11-08 20:53:18 -05:00
opencode 1fd676528d release: v1.0.48 2025-11-09 01:44:10 +00:00
GitHub Action 0a2801444b chore: format code 2025-11-09 01:38:01 +00:00
Dax Raad c9adbc7c21 tui: add logging when creating project instances to help users debug startup issues 2025-11-08 20:37:08 -05:00
opencode ba8de38435 release: v1.0.47 2025-11-09 01:34:24 +00:00
Dax Raad 8166612467 tui: fix continue session navigation to use most recent session instead of oldest session 2025-11-08 20:28:23 -05:00
Dax Raad 4d20e1c3c6 Merge remote-tracking branch 'origin/dev' into dev 2025-11-08 20:21:02 -05:00
Dax Raad 4bb7ea9127 improve startup speed 2025-11-08 20:18:36 -05:00
GitHub Action 969af4d541 chore: format code 2025-11-08 22:32:25 +00:00
Christopher Sacca 271b679058 fix(lsp): handle optional requests to avoid MethodNotFound (-32601) with MATLAB Language Server (#4007) 2025-11-08 16:31:39 -06:00
GitHub Action 83b16cb18e chore: format code 2025-11-08 22:27:51 +00:00
Kamaal Farah 431ffc94f5 fix(theme): filter out null values from theme palette (#4083)
Signed-off-by: Kamaal Farah <kamaal.f1@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-08 16:27:07 -06:00
opencode 47b4cc6d53 release: v1.0.46 2025-11-08 21:48:37 +00:00
Dax Raad ce9b758d0a tui: display 'Free' badge for zero-cost models in model selection dialog 2025-11-08 16:20:30 -05:00
Frank f8a1a0b26f zen: fix billing button 2025-11-08 10:33:56 -05:00
GitHub Action 6ecaf83f76 chore: format code 2025-11-08 15:20:53 +00:00
Frank 30b1ae5d4b zen: rate limit 2025-11-08 10:18:21 -05:00
GitHub Action 9cd465f9c0 ignore: update download stats 2025-11-08 2025-11-08 12:04:10 +00:00
opencode 1747979568 release: v1.0.45 2025-11-08 02:25:51 +00:00
Dax Raad 062023fa06 chore(prompt): refine polaris instructions for practical behavior 2025-11-07 21:18:49 -05:00
Dax Raad 954a796b8a core: route polaris-alpha models to polaris system prompt 2025-11-07 21:07:59 -05:00
GitHub Action 34ff87d504 chore: format code 2025-11-08 01:59:02 +00:00
Dax Raad 16357e8041 chore: standardize prettier printWidth to 120 2025-11-07 20:58:17 -05:00
Dax Raad dabb1aa719 fix(prompt): prevent title generation refusal responses 2025-11-07 20:50:15 -05:00
Aiden Cline 7af3380455 chore: adjust cfg code 2025-11-07 19:18:57 -06:00
GitHub Action dcaa90808e chore: format code 2025-11-08 00:59:52 +00:00
Jay V 01705fd467 ignore: docs: fix type errors in sitemap generation script 2025-11-07 19:55:25 -05:00
Jay V 006f3bdeb6 ignore: docs: add sitemap.xml to gitignore 2025-11-07 19:51:47 -05:00
Jay V 1d43b4e6d7 ignore: docs: add automated sitemap generation for main app and docs routes 2025-11-07 19:51:46 -05:00
Aiden Cline 8cef7940fe allow reading of opencode.json(c) from .opencode/ 2025-11-07 17:35:08 -06:00
Sebastian Herrlinger b2dd9fdfdf upgrade to opentui 0.1.39 supporting shift/ctrl+return/esc custom csi in ghostty 2025-11-08 00:17:07 +01:00
Aiden Cline b82a52cb85 ignore: update log 2025-11-07 15:56:37 -06:00
Aiden Cline 7294d86778 ignore: symlink zed extension license 2025-11-07 15:41:01 -06:00
Aiden Cline 3bb3f4f2c9 ignore: fix 2025-11-07 15:32:19 -06:00
GitHub Action d31f97343c chore: format code 2025-11-07 21:28:26 +00:00
Jay V 536d10e5ab ignore: lander add canonical urls and h1 tags to all landing pages 2025-11-07 16:27:35 -05:00
Jay V 9885c716f3 ignore: lander use h1 tags for main headings on landing and zen pages 2025-11-07 16:27:34 -05:00
opencode 39461fbbce release: v1.0.44 2025-11-07 21:24:15 +00:00
Dax Raad 1a2b3701f2 tui: show more sessions in list and fix sync timing to prevent race conditions 2025-11-07 16:19:44 -05:00
Aiden Cline 0a395d8783 ignore: update version 2025-11-07 15:18:44 -06:00
Aiden Cline 79bb22a573 ci: auto update zed extension 2025-11-07 15:17:26 -06:00
GitHub Action 4271df96d2 chore: format code 2025-11-07 21:09:47 +00:00
Aiden Cline aa07be09e1 ignore: update zed extension 2025-11-07 15:08:50 -06:00
opencode 5d6bdca6d0 release: v1.0.43 2025-11-07 21:04:26 +00:00
Dax Raad 58bbe9e689 ci: add optional version parameter to publish workflow
Allows overriding the version when publishing releases instead of only using semantic bumping. This gives maintainers more control over release versioning for special cases or hotfixes.
2025-11-07 15:58:51 -05:00
Sebastian Herrlinger b5a035ceab upgrade to opentui to fix disappearing content (again) and sticky scroll 2025-11-07 21:38:40 +01:00
Aiden Cline b3c6d0b08a fix formatters 2025-11-07 14:10:20 -06:00
Aiden Cline 090d27df11 chore: rm debug logs 2025-11-07 14:00:18 -06:00
Adam b374a6cac9 fix(desktop): stop icon size 2025-11-07 13:58:52 -06:00
Aiden Cline 73cd8a334c rework acp to compensate for changes in Zed IDE (#4050) 2025-11-07 13:57:12 -06:00
Adam b46c3f2a26 fix(desktop): prompt input issues (wip) 2025-11-07 13:54:53 -06:00
Adam 45fabec091 fix(desktop): prompt input on non-chat tabs 2025-11-07 13:54:53 -06:00
Dax Raad a96365fd81 Add command bar action to rename sessions 2025-11-07 14:51:44 -05:00
GitHub Action 5f7e1e099b chore: format code 2025-11-07 19:47:53 +00:00
Dax Raad d462e380f4 fix: update references after moving message functions to MessageV2 namespace 2025-11-07 14:47:05 -05:00
Adam c5a558f3da chore(desktop): remove dead code 2025-11-07 13:34:41 -06:00
Adam 7f51b181d4 chore(desktop): cleanup shiki theme stuff 2025-11-07 13:30:07 -06:00
Adam 7adbc3ad44 fix(desktop): code tab padding 2025-11-07 13:14:57 -06:00
Adam 89922a8598 fix(desktop): prompt input missing on new session 2025-11-07 12:56:07 -06:00
Adam 3a1d1a6284 feat(desktop): custom syntax colors 2025-11-07 12:48:17 -06:00
Dax Raad 4463d319c9 fix scroll when no session exists 2025-11-07 13:22:03 -05:00
GitHub Action c6eea0343d chore: format code 2025-11-07 18:11:40 +00:00
opencode e317e7e481 release: v1.0.41 2025-11-07 18:11:39 +00:00
Dax Raad 287855336d allow not specifying a limit on messages endpoint 2025-11-07 13:05:19 -05:00
opencode d55f4f3322 release: v1.0.40 2025-11-07 17:37:31 +00:00
Dax Raad b708d0ecec disable scrollbar temporarily because of text wrap issues 2025-11-07 12:31:28 -05:00
Jinhyeok Lee afb831c93c vscode: Add VS Code Insiders support (#4019) 2025-11-07 10:31:16 -06:00
Adam 14397651b5 ignore: test file 2025-11-07 09:05:09 -06:00
Adam e5804f64f9 fix(desktop): layout quirks 2025-11-07 06:26:26 -06:00
GitHub Action ce7b73170f ignore: update download stats 2025-11-07 2025-11-07 12:04:25 +00:00
Dax Raad 9554abb56e message storage performance improvements 2025-11-07 01:11:47 -05:00
Charles David Mupende d0f5c825bd feat: implement network IP retrieval for remote access in web command (#3945) 2025-11-06 19:37:23 -06:00
opencode-agent[bot] 9f603e39a6 Fixed ACP to respect user's default model config. (#4006)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 19:07:27 -06:00
GitHub Action da51c9dfac chore: format code 2025-11-07 01:06:38 +00:00
Nicolai van der Smagt 9e04ff013c fix: resolve Mistral API compatibility issues (#2440)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 20:05:59 -05:00
opencode 6bfccace0c release: v1.0.39 2025-11-07 01:04:05 +00:00
Dax Raad b25d4f9dfb fix issue with input randomly breaking 2025-11-06 19:48:10 -05:00
GitHub Action d1962ca5a7 chore: format code 2025-11-07 00:14:59 +00:00
Dax Raad 25f31f3096 codex tweaks 2025-11-06 19:14:01 -05:00
opencode 11a6f0886e release: v1.0.37 2025-11-06 23:14:32 +00:00
Dax 3ba7e243d0 system theme (#4010) 2025-11-06 18:00:09 -05:00
Nicolai van der Smagt a2ab019317 fix: resolve Mistral API compatibility issues (#2440)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 16:53:02 -06:00
Jay V 21957406ff docs: add Deep Infra provider documentation 2025-11-06 17:08:55 -05:00
Adam 61c4747fbe fix(desktop): diff highlight rendering 2025-11-06 15:58:45 -06:00
Adam 957c43aa09 fix(desktop): review tab padding 2025-11-06 15:34:47 -06:00
Adam 96c57418f3 feat(desktop): review flow 2025-11-06 15:13:06 -06:00
Shanjai Raj b8c51e307f Fix: Auto-scroll to bottom when selecting session from list (#3988)
Co-authored-by: shanjairaj7 <shanjairaj7@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 14:29:39 -06:00
Frank 6791233ca0 Add desktop-feedback 2025-11-06 14:29:11 -05:00
GitHub Action cd6072ec58 chore: format code 2025-11-06 18:59:34 +00:00
Jay V 017e42bbcd docs: align web package favicon with console design for consistency 2025-11-06 13:58:52 -05:00
opencode 2d20582802 release: v1.0.36 2025-11-06 18:55:54 +00:00
GitHub Action 2bcc00dbf0 chore: format code 2025-11-06 18:21:59 +00:00
Dax Raad e45e94634f only display last 100 messages in TUI 2025-11-06 13:21:15 -05:00
Aiden Cline de1278414f fix: opencode run not parsing model string 2025-11-06 12:09:13 -06:00
Dax Raad 3c2803fd9a flaky test 2025-11-06 13:05:53 -05:00
GitHub Action 90c2b26733 chore: format code 2025-11-06 18:03:50 +00:00
Dax Raad 1ea3a8eb9b big format 2025-11-06 13:03:12 -05:00
Aiden Cline 8729edc5e0 update import command to accept share links 2025-11-06 11:55:57 -06:00
Aiden Cline d8bcf1f5f3 ci: update auto label 2025-11-06 11:46:42 -06:00
Dax Raad 67f3c934fe fix tests 2025-11-06 11:42:46 -05:00
Aiden Cline 065f656fb0 chore: rm hanging test 2025-11-06 10:39:32 -06:00
Dax Raad f636d937c4 fix undo command breaking other commands 2025-11-06 11:25:37 -05:00
Adam 492bf51a0d fix(desktop): sidebar collapsed width 2025-11-06 09:56:46 -06:00
Adam 81ab127f63 fix(desktop): demo type error 2025-11-06 09:49:39 -06:00
Adam 6ba7c54bab feat(desktop): collapsible sidebar 2025-11-06 09:48:51 -06:00
Adam 146bae82cb fix(desktop): button styles 2025-11-06 09:48:50 -06:00
Adam ab345cf0da feat(desktop): better tooltips 2025-11-06 09:48:50 -06:00
GitHub Action a1836527ce ignore: update download stats 2025-11-06 2025-11-06 12:04:56 +00:00
opencode-agent[bot] 49e4cfb286 Added big dot (●) indicator for current session in modal (#3980)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 01:09:59 -06:00
Frank e52bfab79d Update sst 2025-11-06 05:42:25 +00:00
opencode cc6d5c8ddd release: v1.0.35 2025-11-06 05:42:24 +00:00
Dax Raad afe8508949 fix homebrew upgrade 2025-11-06 00:37:44 -05:00
opencode 7c098c8849 release: v1.0.34 2025-11-06 02:35:17 +00:00
Dax Raad 11d6005b77 tui: reduce scrollbar visual prominence for less distracting interface 2025-11-05 21:30:40 -05:00
Dax Raad 2cc072b3dc enable scrollbar 2025-11-05 21:27:23 -05:00
opencode 86247b8ea9 release: v1.0.33 2025-11-06 02:18:41 +00:00
Dax Raad 0a5a02043c tui: move debug shortcuts to command palette for better discoverability 2025-11-05 21:13:35 -05:00
Sebastian Herrlinger 6e553f7e20 upgrade to opentui v0.1.36 2025-11-06 01:43:35 +00:00
opencode bb6acc0ec6 release: v1.0.32 2025-11-06 01:43:35 +00:00
Dax Raad 5a84b9f467 temporarily use strip-ansi package till bun bug is fixed 2025-11-05 20:38:37 -05:00
opencode c7031dfd77 release: v1.0.31 2025-11-06 01:35:40 +00:00
Dax Raad e136a40771 ignore tmp type rrror 2025-11-05 20:31:13 -05:00
Dax Raad ef25650ced regen bunlock 2025-11-05 20:30:40 -05:00
Dax Raad 6555a33eff type errors 2025-11-05 20:14:31 -05:00
Dax Raad 247ce44776 fix log 2025-11-05 20:01:57 -05:00
Dax Raad 4e7bfaab8b fix log 2025-11-05 20:00:09 -05:00
Sebastian Herrlinger 8b26a1f9bd upgrade to opentui 0.1.35, mitigating disappearing content and crashes 2025-11-06 01:38:58 +01:00
Dax Raad 2a9b6a85de core: ensure export command output can be piped without UI interference 2025-11-05 18:36:06 -05:00
opencode c9ae89a38b release: v1.0.30 2025-11-05 23:32:22 +00:00
Dax Raad e316050bf5 temporarily remove bun strip ansi due to bug 2025-11-05 18:27:19 -05:00
Dax Raad 306f45f04a add opencode import command to restore sessions from JSON exports 2025-11-05 18:05:01 -05:00
Adam e006e3355c feat(desktop): incrementally load sessions in side nav 2025-11-05 16:32:08 -06:00
opencode d7e31f76c4 release: v1.0.29 2025-11-05 22:14:46 +00:00
Aiden Cline d425723249 ask instead of throwing tool error if file is outside cwd 2025-11-05 16:09:47 -06:00
Adam c59ec71918 fix(desktop): max height on message diffs, session stats 2025-11-05 16:00:20 -06:00
Dax Raad 05ae99a09b fix sidebar modified files 2025-11-05 16:49:17 -05:00
opencode 6e22b45905 release: v1.0.28 2025-11-05 21:08:58 +00:00
Aiden Cline c664f92829 acp: update auth method 2025-11-05 15:01:14 -06:00
Aiden Cline f95333aaa4 acp: default to big pickle 2025-11-05 14:59:02 -06:00
opencode ef0b5e3dcb release: v1.0.27 2025-11-05 20:44:40 +00:00
Dax Raad b7262b8527 performance improvements 2025-11-05 15:33:23 -05:00
Dax Raad 1f44c7f750 include file count in summary 2025-11-05 14:23:10 -05:00
Dax Raad 7dba570195 ci: fix aur package 2025-11-05 14:22:25 -05:00
Haris Gušić 3d04ba26a3 fix(autocomplete): Prioritize exact matches (#3760) 2025-11-05 12:25:05 -06:00
Adam 3660e2c481 fix(desktop): local dev url 2025-11-05 12:09:30 -06:00
opencode 06ca45189b release: v1.0.26 2025-11-05 18:08:19 +00:00
Adam 674febcf60 fix(desktop): type issue 2025-11-05 11:59:10 -06:00
Adam 582d9a9622 fix(desktop): type issue 2025-11-05 11:56:32 -06:00
Adam d525fbf829 feat(desktop): session router, interrupt agent, visual cleanup 2025-11-05 11:55:35 -06:00
OpeOginni 69a499f807 fix(tui): restructure Sidebar component to be scrollable (#3946) 2025-11-05 11:33:30 -06:00
Dax Raad 37e564139f tui: lower paste summary threshold to trigger on shorter content 2025-11-05 12:32:23 -05:00
monke-yo ee8b81269b feat: add --attach flag to opencode run (#3889) 2025-11-05 11:31:01 -06:00
Filip 53998a2fed chore: remove unused patch tool from registry (to avoid accidental inclusions of it) (#3938) 2025-11-05 11:26:28 -06:00
Aiden Cline af7b9e77d1 fix: eu-west-2 aws bedrock issue 2025-11-05 10:14:08 -06:00
Aiden Cline 77c65b18b5 tweak: normalize escape keybind 2025-11-05 10:13:04 -06:00
Matthew Fitzpatrick c9dfe6d964 docs: include "limit" example (#3925)
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-05 09:47:11 -06:00
Aiden Cline 03f7f18260 ci: adjust auto label 2025-11-05 09:36:38 -06:00
GitHub Action 2db76fc6dd ignore: update download stats 2025-11-05 2025-11-05 12:04:41 +00:00
opencode 7269c2316d release: v1.0.25 2025-11-05 07:00:07 +00:00
Aiden Cline 1e0596bc46 ACP: update package, fix slash command bug (#3906) 2025-11-05 00:50:48 -06:00
Aiden Cline 3ebec2435a allow @ agents to work even if not first thing in prompt 2025-11-04 23:37:09 -06:00
Christian Stewart b90c0b5fac feat(tui): add /export and /copy commands (#3883)
Signed-off-by: Christian Stewart <christian@aperture.us>
2025-11-04 23:02:45 -06:00
Err 3b1ab444fd feat: add Clojure syntax highlighting support (#3912)
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-04 22:47:05 -06:00
Dax Raad 234db24f1f tui: fix command validation to prevent invalid commands from being executed 2025-11-04 20:46:01 -05:00
Kyle F Butts 04546c0873 Add support for R formatter in formatters (#3918)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-04 18:18:33 -06:00
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
David Hill 7088bfabd7 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-11-04 21:36:46 +00: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
David Hill dbdbfb8543 Update button.css 2025-11-04 17:31:56 +00:00
David Hill 521803aaa3 Update theme.css 2025-11-04 17:31:51 +00: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
527 changed files with 17356 additions and 41449 deletions
+4 -2
View File
@@ -5,15 +5,17 @@ runs:
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v4
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb', 'bun.lock') }}
key: ${{ runner.os }}-bun-${{ hashFiles('package.json') }}-${{ hashFiles('bun.lockb', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
${{ runner.os }}-bun-${{ hashFiles('package.json') }}-
- name: Install dependencies
run: bun install
+26 -11
View File
@@ -11,7 +11,7 @@ jobs:
contents: read
issues: write
steps:
- name: Add opentui label
- name: Auto-label and assign issues
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -20,21 +20,36 @@ jobs:
const title = issue.title;
const description = issue.body || '';
// Check for version patterns like v1.0.x or 1.0.x
const versionPattern = /\b[v]?1\.0\.[x\d]\b/i;
// Check for "opencode web" keyword
const webPattern = /(opencode web)/i;
const isWebRelated = webPattern.test(title) || webPattern.test(description);
if (versionPattern.test(title) || versionPattern.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']
});
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: ['thdxr']
});
}
+3 -3
View File
@@ -27,12 +27,12 @@ jobs:
{
"bash": {
"gh issue*": "allow",
"*": "deny"
},
"*": "deny"
},
"webfetch": "deny"
}
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created:'
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
Issue number:
${{ github.event.issue.number }}
+5
View File
@@ -12,6 +12,10 @@ on:
- major
- minor
- patch
version:
description: "Override version (optional)"
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -62,6 +66,7 @@ jobs:
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_CHANNEL: latest
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
+1 -1
View File
@@ -4,7 +4,7 @@ on:
push:
branches:
- dev
- opentui
- fix-build
- v0
concurrency: ${{ github.workflow }}-${{ github.ref }}
+34
View File
@@ -0,0 +1,34 @@
name: "sync-zed-extension"
on:
workflow_dispatch:
release:
types: [published]
jobs:
zed:
name: Release Zed Extension
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/setup-bun
- name: Get version tag
id: get_tag
run: |
if [ "${{ github.event_name }}" = "release" ]; then
TAG="${{ github.event.release.tag_name }}"
else
TAG=$(git tag --list 'v[0-9]*.*' --sort=-version:refname | head -n 1)
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Using tag: ${TAG}"
- name: Sync Zed extension
run: |
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
+6
View File
@@ -28,3 +28,9 @@ jobs:
bun turbo test
env:
CI: true
- name: Check SDK is up to date
run: |
bun ./packages/sdk/js/script/build.ts
git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist
continue-on-error: false
+6
View File
@@ -5,8 +5,14 @@ node_modules
.env
.idea
.vscode
*~
openapi.json
playground
tmp
dist
.turbo
**/.serena
.serena/
refs
Session.vim
opencode.json
+2 -1
View File
@@ -1,5 +1,6 @@
---
description: Git commit and push
subtask: true
---
commit and push
@@ -19,5 +20,5 @@ 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 "improvied agent experience" be very specific
do not do generic messages like "improved agent experience" be very specific
about what user facing changes were made
+23
View File
@@ -0,0 +1,23 @@
---
description: "Find issue(s) on github"
model: opencode/claude-haiku-4-5
---
Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
$ARGUMENTS
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
Please list any matching issues with:
- Issue number and title
- Brief explanation of why it matches the query
- Link to the issue
If no clear matches are found, say so.
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
"provider": {
"opencode": {
"options": {
// "baseURL": "http://localhost:8080",
},
},
},
}
+223
View File
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord4",
"light": "nord0"
},
"textMuted": {
"dark": "nord3",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord1",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "nord3",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "nord3",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "nord3",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "nord3",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "nord3",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"request": "attach",
"name": "opencode (attach)",
"url": "ws://localhost:6499/"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"oven.bun-vscode"
]
}
+24 -24
View File
@@ -17,31 +17,31 @@
## 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
{
"recipient_name": "multi_tool_use.parallel",
"parameters": {
"tool_uses": [
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.tsx"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.ts"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.md"
}
}
]
}
"recipient_name": "multi_tool_use.parallel",
"parameters": {
"tool_uses": [
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.tsx"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.ts"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.md"
}
}
]
}
}
+35 -3
View File
@@ -26,7 +26,7 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
## Developing OpenCode
- Requirements: Bun 1.3+, Go 1.24.x.
- Requirements: Bun 1.3+
- Install dependencies and start the dev server from the repo root:
```bash
@@ -36,11 +36,43 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
- Core pieces:
- `packages/opencode`: OpenCode core business logic & server.
- `packages/tui`: The TUI code, written in Go (will be removed soon in favor of [opentui](https://github.com/sst/opentui))
- `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`, the OpenCode team must regenerate the Stainless SDK before any client updates merge.
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
### Setting up a Debugger
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
The most reliable way to debug OpenCode is to run it manually in a terminal via `bun run --inspect=<url> dev ...` and attach
your debugger via that URL. Other methods can result in breakpoints being mapped incorrectly, at least in VSCode (YMMV).
Caveats:
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
is triggered.
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
Other tips and tricks:
- You might want to use `--inspect-wait` or `--inspect-brk` instead of `--inspect`, depending on your workflow
- Specifying `--inspect=ws://localhost:6499/` on every invocation can be tiresome, you may want to `export BUN_OPTIONS=--inspect=ws://localhost:6499/` instead
#### VSCode Setup
If you use VSCode, you can use our example configurations [.vscode/settings.example.json](.vscode/settings.example.json) and [.vscode/launch.example.json](.vscode/launch.example.json).
Some debug methods that can be problematic:
- Debug configurations with `"request": "launch"` can have breakpoints incorrectly mapped and thus unusable
- The same problem arises when running OpenCode in the VSCode `JavaScript Debug Terminal`
With that said, you may want to try these methods, as they might work for you.
## Pull Request Expectations
+22 -1
View File
@@ -28,8 +28,9 @@ curl -fsSL https://opencode.ai/install | bash
npm i -g opencode-ai@latest # or bun/pnpm/yarn
scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install sst/tap/opencode # macOS and Linux
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
mise use --pin -g ubi:sst/opencode # Any OS
```
> [!TIP]
@@ -50,6 +51,22 @@ OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bas
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode includes two built-in agents you can switch between,
you can switch between these using the `Tab` key.
- **build** - Default, full access agent for development work
- **plan** - Read-only agent for analysis and code exploration
- Denies file edits by default
- Asks permission before running bash commands
- Ideal for exploring unfamiliar codebases or planning changes
Also, included is a **general** subagent for complex searches and multi-step tasks.
This is used internally and can be invoked using `@general` in messages.
Learn more about [agents](https://opencode.ai/docs/agents).
### Documentation
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
@@ -58,6 +75,10 @@ For more info on how to configure OpenCode [**head over to our docs**](https://o
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
### Building on OpenCode
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway.
### FAQ
#### How is this different than Claude Code?
+17
View File
@@ -126,3 +126,20 @@
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
View File
+255 -210
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,2 +1,2 @@
[install]
exact = true
exact = true
+5 -1
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
@@ -730,12 +733,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
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../sst-env.d.ts" />
import "sst"
export {}
export {}
+12 -2
View File
@@ -97,7 +97,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!) },
@@ -105,6 +106,7 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
properties: { value: stripeWebhook.secret },
})
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
////////////////
// CONSOLE
@@ -130,10 +132,18 @@ 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,
...($dev
? [
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
new sst.Secret("CLOUDFLARE_API_TOKEN", process.env.CLOUDFLARE_API_TOKEN!),
]
: []),
gatewayKv,
],
environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!),
@@ -1,15 +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"
}
"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"
}
-17
View File
@@ -1,17 +0,0 @@
{
"$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}"
}
}
}
}
+4 -4
View File
@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.0",
"packageManager": "bun@1.3.2",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
@@ -28,7 +28,7 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.4.1",
"@pierre/precision-diffs": "0.4.4",
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
@@ -52,7 +52,7 @@
"@tsconfig/bun": "catalog:",
"husky": "9.1.7",
"prettier": "3.6.2",
"sst": "3.17.19",
"sst": "3.17.23",
"turbo": "2.5.6"
},
"dependencies": {
@@ -66,7 +66,7 @@
"license": "MIT",
"prettier": {
"semi": false,
"printWidth": 100
"printWidth": 120
},
"trustedDependencies": [
"esbuild",
+3
View File
@@ -23,6 +23,9 @@ app.config.timestamp_*.js
# Temp
gitignore
# Generated files
public/sitemap.xml
# System Files
.DS_Store
Thumbs.db
+2 -2
View File
@@ -5,9 +5,9 @@
"typecheck": "tsgo --noEmit",
"dev": "vinxi dev --host 0.0.0.0",
"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",
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.7"
"version": "1.0.71"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bun
import { readdir, writeFile } from "fs/promises"
import { join, dirname } from "path"
import { fileURLToPath } from "url"
import { config } from "../src/config.js"
const __dirname = dirname(fileURLToPath(import.meta.url))
const BASE_URL = config.baseUrl
const PUBLIC_DIR = join(__dirname, "../public")
const ROUTES_DIR = join(__dirname, "../src/routes")
const DOCS_DIR = join(__dirname, "../../../web/src/content/docs")
interface SitemapEntry {
url: string
priority: number
changefreq: string
}
async function getMainRoutes(): Promise<SitemapEntry[]> {
const routes: SitemapEntry[] = []
// Add main static routes
const staticRoutes = [
{ path: "/", priority: 1.0, changefreq: "daily" },
{ path: "/enterprise", priority: 0.8, changefreq: "weekly" },
{ path: "/brand", priority: 0.6, changefreq: "monthly" },
{ path: "/zen", priority: 0.8, changefreq: "weekly" },
]
for (const route of staticRoutes) {
routes.push({
url: `${BASE_URL}${route.path}`,
priority: route.priority,
changefreq: route.changefreq,
})
}
return routes
}
async function getDocsRoutes(): Promise<SitemapEntry[]> {
const routes: SitemapEntry[] = []
try {
const files = await readdir(DOCS_DIR)
for (const file of files) {
if (!file.endsWith(".mdx")) continue
const slug = file.replace(".mdx", "")
const path = slug === "index" ? "/docs/" : `/docs/${slug}`
routes.push({
url: `${BASE_URL}${path}`,
priority: slug === "index" ? 0.9 : 0.7,
changefreq: "weekly",
})
}
} catch (error) {
console.error("Error reading docs directory:", error)
}
return routes
}
function generateSitemapXML(entries: SitemapEntry[]): string {
const urls = entries
.map(
(entry) => ` <url>
<loc>${entry.url}</loc>
<changefreq>${entry.changefreq}</changefreq>
<priority>${entry.priority}</priority>
</url>`,
)
.join("\n")
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`
}
async function main() {
console.log("Generating sitemap...")
const mainRoutes = await getMainRoutes()
const docsRoutes = await getDocsRoutes()
const allRoutes = [...mainRoutes, ...docsRoutes]
console.log(`Found ${mainRoutes.length} main routes`)
console.log(`Found ${docsRoutes.length} docs routes`)
console.log(`Total: ${allRoutes.length} routes`)
const xml = generateSitemapXML(allRoutes)
const outputPath = join(PUBLIC_DIR, "sitemap.xml")
await writeFile(outputPath, xml, "utf-8")
console.log(`✓ Sitemap generated at ${outputPath}`)
}
main()
+1 -1
View File
@@ -12,7 +12,7 @@ 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>
)}
@@ -77,4 +77,4 @@
background-color: var(--color-accent-alpha);
}
}
}
}
@@ -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>
)
@@ -18,6 +18,7 @@ import { createMemo, Match, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { github } from "~/lib/github"
import { createEffect, onCleanup } from "solid-js"
import { config } from "~/config"
import "./header-context-menu.css"
const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches
@@ -42,7 +43,7 @@ export function Header(props: { zen?: boolean }) {
notation: "compact",
compactDisplay: "short",
}).format(githubData()?.stars!)
: "29K",
: config.github.starsFormatted.compact,
)
const [store, setStore] = createStore({
@@ -148,7 +149,7 @@ export function Header(props: { zen?: boolean }) {
<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>
@@ -222,7 +223,7 @@ 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>
+1 -1
View File
@@ -63,4 +63,4 @@
font-weight: 600;
color: var(--color-text);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Application-wide constants and configuration
*/
export const config = {
// Base URL
baseUrl: "https://opencode.ai",
// 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
+5 -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,12 @@ 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(
@@ -264,7 +264,7 @@
[data-component="brand-content"] {
padding: 4rem 5rem;
h2 {
h1 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
@@ -299,7 +299,6 @@
transition: all 0.2s ease;
text-decoration: none;
&:hover:not(:disabled) {
background: var(--color-background-strong-hover);
}
@@ -385,23 +384,21 @@
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)
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);
box-shadow:
0 0 0 1px rgba(19, 16, 16, 0.08),
0 6px 8px -8px rgba(19, 16, 16, 0.5);
}
}
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
+25 -125
View File
@@ -1,6 +1,7 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { Title, Meta, Link } from "@solidjs/meta"
import { Header } from "~/component/header"
import { config } from "~/config"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png"
@@ -53,26 +54,21 @@ export default function Brand() {
return (
<main data-page="enterprise">
<Title>OpenCode | Brand</Title>
<Link rel="canonical" href={`${config.baseUrl}/brand`} />
<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>
<h1>Brand guidelines</h1>
<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"
>
<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"
@@ -88,13 +84,7 @@ export default function Brand() {
<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"
>
<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"
@@ -105,13 +95,7 @@ export default function Brand() {
</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"
>
<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"
@@ -127,13 +111,7 @@ export default function Brand() {
<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"
>
<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"
@@ -144,13 +122,7 @@ export default function Brand() {
</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"
>
<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"
@@ -164,17 +136,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}
>
<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"
>
<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"
@@ -183,17 +147,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}
>
<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"
>
<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"
@@ -207,17 +163,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}
>
<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"
>
<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"
@@ -226,17 +174,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}
>
<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"
>
<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"
@@ -250,19 +190,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() =>
downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")
}
>
<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"
>
<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"
@@ -271,19 +201,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() =>
downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")
}
>
<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"
>
<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"
@@ -297,19 +217,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() =>
downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")
}
>
<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"
>
<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"
@@ -318,19 +228,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() =>
downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")
}
>
<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"
>
<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"
@@ -0,0 +1,5 @@
import { redirect } from "@solidjs/router"
export async function GET() {
return redirect("https://discord.gg/h5TNnkFVNy")
}
@@ -287,7 +287,7 @@
}
[data-component="enterprise-column-1"] {
h2 {
h1 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
@@ -1,6 +1,7 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { Title, Meta, Link } from "@solidjs/meta"
import { createSignal, Show } from "solid-js"
import { config } from "~/config"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
@@ -54,6 +55,7 @@ export default function Enterprise() {
return (
<main data-page="enterprise">
<Title>OpenCode | Enterprise solutions for your organisation</Title>
<Link rel="canonical" href={`${config.baseUrl}/enterprise`} />
<Meta name="description" content="Contact OpenCode for enterprise solutions" />
<div data-component="container">
<Header />
@@ -62,41 +64,28 @@ export default function Enterprise() {
<section data-component="enterprise-content">
<div data-component="enterprise-columns">
<div data-component="enterprise-column-1">
<h2>Your code is yours</h2>
<h1>Your code is yours</h1>
<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.
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"
>
<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.
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"
>
<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"
@@ -213,11 +202,7 @@ export default function Enterprise() {
</button>
</form>
{showSuccess() && (
<div data-component="success-message">
Message sent, we'll be in touch soon.
</div>
)}
{showSuccess() && <div data-component="success-message">Message sent, we'll be in touch soon.</div>}
</div>
</div>
</div>
@@ -230,31 +215,29 @@ export default function Enterprise() {
<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.
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.
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.
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.
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>
+1 -1
View File
@@ -479,7 +479,7 @@ body {
border-bottom: 1px solid var(--color-border-weak);
}
strong {
h1 {
font-size: 28px;
color: var(--color-text-strong);
font-weight: 500;
+13 -14
View File
@@ -13,6 +13,7 @@ import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import { github } from "~/lib/github"
import { createMemo } from "solid-js"
import { config } from "~/config"
function CopyStatus() {
return (
@@ -43,6 +44,7 @@ export default function Home() {
<main data-page="opencode">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
<Title>OpenCode | The AI coding agent built for the terminal</Title>
<Link rel="canonical" href={config.baseUrl} />
<Link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<Meta property="og:image" content="/social-share.png" />
<Meta name="twitter:image" content="/social-share.png" />
@@ -52,14 +54,10 @@ export default function Home() {
<div data-component="content">
<section data-component="hero">
<div data-slot="hero-copy">
<a
data-slot="releases"
href={release()?.url ?? "https://github.com/sst/opencode/releases"}
target="_blank"
>
<a data-slot="releases" href={release()?.url ?? `${config.github.repoUrl}/releases`} target="_blank">
Whats new in {release()?.name ?? "the latest release"}
</a>
<strong>The AI coding agent built for the terminal</strong>
<h1>The AI coding agent built for the terminal</h1>
<p>
OpenCode is fully open source, giving you control and freedom to use any provider, any model, and any
editor.
@@ -219,9 +217,10 @@ export default function Home() {
<div>
<span>[*]</span>
<p>
With over <strong>29,000</strong> GitHub stars, <strong>230</strong> contributors, and almost{" "}
<strong>3,500</strong> commits, OpenCode is used and trusted by over <strong>250,000</strong>{" "}
developers every month.
With over <strong>{config.github.starsFormatted.full}</strong> GitHub stars,{" "}
<strong>{config.stats.contributors}</strong> contributors, and almost{" "}
<strong>{config.stats.commits}</strong> commits, OpenCode is used and trusted by over{" "}
<strong>{config.stats.monthlyUsers}</strong> developers every month.
</p>
</div>
@@ -274,7 +273,7 @@ export default function Home() {
</svg>
</div>
<span>
<figure>Fig 1.</figure> <strong>29K</strong> GitHub Stars
<figure>Fig 1.</figure> <strong>{config.github.starsFormatted.compact}</strong> GitHub Stars
</span>
</div>
@@ -577,7 +576,7 @@ export default function Home() {
</svg>
</div>
<span>
<figure>Fig 2.</figure> <strong>230</strong> Contributors
<figure>Fig 2.</figure> <strong>{config.stats.contributors}</strong> Contributors
</span>
</div>
@@ -619,7 +618,7 @@ export default function Home() {
</svg>
</div>
<span>
<figure>Fig 3.</figure> <strong>250K</strong> Monthly Devs
<figure>Fig 3.</figure> <strong>{config.stats.monthlyUsers}</strong> Monthly Devs
</span>
</div>
</div>
@@ -688,11 +687,11 @@ export default function Home() {
<li>
<Faq question="Is OpenCode open source?">
Yes, OpenCode is fully open source. The source code is public on{" "}
<a href="https://github.com/sst/opencode" target="_blank">
<a href={config.github.repoUrl} target="_blank">
GitHub
</a>{" "}
under the{" "}
<a href="https://github.com/sst/opencode?tab=MIT-1-ov-file#readme" target="_blank">
<a href={`${config.github.repoUrl}?tab=MIT-1-ov-file#readme`} target="_blank">
MIT License
</a>
, meaning anyone can use, modify, or contribute to its development. Anyone from the community can file
+125 -127
View File
@@ -13,146 +13,144 @@ 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 ?? null,
paymentMethodType: paymentMethod.type,
})
.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")
const oldBillingInfo = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspaceID))
.then((rows) => rows[0]),
)
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,
paymentMethodID,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
// enable reload if first time enabling billing
...(oldBillingInfo?.customerID
? {}
: {
reload: true,
reloadError: null,
timeReloadError: null,
}),
})
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(Billing.CHARGE_AMOUNT),
paymentID,
invoiceID,
customerID,
.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,
})
})
})
}
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 })
}
+1 -1
View File
@@ -121,7 +121,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>
@@ -14,4 +14,4 @@
color: var(--color-danger);
}
}
}
}
@@ -71,4 +71,4 @@
color: var(--color-text-muted);
}
}
}
}
@@ -104,4 +104,4 @@
}
}
}
}
}
@@ -19,7 +19,7 @@ const getUserEmail = query(async (workspaceID: string) => {
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userEmail = createAsync(() => getUserEmail(params.id))
const userEmail = createAsync(() => getUserEmail(params.id!))
return (
<main data-page="workspace">
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
@@ -5,7 +5,7 @@ import "./[id].css"
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id!))
return (
<main data-page="workspace">
@@ -71,6 +71,57 @@
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-2) var(--space-4);
background-color: var(--color-bg-surface);
@@ -131,4 +182,4 @@
padding: var(--space-4);
min-width: 150px;
}
}
}
@@ -1,24 +1,86 @@
import { action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
import { createMemo, Match, Show, Switch } 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, IconStripe } from "~/component/icon"
import styles from "./billing-section.module.css"
import { createCheckoutUrl, queryBillingInfo } from "../../common"
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")
export function BillingSection() {
const params = useParams()
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
const balanceInfo = createAsync(() => queryBillingInfo(params.id))
const createCheckoutUrlAction = useAction(createCheckoutUrl)
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
const createSessionUrlAction = useAction(createSessionUrl)
const createSessionUrlSubmission = useSubmission(createSessionUrl)
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: billingInfo()?.reloadAmount.toString() ?? "",
checkoutRedirecting: false,
sessionRedirecting: false,
})
createEffect(() => {
const info = billingInfo()
if (info) {
setStore("addBalanceAmount", info.reloadAmount.toString())
}
})
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,
})
}
function hideAddBalanceForm() {
setStore("showAddBalanceForm", false)
checkoutSubmission.clear()
}
// DUMMY DATA FOR TESTING - UNCOMMENT ONE OF THE SCENARIOS BELOW
@@ -72,97 +134,104 @@ export function BillingSection() {
// timeReloadError: null as Date | null
// })
const balanceAmount = createMemo(() => {
return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
})
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">
<div data-slot="balance-display">
<div data-slot="balance-amount">
<span data-slot="balance-value">
${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}
</span>
<span data-slot="balance-value">${balance()}</span>
<span data-slot="balance-label">Current Balance</span>
</div>
<Show when={balanceInfo()?.customerID}>
<Show when={billingInfo()?.customerID}>
<div data-slot="balance-right-section">
<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={!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}>
{(err: any) => <div data-slot="form-error">{err()}</div>}
</Show>
</div>
}
>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Add Balance"}
</button>
<button data-color="primary" onClick={() => showAddBalanceForm()}>
Add Balance
</button>
</Show>
<div data-slot="credit-card">
<div data-slot="card-icon">
<Switch fallback={<IconCreditCard style={{ width: "24px", height: "24px" }} />}>
<Match when={balanceInfo()?.paymentMethodType === "link"}>
<Match when={billingInfo()?.paymentMethodType === "link"}>
<IconStripe style={{ width: "24px", height: "24px" }} />
</Match>
</Switch>
</div>
<div data-slot="card-details">
<Switch>
<Match when={balanceInfo()?.paymentMethodType === "card"}>
<Show
when={balanceInfo()?.paymentMethodLast4}
fallback={<span data-slot="number">----</span>}
>
<Match when={billingInfo()?.paymentMethodType === "card"}>
<Show when={billingInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
<span data-slot="secret"></span>
<span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span>
<span data-slot="number">{billingInfo()?.paymentMethodLast4}</span>
</Show>
</Match>
<Match when={balanceInfo()?.paymentMethodType === "link"}>
<Match when={billingInfo()?.paymentMethodType === "link"}>
<span data-slot="type">Linked to Stripe</span>
</Match>
</Switch>
</div>
<button
data-color="ghost"
disabled={createSessionUrlSubmission.pending}
onClick={async () => {
const baseUrl = window.location.href
const sessionUrl = await createSessionUrlAction(params.id, baseUrl)
if (sessionUrl) {
window.location.href = sessionUrl
}
}}
disabled={sessionSubmission.pending || store.sessionRedirecting}
onClick={onClickSession}
>
{createSessionUrlSubmission.pending ? "Loading..." : "Manage"}
{sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage"}
</button>
</div>
</div>
</Show>
</div>
<Show when={!balanceInfo()?.customerID}>
<Show when={!billingInfo()?.customerID}>
<button
data-slot="enable-billing-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
}
}}
disabled={checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout}
>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"}
{checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable Billing"}
</button>
</Show>
</div>
@@ -8,8 +8,8 @@ import { queryBillingInfo, querySessionInfo } from "../../common"
export default function () {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id!))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
return (
<div data-page="workspace-[id]">
@@ -93,4 +93,4 @@
margin: 0;
line-height: 1.4;
}
}
}
@@ -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,15 @@ 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 $
{(() => {
const dateLastUsed = balanceInfo()?.timeMonthlyUsageUpdated
const dateLastUsed = billingInfo()?.timeMonthlyUsageUpdated
if (!dateLastUsed) return "0"
const current = new Date().toLocaleDateString("en-US", {
@@ -128,7 +122,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>
@@ -19,7 +19,7 @@ const downloadReceipt = action(async (workspaceID: string, paymentID: string) =>
export function PaymentSection() {
const params = useParams()
const payments = createAsync(() => getPaymentsInfo(params.id))
const payments = createAsync(() => getPaymentsInfo(params.id!))
const downloadReceiptAction = useAction(downloadReceipt)
// DUMMY DATA FOR TESTING
@@ -89,7 +89,7 @@ export function PaymentSection() {
<td data-slot="payment-receipt">
<button
onClick={async () => {
const receiptUrl = await downloadReceiptAction(params.id, payment.paymentID!)
const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
if (receiptUrl) {
window.open(receiptUrl, "_blank")
}
@@ -34,6 +34,206 @@
}
}
[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;
@@ -54,7 +254,8 @@
gap: var(--space-2);
margin: 0;
flex-shrink: 0;
padding: 0;
border: none;
}
}
}
@@ -1,17 +1,19 @@
import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { Show } from "solid-js"
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: getBillingInfo.key,
revalidate: queryBillingInfo.key,
})
}, "billing.reload")
@@ -20,12 +22,27 @@ const setReload = action(async (form: FormData) => {
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,
@@ -35,22 +52,43 @@ const setReload = action(async (form: FormData) => {
})
.where(eq(BillingTable.workspaceID, workspaceID)),
),
{ revalidate: getBillingInfo.key },
{ revalidate: queryBillingInfo.key },
)
}, "billing.setReload")
const getBillingInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return await Billing.get()
}, workspaceID)
}, "billing.get")
export function ReloadSection() {
const params = useParams()
const balanceInfo = createAsync(() => getBillingInfo(params.id))
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}>
@@ -58,44 +96,102 @@ export function ReloadSection() {
<h2>Auto Reload</h2>
<div data-slot="title-row">
<Show
when={balanceInfo()?.reload}
when={billingInfo()?.reload}
fallback={
<p>Auto reload is disabled. Enable to automatically reload when balance is low.</p>
<p>
Auto reload is <b>disabled</b>. Enable to automatically reload when balance is low.
</p>
}
>
<p>
We'll automatically reload <b>$20</b> (+$1.23 processing fee) when it reaches{" "}
<b>$5</b>.
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>
<form action={setReload} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="reload" value={balanceInfo()?.reload ? "false" : "true"} />
<button data-color="primary" type="submit" disabled={setReloadSubmission.pending}>
<Show
when={balanceInfo()?.reload}
fallback={setReloadSubmission.pending ? "Enabling..." : "Enable"}
>
{setReloadSubmission.pending ? "Disabling..." : "Disable"}
</Show>
</button>
</form>
<button data-color="primary" type="button" onClick={() => show()}>
{billingInfo()?.reload ? "Edit" : "Enable"}
</button>
</div>
</div>
<div data-slot="section-content">
<Show when={balanceInfo()?.reload && balanceInfo()?.reloadError}>
<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{" "}
{balanceInfo()?.timeReloadError!.toLocaleString("en-US", {
{billingInfo()?.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.
. 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} />
@@ -104,8 +200,8 @@ export function ReloadSection() {
</button>
</form>
</div>
</Show>
</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 userInfo = createAsync(() => querySessionInfo(params.id!))
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
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,15 @@ 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>
@@ -171,7 +171,6 @@
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
@@ -181,8 +180,7 @@
th {
&:nth-child(3)
/* Date */
{
/* Date */ {
display: none;
}
}
@@ -190,11 +188,10 @@
td {
&:nth-child(3)
/* Date */
{
/* Date */ {
display: none;
}
}
}
}
}
}
@@ -45,7 +45,7 @@ const listKeys = query(async (workspaceID: string) => {
export function KeySection() {
const params = useParams()
const keys = createAsync(() => listKeys(params.id))
const keys = createAsync(() => listKeys(params.id!))
const submission = useSubmission(createKey)
const [store, setStore] = createStore({ show: false })
@@ -209,7 +209,7 @@ const roleOptions = [
export function MemberSection() {
const params = useParams()
const data = createAsync(() => listMembers(params.id))
const data = createAsync(() => listMembers(params.id!))
const submission = useSubmission(inviteMember)
const [store, setStore] = createStore({
show: false,
@@ -328,7 +328,7 @@ export function MemberSection() {
{(member) => (
<MemberRow
member={member}
workspaceID={params.id}
workspaceID={params.id!}
actorID={data()!.actorID}
actorRole={data()!.actorRole}
/>
@@ -69,4 +69,4 @@
}
}
}
}
}
@@ -23,7 +23,7 @@ const getModelsInfo = query(async (workspaceID: string) => {
return {
all: Object.entries(ZenData.list().models)
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
.filter(([id, _model]) => !id.startsWith("an-"))
.filter(([id, _model]) => !id.startsWith("alpha-"))
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
.map(([id, model]) => ({ id, name: model.name })),
disabled: await Model.listDisabled(),
@@ -52,8 +52,8 @@ const updateModel = action(async (form: FormData) => {
export function ModelSection() {
const params = useParams()
const modelsInfo = createAsync(() => getModelsInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id))
const modelsInfo = createAsync(() => getModelsInfo(params.id!))
const userInfo = createAsync(() => querySessionInfo(params.id!))
const modelsWithLab = createMemo(() => {
const info = modelsInfo()
@@ -140,4 +140,4 @@
}
}
}
}
}
@@ -21,8 +21,8 @@ const listKeys = query(async (workspaceID: string) => {
export function NewUserSection() {
const params = useParams()
const [copiedKey, setCopiedKey] = createSignal(false)
const keys = createAsync(() => listKeys(params.id))
const usage = createAsync(() => getUsageInfo(params.id))
const keys = createAsync(() => listKeys(params.id!))
const usage = createAsync(() => getUsageInfo(params.id!))
const isNew = createMemo(() => {
const keysList = keys()
const usageList = usage()
@@ -128,7 +128,6 @@
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
@@ -136,4 +135,4 @@
}
}
}
}
}
@@ -22,7 +22,9 @@ const removeProvider = action(async (form: FormData) => {
if (!provider) return { error: "Provider is required" }
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
return json(await withActor(() => Provider.remove({ provider }), workspaceID), { revalidate: listProviders.key })
return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
revalidate: listProviders.key,
})
}, "provider.remove")
const saveProvider = action(async (form: FormData) => {
@@ -52,7 +54,7 @@ const listProviders = query(async (workspaceID: string) => {
function ProviderRow(props: { provider: Provider }) {
const params = useParams()
const providers = createAsync(() => listProviders(params.id))
const providers = createAsync(() => listProviders(params.id!))
const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
const removeSubmission = useSubmission(
removeProvider,
@@ -31,7 +31,7 @@
margin: 0;
}
>button {
> button {
align-self: flex-start;
}
}
@@ -80,7 +80,7 @@
}
}
>button[type="reset"] {
> button[type="reset"] {
align-self: flex-start;
}
@@ -91,4 +91,4 @@
margin-top: calc(var(--space-1) * -1);
}
}
}
}
@@ -46,7 +46,7 @@ const updateWorkspace = action(async (form: FormData) => {
export function SettingsSection() {
const params = useParams()
const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id))
const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id!))
const submission = useSubmission(updateWorkspace)
const [store, setStore] = createStore({ show: false })
@@ -1,88 +1,111 @@
.root {
[data-component="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
flex-direction: column;
gap: var(--space-2);
/* Empty state */
[data-component="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
p {
line-height: 1.5;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
[data-slot="usage-table"] {
overflow-x: auto;
}
[data-slot="usage-table-element"] {
width: 100%;
border-collapse: collapse;
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
thead {
border-bottom: 1px solid var(--color-border);
/* Table container */
[data-slot="usage-table"] {
overflow-x: auto;
}
/* Table element */
[data-slot="usage-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="usage-date"] {
color: var(--color-text);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
&[data-slot="usage-model"] {
font-family: var(--font-sans);
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="usage-cost"] {
color: var(--color-text);
font-weight: 500;
}
}
&[data-slot="usage-date"] {
color: var(--color-text);
}
tbody tr:last-child td {
border-bottom: none;
}
}
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
/* Pagination */
[data-slot="pagination"] {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding: var(--space-4) 0;
border-top: 1px solid var(--color-border-muted);
margin-top: var(--space-2);
&[data-slot="usage-cost"] {
color: var(--color-text);
}
button {
padding: var(--space-2) var(--space-4);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
color: var(--color-text);
font-size: var(--font-size-sm);
cursor: pointer;
transition: all 0.15s ease;
&:hover:not(:disabled) {
background: var(--color-bg-tertiary);
border-color: var(--color-border-hover);
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Model */ {
display: none;
}
}
td {
&:nth-child(2) /* Model */ {
display: none;
}
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
/* Mobile responsive */
@media (max-width: 40rem) {
[data-slot="usage-table-element"] {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
/* Hide Model column on mobile */
th:nth-child(2),
td:nth-child(2) {
display: none;
}
}
}
@@ -1,91 +1,59 @@
import { Billing } from "@opencode-ai/console-core/billing.js"
import { query, useParams, createAsync } from "@solidjs/router"
import { createMemo, For, Show } from "solid-js"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, For, Show, createEffect } from "solid-js"
import { formatDateUTC, formatDateForTable } from "../common"
import { withActor } from "~/context/auth.withActor"
import styles from "./usage-section.module.css"
import "./usage-section.module.css"
import { createStore } from "solid-js/store"
const getUsageInfo = query(async (workspaceID: string) => {
const PAGE_SIZE = 50
async function getUsageInfo(workspaceID: string, page: number) {
"use server"
return withActor(async () => {
return await Billing.usages()
return await Billing.usages(page, PAGE_SIZE)
}, workspaceID)
}, "usage.list")
}
const queryUsageInfo = query(getUsageInfo, "usage.list")
export function UsageSection() {
const params = useParams()
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
const usage = createAsync(() => getUsageInfo(params.id))
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
// DUMMY DATA FOR TESTING
// const usage = () => [
// {
// timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today
// model: "claude-3-5-sonnet-20241022",
// inputTokens: 1247,
// outputTokens: 423,
// cost: 125400000, // $1.254
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago
// model: "claude-3-haiku-20240307",
// inputTokens: 892,
// outputTokens: 156,
// cost: 23500000, // $0.235
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday
// model: "claude-3-5-sonnet-20241022",
// inputTokens: 2134,
// outputTokens: 687,
// cost: 234700000, // $2.347
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago
// model: "gpt-4o-mini",
// inputTokens: 567,
// outputTokens: 234,
// cost: 8900000, // $0.089
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago
// model: "claude-3-opus-20240229",
// inputTokens: 1893,
// outputTokens: 945,
// cost: 445600000, // $4.456
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago
// model: "gpt-4o",
// inputTokens: 1456,
// outputTokens: 532,
// cost: 156800000, // $1.568
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago
// model: "claude-3-haiku-20240307",
// inputTokens: 634,
// outputTokens: 89,
// cost: 12300000, // $0.123
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago
// model: "claude-3-5-sonnet-20241022",
// inputTokens: 3245,
// outputTokens: 1123,
// cost: 387200000, // $3.872
// },
// ]
createEffect(() => {
setStore({ usage: usage() })
}, [usage])
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
const canGoPrev = createMemo(() => store.page > 0)
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
const goPrev = async () => {
const usage = await getUsageInfo(params.id!, store.page - 1)
setStore({
page: store.page - 1,
usage,
})
}
const goNext = async () => {
const usage = await getUsageInfo(params.id!, store.page + 1)
setStore({
page: store.page + 1,
usage,
})
}
return (
<section class={styles.root}>
<section>
<div data-slot="section-title">
<h2>Usage History</h2>
<p>Recent API usage and costs.</p>
</div>
<div data-slot="usage-table">
<Show
when={usage() && usage()!.length > 0}
when={hasResults()}
fallback={
<div data-component="empty-state">
<p>Make your first API call to get started.</p>
@@ -103,7 +71,7 @@ export function UsageSection() {
</tr>
</thead>
<tbody>
<For each={usage()!}>
<For each={store.usage}>
{(usage) => {
const date = createMemo(() => new Date(usage.timeCreated))
return (
@@ -121,6 +89,16 @@ export function UsageSection() {
</For>
</tbody>
</table>
<Show when={canGoPrev() || canGoNext()}>
<div data-slot="pagination">
<button disabled={!canGoPrev()} onClick={goPrev}>
</button>
<button disabled={!canGoNext()} onClick={goNext}>
</button>
</div>
</Show>
</Show>
</div>
</section>
@@ -1,6 +1,6 @@
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"
@@ -34,6 +34,11 @@ export function formatDateUTC(date: Date) {
return date.toLocaleDateString("en-US", options)
}
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(async () => {
@@ -62,23 +67,40 @@ export const querySessionInfo = query(async (workspaceID: string) => {
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) => {
async (workspaceID: string, amount: number, successUrl: string, cancelUrl: string) => {
"use server"
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
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")
@@ -277,7 +277,7 @@ body {
margin-bottom: 24px;
}
strong {
h1 {
font-size: 28px;
color: var(--color-text-strong);
font-weight: 500;
@@ -3,6 +3,7 @@ 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"
import { config } from "~/config"
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
import compareVideo from "../../asset/lander/opencode-comparison-min.mp4"
import compareVideoPoster from "../../asset/lander/opencode-comparison-poster.png"
@@ -30,6 +31,7 @@ export default function Home() {
<main data-page="zen">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
<Meta property="og:image" content="/social-share-zen.png" />
<Meta name="twitter:image" content="/social-share-zen.png" />
@@ -42,7 +44,7 @@ export default function Home() {
<div data-slot="hero-copy">
<img data-slot="zen logo light" src={zenLogoLight} alt="zen logo light" />
<img data-slot="zen logo dark" src={zenLogoDark} alt="zen logo dark" />
<strong>Reliable optimized models for coding agents</strong>
<h1>Reliable optimized models for coding agents</h1>
<p>
Zen gives you access to a curated set of AI models that OpenCode has tested and benchmarked specifically
for coding agents. No need to worry about inconsistent performance and quality, use validated models
@@ -3,3 +3,4 @@ export class CreditsError extends Error {}
export class MonthlyLimitError extends Error {}
export class UserLimitError extends Error {}
export class ModelError extends Error {}
export class RateLimitError extends Error {}
@@ -1 +0,0 @@
export type Format = "anthropic" | "openai" | "oa-compat"
@@ -12,27 +12,31 @@ 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 { Format } from "./format"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } 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"
import { createRateLimiter } from "./rateLimiter"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type Model = ZenData["models"][string]
type RetryOptions = {
excludeProviders: string[]
retryCount: number
}
export async function handler(
input: APIEvent,
opts: {
format: Format
format: ZenData.Format
parseApiKey: (headers: Headers) => string | undefined
},
) {
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
type ModelInfo = Awaited<ReturnType<typeof validateModel>>
type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
const MAX_RETRIES = 3
const FREE_WORKSPACES = [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
@@ -40,6 +44,7 @@ export async function handler(
try {
const body = await input.request.json()
const ip = input.request.headers.get("x-real-ip") ?? ""
logger.metric({
is_tream: !!body.stream,
session: input.request.headers.get("x-opencode-session"),
@@ -47,42 +52,56 @@ export async function handler(
})
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)
updateProviderKey(authInfo, providerInfo)
logger.metric({ provider: providerInfo.id })
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
// Request to model provider
const startTimestamp = Date.now()
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")
providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider(zenData, modelInfo, ip, retry)
const authInfo = await authenticate(modelInfo, providerInfo)
validateBilling(authInfo, modelInfo)
validateModelSettings(authInfo)
updateProviderKey(authInfo, providerInfo)
logger.metric({ provider: providerInfo.id })
const startTimestamp = Date.now()
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.substring(0, 300) + "...")
const res = await fetch(reqUrl, {
method: "POST",
headers: (() => {
const headers = new Headers(input.request.headers)
providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
})
headers.delete("host")
headers.delete("content-length")
headers.delete("x-opencode-request")
headers.delete("x-opencode-session")
return headers
})(),
body: reqBody,
})
// Try another provider => stop retrying if using fallback provider
if (res.status !== 200 && modelInfo.fallbackProvider && providerInfo.id !== modelInfo.fallbackProvider) {
return retriableRequest({
excludeProviders: [...retry.excludeProviders, providerInfo.id],
retryCount: retry.retryCount + 1,
})
return headers
})(),
body: reqBody,
})
}
return { providerInfo, authInfo, res, startTimestamp }
}
const { providerInfo, authInfo, res, startTimestamp } = await retriableRequest()
// Scrub response headers
const resHeaders = new Headers()
@@ -93,9 +112,6 @@ export async function handler(
}
}
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) {
@@ -104,6 +120,7 @@ export async function handler(
const body = JSON.stringify(responseConverter(json))
logger.metric({ response_length: body.length })
logger.debug("RESPONSE: " + body)
await rateLimiter?.track()
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
await reload(authInfo)
return new Response(body, {
@@ -132,6 +149,7 @@ export async function handler(
response_length: responseLength,
"timestamp.last_byte": Date.now(),
})
await rateLimiter?.track()
const usage = usageParser.retrieve()
if (usage) {
await trackUsage(authInfo, modelInfo, providerInfo, usage)
@@ -206,6 +224,15 @@ export async function handler(
{ status: 401 },
)
if (error instanceof RateLimitError)
return new Response(
JSON.stringify({
type: "error",
error: { type: error.constructor.name, message: error.message },
}),
{ status: 429 },
)
return new Response(
JSON.stringify({
type: "error",
@@ -230,42 +257,42 @@ export async function handler(
return { id: modelId, ...modelData }
}
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))
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, ip: string, retry: RetryOptions) {
const provider = (() => {
if (retry.retryCount === MAX_RETRIES) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
}
// Use last character of IP address to select a provider
const lastChar = ip.charCodeAt(ip.length - 1) || 0
const index = lastChar % providers.length
const provider = providers[index]
const providers = modelInfo.providers
.filter((provider) => !provider.disabled)
.filter((provider) => !retry.excludeProviders.includes(provider.id))
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
if (!(provider.id in zenData.providers)) {
throw new ModelError(`Provider ${provider.id} not supported`)
}
// Use the last 2 characters of IP address to select a provider
const lastChars = ip.slice(-2)
const index = parseInt(lastChars, 16) % providers.length
return providers[index || 0]
})()
if (!provider) throw new ModelError("No provider available")
if (!(provider.id in zenData.providers)) throw new ModelError(`Provider ${provider.id} not supported`)
return {
...provider,
...zenData.providers[provider.id],
...(provider.id === "anthropic"
? anthropicHelper
: provider.id === "openai"
? openaiHelper
: oaCompatHelper),
...(() => {
const format = zenData.providers[provider.id].format
if (format === "anthropic") return anthropicHelper
if (format === "openai") return openaiHelper
return oaCompatHelper
})(),
}
}
async function authenticate(
model: Awaited<ReturnType<typeof validateModel>>,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
) {
async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) {
const apiKey = opts.parseApiKey(input.request.headers)
if (!apiKey) {
if (model.allowAnonymous) return
if (!apiKey || apiKey === "public") {
if (modelInfo.allowAnonymous) return
throw new AuthError("Missing API key.")
}
@@ -280,6 +307,7 @@ export async function handler(
monthlyLimit: BillingTable.monthlyLimit,
monthlyUsage: BillingTable.monthlyUsage,
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
reloadTrigger: BillingTable.reloadTrigger,
},
user: {
id: UserTable.id,
@@ -295,20 +323,11 @@ 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, modelInfo.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]),
@@ -331,11 +350,11 @@ export async function handler(
}
}
function validateBilling(model: Model, authInfo: Awaited<ReturnType<typeof authenticate>>) {
function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo) {
if (!authInfo) return
if (authInfo.provider?.credentials) return
if (authInfo.isFree) return
if (model.allowAnonymous) return
if (modelInfo.allowAnonymous) return
const billing = authInfo.billing
if (!billing.paymentMethodID)
@@ -379,39 +398,24 @@ export async function handler(
}
}
function validateModelSettings(authInfo: Awaited<ReturnType<typeof authenticate>>) {
function validateModelSettings(authInfo: AuthInfo) {
if (!authInfo) return
if (authInfo.isDisabled) throw new ModelError("Model is disabled")
}
function updateProviderKey(
authInfo: Awaited<ReturnType<typeof authenticate>>,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
) {
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
if (!authInfo) return
if (!authInfo.provider?.credentials) return
providerInfo.apiKey = authInfo.provider.credentials
}
async function trackUsage(
authInfo: Awaited<ReturnType<typeof authenticate>>,
modelInfo: ReturnType<typeof validateModel>,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
usage: any,
) {
const {
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWrite5mTokens,
cacheWrite1hTokens,
} = providerInfo.normalizeUsage(usage)
async function trackUsage(authInfo: AuthInfo, modelInfo: ModelInfo, providerInfo: ProviderInfo, usage: any) {
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
@@ -462,8 +466,7 @@ 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,
@@ -503,9 +506,7 @@ 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) =>
@@ -516,7 +517,7 @@ export async function handler(
)
}
async function reload(authInfo: Awaited<ReturnType<typeof authenticate>>) {
async function reload(authInfo: AuthInfo) {
if (!authInfo) return
if (authInfo.isFree) return
if (authInfo.provider?.credentials) return
@@ -531,11 +532,11 @@ 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()`)),
),
),
)
@@ -98,7 +98,10 @@ export function fromAnthropicRequest(body: any): CommonRequest {
typeof (src as any).media_type === "string" &&
typeof (src as any).data === "string"
)
return { type: "image_url", image_url: { url: `data:${(src as any).media_type};base64,${(src as any).data}` } }
return {
type: "image_url",
image_url: { url: `data:${(src as any).media_type};base64,${(src as any).data}` },
}
return undefined
}
@@ -165,7 +168,11 @@ export function fromAnthropicRequest(body: any): CommonRequest {
.filter((t: any) => t && typeof t === "object" && "input_schema" in t)
.map((t: any) => ({
type: "function",
function: { name: (t as any).name, description: (t as any).description, parameters: (t as any).input_schema },
function: {
name: (t as any).name,
description: (t as any).description,
parameters: (t as any).input_schema,
},
}))
: undefined
@@ -452,7 +459,12 @@ export function toAnthropicResponse(resp: CommonResponse) {
} catch {
input = (tc as any).function.arguments
}
content.push({ type: "tool_use", id: (tc as any).id, name: (tc as any).function.name, input })
content.push({
type: "tool_use",
id: (tc as any).id,
name: (tc as any).function.name,
input,
})
}
}
}
@@ -511,13 +523,22 @@ export function fromAnthropicChunk(chunk: string): CommonChunk | string {
if (json.type === "content_block_start") {
const cb = json.content_block
if (cb?.type === "text") {
out.choices.push({ index: json.index ?? 0, delta: { role: "assistant", content: "" }, finish_reason: null })
out.choices.push({
index: json.index ?? 0,
delta: { role: "assistant", content: "" },
finish_reason: null,
})
} else if (cb?.type === "tool_use") {
out.choices.push({
index: json.index ?? 0,
delta: {
tool_calls: [
{ index: json.index ?? 0, id: cb.id, type: "function", function: { name: cb.name, arguments: "" } },
{
index: json.index ?? 0,
id: cb.id,
type: "function",
function: { name: cb.name, arguments: "" },
},
],
},
finish_reason: null,
@@ -532,7 +553,9 @@ export function fromAnthropicChunk(chunk: string): CommonChunk | string {
} else if (d?.type === "input_json_delta") {
out.choices.push({
index: json.index ?? 0,
delta: { tool_calls: [{ index: json.index ?? 0, function: { arguments: d.partial_json } }] },
delta: {
tool_calls: [{ index: json.index ?? 0, function: { arguments: d.partial_json } }],
},
finish_reason: null,
})
}
@@ -532,7 +532,9 @@ export function toOaCompatibleChunk(chunk: CommonChunk): string {
total_tokens: chunk.usage.total_tokens,
...(chunk.usage.prompt_tokens_details?.cached_tokens
? {
prompt_tokens_details: { cached_tokens: chunk.usage.prompt_tokens_details.cached_tokens },
prompt_tokens_details: {
cached_tokens: chunk.usage.prompt_tokens_details.cached_tokens,
},
}
: {}),
}
@@ -77,7 +77,10 @@ export function fromOpenaiRequest(body: any): CommonRequest {
typeof (s as any).media_type === "string" &&
typeof (s as any).data === "string"
)
return { type: "image_url", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } }
return {
type: "image_url",
image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` },
}
return undefined
}
@@ -153,7 +156,11 @@ export function fromOpenaiRequest(body: any): CommonRequest {
}
if ((m as any).role === "tool") {
msgs.push({ role: "tool", tool_call_id: (m as any).tool_call_id, content: (m as any).content })
msgs.push({
role: "tool",
tool_call_id: (m as any).tool_call_id,
content: (m as any).content,
})
continue
}
}
@@ -210,7 +217,10 @@ export function toOpenaiRequest(body: CommonRequest) {
typeof (s as any).media_type === "string" &&
typeof (s as any).data === "string"
)
return { type: "input_image", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } }
return {
type: "input_image",
image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` },
}
return undefined
}
@@ -498,7 +508,9 @@ export function fromOpenaiChunk(chunk: string): CommonChunk | string {
if (typeof name === "string" && name.length > 0) {
out.choices.push({
index: 0,
delta: { tool_calls: [{ index: 0, id, type: "function", function: { name, arguments: "" } }] },
delta: {
tool_calls: [{ index: 0, id, type: "function", function: { name, arguments: "" } }],
},
finish_reason: null,
})
}
@@ -555,7 +567,12 @@ export function toOpenaiChunk(chunk: CommonChunk): string {
const model = chunk.model
if (d.content) {
const data = { id, type: "response.output_text.delta", delta: d.content, response: { id, model } }
const data = {
id,
type: "response.output_text.delta",
delta: d.content,
response: { id, model },
}
return `event: response.output_text.delta\ndata: ${JSON.stringify(data)}`
}
@@ -565,7 +582,13 @@ export function toOpenaiChunk(chunk: CommonChunk): string {
const data = {
type: "response.output_item.added",
output_index: 0,
item: { id: tc.id, type: "function_call", name: tc.function.name, call_id: tc.id, arguments: "" },
item: {
id: tc.id,
type: "function_call",
name: tc.function.name,
call_id: tc.id,
arguments: "",
},
}
return `event: response.output_item.added\ndata: ${JSON.stringify(data)}`
}
@@ -593,7 +616,11 @@ export function toOpenaiChunk(chunk: CommonChunk): string {
}
: undefined
const data: any = { id, type: "response.completed", response: { id, model, ...(usage ? { usage } : {}) } }
const data: any = {
id,
type: "response.completed",
response: { id, model, ...(usage ? { usage } : {}) },
}
return `event: response.completed\ndata: ${JSON.stringify(data)}`
}
@@ -1,5 +1,4 @@
import { Format } from "../format"
import { ZenData } from "@opencode-ai/console-core/model.js"
import {
fromAnthropicChunk,
fromAnthropicRequest,
@@ -26,7 +25,7 @@ import {
} from "./openai-compatible"
export type ProviderHelper = {
format: Format
format: ZenData.Format
modifyUrl: (providerApi: string) => string
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
modifyBody: (body: Record<string, any>) => Record<string, any>
@@ -158,7 +157,7 @@ export interface CommonChunk {
}
}
export function createBodyConverter(from: Format, to: Format) {
export function createBodyConverter(from: ZenData.Format, to: ZenData.Format) {
return (body: any): any => {
if (from === to) return body
@@ -173,7 +172,7 @@ export function createBodyConverter(from: Format, to: Format) {
}
}
export function createStreamPartConverter(from: Format, to: Format) {
export function createStreamPartConverter(from: ZenData.Format, to: ZenData.Format) {
return (part: any): any => {
if (from === to) return part
@@ -191,7 +190,7 @@ export function createStreamPartConverter(from: Format, to: Format) {
}
}
export function createResponseConverter(from: Format, to: Format) {
export function createResponseConverter(from: ZenData.Format, to: ZenData.Format) {
return (response: any): any => {
if (from === to) return response
@@ -0,0 +1,35 @@
import { Resource } from "@opencode-ai/console-resource"
import { RateLimitError } from "./error"
import { logger } from "./logger"
export function createRateLimiter(model: string, limit: number | undefined, ip: string) {
if (!limit) return
const now = Date.now()
const currKey = `usage:${ip}:${model}:${buildYYYYMMDDHH(now)}`
const prevKey = `usage:${ip}:${model}:${buildYYYYMMDDHH(now - 3_600_000)}`
let currRate: number
let prevRate: number
return {
track: async () => {
await Resource.GatewayKv.put(currKey, currRate + 1, { expirationTtl: 3600 })
},
check: async () => {
const values = await Resource.GatewayKv.get([currKey, prevKey])
const prevValue = values?.get(prevKey)
const currValue = values?.get(currKey)
prevRate = prevValue ? parseInt(prevValue) : 0
currRate = currValue ? parseInt(currValue) : 0
logger.debug(`rate limit ${model} prev/curr: ${prevRate}/${currRate}`)
if (prevRate + currRate >= limit) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
},
}
}
function buildYYYYMMDDHH(timestamp: number) {
return new Date(timestamp)
.toISOString()
.replace(/[^0-9]/g, "")
.substring(0, 10)
}
@@ -50,10 +50,7 @@ export async function GET(input: APIEvent) {
})
.from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.leftJoin(
ModelTable,
and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)),
)
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)))
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows.map((row) => row.model)),
)
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
export {}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,10 +578,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -610,10 +586,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -670,9 +643,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -680,9 +651,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -699,4 +668,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,10 +578,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -610,10 +586,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -670,9 +643,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -680,9 +651,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -699,4 +668,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -592,10 +571,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -603,10 +579,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -663,9 +636,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -673,9 +644,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -692,4 +661,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,10 +578,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -610,10 +586,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -670,9 +643,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -680,9 +651,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -699,4 +668,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -613,18 +592,12 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -632,10 +605,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -692,9 +662,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -702,9 +670,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -721,4 +687,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -613,32 +592,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -646,10 +615,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -706,9 +672,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -716,9 +680,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -735,4 +697,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,32 +578,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -632,10 +601,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -692,9 +658,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -702,9 +666,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -721,4 +683,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -493,17 +480,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -511,10 +493,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -606,32 +585,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -639,10 +608,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -699,9 +665,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -709,9 +673,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -728,4 +690,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -493,9 +480,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -503,10 +488,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -598,32 +580,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -631,10 +603,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -691,9 +660,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -701,9 +668,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -720,4 +685,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -584,32 +566,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -617,10 +589,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -677,9 +646,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -687,9 +654,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -706,4 +671,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -584,32 +566,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -617,10 +589,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -677,9 +646,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -687,9 +654,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -706,4 +671,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -605,32 +587,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -638,10 +610,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -698,9 +667,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -708,9 +675,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -727,4 +692,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -549,10 +531,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -560,10 +539,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -676,32 +652,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -709,10 +675,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -769,9 +732,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -779,9 +740,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -798,4 +757,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -549,10 +531,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -560,10 +539,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -627,10 +603,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -638,10 +611,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -754,32 +724,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -787,10 +747,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -847,9 +804,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -857,9 +812,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -876,4 +829,4 @@
"tables": {},
"indexes": {}
}
}
}

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